Passed
Push — master ( 3876a5...3711b5 )
by Morten Poul
02:11
created

Translatable::attributesToArray()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 11
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 2
eloc 6
nc 2
nop 0
dl 0
loc 11
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Signifly\Translator\Concerns;
4
5
use Illuminate\Support\Arr;
6
use Illuminate\Support\Collection;
7
use Illuminate\Support\Facades\DB;
8
use Illuminate\Database\Eloquent\Model;
9
use Illuminate\Database\Eloquent\Builder;
10
use Signifly\Translator\Facades\Translator;
11
use Illuminate\Database\Eloquent\SoftDeletes;
12
use Illuminate\Database\Eloquent\Relations\MorphMany;
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
92
        if (! Translator::autoTranslates()) {
93
            return parent::getAttributeValue($key);
94
        }
95
96
        if (! $this->hasTranslation($activeLangCode, $key)) {
97
            return parent::getAttributeValue($key);
98
        }
99
100
        return $this->getTranslationValue($activeLangCode, $key);
101
    }
102
103
    /**
104
     * Returns the columns from the database.
105
     *
106
     * @return \Illuminate\Support\Collection
107
     */
108
    public function getColumnsFromDatabase(): Collection
109
    {
110
        $columns = Collection::make(
111
            DB::select('SHOW COLUMNS FROM '.$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? ( Ignorable by Annotation )

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

111
            DB::select('SHOW COLUMNS FROM '.$this->/** @scrutinizer ignore-call */ getTable())
Loading history...
112
        );
113
114
        return $columns->pluck('Field');
115
    }
116
117
    /**
118
     * Get the translatable attributes.
119
     *
120
     * @return array
121
     */
122
    public function getTranslatableAttributes(): array
123
    {
124
        if (isset($this->translatable)) {
125
            return $this->translatable;
126
        }
127
128
        return [];
129
    }
130
131
    /**
132
     * Get the translated values.
133
     *
134
     * @param  string $langCode
135
     * @return array
136
     */
137
    public function getTranslatedValues(string $langCode) : array
138
    {
139
        return collect($this->getTranslatableAttributes())
140
            ->filter(function ($attribute) use ($langCode) {
141
                return $this->hasTranslation($langCode, $attribute);
142
            })
143
            ->values()
144
            ->mapWithKeys(function ($attribute) use ($langCode) {
145
                return [$attribute => $this->getTranslationValue($langCode, $attribute)];
146
            })
147
            ->toArray();
148
    }
149
150
    /**
151
     * Get the translation value for a given key.
152
     *
153
     * @param  string $attribute
154
     * @return mixed
155
     */
156
    public function getTranslationValue(string $langCode, string $attribute)
157
    {
158
        $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...
159
            ->where('language_code', $langCode)
160
            ->first();
161
162
        return $translation ? $translation->value : null;
163
    }
164
165
    /**
166
     * Get the updatable attributes.
167
     *
168
     * @return array
169
     */
170
    public function getUpdatableAttributes(): array
171
    {
172
        $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

172
        $fillable = collect($this->/** @scrutinizer ignore-call */ getFillable());
Loading history...
173
174
        if ($fillable->isEmpty()) {
175
            $fillable = $this->getColumnsFromDatabase();
176
        }
177
178
        return $fillable->diff($this->getTranslatableAttributes())
179
            ->values()
180
            ->toArray();
181
    }
182
183
    /**
184
     * Check if a translation exists for a given attribute.
185
     *
186
     * @param  string  $langCode
187
     * @param  string  $attribute
188
     * @return bool
189
     */
190
    public function hasTranslation(string $langCode, string $attribute): bool
191
    {
192
        if (! $this->shouldBeTranslated($attribute)) {
193
            return false;
194
        }
195
196
        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

196
        if ($this->/** @scrutinizer ignore-call */ relationLoaded('translations')) {
Loading history...
197
            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...
198
                ->where('language_code', $langCode)
199
                ->count() > 0;
200
        }
201
202
        return $this->translations()->where('key', $attribute)
203
            ->forLang($langCode)
204
            ->count() > 0;
205
    }
206
207
    /**
208
     * Check if a translation is outdated.
209
     *
210
     * @param  string  $langCode
211
     * @param  string  $attribute
212
     * @return bool
213
     */
214
    public function isTranslationOutdated(string $langCode, string $attribute): bool
215
    {
216
        if (Translator::isDefaultLanguage($langCode)) {
217
            return false;
218
        }
219
220
        $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...
221
            ->where('language_code', Translator::defaultLanguageCode())
222
            ->first();
223
224
        if (! $defaultTranslation) {
225
            return false;
226
        }
227
228
        $targetTranslation = $this->translations->where('key', $attribute)
229
            ->where('language_code', $langCode)
230
            ->first();
231
232
        if (! $targetTranslation) {
233
            return false;
234
        }
235
236
        // Compare target translation to default translation
237
        return $targetTranslation->updated_at->lt($defaultTranslation->updated_at);
238
    }
239
240
    /**
241
     * Checks if the given attribute should be translated.
242
     *
243
     * @param  string $attribute
244
     * @return bool
245
     */
246
    public function shouldBeTranslated(string $attribute): bool
247
    {
248
        return in_array($attribute, $this->getTranslatableAttributes());
249
    }
250
251
    /**
252
     * Set the translatable attributes for the model.
253
     *
254
     * @param  array  $attributes
255
     * @return self
256
     */
257
    public function translatable(array $attributes): self
258
    {
259
        $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...
260
261
        return $this;
262
    }
263
264
    /**
265
     * Translate an array of attribute, value pairs.
266
     *
267
     * @param  string $langCode
268
     * @param  array  $data
269
     * @return \Illuminate\Support\Collection
270
     */
271
    public function translate(string $langCode, array $data): Collection
272
    {
273
        return collect($data)
274
            ->filter(function ($value, $attribute) {
275
                return $this->shouldBeTranslated($attribute) && ! is_null($value);
276
            })
277
            ->map(function ($value, $attribute) use ($langCode) {
278
                return $this->translateAttribute($langCode, $attribute, $value);
279
            })
280
            ->values();
281
    }
282
283
    /**
284
     * Translate the specified attribute, value pair.
285
     *
286
     * @param  string $langCode
287
     * @param  string $attribute
288
     * @param  mixed $value
289
     * @return \Illuminate\Database\Eloquent\Model
290
     */
291
    public function translateAttribute(string $langCode, string $attribute, $value): Model
292
    {
293
        return $this->translations()->updateOrCreate([
294
            'language_code' => $langCode,
295
            'key' => $attribute,
296
        ], compact('value'));
297
    }
298
299
    /**
300
     * Create and translate for the specified language code.
301
     *
302
     * @param  string $langCode
303
     * @param  array  $data
304
     * @return \Illuminate\Database\Eloquent\Model
305
     */
306
    public static function createAndTranslate(string $langCode, array $data): Model
307
    {
308
        $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

308
        /** @scrutinizer ignore-call */ 
309
        $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...
309
        $model->translate($langCode, $data);
310
311
        return $model;
312
    }
313
314
    /**
315
     * Update and translate for the specified language code.
316
     *
317
     * @param  string $langCode
318
     * @param  array  $data
319
     * @return \Illuminate\Database\Eloquent\Model
320
     */
321
    public function updateAndTranslate(string $langCode, array $data): Model
322
    {
323
        $this->update(
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

323
        $this->/** @scrutinizer ignore-call */ 
324
               update(

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...
324
            Translator::isDefaultLanguage($langCode)
325
            ? $data
326
            : Arr::only($data, $this->getUpdatableAttributes())
327
        );
328
329
        $this->translate($langCode, $data);
330
331
        return $this;
332
    }
333
334
    /**
335
     * Scope a query to include translation stats.
336
     *
337
     * @param  \Illuminate\Database\Eloquent\Builder $query
338
     * @param  string  $langCode
339
     * @return \Illuminate\Database\Eloquent\Builder
340
     */
341
    public function scopeWithTranslationStats(Builder $query, string $langCode): Builder
342
    {
343
        $relation = $query->getRelation('translations');
344
345
        $model = get_class($this);
346
        $translationModel = Translator::determineModel();
347
348
        $subQuery = $translationModel::selectRaw('count(*)')
349
            ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
350
            ->where('translatable_type', $model)
351
            ->where('language_code', $langCode);
352
353
        return $query->defaultSelectAll()
354
            ->selectRaw("@modifier_count := ({$subQuery->toSql()}) as translations_count", $subQuery->getBindings())
355
            ->selectRaw('@modifier_count / ? * 100 as translations_percentage', [
356
                count($this->getTranslatableAttributes()),
357
            ])
358
            ->addSubSelect(
359
                'translations_last_modified_at',
360
                $translationModel::select('updated_at')
361
                    ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
362
                    ->where('translatable_type', $model)
363
                    ->where('language_code', $langCode)
364
                    ->orderBy('updated_at', 'desc')
365
            );
366
    }
367
}
368