Taggable::removeSingleTag()   A
last analyzed

Complexity

Conditions 2
Paths 2

Size

Total Lines 13
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
c 0
b 0
f 0
nc 2
nop 1
dl 0
loc 13
rs 10
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
use Illuminate\Database\Eloquent\Model;
12
use Illuminate\Database\Eloquent\Relations\MorphMany;
13
14
/**
15
 * @mixin Model
16
 * @mixin Builder
17
 *
18
 * @method static Builder withAllTags(array $tags)
19
 * @method static Builder withAnyTag(array $tags)
20
 * @method static Builder withoutTags(array $tags)
21
 *
22
 * @property Collection|Tagged[] tagged
23
 * @property Collection|Tag[] tags
24
 * @property string[] tag_names
25
 */
26
trait Taggable
27
{
28
    /**
29
     * Temp storage for auto tag
30
     *
31
     * @var mixed
32
     */
33
    protected $autoTagValue;
34
35
    /**
36
     * Track if auto tag has been manually set
37
     *
38
     * @var bool
39
     */
40
    protected $autoTagSet = false;
41
42
    /**
43
     * Boot the soft taggable trait for a model.
44
     *
45
     * @return void
46
     */
47
    public static function bootTaggable()
48
    {
49
        if (static::untagOnDelete()) {
50
            static::deleting(function ($model) {
51
                $model->untag();
52
            });
53
        }
54
55
        static::saved(function ($model) {
56
            $model->autoTagPostSave();
57
        });
58
    }
59
60
    /**
61
     * Return collection of tagged rows related to the tagged model
62
     *
63
     * @return MorphMany
64
     */
65
    public function tagged()
66
    {
67
        return $this
68
            ->morphMany(TaggingUtility::taggedModelString(), 'taggable')
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

68
            ->/** @scrutinizer ignore-call */ morphMany(TaggingUtility::taggedModelString(), 'taggable')
Loading history...
69
            ->with('tag');
70
    }
71
72
    /**
73
     * Return collection of tags related to the tagged model
74
     * TODO : I'm sure there is a faster way to build this, but
75
     * If anyone knows how to do that, me love you long time.
76
     *
77
     * @return Collection|Tagged[]
78
     */
79
    public function getTagsAttribute()
80
    {
81
        return $this->tagged->map(function (Tagged $item) {
82
            return $item->tag;
83
        });
84
    }
85
86
    /**
87
     * Get the tag names via attribute, example $model->tag_names
88
     */
89
    public function getTagNamesAttribute(): array
90
    {
91
        return $this->tagNames();
92
    }
93
94
    /**
95
     * Perform the action of tagging the model with the given string
96
     *
97
     * @param  string|array  $tagNames
98
     * @return void
99
     */
100
    public function addTags($tagNames, $locale)
101
    {
102
        $tagNames = TaggingUtility::makeTagArray($tagNames);
103
104
        foreach ($tagNames as $tagName) {
105
            $this->addSingleTag($tagName, $locale);
106
        }
107
    }
108
109
    /**
110
     * Perform the action of tagging the model with the given string
111
     *
112
     * @param  string|array  $tagNames
113
     * @return void
114
     */
115
    public function tag($tagNames, $locale = 'en')
116
    {
117
        $this->addTags($tagNames, $locale);
118
    }
119
120
    /**
121
     * Return array of the tag names related to the current model
122
     */
123
    public function tagNames(): array
124
    {
125
        return $this->tagged->map(function ($item) {
126
            return $item->tag_name;
127
        })->toArray();
128
    }
129
130
    /**
131
     * Return array of the tag slugs related to the current model
132
     */
133
    public function tagSlugs(): array
134
    {
135
        return $this->tagged->map(function ($item) {
136
            return $item->tag_slug;
137
        })->toArray();
138
    }
139
140
    /**
141
     * Remove the tag from this model
142
     *
143
     * @param  string|array|null  $tagNames  (or null to remove all tags)
144
     */
145
    public function untag($tagNames = null)
146
    {
147
        if (is_null($tagNames)) {
148
            $tagNames = $this->tagNames();
149
        }
150
151
        $tagNames = TaggingUtility::makeTagArray($tagNames);
152
153
        foreach ($tagNames as $tagName) {
154
            $this->removeSingleTag($tagName);
155
        }
156
157
        if (static::shouldDeleteUnused()) {
158
            TaggingUtility::deleteUnusedTags();
159
        }
160
    }
161
162
    /**
163
     * Replace the tags from this model
164
     *
165
     * @param  string|array  $tagNames
166
     */
167
    public function retag($tagNames)
168
    {
169
        $tagNames = TaggingUtility::makeTagArray($tagNames);
170
        $currentTagNames = $this->tagNames();
171
172
        $deletions = array_diff($currentTagNames, $tagNames);
173
        $additions = array_diff($tagNames, $currentTagNames);
174
175
        $this->untag($deletions);
176
177
        foreach ($additions as $tagName) {
178
            $this->addSingleTag($tagName);
179
        }
180
    }
181
182
    /**
183
     * Filter model to subset with the given tags
184
     *
185
     * @param  array|string  $tagNames
186
     */
187
    public function scopeWithAllTags(Builder $query, $tagNames): Builder
188
    {
189
        if (! is_array($tagNames)) {
190
            $tagNames = func_get_args();
191
            array_shift($tagNames);
192
        }
193
194
        $tagNames = TaggingUtility::makeTagArray($tagNames);
195
196
        $className = $query->getModel()->getMorphClass();
197
198
        foreach ($tagNames as $tagSlug) {
199
            $model = TaggingUtility::taggedModelString();
200
201
            /** @var Tag $tags */
202
            $tags = $model::query()
203
                ->where('tag_slug', TaggingUtility::normalize($tagSlug))
204
                ->where('taggable_type', $className)
205
                ->get()
206
                ->pluck('taggable_id')
207
                ->unique();
208
209
            $primaryKey = $this->getKeyName();
0 ignored issues
show
Bug introduced by
It seems like getKeyName() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

209
            /** @scrutinizer ignore-call */ 
210
            $primaryKey = $this->getKeyName();
Loading history...
210
            $query->whereIn($this->getTable().'.'.$primaryKey, $tags);
0 ignored issues
show
Bug introduced by
It seems like getTable() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

210
            $query->whereIn($this->/** @scrutinizer ignore-call */ getTable().'.'.$primaryKey, $tags);
Loading history...
211
        }
212
213
        return $query;
214
    }
215
216
    /**
217
     * Filter model to subset with the given tags
218
     *
219
     * @param  array|string  $tagNames
220
     */
221
    public function scopeWithAnyTag(Builder $query, $tagNames): Builder
222
    {
223
        $tags = $this->assembleTagsForScoping($query, $tagNames);
224
225
        return $query->whereIn($this->getTable().'.'.$this->getKeyName(), $tags);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereIn($...s->getKeyName(), $tags) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
226
    }
227
228
    /**
229
     * Filter model to subset without the given tags
230
     *
231
     * @param  array|string  $tagNames
232
     */
233
    public function scopeWithoutTags(Builder $query, $tagNames): Builder
234
    {
235
        $tags = $this->assembleTagsForScoping($query, $tagNames);
236
237
        return $query->whereNotIn($this->getTable().'.'.$this->getKeyName(), $tags);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $query->whereNotI...s->getKeyName(), $tags) could return the type Illuminate\Database\Query\Builder which is incompatible with the type-hinted return Illuminate\Database\Eloquent\Builder. Consider adding an additional type-check to rule them out.
Loading history...
238
    }
