Completed
Push — master ( 73d7be...2c4dc2 )
by Colin
01:20
created

Taggable::detag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 6
c 0
b 0
f 0
ccs 2
cts 2
cp 1
rs 9.4285
cc 1
eloc 3
nc 1
nop 0
crap 1
1
<?php namespace Cviebrock\EloquentTaggable;
2
3
use Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException;
4
use Cviebrock\EloquentTaggable\Models\Tag;
5
use Cviebrock\EloquentTaggable\Services\TagService;
6
use Illuminate\Database\Eloquent\Builder;
7
use Illuminate\Database\Eloquent\Collection;
8
9
10
/**
11
 * Class Taggable
12
 *
13
 * @package Cviebrock\EloquentTaggable
14
 */
15
trait Taggable
16
{
17
18
    /**
19
     * Get a collection of all tags the model has.
20
     *
21
     * @return \Illuminate\Database\Eloquent\Relations\MorphToMany
22 23
     */
23
    public function tags()
24 23
    {
25 23
        return $this->morphToMany(Tag::class, '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...
26
            ->withTimestamps();
27
    }
28
29
    /**
30
     * Attach one or multiple tags to the model.
31
     *
32
     * @param string|array $tags
33
     *
34
     * @return $this
35 23
     */
36
    public function tag($tags)
37 23
    {
38
        $tags = app(TagService::class)->buildTagArray($tags);
39 23
40 23
        foreach ($tags as $tagName) {
41 23
            $this->addOneTag($tagName);
42
        }
43 23
44
        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...
45
    }
46
47
    /**
48
     * Detach one or multiple tags from the model.
49
     *
50
     * @param string|array $tags
51
     *
52
     * @return $this
53 1
     */
54
    public function untag($tags)
55 1
    {
56
        $tags = app(TagService::class)->buildTagArray($tags);
57 1
58 1
        foreach ($tags as $tagName) {
59 1
            $this->removeOneTag($tagName);
60
        }
61 1
62
        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...
63
    }
64
65
    /**
66
     * Remove all tags from the model and assign the given ones.
67
     *
68
     * @param string|array $tags
69
     *
70
     * @return $this
71 1
     */
72
    public function retag($tags)
73 1
    {
74
        return $this->detag()->tag($tags);
75
    }
76
77
    /**
78
     * Remove all tags from the model.
79
     *
80
     * @return $this
81 2
     */
82
    public function detag()
83 2
    {
84
        $this->tags()->sync([]);
85 2
86
        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...
87
    }
88
89
    /**
90
     * Add one tag to the model.
91
     *
92
     * @param string $tagName
93 23
     */
94
    protected function addOneTag($tagName)
95 23
    {
96
        $tag = app(TagService::class)->findOrCreate($tagName);
97 23
98 23
        if (!$this->tags->contains($tag->getKey())) {
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...
99 23
            $this->tags()->attach($tag->getKey());
100 23
        }
101
    }
102
103
    /**
104
     * Remove one tag from the model
105
     *
106
     * @param string $tagName
107 1
     */
108
    protected function removeOneTag($tagName)
109 1
    {
110
        $tag = app(TagService::class)->find($tagName);
111 1
112 1
        if ($tag) {
113 1
            $this->tags()->detach($tag);
114 1
        }
115
    }
116
117
    /**
118
     * Get all the tags of the model as a delimited string.
119
     *
120
     * @return string
121
     */
122 13
    public function getTagListAttribute()
123
    {
124 2
        return app(TagService::class)->makeTagList($this);
125 13
    }
126
127
    /**
128
     * Get all normalized tags of a model as a delimited string.
129
     *
130
     * @return string
131
     */
132
    public function getTagListNormalizedAttribute()
133 2
    {
134
        return app(TagService::class)->makeTagList($this, 'normalized');
135 2
    }
136
137
    /**
138
     * Get all tags of a model as an array.
139
     *
140
     * @return array
141
     */
142
    public function getTagArrayAttribute()
143 11
    {
144
        return app(TagService::class)->makeTagArray($this);
145 11
    }
146
147
    /**
148
     * Get all normalized tags of a model as an array.
149
     *
150
     * @return array
151
     */
152
    public function getTagArrayNormalizedAttribute()
153 2
    {
154
        return app(TagService::class)->makeTagArray($this, 'normalized');
155 2
    }
156
157
    /**
158
     * Query scope for models that have all of the given tags.
159
     *
160
     * @param \Illuminate\Database\Eloquent\Builder $query
161
     * @param array|string $tags
162
     *
163
     * @return \Illuminate\Database\Query\Builder
164
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
165
     * @throws \ErrorException
166 1
     */
167
    public function scopeWithAllTags(Builder $query, $tags)
168 1
    {
169
        /** @var TagService $service */
170
        $service = app(TagService::class);
171 1
        $normalized = $service->buildTagArrayNormalized($tags);
172 1
173
        // If there are no tags specified, then there
174
        // can't be any results so short-circuit
175
        if (count($normalized) === 0) {
176
            if (config('taggable.throwEmptyExceptions')) {
177
                throw new NoTagsSpecifiedException('Empty tag data passed to withAllTags scope.');
178
            }
179
180
            return $query->where(\DB::raw(1), 0);
181
        }
182
183 1
        $tagKeys = $service->getTagModelKeys($normalized);
184
185 1
        // If some of the tags specified don't exist, then there can't
186
        // be any models with all the tags, so so short-circuit
187 1
        if (count($tagKeys) !== count($normalized)) {
188
            return $query->where(\DB::raw(1), 0);
189
        }
190
191 1
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
192 1
193 1
        return $this->prepareTableJoin($query, 'inner')
194
            ->whereIn($morphTagKeyName, $tagKeys)
195
            ->havingRaw("COUNT({$morphTagKeyName}) = ?", [count($tagKeys)]);
196
    }
197
198
    /**
199
     * Query scope for models that have any of the given tags.
200
     *
201
     * @param \Illuminate\Database\Eloquent\Builder $query
202
     * @param array|string $tags
203 1
     *
204
     * @return mixed
205 1
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
206
     * @throws \ErrorException
207
     */
208
    public function scopeWithAnyTags(Builder $query, $tags)
209
    {
210
        /** @var TagService $service */
211
        $service = app(TagService::class);
212
        $normalized = $service->buildTagArrayNormalized($tags);
213 2
214
        // If there are no tags specified, then there is
215
        // no filtering to be done so short-circuit
216 2
        if (count($normalized) === 0) {
217
            if (config('taggable.throwEmptyExceptions')) {
218 2
                throw new NoTagsSpecifiedException('Empty tag data passed to withAnyTags scope.');
219
            }
220
221
            return $query->where(\DB::raw(1), 0);
222
        }
223
224
        $tagKeys = $service->getTagModelKeys($normalized);
225
226 1
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
227
228 1
        return $this->prepareTableJoin($query, 'inner')
229
            ->whereIn($morphTagKeyName, $tagKeys);
230
    }
231
232
    /**
233
     * Query scope for models that have any tag.
234
     *
235
     * @param \Illuminate\Database\Eloquent\Builder $query
236
     *
237
     * @return \Illuminate\Database\Eloquent\Builder
238
     */
239
    public function scopeIsTagged(Builder $query)
240
    {
241
        return $this->prepareTableJoin($query, 'inner');
242
    }
243
244
    /**
245
     * Query scope for models that do not have all of the given tags.
246
     *
247
     * @param \Illuminate\Database\Eloquent\Builder $query
248
     * @param string|array $tags
249
     * @param bool $includeUntagged
250
     *
251
     * @return \Illuminate\Database\Eloquent\Builder
252
     * @throws \ErrorException
253
     */
254
    public function scopeWithoutAllTags(Builder $query, $tags, $includeUntagged = false)
255
    {
256
        /** @var TagService $service */
257
        $service = app(TagService::class);
258
        $normalized = $service->buildTagArrayNormalized($tags);
259
        $tagKeys = $service->getTagModelKeys($normalized);
260
        $tagKeyList = implode(',', $tagKeys);
261
262
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
263
264
        $query = $this->prepareTableJoin($query, 'left')
265
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) < ?",
266
                [count($tagKeys)]);
267
268
        if (!$includeUntagged) {
269
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
270
        }
271
272
        return $query;
273
    }
