Passed
Push — master ( e80968...dc7aef )
by Morten Poul
03:49
created

Translatable::getTableColumns()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 5
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 0
dl 0
loc 5
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\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\Builder;
9
use Signifly\Translator\Facades\Translator;
10
use Illuminate\Database\Eloquent\SoftDeletes;
11
use Signifly\Translator\Contracts\Translation;
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
        $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
        return $translation ? $translation->value : null;
166
    }
167
168
    /**
169
     * Get the updatable attributes.
170
     *
171
     * @return array
172
     */
173
    public function getUpdatableAttributes(): array
174
    {
175
        $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

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

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

324
        /** @scrutinizer ignore-call */ 
325
        $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...
325
        $model->translate($langCode, $data);
326
327
        return $model;
328
    }
329
330
    /**
331
     * Update and translate for the specified language code.
332
     *
333
     * @param  string $langCode
334
     * @param  array  $data
335
     * @return \Illuminate\Database\Eloquent\Model
336
     */
337
    public function updateAndTranslate(string $langCode, array $data): Model
338
    {
339
        // We update all the model's attributes, when it is the default language
340
        if (Translator::isDefaultLanguage($langCode)) {
341
            $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

341
            $this->/** @scrutinizer ignore-call */ 
342
                   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...
342
        }
343
344
        // Otherwise we only update the non-translatable attributes
345
        else {
346
            $this->update(Arr::only($data, $this->getUpdatableAttributes()));
347
        }
348
349
        $this->translate($langCode, $data);
350
351
        return $this;
352
    }
353
354
    /**
355
     * Scope a query to include translation stats.
356
     *
357
     * @param  \Illuminate\Database\Eloquent\Builder $query
358
     * @param  string  $langCode
359
     * @return \Illuminate\Database\Eloquent\Builder
360
     */
361
    public function scopeWithTranslationStats(Builder $query, string $langCode): Builder
362
    {
363
        $relation = $query->getRelation('translations');
364
365
        $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

365
        /** @scrutinizer ignore-call */ 
366
        $model = $this->getMorphClass();
Loading history...
366
        $translationModel = Translator::determineModel();
367
368
        $subQuery = $translationModel::selectRaw('count(*)')
369
            ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
370
            ->where('translatable_type', $model)
371
            ->where('language_code', $langCode);
372
373
        return $query->defaultSelectAll()
374
            ->selectRaw("@modifier_count := ({$subQuery->toSql()}) as translations_count", $subQuery->getBindings())
375
            ->selectRaw('@modifier_count / ? * 100 as translations_percentage', [
376
                count($this->getTranslatableAttributes()),
377
            ])
378
            ->addSubSelect(
379
                'translations_last_modified_at',
380
                $translationModel::select('updated_at')
381
                    ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
382
                    ->where('translatable_type', $model)
383
                    ->where('language_code', $langCode)
384
                    ->orderBy('updated_at', 'desc')
385
            );
386
    }
387
}
388