239
240
    /**
241
     * Adds a single tag
242
     *
243
     * @param  string  $tagName
244
     */
245
    private function addSingleTag($tagName, $locale = 'en')
246
    {
247
        $tagName = trim($tagName);
248
249
        if (strlen($tagName) == 0) {
250
            return;
251
        }
252
253
        $tagSlug = TaggingUtility::normalize($tagName);
254
255
        $previousCount = $this->tagged()->where('tag_slug', '=', $tagSlug)->take(1)->count();
256
        if ($previousCount >= 1) {
257
            return;
258
        }
259
260
        $model = TaggingUtility::taggedModelString();
261
262
        $tagged = new $model([
263
            'tag_name' => TaggingUtility::displayize($tagName),
264
            'tag_slug' => $tagSlug,
265
            'locale' => $locale,
266
        ]);
267
268
        $this->tagged()->save($tagged);
269
270
        TaggingUtility::incrementCount($tagName, $tagSlug, 1, $locale);
271
272
        unset($this->relations['tagged']);
273
274
        event(new TagAdded($this, $tagSlug, $tagged));
275
    }
276
277
    /**
278
     * Removes a single tag
279
     *
280
     * @param  $tagName  string
281
     */
282
    private function removeSingleTag($tagName)
283
    {
284
        $tagName = trim($tagName);
285
286
        $tagSlug = TaggingUtility::normalize($tagName);
287
288
        if ($count = $this->tagged()->where('tag_slug', '=', $tagSlug)->delete()) {
289
            TaggingUtility::decrementCount($tagSlug, $count);
290
        }
291
292
        unset($this->relations['tagged']); // clear the "cache"
293
294
        event(new TagRemoved($this, $tagSlug));
295
    }
