Passed
Branch laravel-7 (24d9b5)
by Robert
05:35
created

Taggable::untag()   A

Complexity

Conditions 4
Paths 8

Size

Total Lines 14
Code Lines 7

Duplication

Lines 0
Ratio 0 %

Importance

Changes 4
Bugs 1 Features 0
Metric Value
cc 4
eloc 7
c 4
b 1
f 0
nc 8
nop 1
dl 0
loc 14
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
12
/**
13
 * @package Conner\Tagging
14
 * @method static Builder withAllTags(array $tags)
15
 * @method static Builder withAnyTag(array $tags)
16
 * @method static Builder 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
     * @access private
62
     */
63
    public function tagged()
64
    {
65
        return $this->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

65
        return $this->/** @scrutinizer ignore-call */ morphMany(TaggingUtility::taggedModelString(), 'taggable')
Loading history...
66
            ->with('tag');
67
    }
68
69
    /**
70
     * Return collection of tags related to the tagged model
71
     * TODO : I'm sure there is a faster way to build this, but
72
     * If anyone knows how to do that, me love you long time.
73
     *
74
     * @return \Illuminate\Database\Eloquent\Collection|Tagged[]
75
     */
76
    public function getTagsAttribute()
77
    {
78
        return $this->tagged->map(function(Tagged $item){
79
            return $item->tag;
80
        });
81
    }
82
83
    /**
84
     * Get the tag names via attribute, example $model->tag_names
85
     */
86
    public function getTagNamesAttribute(): array
87
    {
88
        return $this->tagNames();
89
    }
90
91
    /**
92
     * Perform the action of tagging the model with the given string
93
     *
94
     * @param string|array $tagNames
95
     */
96
    public function addTags($tagNames)
97
    {
98
        $tagNames = TaggingUtility::makeTagArray($tagNames);
99
100
        foreach($tagNames as $tagName) {
101
            $this->addSingleTag($tagName);
102
        }
103
    }
104
105
    /**
106
     * Perform the action of tagging the model with the given string
107
     *
108
     * @param string|array $tagNames
109
     */
110
    public function tag($tagNames)
111
    {
112
        return $this->addTags($tagNames);
0 ignored issues
show
Bug introduced by
Are you sure the usage of $this->addTags($tagNames) targeting Conner\Tagging\Taggable::addTags() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
113
    }
114
115
    /**
116
     * Return array of the tag names related to the current model
117
     *
118
     * @return array
119
     */
120
    public function tagNames(): array
121
    {
122
        return $this->tagged->map(function($item){
123
            return $item->tag_name;
124
        })->toArray();
125
    }
126
127
    /**
128
     * Return array of the tag slugs related to the current model
129
     *
130
     * @return array
131
     */
132
    public function tagSlugs(): array
133
    {
134
        return $this->tagged->map(function($item){
135
            return $item->tag_slug;
136
        })->toArray();
137
    }
138
139
    /**
140
     * Remove the tag from this model
141
     *
142
     * @param string|array|null $tagNames (or null to remove all tags)
143
     */
144
    public function untag($tagNames = null)
145
    {
146
        if(is_null($tagNames)) {
147
            $tagNames = $this->tagNames();
148
        }
149
150
        $tagNames = TaggingUtility::makeTagArray($tagNames);
151
152
        foreach($tagNames as $tagName) {
153
            $this->removeSingleTag($tagName);
154
        }
155
156
        if(static::shouldDeleteUnused()) {
157
            TaggingUtility::deleteUnusedTags();
158
        }
159
    }
160
161
    /**
162
     * Replace the tags from this model
163
     *
164
     * @param string|array $tagNames
165
     */
166
    public function retag($tagNames)
167
    {
168
        $tagNames = TaggingUtility::makeTagArray($tagNames);
169
        $currentTagNames = $this->tagNames();
170
171
        $deletions = array_diff($currentTagNames, $tagNames);
172
        $additions = array_diff($tagNames, $currentTagNames);
173
174
        $this->untag($deletions);
175
176
        foreach($additions as $tagName) {
177
            $this->addSingleTag($tagName);
178
        }
179
    }
