Completed
Push — master ( 5f8f07...3e797b )
by Morten Poul
02:50
created

Translatable::createAndTranslate()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 3
nc 1
nop 2
dl 0
loc 6
rs 10
c 0
b 0
f 0
1
<?php
2
3
namespace Signifly\Translator\Concerns;
4
5
use Illuminate\Support\Collection;
6
use Illuminate\Support\Facades\DB;
7
use Illuminate\Database\Eloquent\Model;
8
use Illuminate\Database\Eloquent\Builder;
9
use Illuminate\Database\Eloquent\SoftDeletes;
10
use Signifly\Translator\TranslatorServiceProvider;
11
use Illuminate\Database\Eloquent\Relations\MorphMany;
12
13
trait Translatable
14
{
15
    public static function bootTranslatable(): void
16
    {
17
        // Clean up translations
18
        static::deleting(function (Model $model) {
19
            collect(class_uses_recursive($model))->contains(SoftDeletes::class)
20
                ? $model->clearTranslations($model->forceDeleting)
21
                : $model->clearTranslations(true);
22
        });
23
24
        if (collect(class_uses_recursive(static::class))->contains(SoftDeletes::class)) {
25
            static::restoring(function (Model $model) {
26
                if (! config('translator.soft_deletes')) {
27
                    return;
28
                }
29
30
                $model->translations()->restore();
31
            });
32
        }
33
    }
34
35
    abstract public function getTranslatableAttributes(): array;
36
37
    /**
38
     * The associated translations relation.
39
     *
40
     * @return \Illuminate\Database\Eloquent\Relations\MorphMany
41
     */
42
    public function translations(): MorphMany
43
    {
44
        return $this->morphMany(
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

44
        return $this->/** @scrutinizer ignore-call */ morphMany(
Loading history...
45
            TranslatorServiceProvider::determineTranslationModel(),
46
            'translatable'
47
        );
48
    }
49
50
    /**
51
     * Clear translations based on model deletion.
52
     *
53
     * @param  bool $forceDelete
54
     * @return void
55
     */
56
    protected function clearTranslations($forceDelete = false): void
57
    {
58
        $translatorSoftDeletes = config('translator.soft_deletes');
59
60
        if ($translatorSoftDeletes && $forceDelete) {
61
            $this->translations()->forceDelete();
62
63
            return;
64
        }
65
66
        $this->translations()->delete();
67
    }
68
69
    /**
70
     * Get a plain attribute (not a relationship).
71
     *
72
     * @param  string  $key
73
     * @return mixed
74
     */
75
    public function getAttributeValue($key)
76
    {
77
        $value = parent::getAttributeValue($key);
78
        $activeLangCode = config('translator.active_language_code');
79
80
        if (
81
            config('translator.auto_translate_attributes')
82
            && $this->hasTranslation($activeLangCode, $key)
83
        ) {
84
            return $this->getTranslationValue($activeLangCode, $key);
85
        }
86
87
        return $value;
88
    }
89
90
    /**
91
     * Returns the columns from the database.
92
     *
93
     * @return \Illuminate\Support\Collection
94
     */
95
    public function getColumnsFromDatabase(): Collection
96
    {
97
        $columns = Collection::make(
98
            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

98
            DB::select('SHOW COLUMNS FROM '.$this->/** @scrutinizer ignore-call */ getTable())
Loading history...
99
        );
100
101
        return $columns->pluck('Field');
102
    }
103
104
    /**
105
     * Get the translated values.
106
     *
107
     * @param  string $langCode
108
     * @return array
109
     */
110
    public function getTranslatedValues(string $langCode) : array
111
    {
112
        return collect($this->getTranslatableAttributes())
113
            ->filter(function ($attribute) use ($langCode) {
114
                return $this->hasTranslation($langCode, $attribute);
115
            })
116
            ->values()
117
            ->mapWithKeys(function ($attribute) use ($langCode) {
118
                return [$attribute => $this->getTranslationValue($langCode, $attribute)];
119
            })
120
            ->toArray();
121
    }
122
123
    /**
124
     * Get the translation value for a given key.
125
     *
126
     * @param  string $attribute
127
     * @return mixed
128
     */
129
    public function getTranslationValue(string $langCode, string $attribute)
130
    {
131
        $translation = $this->translations->where('key', $attribute)
132
            ->where('language_code', $langCode)
133
            ->first();
134
135
        return $translation ? $translation->value : null;
136
    }
137
138
    /**
139
     * Get the updatable attributes.
140
     *
141
     * @return array
142
     */
143
    public function getUpdatableAttributes(): array
144
    {
145
        $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

145
        $fillable = collect($this->/** @scrutinizer ignore-call */ getFillable());
Loading history...
146
147
        if ($fillable->isEmpty()) {
148
            $fillable = $this->getColumnsFromDatabase();
149
        }
150
151
        return $fillable->diff($this->getTranslatableAttributes())
152
            ->values()
153
            ->toArray();
154
    }
155
156
    /**
157
     * Check if a translation exists for a given attribute.
158
     *
159
     * @param  string  $langCode
160
     * @param  string  $attribute
161
     * @return bool
162
     */
163
    public function hasTranslation(string $langCode, string $attribute): bool
164
    {
165
        if (! $this->shouldBeTranslated($attribute)) {
166
            return false;
167
        }
168
169
        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

169
        if ($this->/** @scrutinizer ignore-call */ relationLoaded('translations')) {
Loading history...
170
            return $this->translations->where('key', $attribute)
171
                ->where('language_code', $langCode)
172
                ->count() > 0;
173
        }
174
175
        return $this->translations()->where('key', $attribute)
176
            ->forLang($langCode)
177
            ->count() > 0;
178
    }
179
180
    /**
181
     * Check if it's the default language code.
182
     *
183
     * @param  string  $langCode
184
     * @return bool
185
     */
186
    public function isDefaultLanguage(string $langCode): bool
187
    {
188
        return config('translator.default_language_code') === $langCode;
189
    }
190
191
    /**
192
     * Check if a translation is outdated.
193
     *
194
     * @param  string  $langCode
195
     * @param  string  $attribute
196
     * @return bool
197
     */
198
    public function isTranslationOutdated(string $langCode, string $attribute): bool
199
    {
200
        if ($this->isDefaultLanguage($langCode)) {
201
            return false;
202
        }
203
204
        $defaultTranslation = $this->translations->where('key', $attribute)
205
            ->where('language_code', config('translator.default_language_code'))
206
            ->first();
207
208
        if (! $defaultTranslation) {
209
            return false;
210
        }
211
212
        $targetTranslation = $this->translations->where('key', $attribute)
213
            ->where('language_code', $langCode)
214
            ->first();
215
216
        if (! $targetTranslation) {
217
            return false;
218
        }
219
220
        // Compare target translation to default translation
221
        return $targetTranslation->updated_at->lt($defaultTranslation->updated_at);
222
    }
223
224
    /**
225
     * Checks if the given attribute should be translated.
226
     *
227
     * @param  string $attribute
228
     * @return bool
229
     */
230
    public function shouldBeTranslated(string $attribute): bool
231
    {
232
        return in_array($attribute, $this->getTranslatableAttributes());
233
    }
234
235
    /**
236
     * Translate an array of attribute, value pairs.
237
     *
238
     * @param  string $langCode
239
     * @param  array  $data
240
     * @return \Illuminate\Support\Collection
241
     */
242
    public function translate(string $langCode, array $data): Collection
243
    {
244
        return collect($data)
245
            ->filter(function ($value, $attribute) {
246
                return $this->shouldBeTranslated($attribute) && ! is_null($value);
247
            })
248
            ->map(function ($value, $attribute) use ($langCode) {
249
                return $this->translateAttribute($langCode, $attribute, $value);
250
            })
251
            ->values();
252
    }
253
254
    /**
255
     * Translate the specified attribute, value pair.
256
     *
257
     * @param  string $langCode
258
     * @param  string $attribute
259
     * @param  mixed $value
260
     * @return \Illuminate\Database\Eloquent\Model
261
     */
262
    public function translateAttribute(string $langCode, string $attribute, $value): Model
263
    {
264
        return $this->translations()->updateOrCreate([
265
            'language_code' => $langCode,
266
            'key' => $attribute,
267
        ], compact('value'));
268
    }
269
270
    /**
271
     * Create and translate for the specified language code.
272
     *
273
     * @param  string $langCode
274
     * @param  array  $data
275
     * @return self
276
     */
277
    public function createAndTranslate(string $langCode, array $data): self
278
    {
279
        $this->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

279
        $this->/** @scrutinizer ignore-call */ 
280
               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...
280
        $this->translate($langCode, $data);
281
282
        return $this;
283
    }
284
285
    /**
286
     * Update and translate for the specified language code.
287
     *
288
     * @param  string $langCode
289
     * @param  array  $data
290
     * @return self
291
     */
292
    public function updateAndTranslate(string $langCode, array $data): self
293
    {
294
        $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

294
        $this->/** @scrutinizer ignore-call */ 
295
               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...
295
            $this->isDefaultLanguage($langCode)
296
            ? $data
297
            : Arr::only($data, $this->getUpdatableAttributes())
0 ignored issues
show
Bug introduced by
The type Signifly\Translator\Concerns\Arr was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
298
        );
299
300
        $this->translate($langCode, $data);
301
302
        return $this;
303
    }
304
305
    /**
306
     * Scope a query to include translation stats.
307
     *
308
     * @param  \Illuminate\Database\Eloquent\Builder $query
309
     * @param  string  $langCode
310
     * @return \Illuminate\Database\Eloquent\Builder
311
     */
312
    public function scopeWithTranslationStats(Builder $query, string $langCode): Builder
313
    {
314
        $relation = $query->getRelation('translations');
315
316
        $model = get_class($this);
317
        $translationModel = TranslatorServiceProvider::determineTranslationModel();
318
319
        $subQuery = $translationModel::selectRaw('count(*)')
320
            ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
321
            ->where('translatable_type', $model)
322
            ->where('language_code', $langCode);
323
324
        return $query->defaultSelectAll()
325
            ->selectRaw("@modifier_count := ({$subQuery->toSql()}) as translations_count", $subQuery->getBindings())
326
            ->selectRaw('@modifier_count / ? * 100 as translations_percentage', [
327
                count($this->getTranslatableAttributes()),
328
            ])
329
            ->addSubSelect(
330
                'translations_last_modified_at',
331
                $translationModel::select('updated_at')
332
                    ->whereColumn($relation->getQualifiedForeignKeyName(), $relation->getQualifiedParentKeyName())
333
                    ->where('translatable_type', $model)
334
                    ->where('language_code', $langCode)
335
                    ->orderBy('updated_at', 'desc')
336
            );
337
    }
338
}
339