Initial commit
This commit is contained in:
986
app/Traits/TaggableWithLocale.php
Normal file
986
app/Traits/TaggableWithLocale.php
Normal file
@@ -0,0 +1,986 @@
|
||||
<?php
|
||||
|
||||
namespace App\Traits;
|
||||
|
||||
use App\Helpers\StringHelper;
|
||||
use App\Models\Category;
|
||||
use App\Models\CategoryTranslation;
|
||||
use App\Models\TaggableLocale;
|
||||
use Cviebrock\EloquentTaggable\Events\ModelTagged;
|
||||
use Cviebrock\EloquentTaggable\Events\ModelUntagged;
|
||||
use Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException;
|
||||
use Cviebrock\EloquentTaggable\Models\Tag;
|
||||
use Cviebrock\EloquentTaggable\Services\TagService;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Database\Eloquent\Collection;
|
||||
use Illuminate\Database\Eloquent\Relations\MorphToMany;
|
||||
use Illuminate\Database\Query\JoinClause;
|
||||
use Illuminate\Support\Facades\App;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
/**
|
||||
* Class TaggableWithLocale
|
||||
*
|
||||
* Is a customized copy of:
|
||||
* @package Cviebrock\EloquentTaggable 9.0
|
||||
*
|
||||
* This trait includes modified Taggable methods to use also the extra table tag_contexts,
|
||||
* which is not included in the original Taggable package.
|
||||
*/
|
||||
|
||||
trait TaggableWithLocale
|
||||
{
|
||||
public function translateTagName($tagName, $fromLocale, $toLocale)
|
||||
{
|
||||
$tagName = mb_strtolower($tagName);
|
||||
$result = Tag::where('name', $tagName)
|
||||
->whereHas('locale', function ($query) use ($fromLocale) {
|
||||
$query->where('locale', $fromLocale);
|
||||
})
|
||||
->with(
|
||||
'contexts.tags',
|
||||
function ($q) use ($toLocale) {
|
||||
$q->whereHas('locale', function ($q) use ($toLocale) {
|
||||
$q->where('locale', $toLocale);
|
||||
})->select('normalized');
|
||||
}
|
||||
)
|
||||
->get();
|
||||
|
||||
if ($result->count() != 0) {
|
||||
|
||||
$result = $result->first()
|
||||
->contexts;
|
||||
|
||||
if ($result->first()) {
|
||||
$result = $result
|
||||
->first()
|
||||
->tags
|
||||
->pluck('normalized')
|
||||
->unique()
|
||||
->values()
|
||||
->reject($tagName)
|
||||
->flatten()
|
||||
;
|
||||
} else {
|
||||
$result = [];
|
||||
}
|
||||
} else {
|
||||
$result = [];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
//! TODO conditionally select fallback or source locale, similar method as in translateTagIdsWithContexts()
|
||||
public function translateTagNameWithContext($name = null, $toLocale = null)
|
||||
{
|
||||
if (!$toLocale) {
|
||||
$toFallbackLocale = app()->getFallBackLocale();
|
||||
}
|
||||
|
||||
if (!$toFallbackLocale) {
|
||||
$toFallbackLocale = app()->getFallBackLocale();
|
||||
}
|
||||
|
||||
$result = Tag::where('name', $name)
|
||||
->with([
|
||||
'contexts.tags' => function ($query) use ($toLocale) {
|
||||
$query->with(['locale' => function ($query) use ($toLocale) {
|
||||
$query->where('locale', $toLocale)->select('taggable_tag_id', 'locale');
|
||||
}])
|
||||
->whereHas('locale', function ($query) use ($toLocale) {
|
||||
$query->where('locale', $toLocale);
|
||||
})
|
||||
->select('taggable_tags.tag_id', 'normalized');
|
||||
},
|
||||
'contexts.category' => function ($query) use ($toLocale) {
|
||||
$query->with(['translations' => function ($query) use ($toLocale) {
|
||||
$query->where('locale', $toLocale)->select('category_id', 'name');
|
||||
}])
|
||||
->select('id');
|
||||
},
|
||||
])
|
||||
->get();
|
||||
|
||||
|
||||
if ($result->count() != 0) {
|
||||
|
||||
$result = $result->first()
|
||||
->contexts;
|
||||
|
||||
if ($result->first()) {
|
||||
|
||||
$tag = $result
|
||||
->first()
|
||||
->tags
|
||||
->unique()
|
||||
->values()
|
||||
->flatten();
|
||||
$category = $result
|
||||
->first()
|
||||
->category
|
||||
->translations
|
||||
->first();
|
||||
$categoryPath = $result
|
||||
->first()
|
||||
->category
|
||||
->ancestorsAndSelf
|
||||
->sortBy('id')
|
||||
->pluck('id');
|
||||
|
||||
$result = $tag->map(function ($item) use ($category, $categoryPath, $toLocale) {
|
||||
$localeItem = $item->locale ? $item->locale : null;
|
||||
$mapped = [
|
||||
'tag_id' => $item->tag_id,
|
||||
'tag' => StringHelper::dutchTitleCase($item->normalized),
|
||||
'category_id' => $category->category_id,
|
||||
'category' => StringHelper::dutchTitleCase($item->normalized),
|
||||
'category_path' => implode(
|
||||
' > ',
|
||||
CategoryTranslation::whereIn('category_id', $categoryPath)->where('locale', $toLocale)->pluck('name')->toArray()
|
||||
) . ' > ' . StringHelper::dutchTitleCase($item->normalized),
|
||||
'locale' => $localeItem->find($item->tag_id)
|
||||
];
|
||||
return $mapped;
|
||||
});
|
||||
|
||||
} else {
|
||||
$result = [];
|
||||
}
|
||||
} else {
|
||||
$result = [];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
//! TODO conditionally select fallback or source locale, similar method as in translateTagIdsWithContexts()
|
||||
public function translateTagNamesWithContexts($array, $toLocale = null, $toFallbackLocale = null)
|
||||
{
|
||||
|
||||
if (!$toLocale) {
|
||||
$toFallbackLocale = app()->getFallBackLocale();
|
||||
}
|
||||
|
||||
if (!$toFallbackLocale) {
|
||||
$toFallbackLocale = app()->getFallBackLocale();
|
||||
}
|
||||
|
||||
$collection = collect($array);
|
||||
$translated = $collection->map(function ($item, $key) use ($toLocale, $toFallbackLocale) {
|
||||
|
||||
$source = $item;
|
||||
|
||||
$transLocale = $this->translateTagNameWithContext($source, $toLocale);
|
||||
$transFallbackLocale = $this->translateTagNameWithContext($source, $toFallbackLocale);
|
||||
|
||||
if ($transLocale === $source) {
|
||||
return $source;
|
||||
} elseif (count($transLocale) > 0) {
|
||||
return $transLocale;
|
||||
} elseif (count($transFallbackLocale) > 0) {
|
||||
return $transFallbackLocale;
|
||||
} else {
|
||||
return $source;
|
||||
}
|
||||
})
|
||||
->flatMap(function ($innerCollection) {
|
||||
return $innerCollection;
|
||||
});
|
||||
|
||||
return $translated;
|
||||
}
|
||||
|
||||
|
||||
public function translateTagId($tagId, $toLocale)
|
||||
{
|
||||
$result = Tag::where('tag_id', $tagId)
|
||||
->with(
|
||||
'contexts.tags',
|
||||
function ($q) use ($toLocale) {
|
||||
$q->whereHas('locale', function ($q) use ($toLocale) {
|
||||
$q->where('locale', $toLocale);
|
||||
})->select('normalized');
|
||||
}
|
||||
)
|
||||
->get();
|
||||
|
||||
if ($result->count() != 0) {
|
||||
|
||||
$result = $result->first()
|
||||
->contexts;
|
||||
|
||||
if ($result->first()) {
|
||||
$result = $result
|
||||
->first()
|
||||
->tags
|
||||
->pluck('pivot')
|
||||
->pluck('tag_id')
|
||||
->unique()
|
||||
->values()
|
||||
->flatten()
|
||||
;
|
||||
} else {
|
||||
$result = [];
|
||||
}
|
||||
} else {
|
||||
$result = [];
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
|
||||
public function translateTagIdWithContext($tagId)
|
||||
{
|
||||
if ($tagId) {
|
||||
$sourceLocale = TaggableLocale::where('taggable_tag_id', $tagId)->value('locale');
|
||||
$tag = Tag::find($tagId);
|
||||
$translatedTag = $tag->translation;
|
||||
$category = Category::find($tag->contexts->pluck('category_id')->first());
|
||||
$translatedCategory = $category->translation ?? '';
|
||||
if ($translatedCategory) {
|
||||
$categoryPathIds = $category->ancestorsAndSelf->sortBy('id')->pluck('id');
|
||||
$categoryPath = implode(
|
||||
' > ',
|
||||
CategoryTranslation::whereIn('category_id', $categoryPathIds)
|
||||
->where('locale', App::getLocale())
|
||||
->pluck('name')
|
||||
->toArray()
|
||||
);
|
||||
$categoryColor = $category->rootAncestor ? $category->rootAncestor->color : $category->color;
|
||||
}
|
||||
// Map and return the finalized result
|
||||
return [
|
||||
'original_tag_id' => $tagId,
|
||||
'tag_id' => $translatedTag->tag_id,
|
||||
'tag' => $translatedTag->name,
|
||||
'comment' => $translatedTag->comment,
|
||||
'locale' => $translatedTag->locale,
|
||||
'category_id' => $category->id ?? null,
|
||||
'category' => $translatedCategory->name ?? '',
|
||||
'category_path' => $categoryPath ?? '',
|
||||
'category_color' => $categoryColor ?? 'gray',
|
||||
];
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public function translateTagIds($array, $toLocale = null, $toFallbackLocale = null)
|
||||
{
|
||||
if (!$toLocale) {
|
||||
$toFallbackLocale = app()->getFallBackLocale();
|
||||
}
|
||||
|
||||
if (!$toFallbackLocale) {
|
||||
$toFallbackLocale = app()->getFallBackLocale();
|
||||
}
|
||||
|
||||
$collection = collect($array);
|
||||
$translated = $collection->map(function ($item, $key) use ($toLocale, $toFallbackLocale) {
|
||||
|
||||
$source = $item;
|
||||
|
||||
$transLocale = $this->translateTagId($source, $toLocale);
|
||||
$transFallbackLocale = $this->translateTagId($source, $toFallbackLocale);
|
||||
|
||||
if ($transLocale === $source) {
|
||||
return $source;
|
||||
} elseif (count($transLocale) > 0) {
|
||||
return $transLocale;
|
||||
} elseif (count($transFallbackLocale) > 0) {
|
||||
return $transFallbackLocale;
|
||||
} else {
|
||||
return $source;
|
||||
}
|
||||
})->flatten()->toArray();
|
||||
|
||||
return $translated;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function translateTagIdsWithContexts($tagIds)
|
||||
{
|
||||
$collectionIds = collect($tagIds);
|
||||
$translated = $collectionIds->map(function ($item) {
|
||||
$item = $this->translateTagIdWithContext($item);
|
||||
return $item;
|
||||
});
|
||||
|
||||
return $translated;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of normalized tags for a given locale.
|
||||
*
|
||||
* @param string $locale The locale to get tags for.
|
||||
*
|
||||
* @return array An array of normalized tags for the given locale.
|
||||
*/
|
||||
public function localTagArray($locale)
|
||||
{
|
||||
$array = Tag::whereHas('locale', function ($query) use ($locale) {
|
||||
$query->where('locale', $locale);
|
||||
})->pluck('name')->toArray();
|
||||
return $array;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Clean up duplicate taggables with different locales
|
||||
* and clean up any orphaned taggables
|
||||
* Example Usage:
|
||||
* $user = User::find(161)->cleantaggables();
|
||||
*
|
||||
* @return mixed The result of the untagging operation.
|
||||
*/
|
||||
public function cleanTaggables()
|
||||
{
|
||||
$this->cleanForeignTaggables();
|
||||
$this->cleanOrhphanedTaggables();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
public function cleanForeignTaggables()
|
||||
{
|
||||
// Get the tags with their contexts
|
||||
$tagsWithContexts = $this->tags()->with('localeContext')->get();
|
||||
$tagIds = $tagsWithContexts->pluck('tag_id');
|
||||
$contextIds = $tagsWithContexts->pluck('localeContext.context_id')->flatten();
|
||||
|
||||
// Find the context IDs that appear more than once
|
||||
$duplicateContextIds = $contextIds->duplicates()->flatten();
|
||||
|
||||
$duplicateTagIds = DB::table('taggable_locale_context')
|
||||
->whereIn('context_id', $duplicateContextIds)
|
||||
->whereIn('tag_id', $tagIds)
|
||||
->pluck('tag_id')
|
||||
->flatten();
|
||||
|
||||
$duplicateTagsIdsForeign = DB::table('taggable_locales')
|
||||
->where('locale', '!=', App::getLocale())
|
||||
->whereIn('taggable_tag_id', $duplicateTagIds)
|
||||
->pluck('taggable_tag_id');
|
||||
|
||||
$orhpanedTagIds = DB::table('taggable_taggables')
|
||||
->leftJoin('taggable_tags', 'taggable_taggables.tag_id', '=', 'taggable_tags.tag_id')
|
||||
->whereNull('taggable_tags.tag_id')
|
||||
->select('taggable_taggables.tag_id')
|
||||
->distinct()
|
||||
->pluck('tag_id');
|
||||
|
||||
return $this->untagById($duplicateTagsIdsForeign);
|
||||
}
|
||||
|
||||
|
||||
Public function cleanOrhphanedTaggables()
|
||||
{
|
||||
$orhpanedTagIds = DB::table('taggable_taggables')
|
||||
->leftJoin('taggable_tags', 'taggable_taggables.tag_id', '=', 'taggable_tags.tag_id')
|
||||
->whereNull('taggable_tags.tag_id')
|
||||
->select('taggable_taggables.tag_id')
|
||||
->distinct()
|
||||
->pluck('tag_id');
|
||||
|
||||
return $this->tags()->detach($orhpanedTagIds);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Returns a string of normalized tags for the given locale.
|
||||
*
|
||||
* @param string $locale The locale to filter tags by.
|
||||
* @return string The string of normalized tags.
|
||||
*/
|
||||
public function localTagList($locale)
|
||||
{
|
||||
$array = Tag::whereHas('locale', function ($query) use ($locale) {
|
||||
$query->where('locale', $locale);
|
||||
})->pluck('normalized')->toArray();
|
||||
return implode(config('taggable.glue'), $array);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Find the tag with the given name.
|
||||
*
|
||||
* @param string $value
|
||||
*
|
||||
* @return static|null
|
||||
*/
|
||||
public static function findByName(string $value)
|
||||
{
|
||||
return app(TagService::class)->find($value);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Property to control sequence on alias
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $taggableAliasSequence = 0;
|
||||
|
||||
|
||||
/**
|
||||
* Boot the trait.
|
||||
*
|
||||
* Listen for the deleting event of a model, then remove the relation between it and tags
|
||||
*/
|
||||
protected static function bootTaggable(): void
|
||||
{
|
||||
static::deleting(function ($model) {
|
||||
if (!method_exists($model, 'runSoftDelete') || $model->isForceDeleting()) {
|
||||
$model->detag();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Get a collection of all tags the model has.
|
||||
*
|
||||
* @return \Illuminate\Database\Eloquent\Relations\MorphToMany
|
||||
*/
|
||||
public function tags(): MorphToMany
|
||||
{
|
||||
$model = config('taggable.model');
|
||||
$table = config('taggable.tables.taggable_taggables', 'taggable_taggables');
|
||||
return $this->morphToMany($model, 'taggable', $table, 'taggable_id', 'tag_id')
|
||||
->withTimestamps();
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* Attach one or multiple tags to the model.
|
||||
*
|
||||
* @param string|array $tags
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function tag($tags): self
|
||||
{
|
||||
$tags = app(TagService::class)->buildTagArray($tags);
|
||||
|
||||
foreach ($tags as $tagName) {
|
||||
$this->addOneTag($tagName);
|
||||
$this->load('tags');
|
||||
}
|
||||
|
||||
event(new ModelTagged($this, $tags));
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Attach one or more existing tags to a model,
|
||||
* identified by the tag's IDs.
|
||||
*
|
||||
* @param int|int[] $ids
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function tagById($ids): self
|
||||
{
|
||||
$tags = app(TagService::class)->findByIds($ids);
|
||||
$names = $tags->pluck('name')->all();
|
||||
|
||||
return $this->tag($names);
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach one or multiple tags from the model.
|
||||
*
|
||||
* @param string|array $tags
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function untag($tags): self
|
||||
{
|
||||
$tags = app(TagService::class)->buildTagArray($tags);
|
||||
|
||||
foreach ($tags as $tagName) {
|
||||
$this->removeOneTag($tagName);
|
||||
}
|
||||
|
||||
event(new ModelUntagged($this, $tags));
|
||||
|
||||
return $this->load('tags');
|
||||
}
|
||||
|
||||
/**
|
||||
* Detach one or more existing tags to a model,
|
||||
* identified by the tag's IDs.
|
||||
*
|
||||
* @param int|int[] $ids
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function untagById($ids): self
|
||||
{
|
||||
$tags = app(TagService::class)->findByIds($ids);
|
||||
$names = $tags->pluck('name')->all();
|
||||
|
||||
return $this->untag($names);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove all tags from the model and assign the given ones.
|
||||
*
|
||||
* @param string|array $tags
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function retag($tags): self
|
||||
{
|
||||
return $this->detag()->tag($tags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove all tags from the model and assign the given ones by ID.
|
||||
*
|
||||
* @param int|int[] $ids
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function retagById($ids): self
|
||||
{
|
||||
return $this->detag()->tagById($ids);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Remove all tags from the model.
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function detag(): self
|
||||
{
|
||||
$this->tags()->sync([]);
|
||||
|
||||
return $this->load('tags');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Add one tag to the model.
|
||||
*
|
||||
* @param string $tagName
|
||||
*/
|
||||
protected function addOneTag(string $tagName): void
|
||||
{
|
||||
/** @var Tag $tag */
|
||||
$tag = app(TagService::class)->findOrCreate($tagName);
|
||||
$tagKey = $tag->getKey();
|
||||
if (!$this->getAttribute('tags')->contains($tagKey)) {
|
||||
$this->tags()->attach($tagKey);
|
||||
}
|
||||
|
||||
$locale = ['locale' => App::getLocale()]; // Customization: include App Locale when adding a tag
|
||||
TaggableLocale::updateOrCreate(['taggable_tag_id' => $tag->getKey()], $locale); // Customization: include App Locale when adding a tag
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one tag from the model
|
||||
*
|
||||
* @param string $tagName
|
||||
*/
|
||||
protected function removeOneTag(string $tagName): void
|
||||
{
|
||||
$tag = app(TagService::class)->find($tagName);
|
||||
|
||||
if ($tag) {
|
||||
$this->tags()->detach($tag);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the tags of the model as a delimited string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTagListAttribute(): string
|
||||
{
|
||||
return app(TagService::class)->makeTagList($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all normalized tags of a model as a delimited string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public function getTagListNormalizedAttribute(): string
|
||||
{
|
||||
return app(TagService::class)->makeTagList($this, 'normalized');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all tags of a model as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTagArrayAttribute(): array
|
||||
{
|
||||
return app(TagService::class)->makeTagArray($this);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all normalized tags of a model as an array.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public function getTagArrayNormalizedAttribute(): array
|
||||
{
|
||||
return app(TagService::class)->makeTagArray($this, 'normalized');
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Determine if a given tag is attached to the model.
|
||||
*
|
||||
* @param Tag|string $tag
|
||||
*
|
||||
* @return bool
|
||||
*/
|
||||
public function hasTag($tag): bool
|
||||
{
|
||||
if ($tag instanceof Tag) {
|
||||
$normalized = $tag->getAttribute('normalized');
|
||||
} else {
|
||||
$normalized = app(TagService::class)->normalize($tag);
|
||||
}
|
||||
|
||||
return in_array($normalized, $this->getTagArrayNormalizedAttribute(), true);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Query scope for models that have all of the given tags.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array|string $tags
|
||||
*
|
||||
* @return Builder
|
||||
* @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
|
||||
* @throws \ErrorException
|
||||
*/
|
||||
public function scopeWithAllTags(Builder $query, $tags): Builder
|
||||
{
|
||||
/** @var TagService $service */
|
||||
$service = app(TagService::class);
|
||||
$normalized = $service->buildTagArrayNormalized($tags);
|
||||
|
||||
// If there are no tags specified, then there
|
||||
// can't be any results so short-circuit
|
||||
if (count($normalized) === 0) {
|
||||
if (config('taggable.throwEmptyExceptions')) {
|
||||
throw new NoTagsSpecifiedException('Empty tag data passed to withAllTags scope.');
|
||||
}
|
||||
|
||||
return $query->where(\DB::raw(1), 0);
|
||||
}
|
||||
|
||||
$tagKeys = $service->getTagModelKeys($normalized);
|
||||
|
||||
// If some of the tags specified don't exist, then there can't
|
||||
// be any models with all the tags, so so short-circuit
|
||||
if (count($tagKeys) !== count($normalized)) {
|
||||
return $query->where(\DB::raw(1), 0);
|
||||
}
|
||||
|
||||
$alias = $this->taggableCreateNewAlias(__FUNCTION__);
|
||||
$morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
|
||||
|
||||
return $this->prepareTableJoin($query, 'inner', $alias)
|
||||
->whereIn($morphTagKeyName, $tagKeys)
|
||||
->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) = ?", [count($tagKeys)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope for models that have any of the given tags.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param array|string $tags
|
||||
*
|
||||
* @return Builder
|
||||
* @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
|
||||
* @throws \ErrorException
|
||||
*/
|
||||
public function scopeWithAnyTags(Builder $query, $tags): Builder
|
||||
{
|
||||
/** @var TagService $service */
|
||||
$service = app(TagService::class);
|
||||
$normalized = $service->buildTagArrayNormalized($tags);
|
||||
|
||||
// If there are no tags specified, then there is
|
||||
// no filtering to be done so short-circuit
|
||||
if (count($normalized) === 0) {
|
||||
if (config('taggable.throwEmptyExceptions')) {
|
||||
throw new NoTagsSpecifiedException('Empty tag data passed to withAnyTags scope.');
|
||||
}
|
||||
|
||||
return $query->where(\DB::raw(1), 0);
|
||||
}
|
||||
|
||||
$tagKeys = $service->getTagModelKeys($normalized);
|
||||
|
||||
$alias = $this->taggableCreateNewAlias(__FUNCTION__);
|
||||
$morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
|
||||
|
||||
return $this->prepareTableJoin($query, 'inner', $alias)
|
||||
->whereIn($morphTagKeyName, $tagKeys);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope for models that have any tag.
|
||||
*
|
||||
* @param Builder $query
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function scopeIsTagged(Builder $query): Builder
|
||||
{
|
||||
$alias = $this->taggableCreateNewAlias(__FUNCTION__);
|
||||
return $this->prepareTableJoin($query, 'inner', $alias);
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope for models that do not have all of the given tags.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param string|array $tags
|
||||
* @param bool $includeUntagged
|
||||
*
|
||||
* @return Builder
|
||||
* @throws \ErrorException
|
||||
*/
|
||||
public function scopeWithoutAllTags(Builder $query, $tags, bool $includeUntagged = false): Builder
|
||||
{
|
||||
/** @var TagService $service */
|
||||
$service = app(TagService::class);
|
||||
$normalized = $service->buildTagArrayNormalized($tags);
|
||||
$tagKeys = $service->getTagModelKeys($normalized);
|
||||
$tagKeyList = implode(',', $tagKeys);
|
||||
|
||||
$alias = $this->taggableCreateNewAlias(__FUNCTION__);
|
||||
$morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
|
||||
|
||||
$query = $this->prepareTableJoin($query, 'left', $alias)
|
||||
->havingRaw(
|
||||
"COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) < ?",
|
||||
[count($tagKeys)]
|
||||
);
|
||||
|
||||
if (!$includeUntagged) {
|
||||
$query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope for models that do not have any of the given tags.
|
||||
*
|
||||
* @param Builder $query
|
||||
* @param string|array $tags
|
||||
* @param bool $includeUntagged
|
||||
*
|
||||
* @return Builder
|
||||
* @throws \ErrorException
|
||||
*/
|
||||
public function scopeWithoutAnyTags(Builder $query, $tags, bool $includeUntagged = false): Builder
|
||||
{
|
||||
/** @var TagService $service */
|
||||
$service = app(TagService::class);
|
||||
$normalized = $service->buildTagArrayNormalized($tags);
|
||||
$tagKeys = $service->getTagModelKeys($normalized);
|
||||
$tagKeyList = implode(',', $tagKeys);
|
||||
|
||||
$alias = $this->taggableCreateNewAlias(__FUNCTION__);
|
||||
$morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
|
||||
|
||||
$query = $this->prepareTableJoin($query, 'left', $alias)
|
||||
->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) = 0");
|
||||
|
||||
if (!$includeUntagged) {
|
||||
$query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query scope for models that does not have have any tags.
|
||||
*
|
||||
* @param Builder $query
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
public function scopeIsNotTagged(Builder $query): Builder
|
||||
{
|
||||
$alias = $this->taggableCreateNewAlias(__FUNCTION__);
|
||||
$morphForeignKeyName = $this->getQualifiedForeignPivotKeyNameWithAlias($alias);
|
||||
|
||||
return $this->prepareTableJoin($query, 'left', $alias)
|
||||
->havingRaw("COUNT(DISTINCT {$morphForeignKeyName}) = 0");
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Builder $query
|
||||
* @param string $joinType
|
||||
*
|
||||
* @return Builder
|
||||
*/
|
||||
private function prepareTableJoin(Builder $query, string $joinType, string $alias): Builder
|
||||
{
|
||||
$morphTable = $this->tags()->getTable();
|
||||
$morphTableAlias = $morphTable.'_'.$alias;
|
||||
|
||||
$modelKeyName = $this->getQualifiedKeyName();
|
||||
$morphForeignKeyName = $this->getQualifiedForeignPivotKeyNameWithAlias($alias);
|
||||
|
||||
$morphTypeName = $morphTableAlias.'.'. $this->tags()->getMorphType();
|
||||
$morphClass = $this->tags()->getMorphClass();
|
||||
|
||||
$closure = function (JoinClause $join) use ($modelKeyName, $morphForeignKeyName, $morphTypeName, $morphClass) {
|
||||
$join->on($modelKeyName, $morphForeignKeyName)
|
||||
->where($morphTypeName, $morphClass);
|
||||
};
|
||||
|
||||
return $query
|
||||
->select($this->getTable() . '.*')
|
||||
->join($morphTable.' as '.$morphTableAlias, $closure, null, null, $joinType)
|
||||
->groupBy($modelKeyName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a collection of all the tag models used for the called class.
|
||||
*
|
||||
* @return Collection
|
||||
*/
|
||||
public static function allTagModels(): Collection
|
||||
{
|
||||
return app(TagService::class)->getAllTags(static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an array of all tags used for the called class.
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function allTags(): array
|
||||
{
|
||||
/** @var \Illuminate\Database\Eloquent\Collection $tags */
|
||||
$tags = static::allTagModels();
|
||||
|
||||
return $tags->pluck('name')->sort()->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all the tags used for the called class as a delimited string.
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
public static function allTagsList(): string
|
||||
{
|
||||
return app(TagService::class)->joinList(static::allTags());
|
||||
}
|
||||
|
||||
/**
|
||||
* Rename one the tags for the called class.
|
||||
*
|
||||
* @param string $oldTag
|
||||
* @param string $newTag
|
||||
*
|
||||
* @return int
|
||||
*/
|
||||
public static function renameTag(string $oldTag, string $newTag): int
|
||||
{
|
||||
return app(TagService::class)->renameTags($oldTag, $newTag, static::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most popular tags for the called class.
|
||||
*
|
||||
* @param int $limit
|
||||
* @param int $minCount
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function popularTags(int $limit = null, int $minCount = 1): array
|
||||
{
|
||||
/** @var Collection $tags */
|
||||
$tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);
|
||||
|
||||
return $tags->pluck('taggable_count', 'name')->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the most popular tags for the called class.
|
||||
*
|
||||
* @param int $limit
|
||||
* @param int $minCount
|
||||
*
|
||||
* @return array
|
||||
*/
|
||||
public static function popularTagsNormalized(int $limit = null, int $minCount = 1): array
|
||||
{
|
||||
/** @var Collection $tags */
|
||||
$tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);
|
||||
|
||||
return $tags->pluck('taggable_count', 'normalized')->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Related Pivot Key Name with the table alias.
|
||||
*
|
||||
* @param string $alias
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getQualifiedRelatedPivotKeyNameWithAlias(string $alias): string
|
||||
{
|
||||
$morph = $this->tags();
|
||||
|
||||
return $morph->getTable() . '_' . $alias .
|
||||
'.' . $morph->getRelatedPivotKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the Foreign Pivot Key Name with the table alias.
|
||||
*
|
||||
* @param string $alias
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function getQualifiedForeignPivotKeyNameWithAlias(string $alias): string
|
||||
{
|
||||
$morph = $this->tags();
|
||||
|
||||
return $morph->getTable() . '_' . $alias .
|
||||
'.' . $morph->getForeignPivotKeyName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new alias to use on scopes to be able to combine many scopes
|
||||
*
|
||||
* @param string $scope
|
||||
*
|
||||
* @return string
|
||||
*/
|
||||
private function taggableCreateNewAlias(string $scope): string
|
||||
{
|
||||
$this->taggableAliasSequence++;
|
||||
$alias = strtolower($scope) . '_' . $this->taggableAliasSequence;
|
||||
|
||||
return $alias;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user