Completed
Push — master ( 3fa0ab...4d90c4 )
by Freek
01:09
created

src/HasTags.php (5 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Spatie\Tags;
4
5
use InvalidArgumentException;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Builder;
8
use Illuminate\Database\Eloquent\Collection;
9
use Illuminate\Database\Eloquent\Relations\MorphToMany;
10
11
trait HasTags
12
{
13
    protected $queuedTags = [];
14
15
    public static function getTagClassName(): string
16
    {
17
        return Tag::class;
18
    }
19
20
    public static function bootHasTags()
21
    {
22
        static::created(function (Model $taggableModel) {
23
            if (count($taggableModel->queuedTags) > 0) {
24
                $taggableModel->attachTags($taggableModel->queuedTags);
25
26
                $taggableModel->queuedTags = [];
27
            }
28
        });
29
30
        static::deleted(function (Model $deletedModel) {
31
            $tags = $deletedModel->tags()->get();
32
33
            $deletedModel->detachTags($tags);
34
        });
35
    }
36
37
    public function tags(): MorphToMany
38
    {
39
        return $this
40
            ->morphToMany(self::getTagClassName(), 'taggable')
41
            ->ordered();
42
    }
43
44
    /**
45
     * @param string $locale
46
     */
47
    public function tagsTranslated($locale = null): MorphToMany
48
    {
49
        $locale = ! is_null($locale) ? $locale : app()->getLocale();
50
51
        return $this
52
            ->morphToMany(self::getTagClassName(), 'taggable')
53
            ->select('*')
54
            ->selectRaw("JSON_UNQUOTE(JSON_EXTRACT(name, '$.\"{$locale}\"')) as name_translated")
55
            ->selectRaw("JSON_UNQUOTE(JSON_EXTRACT(slug, '$.\"{$locale}\"')) as slug_translated")
56
            ->ordered();
57
    }
58
59
    /**
60
     * @param string|array|\ArrayAccess|\Spatie\Tags\Tag $tags
61
     */
62
    public function setTagsAttribute($tags)
63
    {
64
        if (! $this->exists) {
65
            $this->queuedTags = $tags;
66
67
            return;
68
        }
69
70
        $this->attachTags($tags);
71
    }
72
73
    /**
74
     * @param \Illuminate\Database\Eloquent\Builder $query
75
     * @param array|\ArrayAccess|\Spatie\Tags\Tag $tags
76
     *
77
     * @return \Illuminate\Database\Eloquent\Builder
78
     */
79 View Code Duplication
    public function scopeWithAllTags(Builder $query, $tags, string $type = null): Builder
0 ignored issues
show
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
80
    {
81
        $tags = static::convertToTags($tags, $type);
82
83
        collect($tags)->each(function ($tag) use ($query) {
84
            $query->whereIn("{$this->getTable()}.{$this->getKeyName()}", function ($query) use ($tag) {
0 ignored issues
show
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...
It seems like getKeyName() 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...
85
                $query->from('taggables')
86
                    ->select('taggables.taggable_id')
87
                    ->where('taggables.tag_id', $tag ? $tag->id : 0);
88
            });
89
        });
90
91
        return $query;
92
    }
93
94
    /**
95
     * @param \Illuminate\Database\Eloquent\Builder $query
96
     * @param array|\ArrayAccess|\Spatie\Tags\Tag $tags
97
     *
98
     * @return \Illuminate\Database\Eloquent\Builder
99
     */
100 View Code Duplication
    public function scopeWithAnyTags(Builder $query, $tags, string $type = null): Builder
101
    {
102
        $tags = static::convertToTags($tags, $type);
103
104
        return $query->whereHas('tags', function (Builder $query) use ($tags) {
105
            $tagIds = collect($tags)->pluck('id');
106
107
            $query->whereIn('tags.id', $tagIds);
108
        });
109
    }
110
111 View Code Duplication
    public function scopeWithAllTagsOfAnyType(Builder $query, $tags): Builder
112
    {
113
        $tags = static::convertToTagsOfAnyType($tags);
114
115
        collect($tags)->each(function ($tag) use ($query) {
116
            $query->whereIn("{$this->getTable()}.{$this->getKeyName()}", function ($query) use ($tag) {
0 ignored issues
show
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...
It seems like getKeyName() 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...
117
                $query->from('taggables')
118
                    ->select('taggables.taggable_id')
119
                    ->where('taggables.tag_id', $tag ? $tag->id : 0);
120
            });
121
        });
122
123
        return $query;
124
    }
125
126 View Code Duplication
    public function scopeWithAnyTagsOfAnyType(Builder $query, $tags): Builder
127
    {
128
        $tags = static::convertToTagsOfAnyType($tags);
129
130
        return $query->whereHas('tags', function (Builder $query) use ($tags) {
131
            $tagIds = collect($tags)->pluck('id');
132
133
            $query->whereIn('tags.id', $tagIds);
134
        });
135
    }
136
137
    public function tagsWithType(string $type = null): Collection
138
    {
139
        return $this->tags->filter(function (Tag $tag) use ($type) {
140
            return $tag->type === $type;
141
        });
142
    }
143
144
    /**
145
     * @param array|\ArrayAccess|\Spatie\Tags\Tag $tags
146
     *
147
     * @return $this
148
     */
149 View Code Duplication
    public function attachTags($tags)
150
    {
151
        $className = static::getTagClassName();
152
153
        $tags = collect($className::findOrCreate($tags));
154
155
        $this->tags()->syncWithoutDetaching($tags->pluck('id')->toArray());
156
157
        return $this;
158
    }
159
160
    /**
161
     * @param string|\Spatie\Tags\Tag $tag
162
     *
163
     * @return $this
164
     */
165
    public function attachTag($tag)
166
    {
167
        return $this->attachTags([$tag]);
168
    }
169
170
    /**
171
     * @param array|\ArrayAccess $tags
172
     *
173
     * @return $this
174
     */
175
    public function detachTags($tags)
176
    {
177
        $tags = static::convertToTags($tags);
178
179
        collect($tags)
180
            ->filter()
181
            ->each(function (Tag $tag) {
182
                $this->tags()->detach($tag);
183
            });
184
185
        return $this;
186
    }
187
188
    /**
189
     * @param string|\Spatie\Tags\Tag $tag
190
     *
191
     * @return $this
192
     */
193
    public function detachTag($tag)
194
    {
195
        return $this->detachTags([$tag]);
196
    }
197
198
    /**
199
     * @param array|\ArrayAccess $tags
200
     *
201
     * @return $this
202
     */
203 View Code Duplication
    public function syncTags($tags)
204
    {
205
        $className = static::getTagClassName();
206
207
        $tags = collect($className::findOrCreate($tags));
208
209
        $this->tags()->sync($tags->pluck('id')->toArray());
210
211
        return $this;
212
    }
213
214
    /**
215
     * @param array|\ArrayAccess $tags
216
     * @param string|null $type
217
     *
218
     * @return $this
219
     */
220 View Code Duplication
    public function syncTagsWithType($tags, string $type = null)
221
    {
222
        $className = static::getTagClassName();
223
224
        $tags = collect($className::findOrCreate($tags, $type));
225
226
        $this->syncTagIds($tags->pluck('id')->toArray(), $type);
227
228
        return $this;
229
    }
230
231
    protected static function convertToTags($values, $type = null, $locale = null)
232
    {
233
        return collect($values)->map(function ($value) use ($type, $locale) {
234
            if ($value instanceof Tag) {
235
                if (isset($type) && $value->type != $type) {
236
                    throw new InvalidArgumentException("Type was set to {$type} but tag is of type {$value->type}");
237
                }
238
239
                return $value;
240
            }
241
242
            $className = static::getTagClassName();
243
244
            return $className::findFromString($value, $type, $locale);
245
        });
246
    }
247
248
    protected static function convertToTagsOfAnyType($values, $locale = null)
249
    {
250
        return collect($values)->map(function ($value) use ($locale) {
251
            if ($value instanceof Tag) {
252
                return $value;
253
            }
254
255
            $className = static::getTagClassName();
256
257
            return $className::findFromStringOfAnyType($value, $locale);
258
        });
259
    }
260
261
    /**
262
     * Use in place of eloquent's sync() method so that the tag type may be optionally specified.
263
     *
264
     * @param $ids
265
     * @param string|null $type
266
     * @param bool $detaching
267
     */
268
    protected function syncTagIds($ids, string $type = null, $detaching = true)
269
    {
270
        $isUpdated = false;
271
272
        // Get a list of tag_ids for all current tags
273
        $current = $this->tags()
274
            ->newPivotStatement()
275
            ->where('taggable_id', $this->getKey())
276
            ->where('taggable_type', $this->getMorphClass())
277
            ->when($type !== null, function ($query) use ($type) {
278
                $tagModel = $this->tags()->getRelated();
279
280
                return $query->join(
281
                    $tagModel->getTable(),
282
                    'taggables.tag_id',
283
                    '=',
284
                    $tagModel->getTable().'.'.$tagModel->getKeyName()
285
                )
286
                    ->where('tags.type', $type);
287
            })
288
            ->pluck('tag_id')
289
            ->all();
290
291
        // Compare to the list of ids given to find the tags to remove
292
        $detach = array_diff($current, $ids);
293
        if ($detaching && count($detach) > 0) {
294
            $this->tags()->detach($detach);
295
            $isUpdated = true;
296
        }
297
298
        // Attach any new ids
299
        $attach = array_diff($ids, $current);
300
        if (count($attach) > 0) {
301
            collect($attach)->each(function ($id) {
302
                $this->tags()->attach($id, []);
303
            });
304
            $isUpdated = true;
305
        }
306
307
        // Once we have finished attaching or detaching the records, we will see if we
308
        // have done any attaching or detaching, and if we have we will touch these
309
        // relationships if they are configured to touch on any database updates.
310
        if ($isUpdated) {
311
            $this->tags()->touchIfTouching();
312
        }
313
    }
314
}
315