Passed
Push — laravel-5 ( 66f120...6eb64b )
by Robert
02:51
created

src/Taggable.php (2 issues)

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
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...
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
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...
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