Passed
Push — master ( 868a85...47fe80 )
by Morten Poul
03:20
created

Translatable::clearTranslations()   A

Complexity

Conditions 3
Paths 2

Size

Total Lines 9
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 0
Metric Value
cc 3
eloc 4
c 2
b 0
f 0
nc 2
nop 1
dl 0
loc 9
rs 10
1
<?php
2
3
namespace Signifly\Translator\Concerns;
4
5
use Illuminate\Database\Eloquent\Builder;
6
use Illuminate\Database\Eloquent\Model;
7
use Illuminate\Database\Eloquent\Relations\MorphMany;
8
use Illuminate\Database\Eloquent\SoftDeletes;
9
use Illuminate\Support\Arr;
10
use Illuminate\Support\Collection;
11
use Signifly\Translator\Contracts\Translation;
12
use Signifly\Translator\Facades\Translator;
13
14
trait Translatable
15
{
16
    public static function bootTranslatable(): void
17
    {
18
        // Clean up translations
19
        static::deleting(function (Model $model) {
20
            in_array(SoftDeletes::class, class_uses_recursive($model))
21
                ? $model->clearTranslations($model->forceDeleting)
22
                : $model->clearTranslations(true);
23
        });
24
25
        if (in_array(SoftDeletes::class, class_uses_recursive(static::class))) {
26
            static::restoring(function (Model $model) {
27
                if (! Translator::softDeletes()) {
28
                    return;
29
                }
30
31
                $model->translations()->restore();
32
            });
33
        }
34
    }
35
36
    /**
37
     * The associated translations relation.
38
     *
39
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
40
     */
41
    public function translations(): MorphMany
42
    {
43
        return $this->morphMany(Translator::determineModel(), 'translatable');
0 ignored issues
show
Bug introduced by
It seems like morphMany() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

43
        return $this->/** @scrutinizer ignore-call */ morphMany(Translator::determineModel(), 'translatable');
Loading history...
44
    }
45
46
    /**
47
     * Convert the model's attributes to an array.
48
     *
49
     * @return array
50
     */
51
    public function attributesToArray()
52
    {
53
        $activeLangCode = Translator::activeLanguageCode();
54
55
        if (! Translator::autoTranslates()) {
56
            return parent::attributesToArray();
57
        }
58
59
        return array_merge(
60
            parent::attributesToArray(),
61
            $this->getTranslatedValues($activeLangCode)
62
        );
63
    }
64
65
    /**
66
     * Clear translations based on model deletion.
67
     *
68
     * @param  bool $forceDelete
69
     * @return void
70
     */
71
    protected function clearTranslations($forceDelete = false): void
72
    {
73
        if (Translator::softDeletes() && $forceDelete) {
74
            $this->translations()->forceDelete();
75
76
            return;
77
        }
78
79
        $this->translations()->delete();
80
    }
81
82
    /**
83
     * Get a plain attribute (not a relationship).
84
     *
85
     * @param  string  $key
86
     * @return mixed
87
     */
88
    public function getAttributeValue($key)
89
    {
90
        $activeLangCode = Translator::activeLanguageCode();
91
        $value = parent::getAttributeValue($key);
92
93
        if (! Translator::autoTranslates()) {
94
            return $value;
95
        }
96
97
        if (Translator::isDefaultLanguage($activeLangCode)) {
98
            return $value;
99
        }
100
101
        if (! $this->hasTranslation($activeLangCode, $key)) {
102
            return $value;
103
        }
104
105
        return $this->getTranslationValue($activeLangCode, $key);
106
    }
107
108
    /**
109
     * Returns the columns from the database.
110
     *
111
     * @return \Illuminate\Support\Collection
112
     */
113
    public function getTableColumns(): Collection
114
    {
115
        return collect($this->getConnection()
0 ignored issues
show
Bug introduced by
It seems like getConnection() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

115
        return collect($this->/** @scrutinizer ignore-call */ getConnection()
Loading history...
116
            ->getSchemaBuilder()
117
            ->getColumnListing($this->getTable()));
0 ignored issues
show
Bug introduced by
The method getTable() does not exist on Signifly\Translator\Concerns\Translatable. Did you maybe mean getTableColumns()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

117
            ->getColumnListing($this->/** @scrutinizer ignore-call */ getTable()));

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
118
    }
119
120
    /**
121
     * Get the translatable attributes.
122
     *
123
     * @return array
124
     */
125
    public function getTranslatableAttributes(): array
126
    {
127
        if (isset($this->translatable)) {
128
            return $this->translatable;
129
        }
130
131
        return [];
132
    }
133
134
    /**
135
     * Get the translated values.
136
     *
137
     * @param  string $langCode
138
     * @return array
139
     */
140
    public function getTranslatedValues(string $langCode): array
141
    {
142
        return collect($this->getTranslatableAttributes())
143
            ->filter(function ($attribute) use ($langCode) {
144
                return $this->hasTranslation($langCode, $attribute);
145
            })
146
            ->values()
147
            ->mapWithKeys(function ($attribute) use ($langCode) {
148
                return [$attribute => $this->getTranslationValue($langCode, $attribute)];
149
            })
150
            ->toArray();
151
    }
152
153
    /**
154
     * Get the translation value for a given key.
155
     *
156
     * @param  string $attribute
157
     * @return mixed
158
     */
159
    public function getTranslationValue(string $langCode, string $attribute)
160
    {
161
        $translation = $this->translations->where('key', $attribute)
0 ignored issues
show
Bug introduced by
The property translations does not exist on Signifly\Translator\Concerns\Translatable. Did you mean translatable?
Loading history...
162
            ->where('language_code', $langCode)
163
            ->first();
164
165
        if ($translation === null) {
166
            return;
167
        }
168
169
        if ($this->hasCast($attribute)) {
0 ignored issues
show
Bug introduced by
It seems like hasCast() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

169
        if ($this->/** @scrutinizer ignore-call */ hasCast($attribute)) {
Loading history...
170
            return $this->castAttribute($attribute, $translation->value);
0 ignored issues
show
Bug introduced by
It seems like castAttribute() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

170
            return $this->/** @scrutinizer ignore-call */ castAttribute($attribute, $translation->value);
Loading history...
171
        }
172
173
        return $translation->value;
174
    }
175
176
    /**
177
     * Get the updatable attributes.
178
     *
179
     * @return array
180
     */
181
    public function getUpdatableAttributes(): array
182
    {
183
        $fillable = collect($this->getFillable());
0 ignored issues
show
Bug introduced by
It seems like getFillable() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

183
        $fillable = collect($this->/** @scrutinizer ignore-call */ getFillable());
Loading history...
184
185
        if ($fillable->isEmpty()) {
186
            $fillable = $this->getTableColumns();
187
        }
188
189
        return $fillable->diff($this->getTranslatableAttributes())
190
            ->values()
191
            ->toArray();
192
    }
193
194
    /**
195
     * Check if a translation exists for a given attribute.
196
     *
197
     * @param  string  $langCode
198
     * @param  string  $attribute
199
     * @return bool
200
     */
201
    public function hasTranslation(string $langCode, string $attribute): bool
202
    {
203
        if (! $this->shouldBeTranslated($attribute)) {
204
            return false;
205
        }
206
207
        if ($this->relationLoaded('translations')) {
0 ignored issues
show
Bug introduced by
It seems like relationLoaded() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

207
        if ($this->/** @scrutinizer ignore-call */ relationLoaded('translations')) {
Loading history...
208
            return $this->translations->where('key', $attribute)
0 ignored issues
show
Bug introduced by
The property translations does not exist on Signifly\Translator\Concerns\Translatable. Did you mean translatable?
Loading history...
209
                ->where('language_code', $langCode)
210
                ->count() > 0;
211
        }
212
213
        return $this->translations()->where('key', $attribute)
214
            ->forLang($langCode)
215
            ->count() > 0;
216
    }
217
218
    /**
219
     * Check if a translation is outdated.
220
     *
221
     * @param  string  $langCode
222
     * @param  string  $attribute
223
     * @return bool
224
     */
225
    public function isTranslationOutdated(string $langCode, string $attribute): bool
226
    {
227
        if (Translator::isDefaultLanguage($langCode)) {
228
            return false;
229
        }
230
231
        $defaultTranslation = $this->translations->where('key', $attribute)
0 ignored issues
show
Bug introduced by
The property translations does not exist on Signifly\Translator\Concerns\Translatable. Did you mean translatable?
Loading history...
232
            ->where('language_code', Translator::defaultLanguageCode())
233
            ->first();
234
235
        if (! $defaultTranslation) {
236
            return false;
237
        }
238
239
        $targetTranslation = $this->translations->where('key', $attribute)
240
            ->where('language_code', $langCode)
241
            ->first();
242
243
        if (! $targetTranslation) {
244
            return false;
245
        }
246
247
        // Compare target translation to default translation
248
        return $targetTranslation->updated_at->lt($defaultTranslation->updated_at);
249
    }
250
251
    /**
252
     * Checks if the given attribute should be translated.
253
     *
254
     * @param  string $attribute
255
     * @return bool
256
     */
257
    public function shouldBeTranslated(string $attribute): bool
258
    {
259
        return in_array($attribute, $this->getTranslatableAttributes());
260
    }
261
262
    /**
263
     * Set the translatable attributes for the model.
264
     *
265
     * @param  array  $attributes
266
     * @return self
267
     */
268
    public function translatable(array $attributes): self
269
    {
270
        $this->translatable = $attributes;
0 ignored issues
show
Bug Best Practice introduced by
The property translatable does not exist. Although not strictly required by PHP, it is generally a best practice to declare properties explicitly.
Loading history...
271
272
        return $this;
273
    }
274
275
    /**
276
     * Translate an array of attribute, value pairs.
277
     *
278
     * @param  string $langCode
279
     * @param  array  $data
280
     * @return \Illuminate\Support\Collection
281
     */
282
    public function translate(string $langCode, array $data): Collection
283
    {
284
        return collect($data)
285
            ->filter(function ($value, $attribute) {
286
                return $this->shouldBeTranslated($attribute);
287
            })
288
            ->map(function ($value, $attribute) use ($langCode) {
289
                return $this->translateAttribute($langCode, $attribute, $value);
290
            })
291
            ->filter()
292
            ->values();
293
    }
294
295
    /**
296
     * Translate the specified attribute, value pair.
297
     *
298
     * @param  string $langCode
299
     * @param  string $attribute
300
     * @param  mixed $value
301
     * @return \Signifly\Translator\Contracts\Translation|null
302
     */
303
    public function translateAttribute(string $langCode, string $attribute, $value): ?Translation
304
    {
305
        $translation = $this->translations()->firstOrNew([
306
            'language_code' => $langCode,
307
            'key' => $attribute,
308
        ]);
309
310
        // If the value provided for translation is empty
311
        // then delete the translation and return
312
        if (! is_bool($value) && ! is_array($value) && trim((string) $value) === '') {
313
            $translation->delete();
314
315
            return null;
316
        }
317
318
        if ($this->isJsonCastable($attribute)) {
0 ignored issues
show
Bug introduced by
It seems like isJsonCastable() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

318
        if ($this->/** @scrutinizer ignore-call */ isJsonCastable($attribute)) {
Loading history...
319
            $value = $this->castAttributeAsJson($attribute, $value);
0 ignored issues
show
Bug introduced by
It seems like castAttributeAsJson() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

319
            /** @scrutinizer ignore-call */ 
320
            $value = $this->castAttributeAsJson($attribute, $value);
Loading history...
320
        }
321
322
        $translation->fill(compact('value'))->save();
323
324
        return $translation;
325
    }
326
327
    /**
328
     * Create and translate for the specified language code.
329
     *
330
     * @param  string $langCode
331
     * @param  array  $data
332
     * @return \Illuminate\Database\Eloquent\Model
333
     */
334
    public static function createAndTranslate(string $langCode, array $data): Model
335
    {
336
        $model = self::create($data);
0 ignored issues
show
Bug introduced by
The method create() does not exist on Signifly\Translator\Concerns\Translatable. Did you maybe mean createAndTranslate()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

336
        /** @scrutinizer ignore-call */ 
337
        $model = self::create($data);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
337
        $model->translate($langCode, $data);
338
339
        return $model;
340
    }
341
342
    /**
343
     * Update and translate for the specified language code.
344
     *
345
     * @param  string $langCode
346
     * @param  array  $data
347
     * @return \Illuminate\Database\Eloquent\Model
348
     */
349
    public function updateAndTranslate(string $langCode, array $data): Model
350
    {
351
        // We update all the model's attributes, when it is the default language
352
        if (Translator::isDefaultLanguage($langCode)) {
353
            $this->update($data);
0 ignored issues
show
Bug introduced by
The method update() does not exist on Signifly\Translator\Concerns\Translatable. Did you maybe mean updateAndTranslate()? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

353
            $this->/** @scrutinizer ignore-call */ 
354
                   update($data);

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
354
        }
355
356
        // Otherwise we only update the non-translatable attributes
357
        else {
358
            $this->update(Arr::only($data, $this->getUpdatableAttributes()));
359
        }
360
361
        $this->translate($langCode, $data);
362
363
        return $this;
364
    }
365
366
    /**
367
     * Scope a query to include translation stats.
368
     *
369
     * @param  \Illuminate\Database\Eloquent\Builder $query
370
     * @param  string  $langCode
371
     * @return \Illuminate\Database\Eloquent\Builder
372
     */
373
    public function scopeWithTranslationStats(Builder $query, string $langCode): Builder
374
    {
375
        $relation = $query->getRelation('translations');
376
377
        $model = $this->getMorphClass();
0 ignored issues
show
Bug introduced by
It seems like getMorphClass() must be provided by classes using this trait. How about adding it as abstract method to this trait? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

377
        /** @scrutinizer ignore-call */ 
378
        $model = $this->getMorphClass();
Loading history...
378
        $translationModel = Translator::determineModel();
379
380
        $subQuery = $translationModel::selectRaw('count(*)')
381
            ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
382
            ->where('translatable_type', $model)
383
            ->where('language_code', $langCode);
384
385
        // Select *all* columns from the "primary" query table
386
        // if no other columns have been selected
387
        if (is_null($query->getQuery()->columns)) {
0 ignored issues
show
introduced by
The condition is_null($query->getQuery()->columns) is always false.
Loading history...
388
            $query->select($query->getQuery()->from.'.*');
389
        }
390
391
        return $query
392
            ->selectRaw("@modifier_count := ({$subQuery->toSql()}) as translations_count", $subQuery->getBindings())
393
            ->selectRaw('@modifier_count / ? * 100 as translations_percentage', [
394
                count($this->getTranslatableAttributes()),
395
            ])
396
            ->addSelect([
397
                'translations_last_modified_at' => $translationModel::select('updated_at')
398
                    ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
399
                    ->where('translatable_type', $model)
400
                    ->where('language_code', $langCode)
401
                    ->orderBy('updated_at', 'desc')
402
                    ->limit(1),
403
            ]);
404
    }
405
}
406