Completed
Push — master ( d957bb...947bd5 )
by Colin
22s
created

getQualifiedRelatedPivotKeyNameWithAlias()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 6
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 1
crap 2
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
        $alias = strtolower(__FUNCTION__);
210
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
211
212
        return $this->prepareTableJoin($query, 'inner', $alias)
213 2
            ->whereIn($morphTagKeyName, $tagKeys)
214
            ->havingRaw("COUNT({$morphTagKeyName}) = ?", [count($tagKeys)]);
215
    }
216 2
217
    /**
218 2
     * Query scope for models that have any of the given tags.
219
     *
220
     * @param Builder $query
221
     * @param array|string $tags
222
     *
223
     * @return Builder
224
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
225
     * @throws \ErrorException
226 1
     */
227
    public function scopeWithAnyTags(Builder $query, $tags): Builder
228 1
    {
229
        /** @var TagService $service */
230
        $service = app(TagService::class);
231
        $normalized = $service->buildTagArrayNormalized($tags);
232
233
        // If there are no tags specified, then there is
234
        // no filtering to be done so short-circuit
235
        if (count($normalized) === 0) {
236
            if (config('taggable.throwEmptyExceptions')) {
237
                throw new NoTagsSpecifiedException('Empty tag data passed to withAnyTags scope.');
238
            }
239
240
            return $query->where(\DB::raw(1), 0);
241
        }
242
243
        $tagKeys = $service->getTagModelKeys($normalized);
244
245
        $alias = strtolower(__FUNCTION__);
246
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
247
248
        return $this->prepareTableJoin($query, 'inner', $alias)
249
            ->whereIn($morphTagKeyName, $tagKeys);
250
    }
251
252
    /**
253
     * Query scope for models that have any tag.
254
     *
255
     * @param Builder $query
256
     *
257
     * @return Builder
258
     */
259
    public function scopeIsTagged(Builder $query): Builder
260
    {
261
        $alias = strtolower(__FUNCTION__);
262
        return $this->prepareTableJoin($query, 'inner', $alias);
263
    }
264
265
    /**
266
     * Query scope for models that do not have all of the given tags.
267
     *
268
     * @param Builder $query
269
     * @param string|array $tags
270
     * @param bool $includeUntagged
271
     *
272
     * @return Builder
273
     * @throws \ErrorException
274
     */
275
    public function scopeWithoutAllTags(Builder $query, $tags, bool $includeUntagged = false): Builder
276
    {
277
        /** @var TagService $service */
278
        $service = app(TagService::class);
279
        $normalized = $service->buildTagArrayNormalized($tags);
280
        $tagKeys = $service->getTagModelKeys($normalized);
281
        $tagKeyList = implode(',', $tagKeys);
282
283
        $alias = strtolower(__FUNCTION__);
284
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
285
286
        $query = $this->prepareTableJoin($query, 'left', $alias)
287
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) < ?",
288
                [count($tagKeys)]);
289
290
        if (!$includeUntagged) {
291
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
292
        }
293
294
        return $query;
295
    }
296
297
    /**
298
     * Query scope for models that do not have any of the given tags.
299
     *
300
     * @param Builder $query
301
     * @param string|array $tags
302
     * @param bool $includeUntagged
303
     *
304
     * @return Builder
305
     * @throws \ErrorException
306
     */
307
    public function scopeWithoutAnyTags(Builder $query, $tags, bool $includeUntagged = false): Builder
308
    {
309
        /** @var TagService $service */
310
        $service = app(TagService::class);
311
        $normalized = $service->buildTagArrayNormalized($tags);
312
        $tagKeys = $service->getTagModelKeys($normalized);
313
        $tagKeyList = implode(',', $tagKeys);
314
315
        $alias = strtolower(__FUNCTION__);
316
        $morphTagKeyName = $this->getQualifiedRelatedPivotKeyNameWithAlias($alias);
317
318
        $query = $this->prepareTableJoin($query, 'left', $alias)
319
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) = 0");
320
321
        if (!$includeUntagged) {
322
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
323
        }
324
325
        return $query;
326
    }
327
328
    /**
329
     * Query scope for models that does not have have any tags.
330
     *
331
     * @param Builder $query
332
     *
333
     * @return Builder
334
     */
335
    public function scopeIsNotTagged(Builder $query): Builder
