Completed
Push — master ( 091b4d...427b2a )
by Colin
09:32 queued 08:16
created

Taggable::retag()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
c 0
b 0
f 0
ccs 1
cts 1
cp 1
rs 10
cc 1
eloc 2
nc 1
nop 1
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
            return $query->where(\DB::raw(1), 0);
180
        }
181
182
        $tagKeys = $service->getTagModelKeys($normalized);
183 1
184
        // If some of the tags specified don't exist, then there can't
185 1
        // be any models with all the tags, so so short-circuit
186
        if (count($tagKeys) !== count($normalized)) {
187 1
            return $query->where(\DB::raw(1), 0);
188
        }
189
190
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
191 1
192 1
        return $this->prepareTableJoin($query, 'inner')
193 1
            ->whereIn($morphTagKeyName, $tagKeys)
194
            ->havingRaw("COUNT({$morphTagKeyName}) = ?", [count($tagKeys)]);
195
    }
196
197
    /**
198
     * Query scope for models that have any of the given tags.
199
     *
200
     * @param \Illuminate\Database\Eloquent\Builder $query
201
     * @param array|string $tags
202
     *
203 1
     * @return mixed
204
     * @throws \Cviebrock\EloquentTaggable\Exceptions\NoTagsSpecifiedException
205 1
     * @throws \ErrorException
206
     */
207
    public function scopeWithAnyTags(Builder $query, $tags)
208
    {
209
        /** @var TagService $service */
210
        $service = app(TagService::class);
211
        $normalized = $service->buildTagArrayNormalized($tags);
212
213 2
        // If there are no tags specified, then there is
214
        // no filtering to be done so short-circuit
215
        if (count($normalized) === 0) {
216 2
            if (config('taggable.throwEmptyExceptions')) {
217
                throw new NoTagsSpecifiedException('Empty tag data passed to withAnyTags scope.');
218 2
            }
219
            return $query->where(\DB::raw(1), 0);
220
        }
221
222
        $tagKeys = $service->getTagModelKeys($normalized);
223
224
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
225
226 1
        return $this->prepareTableJoin($query, 'inner')
227
            ->whereIn($morphTagKeyName, $tagKeys);
228 1
    }
229
230
    /**
231
     * Query scope for models that have any tag.
232
     *
233
     * @param \Illuminate\Database\Eloquent\Builder $query
234
     *
235
     * @return \Illuminate\Database\Eloquent\Builder
236
     */
237
    public function scopeIsTagged(Builder $query)
238
    {
239
        return $this->prepareTableJoin($query, 'inner');
240
    }
241
242
    /**
243
     * Query scope for models that do not have all of the given tags.
244
     *
245
     * @param \Illuminate\Database\Eloquent\Builder $query
246
     * @param string|array $tags
247
     * @param bool $includeUntagged
248
     *
249
     * @return \Illuminate\Database\Eloquent\Builder
250
     * @throws \ErrorException
251
     */
252
    public function scopeWithoutAllTags(Builder $query, $tags, $includeUntagged = false)
253
    {
254
        /** @var TagService $service */
255
        $service = app(TagService::class);
256
        $normalized = $service->buildTagArrayNormalized($tags);
257
        $tagKeys = $service->getTagModelKeys($normalized);
258
        $tagKeyList = implode(',', $tagKeys);
259
260
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
261
262
        $query = $this->prepareTableJoin($query, 'left')
263
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) < ?",
264
                [count($tagKeys)]);
265
266
        if (!$includeUntagged) {
267
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
268
        }
269
270
        return $query;
271
    }
272
273
    /**
274
     * Query scope for models that do not have any of the given tags.
275
     *
276
     * @param \Illuminate\Database\Eloquent\Builder $query
277
     * @param string|array $tags
278
     * @param bool $includeUntagged
279
     *
280
     * @return \Illuminate\Database\Eloquent\Builder
281
     * @throws \ErrorException
282
     */
283
    public function scopeWithoutAnyTags(Builder $query, $tags, $includeUntagged = false)
284
    {
285
        /** @var TagService $service */
286
        $service = app(TagService::class);
287
        $normalized = $service->buildTagArrayNormalized($tags);
288
        $tagKeys = $service->getTagModelKeys($normalized);
289
        $tagKeyList = implode(',', $tagKeys);
290
291
        $morphTagKeyName = $this->tags()->getQualifiedRelatedKeyName();
292
293
        $query = $this->prepareTableJoin($query, 'left')
294
            ->havingRaw("COUNT(DISTINCT CASE WHEN ({$morphTagKeyName} IN ({$tagKeyList})) THEN {$morphTagKeyName} ELSE NULL END) = 0");
295
296
        if (!$includeUntagged) {
297
            $query->havingRaw("COUNT(DISTINCT {$morphTagKeyName}) > 0");
298
        }
299
300
        return $query;
301
    }
302
303
    /**
304
     * Query scope for models that does not have have any tags.
305
     *
306
     * @param \Illuminate\Database\Eloquent\Builder $query
307
     *
308
     * @return \Illuminate\Database\Eloquent\Builder
309
     */
310
    public function scopeIsNotTagged(Builder $query)
311
    {
312
        $morphForeignKeyName = $this->tags()->getQualifiedForeignKeyName();
313
314
        return $this->prepareTableJoin($query, 'left')
315
            ->havingRaw("COUNT(DISTINCT {$morphForeignKeyName}) = 0");
316
    }
317
318
    /**
319
     * @param \Illuminate\Database\Eloquent\Builder $query
320
     * @param string $joinType
321
     *
322
     * @return mixed
323
     */
324
    private function prepareTableJoin(Builder $query, $joinType)
325
    {
326
        $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...
327
        $morphTable = $this->tags()->getTable();
328
        $morphForeignKeyName = $this->tags()->getQualifiedForeignKeyName();
329
        $morphTypeName = $morphTable . '.' . $this->tags()->getMorphType();
330
331
        $closure = function($join) use ($modelKeyName, $morphForeignKeyName, $morphTypeName) {
332
            $join->on($modelKeyName, $morphForeignKeyName)
333
                ->on($morphTypeName, static::class);
334
            return $join;
335
        };
336
337
        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...
338
            ->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...
339
            ->join($morphTable, $closure, null, null, $joinType)
340
            ->groupBy($modelKeyName);
341
    }
342
343
    /**
344
     * Get a collection of all the tag models used for the called class.
345
     *
346
     * @return Collection
347
     */
348
    public static function allTagModels()
349
    {
350
        return app(TagService::class)->getAllTags(get_called_class());
351
    }
352
353
    /**
354
     * Get an array of all tags used for the called class.
355
     *
356
     * @return array
357
     */
358
    public static function allTags()
359
    {
360
        /** @var \Illuminate\Database\Eloquent\Collection $tags */
361
        $tags = static::allTagModels();
362
363
        return $tags->pluck('name')->sort()->all();
364
    }
365
366
    /**
367
     * Get all the tags used for the called class as a delimited string.
368
     *
369
     * @return string
370
     */
371
    public static function allTagsList()
372
    {
373
        return app(TagService::class)->joinList(static::allTags());
374
    }
375
}
376