Completed
Push — master ( f0d45c...2839e2 )
by Colin
05:27
created

Taggable::bootTaggable()   A

Complexity

Conditions 3
Paths 1

Size

Total Lines 8

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 3

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 3
nc 1
nop 0
crap 3
1
<?php namespace Cviebrock\EloquentTaggable;
2
3
use Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException;
4
use Cviebrock\EloquentTaggable\Services\TagService;
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Collection;
7
use Illuminate\Database\Eloquent\Relations\MorphToMany;
8
use Illuminate\Database\Query\JoinClause;
9
10
11
/**
12
 * Class Taggable
13
 *
14
 * @package Cviebrock\EloquentTaggable
15
 */
16
trait Taggable
17
{
18
19
    /**
20
     * Boot the trait.
21
     *
22 23
     * Listen for the deleting event of a model, then remove the relation between it and tags
23
     */
24 23
    protected static function bootTaggable()
25 23
    {
26
        static::deleting(function ($model) {
27
            if (!method_exists($model, 'runSoftDelete') || $model->isForceDeleting()) {
28
                $model->detag();
29
            }
30
        });
31
    }
32
33
    /**
34
     * Get a collection of all tags the model has.
35 23
     *
36
     * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
37 23
     */
38
    public function tags(): MorphToMany
39 23
    {
40 23
        $model = config('taggable.model');
41 23
        return $this->morphToMany($model, 'taggable', 'taggable_taggables', 'taggable_id', 'tag_id')
0 ignored issues
show
Bug introduced by
It seems like morphToMany() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
42
            ->withTimestamps();
43 23
    }
44
45
    /**
46
     * Attach one or multiple tags to the model.
47
     *
48
     * @param string|array $tags
49
     *
50
     * @return $this
51
     */
52
    public function tag($tags)
53 1
    {
54
        $tags = app(TagService::class)->buildTagArray($tags);
55 1
56
        foreach ($tags as $tagName) {
57 1
            $this->addOneTag($tagName);
58 1
            $this->load('tags');
0 ignored issues
show
Bug introduced by
It seems like load() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
59 1
        }
60
61 1
        return $this;
62
    }
63
64
    /**
65
     * Detach one or multiple tags from the model.
66
     *
67
     * @param string|array $tags
68
     *
69
     * @return $this
70
     */
71 1
    public function untag($tags)
72
    {
73 1
        $tags = app(TagService::class)->buildTagArray($tags);
74
75
        foreach ($tags as $tagName) {
76
            $this->removeOneTag($tagName);
77
        }
78
79
        return $this->load('tags');
0 ignored issues
show
Bug introduced by
It seems like load() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
80
    }
81 2
82
    /**
83 2
     * Remove all tags from the model and assign the given ones.
84
     *
85 2
     * @param string|array $tags
86
     *
87
     * @return $this
88
     */
89
    public function retag($tags)
90
    {
91
        return $this->detag()->tag($tags);
92
    }
93 23
94
    /**
95 23
     * Remove all tags from the model.
96
     *
97 23
     * @return $this
98 23
     */
99 23
    public function detag()
100 23
    {
101
        $this->tags()->sync([]);
102
103
        return $this->load('tags');
0 ignored issues
show
Bug introduced by
It seems like load() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
104
    }
105
106
    /**
107 1
     * Add one tag to the model.
108
     *
109 1
     * @param string $tagName
110
     */
111 1
    protected function addOneTag(string $tagName)
112 1
    {
113 1
        $tag = app(TagService::class)->findOrCreate($tagName);
114 1
        $tagKey = $tag->getKey();
115
116
        if (!$this->tags->contains($tagKey)) {
0 ignored issues
show
Bug introduced by
The property tags does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
117
            $this->tags()->attach($tagKey);
118
        }
119
    }
120
121
    /**
122 13
     * Remove one tag from the model
123
     *
124 2
     * @param string $tagName
125 13
     */
126
    protected function removeOneTag(string $tagName)
127
    {
128
        $tag = app(TagService::class)->find($tagName);
129
130
        if ($tag) {
131
            $this->tags()->detach($tag);
132
        }
133 2
    }
134
135 2
    /**
136
     * Get all the tags of the model as a delimited string.
137
     *
138
     * @return string
139
     */
140
    public function getTagListAttribute(): string
141
    {
142
        return app(TagService::class)->makeTagList($this);
143 11
    }
144
145 11
    /**
146
     * Get all normalized tags of a model as a delimited string.
147
     *
148
     * @return string
149
     */
150
    public function getTagListNormalizedAttribute(): string
151
    {
152
        return app(TagService::class)->makeTagList($this, 'normalized');
153 2
    }
154
155 2
    /**
156
     * Get all tags of a model as an array.
157
     *
158
     * @return array
159
     */
160
    public function getTagArrayAttribute(): array
161
    {
162
        return app(TagService::class)->makeTagArray($this);
163
    }
164
165
    /**
166 1
     * Get all normalized tags of a model as an array.
167
     *
168 1
     * @return array
169
     */
170
    public function getTagArrayNormalizedAttribute(): array
171 1
    {
172 1
        return app(TagService::class)->makeTagArray($this, 'normalized');
173
    }
174
175
    /**
176
     * Query scope for models that have all of the given tags.
177
     *
178
     * @param Builder $query
179
     * @param array|string $tags
180
     *
181
     * @return Builder
182
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
183 1
     * @throws \ErrorException
184
     */
185 1
    public function scopeWithAllTags(Builder $query, $tags): Builder
186
    {
187 1
        /** @var TagService $service */
188
        $service = app(TagService::class);
189
        $normalized = $service->buildTagArrayNormalized($tags);
190
191 1
        // If there are no tags specified, then there
192 1
        // can't be any results so short-circuit
193 1
        if (count($normalized) === 0) {
194
            if (config('taggable.throwEmptyExceptions')) {
195
                throw new NoTagsSpecifiedException('Empty tag data passed to withAllTags scope.');
196
            }
197
198
            return $query->where(\DB::raw(1), 0);
199
        }
200
201
        $tagKeys = $service->getTagModelKeys($normalized);
202
203 1
        // If some of the tags specified don't exist, then there can't
204
        // be any models with all the tags, so so short-circuit
205 1
        if (count($tagKeys) !== count($normalized)) {
206
            return $query->where(\DB::raw(1), 0);
207
        }
208
209
        $morphTagKeyName = $this->tags()->getQualifiedRelatedPivotKeyName();
210
211
        return $this->prepareTableJoin($query, 'inner')
212
            ->whereIn($morphTagKeyName, $tagKeys)
213 2
            ->havingRaw("COUNT({$morphTagKeyName}) = ?", [count($tagKeys)]);
214
    }
215
216 2
    /**
217
     * Query scope for models that have any of the given tags.
218 2
     *
219
     * @param Builder $query
220
     * @param array|string $tags
221
     *
222
     * @return Builder
223
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
224
     * @throws \ErrorException
225
     */
226 1
    public function scopeWithAnyTags(Builder $query, $tags): Builder
227
    {
228 1
        /** @var TagService $service */
229
        $service = app(TagService::class);
230
        $normalized = $service->buildTagArrayNormalized($tags);
231
232
        // If there are no tags specified, then there is
233
        // no filtering to be done so short-circuit
234
        if (count($normalized) === 0) {
235
            if (config('taggable.throwEmptyExceptions')) {
236
                throw new NoTagsSpecifiedException('Empty tag data passed to withAnyTags scope.');
237
            }
238
239
            return $query->where(\DB::raw(1), 0);
240
        }
241
242
        $tagKeys = $service->getTagModelKeys($normalized);
243
244
        $morphTagKeyName = $this->tags()->getQualifiedRelatedPivotKeyName();
245
246
        return $this->prepareTableJoin($query, 'inner')
247
            ->whereIn($morphTagKeyName, $tagKeys);
248
    }
249
250
    /**
251
     * Query scope for models that have any tag.
252
     *
253
     * @param Builder $query
254
     *
255
     * @return Builder
256
     */
257
    public function scopeIsTagged(Builder $query): Builder
258
    {
259
        return $this->prepareTableJoin($query, 'inner');
260
    }
261
262
    /**
263
     * Query scope for models that do not have all of the given tags.
264
     *
265
     * @param Builder $query
266
     * @param string|array $tags
267
     * @param bool $includeUntagged
268
     *
269
     * @return Builder
270
     * @throws \ErrorException
271
     */
272
    public function scopeWithoutAllTags(Builder $query, $tags, bool $includeUntagged = false): Builder
273
    {
274
        /** @var TagService $service */
275
        $service = app(TagService::class);
276
        $normalized = $service->buildTagArrayNormalized($tags);
277
        $tagKeys = $service->getTagModelKeys($normalized);
278
        $tagKeyList = implode(',', $tagKeys);
279
280
        $morphTagKeyName = $this->tags()->getQualifiedRelatedPivotKeyName();
281
282
        $query = $this->prepareTableJoin($query, 'left')
283
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) < ?",
284
                [count($tagKeys)]);