336
    {
337
        $alias = strtolower(__FUNCTION__);
338
        $morphForeignKeyName = $this->getQualifiedForeignPivotKeyNameWithAlias($alias);
339
340
        return $this->prepareTableJoin($query, 'left', $alias)
341
            ->havingRaw("COUNT(DISTINCT {$morphForeignKeyName}) = 0");
342
    }
343
344
    /**
345
     * @param Builder $query
346
     * @param string $joinType
347
     *
348
     * @return Builder
349
     */
350
    private function prepareTableJoin(Builder $query, string $joinType, string $alias): Builder
351
    {
352
        $morphTable = $this->tags()->getTable();
353
        $morphTableAlias = $morphTable.'_'.$alias;
354
355
        $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...
356
        $morphForeignKeyName = $this->getQualifiedForeignPivotKeyNameWithAlias($alias);
357
358
        $morphTypeName = $morphTableAlias.'.'. $this->tags()->getMorphType();
359
        $morphClass = $this->tags()->getMorphClass();
360
361
        $closure = function(JoinClause $join) use ($modelKeyName, $morphForeignKeyName, $morphTypeName, $morphClass) {
362
            $join->on($modelKeyName, $morphForeignKeyName)
363
                ->where($morphTypeName, $morphClass);
364
        };
365
366
        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...
367
            ->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...
368
            ->join($morphTable.' as '.$morphTableAlias, $closure, null, null, $joinType)
369
            ->groupBy($modelKeyName);
370
    }
371
372
    /**
373
     * Get a collection of all the tag models used for the called class.
374
     *
375
     * @return Collection
376
     */
377
    public static function allTagModels(): Collection
378
    {
379
        return app(TagService::class)->getAllTags(static::class);
380
    }
381
382
    /**
383
     * Get an array of all tags used for the called class.
384
     *
385
     * @return array
386
     */
387
    public static function allTags(): array
388
    {
389
        /** @var \Illuminate\Database\Eloquent\Collection $tags */
390
        $tags = static::allTagModels();
391
392
        return $tags->pluck('name')->sort()->all();
393
    }
394
395
    /**
396
     * Get all the tags used for the called class as a delimited string.
397
     *
398
     * @return string
399
     */
400
    public static function allTagsList(): string
401
    {
402
        return app(TagService::class)->joinList(static::allTags());
403
    }
404
405
    /**
406
     * Rename one the tags for the called class.
407
     *
408
     * @param string $oldTag
409
     * @param string $newTag
410
     *
411
     * @return int
412
     */
413
    public static function renameTag(string $oldTag, string $newTag): int
414
    {
415
        return app(TagService::class)->renameTags($oldTag, $newTag, static::class);
416
    }
417
418
    /**
419
     * Get the most popular tags for the called class.
420
     *
421
     * @param int $limit
422
     * @param int $minCount
423
     *
424
     * @return array
425
     */
426
    public static function popularTags(int $limit = null, int $minCount = 1): array
427
    {
428
        /** @var Collection $tags */
429
        $tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);
430
431
        return $tags->pluck('taggable_count', 'name')->all();
432
    }
433
434
    /**
435
     * Get the most popular tags for the called class.
436
     *
437
     * @param int $limit
438
     * @param int $minCount
439
     *
440
     * @return array
441
     */
442
    public static function popularTagsNormalized(int $limit = null, int $minCount = 1): array
443
    {
444
        /** @var Collection $tags */
445
        $tags = app(TagService::class)->getPopularTags($limit, static::class, $minCount);
446
447
        return $tags->pluck('taggable_count', 'normalized')->all();
448
    }
449
450
    /**
451
     * Returns the Related Pivot Key Name with the table alias.
452
     *
453
     * @param $alias
454
     *
455
     * @return string
456
     */
457
    private function getQualifiedRelatedPivotKeyNameWithAlias($alias)
458
    {
459
        $field = $this->tags()->getRelatedPivotKeyName();
460
        $table = $this->tags()->getTable().'_'.$alias;
461
        return $table.'.'.$field;
462
    }
463
464
    /**
465
     * Returns the Foreign Pivot Key Name with the table alias.
466
     *
467
     * @param $alias
468
     *
469
     * @return string
470
     */
471
    private function getQualifiedForeignPivotKeyNameWithAlias($alias)
472
    {
473
        $field = $this->tags()->getForeignPivotKeyName();
474
        $table = $this->tags()->getTable().'_'.$alias;
475
        return $table.'.'.$field;
476
    }
477
478
}
479