Completed
Push — laravel-5 ( 5c1154...174d48 )
by Robert
03:19
created

Taggable::scopeWithAnyTag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 6
Bugs 1 Features 0
Metric Value
cc 1
eloc 2
c 6
b 1
f 0
nc 1
nop 2
dl 0
loc 5
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 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
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
        dump($tagNames);
142
143
        foreach($tagNames as $tagName) {
144
            $this->removeSingleTag($tagName);
145
        }
146
147
        if(static::shouldDeleteUnused()) {
148
            TaggingUtility::deleteUnusedTags();
149
        }
150
    }
151
152
    /**
153
     * Replace the tags from this model
154
     *
155
     * @param string|array $tagNames
156
     */
157
    public function retag($tagNames)
158
    {
159
        $tagNames = TaggingUtility::makeTagArray($tagNames);
160
        $currentTagNames = $this->tagNames();
161
162
        $deletions = array_diff($currentTagNames, $tagNames);
163
        $additions = array_diff($tagNames, $currentTagNames);
164
165
        $this->untag($deletions);
166
167
        foreach($additions as $tagName) {
168
            $this->addTag($tagName);
169
        }
170
    }
171
172
    /**
173
     * Filter model to subset with the given tags
174
     *
175
     * @param Builder $query
176
     * @param array|string $tagNames
177
     * @return Builder
178
     */
179
    public function scopeWithAllTags(Builder $query, $tagNames): Builder
180
    {
181
        if(!is_array($tagNames)) {
182
            $tagNames = func_get_args();
183
            array_shift($tagNames);
184
        }
185
186
        $tagNames = TaggingUtility::makeTagArray($tagNames);
187
188
        $className = $query->getModel()->getMorphClass();
189
190
        foreach($tagNames as $tagSlug) {
191
            $tags = Tagged::query()
192
                ->where('tag_slug', TaggingUtility::normalize($tagSlug))
193
                ->where('taggable_type', $className)
194
                ->get()
195
                ->pluck('taggable_id');
196
197
            $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

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

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

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