180
181
    /**
182
     * Filter model to subset with the given tags
183
     *
184
     * @param Builder $query
185
     * @param array|string $tagNames
186
     * @return Builder
187
     * @access private
188
     */
189
    public function scopeWithAllTags(Builder $query, $tagNames): Builder
190
    {
191
        if(!is_array($tagNames)) {
192
            $tagNames = func_get_args();
193
            array_shift($tagNames);
194
        }
195
196
        $tagNames = TaggingUtility::makeTagArray($tagNames);
197
198
        $className = $query->getModel()->getMorphClass();
199
200
        foreach($tagNames as $tagSlug) {
201
202
            $model = TaggingUtility::taggedModelString();
203
204
            $tags = $model::query()
205
                ->where('tag_slug', TaggingUtility::normalize($tagSlug))
206
                ->where('taggable_type', $className)
207
                ->get()
208
                ->pluck('taggable_id');
209
210
            $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

210
            /** @scrutinizer ignore-call */ 
211
            $primaryKey = $this->getKeyName();
Loading history...
211
            $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

211
            $query->whereIn($this->/** @scrutinizer ignore-call */ getTable().'.'.$primaryKey, $tags);
Loading history...
212
        }
213
214
        return $query;
215
    }
216
217
    /**
218
     * Filter model to subset with the given tags
219
     *
220
     * @param Builder $query
221
     * @param array|string $tagNames
222
     * @return Builder
223
     * @access private
224
     */
225
    public function scopeWithAnyTag(Builder $query, $tagNames): Builder
226
    {
227
        $tags = $this->assembleTagsForScoping($query, $tagNames);
228
229
        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...
230
    }
231
232
    /**
233
     * Filter model to subset without the given tags
234
     *
235
     * @param Builder $query
236
     * @param array|string $tagNames
237
     * @return Builder
238
     * @access private
239
     */
240
    public function scopeWithoutTags(Builder $query, $tagNames): Builder
241
    {
242
        $tags = $this->assembleTagsForScoping($query, $tagNames);
243
244
        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...
245
    }
246
247
    /**
248
     * Adds a single tag
249
     *
250
     * @param string $tagName
251
     */
252
    private function addSingleTag($tagName)
253
    {
254
        $tagName = trim($tagName);
255
256
        if(strlen($tagName) == 0) {
257
            return;
258
        }
259
260
        $tagSlug = TaggingUtility::normalize($tagName);
261
262
        $previousCount = $this->tagged()->where('tag_slug', '=', $tagSlug)->take(1)->count();
263
        if($previousCount >= 1) { return; }
264
265
        $model = TaggingUtility::taggedModelString();
266
267
        $tagged = new $model([
268
            'tag_name' => TaggingUtility::displayize($tagName),
269
            'tag_slug' => $tagSlug,
270
        ]);
271
272
        $this->tagged()->save($tagged);
273
274
        TaggingUtility::incrementCount($tagName, $tagSlug, 1);
275
276
        unset($this->relations['tagged']);
277
278
        event(new TagAdded($this, $tagSlug, $tagged));
279
    }
280
281
    /**
282
     * Removes a single tag
283
     *
284
     * @param $tagName string
285
     */
286
    private function removeSingleTag($tagName)
287
    {
288
        $tagName = trim($tagName);
289
290
        $tagSlug = TaggingUtility::normalize($tagName);
291
292
        if($count = $this->tagged()->where('tag_slug', '=', $tagSlug)->delete()) {
293
            TaggingUtility::decrementCount($tagName, $tagSlug, $count);
294
        }
295
296
        unset($this->relations['tagged']); // clear the "cache"
297
298
        event(new TagRemoved($this, $tagSlug));
299
    }
300
301
    /**
302
     * Return an array of all of the tags that are in use by this model
303
     *
304
     * @return Collection|Tagged[]
305
     */
306
    public static function existingTags(): Collection
307
    {
308
        $model = TaggingUtility::taggedModelString();
309
310
        return $model::query()
311
            ->distinct()
312
            ->join('tagging_tags', 'tag_slug', '=', 'tagging_tags.slug')
313
            ->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

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