296
297
    /**
298
     * Return an array of all of the tags that are in use by this model
299
     *
300
     * @return Collection|Tagged[]
301
     */
302
    public static function existingTags(): Collection
303
    {
304
        $model = TaggingUtility::taggedModelString();
305
306
        return $model::query()
307
            ->distinct()
308
            ->join('tagging_tags', 'tag_slug', '=', 'tagging_tags.slug')
309
            ->where('taggable_type', '=', (new static)->getMorphClass())
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

309
            ->where('taggable_type', '=', (new static)->/** @scrutinizer ignore-call */ getMorphClass())
Loading history...
310
            ->orderBy('tag_slug', 'ASC')
311
            ->get(['tag_slug as slug', 'tag_name as name', 'tagging_tags.count as count']);
312
    }
313
314
    /**
315
     * Return an array of all of the tags that are in use by this model
316
     *
317
     * @param  array  $groups
318
     * @return Collection|Tagged[]
319
     */
320
    public static function existingTagsInGroups($groups): Collection
321
    {
322
        $model = TaggingUtility::taggedModelString();
323
324
        return $model::query()
325
            ->distinct()
326
            ->join('tagging_tags', 'tag_slug', '=', 'tagging_tags.slug')
327
            ->join('tagging_tag_groups', 'tag_group_id', '=', 'tagging_tag_groups.id')
328
            ->where('taggable_type', '=', (new static)->getMorphClass())
329
            ->whereIn('tagging_tag_groups.name', $groups)
330
            ->orderBy('tag_slug', 'ASC')
331
            ->get(['tag_slug as slug', 'tag_name as name', 'tagging_tags.count as count']);
332
    }
333
334
    /**
335
     * Should untag on delete
336
     */
337
    public static function untagOnDelete()
338
    {
339
        return isset(static::$untagOnDelete)
340
            ? static::$untagOnDelete
341
            : config('tagging.untag_on_delete');
342
    }
343
344
    /**
345
     * Delete tags that are not used anymore
346
     */
347
    public static function shouldDeleteUnused(): bool
348
    {
349
        return config('tagging.delete_unused_tags', false);
350
    }
351
352
    /**
353
     * Set tag names to be set on save
354
     *
355
     * @param  mixed  $value  Data for retag
356
     */
357
    public function setTagNamesAttribute($value)
358
    {
359
        $this->autoTagValue = $value;
360
        $this->autoTagSet = true;
361
    }
362
363
    /**
364
     * AutoTag post-save hook
365
     *
366
     * Tags model based on data stored in tmp property, or untags if manually
367
     * set to false value
368
     */
369
    public function autoTagPostSave()
370
    {
371
        if ($this->autoTagSet) {
372
            if ($this->autoTagValue) {
373
                $this->retag($this->autoTagValue);
374
            } else {
375
                $this->untag();
376
            }
377
        }
378
    }
379
380
    private function assembleTagsForScoping($query, $tagNames)
381
    {
382
        if (! is_array($tagNames)) {
383
            $tagNames = func_get_args();
384
            array_shift($tagNames);
385
        }
386
387
        $tagNames = TaggingUtility::makeTagArray($tagNames);
388
389
        $normalizer = [TaggingUtility::class, 'normalize'];
390
391
        $tagNames = array_map($normalizer, $tagNames);
392
        $className = $query->getModel()->getMorphClass();
393
394
        $model = TaggingUtility::taggedModelString();
395
396
        $tags = $model::query()
397
            ->whereIn('tag_slug', $tagNames)
398
            ->where('taggable_type', $className)
399
            ->get()
400
            ->pluck('taggable_id')
401
            ->unique();
402
403
        return $tags;
404
    }
405
}
406