1 | <?php |
||
2 | |||
3 | namespace Conner\Tagging; |
||
4 | |||
5 | use Conner\Tagging\Events\TagAdded; |
||
6 | use Conner\Tagging\Events\TagRemoved; |
||
7 | use Conner\Tagging\Model\Tag; |
||
8 | use Conner\Tagging\Model\Tagged; |
||
9 | use Illuminate\Database\Eloquent\Builder; |
||
10 | use Illuminate\Database\Eloquent\Collection; |
||
11 | |||
12 | /** |
||
13 | * @package Conner\Tagging |
||
14 | * @method static withAllTags(array $tags) |
||
15 | * @method static withAnyTag(array $tags) |
||
16 | * @method static withoutTags(array $tags) |
||
17 | * @property Collection|Tagged[] tagged |
||
18 | * @property Collection|Tag[] tags |
||
19 | * @property string[] tag_names |
||
20 | */ |
||
21 | trait Taggable |
||
22 | { |
||
23 | /** |
||
24 | * Temp storage for auto tag |
||
25 | * |
||
26 | * @var mixed |
||
27 | * @access protected |
||
28 | */ |
||
29 | protected $autoTagValue; |
||
30 | |||
31 | /** |
||
32 | * Track if auto tag has been manually set |
||
33 | * |
||
34 | * @var boolean |
||
35 | * @access protected |
||
36 | */ |
||
37 | protected $autoTagSet = false; |
||
38 | |||
39 | /** |
||
40 | * Boot the soft taggable trait for a model. |
||
41 | * |
||
42 | * @return void |
||
43 | */ |
||
44 | public static function bootTaggable() |
||
45 | { |
||
46 | if(static::untagOnDelete()) { |
||
47 | static::deleting(function($model) { |
||
48 | $model->untag(); |
||
49 | }); |
||
50 | } |
||
51 | |||
52 | static::saved(function ($model) { |
||
53 | $model->autoTagPostSave(); |
||
54 | }); |
||
55 | } |
||
56 | |||
57 | /** |
||
58 | * Return collection of tagged rows related to the tagged model |
||
59 | * |
||
60 | * @return \Illuminate\Database\Eloquent\Collection |
||
61 | */ |
||
62 | public function tagged() |
||
63 | { |
||
64 | return $this->morphMany(TaggingUtility::taggedModelString(), 'taggable') |
||
65 | ->with('tag'); |
||
66 | } |
||
67 | |||
68 | /** |
||
69 | * Return collection of tags related to the tagged model |
||
70 | * TODO : I'm sure there is a faster way to build this, but |
||
71 | * If anyone knows how to do that, me love you long time. |
||
72 | * |
||
73 | * @return \Illuminate\Database\Eloquent\Collection|Tagged[] |
||
74 | */ |
||
75 | public function getTagsAttribute() |
||
76 | { |
||
77 | return $this->tagged->map(function(Tagged $item){ |
||
78 | return $item->tag; |
||
79 | }); |
||
80 | } |
||
81 | |||
82 | /** |
||
83 | * Get the tag names via attribute, example $model->tag_names |
||
84 | */ |
||
85 | public function getTagNamesAttribute(): array |
||
86 | { |
||
87 | return $this->tagNames(); |
||
88 | } |
||
89 | |||
90 | /** |
||
91 | * Perform the action of tagging the model with the given string |
||
92 | * |
||
93 | * @param string|array $tagNames |
||
94 | */ |
||
95 | public function tag($tagNames) |
||
96 | { |
||
97 | $tagNames = TaggingUtility::makeTagArray($tagNames); |
||
98 | |||
99 | foreach($tagNames as $tagName) { |
||
100 | $this->addTag($tagName); |
||
101 | } |
||
102 | } |
||
103 | |||
104 | /** |
||
105 | * Return array of the tag names related to the current model |
||
106 | * |
||
107 | * @return array |
||
108 | */ |
||
109 | public function tagNames(): array |
||
110 | { |
||
111 | return $this->tagged->map(function($item){ |
||
112 | return $item->tag_name; |
||
113 | })->toArray(); |
||
114 | } |
||
115 | |||
116 | /** |
||
117 | * Return array of the tag slugs related to the current model |
||
118 | * |
||
119 | * @return array |
||
120 | */ |
||
121 | public function tagSlugs(): array |
||
122 | { |
||
123 | return $this->tagged->map(function($item){ |
||
124 | return $item->tag_slug; |
||
125 | })->toArray(); |
||
126 | } |
||
127 | |||
128 | /** |
||
129 | * Remove the tag from this model |
||
130 | * |
||
131 | * @param string|array|null $tagNames (or null to remove all tags) |
||
132 | */ |
||
133 | public function untag($tagNames=null) |
||
134 | { |
||
135 | if(is_null($tagNames)) { |
||
136 | $tagNames = $this->tagNames(); |
||
137 | } |
||
138 | |||
139 | $tagNames = TaggingUtility::makeTagArray($tagNames); |
||
140 | |||
141 | foreach($tagNames as $tagName) { |
||
142 | $this->removeTag($tagName); |
||
143 | } |
||
144 | |||
145 | if(static::shouldDeleteUnused()) { |
||
146 | TaggingUtility::deleteUnusedTags(); |
||
147 | } |
||
148 | } |
||
149 | |||
150 | /** |
||
151 | * Replace the tags from this model |
||
152 | * |
||
153 | * @param string|array $tagNames |
||
154 | */ |
||
155 | public function retag($tagNames) |
||
156 | { |
||
157 | $tagNames = TaggingUtility::makeTagArray($tagNames); |
||
158 | $currentTagNames = $this->tagNames(); |
||
159 | |||
160 | $deletions = array_diff($currentTagNames, $tagNames); |
||
161 | $additions = array_diff($tagNames, $currentTagNames); |
||
162 | |||
163 | $this->untag($deletions); |
||
164 | |||
165 | foreach($additions as $tagName) { |
||
166 | $this->addTag($tagName); |
||
167 | } |
||
168 | } |
||
169 | |||
170 | /** |
||
171 | * Filter model to subset with the given tags |
||
172 | * |
||
173 | * @param Builder $query |
||
174 | * @param array|string $tagNames |
||
175 | * @return Builder |
||
176 | */ |
||
177 | public function scopeWithAllTags(Builder $query, $tagNames): Builder |
||
178 | { |
||
179 | if(!is_array($tagNames)) { |
||
180 | $tagNames = func_get_args(); |
||
181 | array_shift($tagNames); |
||
182 | } |
||
183 | |||
184 | $tagNames = TaggingUtility::makeTagArray($tagNames); |
||
185 | |||
186 | $className = $query->getModel()->getMorphClass(); |
||
187 | |||
188 | foreach($tagNames as $tagSlug) { |
||
189 | $tags = Tagged::query() |
||
190 | ->where('tag_slug', TaggingUtility::normalize($tagSlug)) |
||
191 | ->where('taggable_type', $className) |
||
192 | ->get() |
||
193 | ->pluck('taggable_id'); |
||
194 | |||
195 | $primaryKey = $this->getKeyName(); |
||
196 | $query->whereIn($this->getTable().'.'.$primaryKey, $tags); |
||
197 | } |
||
198 | |||
199 | return $query; |
||
200 | } |
||
201 | |||
202 | /** |
||
203 | * Filter model to subset with the given tags |
||
204 | * |
||
205 | * @param Builder $query |
||
206 | * @param array|string $tagNames |
||
207 | * @return Builder |
||
208 | */ |
||
209 | public function scopeWithAnyTag(Builder $query, $tagNames): Builder |
||
210 | { |
||
211 | $tags = $this->assembleTagsForScoping($query, $tagNames); |
||
212 | |||
213 | return $query->whereIn($this->getTable().'.'.$this->getKeyName(), $tags); |
||
0 ignored issues
–
show
Bug
Best Practice
introduced
by
Loading history...
|
|||
214 | } |
||
215 | |||
216 | /** |
||
217 | * Filter model to subset without the given tags |
||
218 | * |
||
219 | * @param Builder $query |
||
220 | * @param array|string $tagNames |
||
221 | * @return Builder |
||
222 | */ |
||
223 | public function scopeWithoutTags(Builder $query, $tagNames): Builder |
||
224 | { |
||
225 | $tags = $this->assembleTagsForScoping($query, $tagNames); |
||
226 | |||
227 | return $query->whereNotIn($this->getTable().'.'.$this->getKeyName(), $tags); |
||
0 ignored issues
–
show
|
|||
228 | } |
||
229 | |||
230 | /** |
||
231 | * Adds a single tag |
||
232 | * |
||
233 | * @param string $tagName |
||
234 | */ |
||
235 | private function addTag($tagName) |
||
236 | { |
||
237 | $tagName = trim($tagName); |
||
238 | |||
239 | if(strlen($tagName) == 0) { |
||
240 | return; |
||
241 | } |
||
242 | |||
243 | $tagSlug = TaggingUtility::normalize($tagName); |
||
244 | |||
245 | $previousCount = $this->tagged()->where('tag_slug', '=', $tagSlug)->take(1)->count(); |
||
246 | if($previousCount >= 1) { return; } |
||
247 | |||
248 | $tagged = new Tagged([ |
||
249 | 'tag_name' => TaggingUtility::displayize($tagName), |
||
250 | 'tag_slug' => $tagSlug, |
||
251 | ]); |
||
252 | |||
253 | $this->tagged()->save($tagged); |
||
254 | |||
255 | TaggingUtility::incrementCount($tagName, $tagSlug, 1); |
||
256 | |||
257 | unset($this->relations['tagged']); |
||
258 | |||
259 | event(new TagAdded($this, $tagSlug, $tagged)); |
||
260 | } |
||
261 | |||
262 | /** |
||
263 | * Removes a single tag |
||
264 | * |
||
265 | * @param $tagName string |
||
266 | */ |
||
267 | private function removeTag($tagName) |
||
268 | { |
||
269 | $tagName = trim($tagName); |
||
270 | |||
271 | $tagSlug = TaggingUtility::normalize($tagName); |
||
272 | |||
273 | if($count = $this->tagged()->where('tag_slug', '=', $tagSlug)->delete()) { |
||
274 | TaggingUtility::decrementCount($tagName, $tagSlug, $count); |
||
275 | } |
||
276 | |||
277 | unset($this->relations['tagged']); // clear the "cache" |
||
278 | |||
279 | event(new TagRemoved($this, $tagSlug)); |
||
280 | } |
||
281 | |||
282 | /** |
||
283 | * Return an array of all of the tags that are in use by this model |
||
284 | * |
||
285 | * @return Collection|Tagged[] |
||
286 | */ |
||
287 | public static function existingTags(): Collection |
||
288 | { |
||
289 | return Tagged::query() |
||
290 | ->distinct() |
||
291 | ->join('tagging_tags', 'tag_slug', '=', 'tagging_tags.slug') |
||
292 | ->where('taggable_type', '=', (new static)->getMorphClass()) |
||
293 | ->orderBy('tag_slug', 'ASC') |
||
294 | ->get(['tag_slug as slug', 'tag_name as name', 'tagging_tags.count as count']); |
||
295 | } |
||
296 | |||
297 | /** |
||
298 | * Return an array of all of the tags that are in use by this model |
||
299 | * @param array $groups |
||
300 | * @return Collection|Tagged[] |
||
301 | */ |
||
302 | public static function existingTagsInGroups($groups): Collection |
||
303 | { |
||
304 | return Tagged::query() |
||
305 | ->distinct() |
||
306 | ->join('tagging_tags', 'tag_slug', '=', 'tagging_tags.slug') |
||
307 | ->join('tagging_tag_groups', 'tag_group_id', '=', 'tagging_tag_groups.id') |
||
308 | ->where('taggable_type', '=', (new static)->getMorphClass()) |
||
309 | ->whereIn('tagging_tag_groups.name', $groups) |
||
310 | ->orderBy('tag_slug', 'ASC') |
||
311 | ->get(array('tag_slug as slug', 'tag_name as name', 'tagging_tags.count as count')); |
||
312 | } |
||
313 | |||
314 | |||
315 | /** |
||
316 | * Should untag on delete |
||
317 | */ |
||
318 | public static function untagOnDelete() |
||
319 | { |
||
320 | return isset(static::$untagOnDelete) |
||
321 | ? static::$untagOnDelete |
||
322 | : config('tagging.untag_on_delete'); |
||
323 | } |
||
324 | |||
325 | /** |
||
326 | * Delete tags that are not used anymore |
||
327 | */ |
||
328 | public static function shouldDeleteUnused(): bool |
||
329 | { |
||
330 | return config('tagging.delete_unused_tags', false); |
||
331 | } |
||
332 | |||
333 | /** |
||
334 | * Set tag names to be set on save |
||
335 | * |
||
336 | * @param mixed $value Data for retag |
||
337 | */ |
||
338 | public function setTagNamesAttribute($value) |
||
339 | { |
||
340 | $this->autoTagValue = $value; |
||
341 | $this->autoTagSet = true; |
||
342 | } |
||
343 | |||
344 | /** |
||
345 | * AutoTag post-save hook |
||
346 | * |
||
347 | * Tags model based on data stored in tmp property, or untags if manually |
||
348 | * set to false value |
||
349 | */ |
||
350 | public function autoTagPostSave() |
||
351 | { |
||
352 | if ($this->autoTagSet) { |
||
353 | if ($this->autoTagValue) { |
||
354 | $this->retag($this->autoTagValue); |
||
355 | } else { |
||
356 | $this->untag(); |
||
357 | } |
||
358 | } |
||
359 | } |
||
360 | |||
361 | private function assembleTagsForScoping($query, $tagNames) |
||
362 | { |
||
363 | if(!is_array($tagNames)) { |
||
364 | $tagNames = func_get_args(); |
||
365 | array_shift($tagNames); |
||
366 | } |
||
367 | |||
368 | $tagNames = TaggingUtility::makeTagArray($tagNames); |
||
369 | |||
370 | $normalizer = [TaggingUtility::class, 'normalize']; |
||
371 | |||
372 | $tagNames = array_map($normalizer, $tagNames); |
||
373 | $className = $query->getModel()->getMorphClass(); |
||
374 | |||
375 | $tags = Tagged::query() |
||
376 | ->whereIn('tag_slug', $tagNames) |
||
377 | ->where('taggable_type', $className) |
||
378 | ->get() |
||
379 | ->pluck('taggable_id'); |
||
380 | |||
381 | return $tags; |
||
382 | } |
||
383 | |||
384 | } |
||
385 |