274
275
    /**
276
     * Query scope for models that do not have any of the given tags.
277
     *
278
     * @param \Illuminate\Database\Eloquent\Builder $query
279
     * @param string|array $tags
280
     * @param bool $includeUntagged
281
     *
282
     * @return \Illuminate\Database\Eloquent\Builder
283
     * @throws \ErrorException
284
     */
285
    public function scopeWithoutAnyTags(Builder $query, $tags, $includeUntagged = false)
286
    {
287
        /** @var TagService $service */
288
        $service = app(TagService::class);
289
        $normalized = $service->buildTagArrayNormalized($tags);
290
        $tagKeys = $service->getTagModelKeys($normalized);
291
        $tagKeyList = implode(',', $tagKeys);
292
293
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
294
295
        $query = $this->prepareTableJoin($query, 'left')
296
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) = 0");
297
298
        if (!$includeUntagged) {
299
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
300
        }
301
302
        return $query;
303
    }
304
305
    /**
306
     * Query scope for models that does not have have any tags.
307
     *
308
     * @param \Illuminate\Database\Eloquent\Builder $query
309
     *
310
     * @return \Illuminate\Database\Eloquent\Builder
311
     */
312
    public function scopeIsNotTagged(Builder $query)
313
    {
314
        $morphForeignKeyName = $this->tags()->getQualifiedForeignKeyName();
315
316
        return $this->prepareTableJoin($query, 'left')
317
            ->havingRaw("COUNT(DISTINCT {$morphForeignKeyName}) = 0");
318
    }