285
286
        if (!$includeUntagged) {
287
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
288
        }
289
290
        return $query;
291
    }
292
293
    /**
294
     * Query scope for models that do not have any of the given tags.
295
     *
296
     * @param Builder $query
297
     * @param string|array $tags
298
     * @param bool $includeUntagged
299
     *
300
     * @return Builder
301
     * @throws \ErrorException
302
     */
303
    public function scopeWithoutAnyTags(Builder $query, $tags, bool $includeUntagged = false): Builder
304
    {
305
        /** @var TagService $service */
306
        $service = app(TagService::class);
307
        $normalized = $service->buildTagArrayNormalized($tags);
308
        $tagKeys = $service->getTagModelKeys($normalized);
309
        $tagKeyList = implode(',', $tagKeys);
310
311
        $morphTagKeyName = $this->tags()->getQualifiedRelatedPivotKeyName();
312
313
        $query = $this->prepareTableJoin($query, 'left')
314
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) = 0");
315
316
        if (!$includeUntagged) {
317
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
318
        }
319
320
        return $query;
321
    }
322
323
    /**
324
     * Query scope for models that does not have have any tags.
325
     *
326
     * @param Builder $query
327
     *
328
     * @return Builder
329
     */
330
    public function scopeIsNotTagged(Builder $query): Builder
