Taggable::addTag()   A
last analyzed

Complexity

Conditions 3
Paths 3

Size

Total Lines 27
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

Changes 3
Bugs 1 Features 0
Metric Value
cc 3
eloc 14
c 3
b 1
f 0
nc 3
nop 1
dl 0
loc 27
rs 9.7998
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')
0 ignored issues
show
Bug introduced by Robert Conner
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

64
        return $this->/** @scrutinizer ignore-call */ morphMany(TaggingUtility::taggedModelString(), 'taggable')
Loading history...
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->removeSingleTag($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
190
            $model = TaggingUtility::taggedModelString();
191
192
            $tags = $model::query()
193
                ->where('tag_slug', TaggingUtility::normalize($tagSlug))
194
                ->where('taggable_type', $className)
195
                ->get()
196
                ->pluck('taggable_id');
197
198
            $primaryKey = $this->getKeyName();
0 ignored issues
show
Bug introduced by Robert Conner
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

198
            /** @scrutinizer ignore-call */ 
199
            $primaryKey = $this->getKeyName();
Loading history...
199
            $query->whereIn($this->getTable().'.'.$primaryKey, $tags);
0 ignored issues
show
Bug introduced by Robert Conner
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

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

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