319
320
    /**
321
     * @param \Illuminate\Database\Eloquent\Builder $query
322
     * @param string $joinType
323
     *
324
     * @return mixed
325
     */
326
    private function prepareTableJoin(Builder $query, $joinType)
327
    {
328
        $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...
329
        $morphTable = $this->tags()->getTable();
330
        $morphForeignKeyName = $this->tags()->getQualifiedForeignKeyName();
331
        $morphTypeName = $morphTable . '.' . $this->tags()->getMorphType();
332
333
        $closure = function($join) use ($modelKeyName, $morphForeignKeyName, $morphTypeName) {
334
            $join->on($modelKeyName, $morphForeignKeyName)
335
                ->on($morphTypeName, static::class);
336
337
            return $join;
338
        };
339
340
        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...
341
            ->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...
342
            ->join($morphTable, $closure, null, null, $joinType)
343
            ->groupBy($modelKeyName);
344
    }
345
346
    /**
347
     * Get a collection of all the tag models used for the called class.
348
     *
349
     * @return Collection
350
     */
351
    public static function allTagModels()
352
    {
353
        return app(TagService::class)->getAllTags(static::class);
354
    }
355
356
    /**
357
     * Get an array of all tags used for the called class.
358
     *
359
     * @return array
360
     */
361
    public static function allTags()
362
    {
363
        /** @var \Illuminate\Database\Eloquent\Collection $tags */
364
        $tags = static::allTagModels();
365
366
        return $tags->pluck('name')->sort()->all();
367
    }
368
369
    /**
370
     * Get all the tags used for the called class as a delimited string.
371
     *
372
     * @return string
373
     */
374
    public static function allTagsList()
375
    {
376
        return app(TagService::class)->joinList(static::allTags());
377
    }
378
379
    /**
380
     * Rename one the tags for the called class.
381
     *
382
     * @param string $oldTag
383
     * @param string $newTag
384
     *
385
     * @return int
386
     */
387
    public static function renameTag($oldTag, $newTag)
388
    {
389
        return app(TagService::class)->renameTags($oldTag, $newTag, static::class);
390
    }
391
392
    /**
393
     * Get the most popular tags for the called class.
394
     *
395
     * @param int $limit
396
     *
397
     * @return array
398
     */
399
    public static function popularTags($limit = 10)
400
    {
401
        /** @var Collection $tags */
402
        $tags = app(TagService::class)->getPopularTags($limit, static::class);
403
404
        return $tags->pluck('taggable_count', 'name')->all();
405
    }
406
407
    /**
408
     * Get the most popular tags for the called class.
409
     *
410
     * @param int $limit
411
     *
412
     * @return array
413
     */
414
    public static function popularTagsNormalized($limit = 10)
415
    {
416
        /** @var Collection $tags */
417
        $tags = app(TagService::class)->getPopularTags($limit, static::class);
418
419
        return $tags->pluck('taggable_count', 'normalized')->all();
420
    }
421
}
422