331
    {
332
        $morphForeignKeyName = $this->tags()->getQualifiedForeignPivotKeyName();
333
334
        return $this->prepareTableJoin($query, 'left')
335
            ->havingRaw("COUNT(DISTINCT {$morphForeignKeyName}) = 0");
336
    }
337
338
    /**
339
     * @param Builder $query
340
     * @param string $joinType
341
     *
342
     * @return Builder
343
     */
344
    private function prepareTableJoin(Builder $query, string $joinType): Builder
345
    {
346
        $modelKeyName = $this->getQualifiedKeyName();
0 ignored issues
show
Bug introduced by
It seems like getQualifiedKeyName() must be provided by classes using this trait. How about adding it as abstract method to this trait?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
347
        $morphTable = $this->tags()->getTable();
348
        $morphForeignKeyName = $this->tags()->getQualifiedForeignPivotKeyName();
349
        $morphTypeName = $morphTable . '.' . $this->tags()->getMorphType();
350
        $morphClass = $this->tags()->getMorphClass();
351
352
        $closure = function(JoinClause $join) use ($modelKeyName, $morphForeignKeyName, $morphTypeName, $morphClass) {
353
            $join->on($modelKeyName, $morphForeignKeyName)
354
                ->where($morphTypeName, $morphClass);
355
        };
356
357
        return $query
0 ignored issues
show
Bug introduced by
The method select() does not exist on Illuminate\Database\Eloquent\Builder. Did you maybe mean createSelectWithConstraint()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
358
            ->select($this->getTable() . '.*')
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?

This check looks for methods that are used by a trait but not required by it.

To illustrate, let’s look at the following code example

trait Idable {
    public function equalIds(Idable $other) {
        return $this->getId() === $other->getId();
    }
}

The trait Idable provides a method equalsId that in turn relies on the method getId(). If this method does not exist on a class mixing in this trait, the method will fail.

Adding the getId() as an abstract method to the trait will make sure it is available.

Loading history...
359
            ->join($morphTable, $closure, null, null, $joinType)
360
            ->groupBy($modelKeyName);
361
    }
362
363
    /**
364
     * Get a collection of all the tag models used for the called class.
365
     *
366
     * @return Collection
367
     */
368
    public static function allTagModels(): Collection
369
    {
370
        return app(TagService::class)->getAllTags(static::class);
371
    }
372
373
    /**
374
     * Get an array of all tags used for the called class.
375
     *
376
     * @return array
377
     */
378
    public static function allTags(): array
379
    {
380
        /** @var \Illuminate\Database\Eloquent\Collection $tags */
381
        $tags = static::allTagModels();
382
383
        return $tags->pluck('name')->sort()->all();
384
    }
385
386
    /**
387
     * Get all the tags used for the called class as a delimited string.
388
     *
389
     * @return string
390
     */
391
    public static function allTagsList(): string
392
    {
393
        return app(TagService::class)->joinList(static::allTags());
394
    }
395
396
    /**
397
     * Rename one the tags for the called class.
398
     *
399
     * @param string $oldTag
400
     * @param string $newTag
401
     *
402
     * @return int
403
     */
404
    public static function renameTag(string $oldTag, string $newTag): int
405
    {
406
        return app(TagService::class)->renameTags($oldTag, $newTag, static::class);
407
    }
408
409
    /**
410
     * Get the most popular tags for the called class.
411
     *
412
     * @param int $limit
413
     * @param int $minCount
414
     *
415
     * @return array
416
     */
417
    public static function popularTags(int $limit = null, int $minCount = 1): array
418
    {
419
        /** @var Collection $tags */
420
        $tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);
421
422
        return $tags->pluck('taggable_count', 'name')->all();
423
    }
424
425
    /**
426
     * Get the most popular tags for the called class.
427
     *
428
     * @param int $limit
429
     * @param int $minCount
430
     *
431
     * @return array
432
     */
433
    public static function popularTagsNormalized(int $limit = null, int $minCount = 1): array
434
    {
435
        /** @var Collection $tags */
436
        $tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);
437
438
        return $tags->pluck('taggable_count', 'normalized')->all();
439
    }
440
}
441