Issues (25)

src/Translatable.php (15 issues)

1
<?php
2
3
namespace Laraplus\Data;
4
5
use Illuminate\Database\Eloquent\Relations\HasMany;
6
7
trait Translatable
8
{
9
    protected $overrideLocale = null;
10
11
    protected $overrideFallbackLocale = null;
12
13
    protected $overrideOnlyTranslated = null;
14
15
    protected $overrideWithFallback = null;
16
17
    protected $localeChanged = false;
18
19
    /**
20
     * Translated attributes cache.
21
     *
22
     * @var array
23
     */
24
    protected static $i18nAttributes = [];
25
26
    /**
27
     * Boot the trait.
28
     */
29
    public static function bootTranslatable()
30
    {
31
        static::addGlobalScope(new TranslatableScope);
32
    }
33
34
    /**
35
     * Save a new model and return the instance.
36
     *
37
     * @param array $attributes
38
     * @param array|string $translations
39
     *
40
     * @return static
41
     */
42
    public static function create(array $attributes = [], $translations = [])
43
    {
44
        $model = new static($attributes);
0 ignored issues
show
The call to Laraplus\Data\Translatable::__construct() has too many arguments starting with $attributes. ( Ignorable by Annotation )

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

44
        $model = /** @scrutinizer ignore-call */ new static($attributes);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
45
46
        if ($model->save() && is_array($translations)) {
0 ignored issues
show
It seems like save() 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

46
        if ($model->/** @scrutinizer ignore-call */ save() && is_array($translations)) {
Loading history...
47
            $model->saveTranslations($translations);
48
        }
49
50
        return $model;
51
    }
52
53
    /**
54
     * Save a new model in provided locale and return the instance.
55
     *
56
     * @param string $locale
57
     * @param array $attributes
58
     * @param array|string $translations
59
     *
60
     * @return Translatable|string
61
     */
62
    public static function createInLocale(string $locale, array $attributes = [], $translations = [])
63
    {
64
        $model = (new static($attributes))->setLocale($locale);
0 ignored issues
show
The call to Laraplus\Data\Translatable::__construct() has too many arguments starting with $attributes. ( Ignorable by Annotation )

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

64
        $model = (/** @scrutinizer ignore-call */ new static($attributes))->setLocale($locale);

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.

Loading history...
65
66
        if ($model->save() && is_array($translations)) {
67
            $model->saveTranslations($translations);
68
        }
69
70
        return $model;
71
    }
72
73
    /**
74
     * Save a new model and return the instance. Allow mass-assignment.
75
     *
76
     * @param array $attributes
77
     * @param array|string $translations
78
     *
79
     * @return static
80
     */
81
    public static function forceCreate(array $attributes, $translations = [])
82
    {
83
        $model = new static;
84
85
        return static::unguarded(function () use ($model, $attributes, $translations) {
86
            return $model->create($attributes, $translations);
87
        });
88
    }
89
90
    /**
91
     * Save a new model in provided locale and return the instance. Allow mass-assignment.
92
     *
93
     * @param string $locale
94
     * @param array $attributes
95
     * @param array|string $translations
96
     *
97
     * @return static
98
     */
99
    public static function forceCreateInLocale($locale, array $attributes, $translations = [])
100
    {
101
        $model = new static;
102
103
        return static::unguarded(function () use ($locale, $model, $attributes, $translations) {
104
            return $model->createInLocale($locale, $attributes, $translations);
105
        });
106
    }
107
108
    /**
109
     * Reload a fresh model instance from the database.
110
     *
111
     * @param array|string $with
112
     *
113
     * @return static|null
114
     */
115
    public function fresh($with = [])
116
    {
117
        if (!$this->exists) {
118
            return null;
119
        }
120
121
        $query = static::newQueryWithoutScopes()
122
            ->with(is_string($with) ? func_get_args() : $with)
123
            ->where($this->getKeyName(), $this->getKey());
0 ignored issues
show
It seems like getKeyName() 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

123
            ->where($this->/** @scrutinizer ignore-call */ getKeyName(), $this->getKey());
Loading history...
It seems like getKey() 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

123
            ->where($this->getKeyName(), $this->/** @scrutinizer ignore-call */ getKey());
Loading history...
124
125
        (new TranslatableScope)->apply($query, $this);
0 ignored issues
show
$this of type Laraplus\Data\Translatable is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Laraplus\Data\TranslatableScope::apply(). ( Ignorable by Annotation )

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

125
        (new TranslatableScope)->apply($query, /** @scrutinizer ignore-type */ $this);
Loading history...
126
127
        return $query->first();
128
    }
129
130
    /**
131
     * Save the translations.
132
     *
133
     * @param array $translations
134
     *
135
     * @return bool
136
     */
137
    public function saveTranslations(array $translations)
138
    {
139
        $success = true;
140
        $fresh = parent::fresh();
141
142
        foreach ($translations as $locale => $attributes) {
143
            $model = clone $fresh;
144
145
            $model->setLocale($locale);
146
            $model->fill($attributes);
147
148
            $success &= $model->save();
149
        }
150
151
        return $success;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $success also could return the type integer which is incompatible with the documented return type boolean.
Loading history...
152
    }
153
154
    /**
155
     * Force saving the translations.
156
     *
157
     * @param array $translations
158
     *
159
     * @return bool
160
     */
161
    public function forceSaveTranslations(array $translations)
162
    {
163
        return static::unguarded(function () use ($translations) {
164
            return $this->saveTranslations($translations);
165
        });
166
    }
167
168
    /**
169
     * Save the translation.
170
     *
171
     * @param $locale
172
     * @param array $attributes
173
     *
174
     * @return bool
175
     */
176
    public function saveTranslation($locale, array $attributes)
177
    {
178
        return $this->saveTranslations([
179
            $locale => $attributes,
180
        ]);
181
    }
182
183
    /**
184
     * Force saving the translation.
185
     *
186
     * @param $locale
187
     * @param array $attributes
188
     *
189
     * @return bool
190
     */
191
    public function forceSaveTranslation($locale, array $attributes)
192
    {
193
        return static::unguarded(function () use ($locale, $attributes) {
194
            return $this->saveTranslation($locale, $attributes);
195
        });
196
    }
197
198
    /**
199
     * Populate the translations.
200
     *
201
     * @param array $attributes
202
     *
203
     * @return $this
204
     *
205
     * @throws \Illuminate\Database\Eloquent\MassAssignmentException
206
     */
207
    public function fill(array $attributes)
208
    {
209
        if (!isset(static::$i18nAttributes[$this->getTable()])) {
0 ignored issues
show
The method getTable() does not exist on Laraplus\Data\Translatable. Did you maybe mean getI18nTable()? ( Ignorable by Annotation )

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

209
        if (!isset(static::$i18nAttributes[$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...
210
            $this->initTranslatableAttributes();
211
        }
212
213
        return parent::fill($attributes);
214
    }
215
216
    /**
217
     * Initialize translatable attributes.
218
     */
219
    protected function initTranslatableAttributes()
220
    {
221
        if (property_exists($this, 'translatable')) {
222
            $attributes = $this->translatable;
223
        } else {
224
            $attributes = $this->getTranslatableAttributesFromSchema();
225
        }
226
227
        static::$i18nAttributes[$this->getTable()] = $attributes;
228
    }
229
230
    /**
231
     * Return an array of translatable attributes from schema.
232
     *
233
     * @return array
234
     */
235
    protected function getTranslatableAttributesFromSchema()
236
    {
237
        if ((!$con = $this->getConnection()) || (!$builder = $con->getSchemaBuilder())) {
0 ignored issues
show
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

237
        if ((!$con = $this->/** @scrutinizer ignore-call */ getConnection()) || (!$builder = $con->getSchemaBuilder())) {
Loading history...
238
            return [];
239
        }
240
241
        if ($columns = TranslatableConfig::cacheGet($this->getI18nTable())) {
242
            return $columns;
243
        }
244
245
        $columns = $builder->getColumnListing($this->getI18nTable());
246
247
        unset($columns[array_search($this->getForeignKey(), $columns)]);
0 ignored issues
show
It seems like getForeignKey() 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

247
        unset($columns[array_search($this->/** @scrutinizer ignore-call */ getForeignKey(), $columns)]);
Loading history...
248
249
        TranslatableConfig::cacheSet($this->getI18nTable(), $columns);
250
251
        return $columns;
252
    }
253
254
    /**
255
     * Return a collection of translated attributes in a given locale.
256
     *
257
     * @param $locale
258
     *
259
     * @return \Laraplus\Data\TranslationModel|null
260
     */
261
    public function translate($locale)
262
    {
263
        $found = $this->translations->where($this->getLocaleKey(), $locale)->first();
264
265
        if (!$found && $this->shouldFallback($locale)) {
266
            return $this->translate($this->getFallbackLocale());
267
        }
268
269
        return $found;
270
    }
271
272
    /**
273
     * Return a collection of translated attributes in a given locale or create a new one.
274
     *
275
     * @param $locale
276
     *
277
     * @return \Laraplus\Data\TranslationModel
278
     */
279
    public function translateOrNew($locale)
280
    {
281
        if (is_null($instance = $this->translate($locale))) {
282
            return $this->newModelInstance();
0 ignored issues
show
It seems like newModelInstance() 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

282
            return $this->/** @scrutinizer ignore-call */ newModelInstance();
Loading history...
283
        }
284
285
        return $instance;
286
    }
287
288
    /**
289
     * Translations relationship.
290
     *
291
     * @return \Illuminate\Database\Eloquent\Relations\HasMany
292
     */
293
    public function translations()
294
    {
295
        $localKey = $this->getKeyName();
296
        $foreignKey = $this->getForeignKey();
297
        $instance = $this->translationModel();
298
299
        return new HasMany($instance->newQuery(), $this, $instance->getTable().'.'.$foreignKey, $localKey);
0 ignored issues
show
$this of type Laraplus\Data\Translatable is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $parent of Illuminate\Database\Eloq...\HasMany::__construct(). ( Ignorable by Annotation )

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

299
        return new HasMany($instance->newQuery(), /** @scrutinizer ignore-type */ $this, $instance->getTable().'.'.$foreignKey, $localKey);
Loading history...
300
    }
301
302
    /**
303
     * Returns the default translation model instance.
304
     *
305
     * @return TranslationModel
306
     */
307
    public function translationModel()
308
    {
309
        $translation = new TranslationModel;
310
311
        $translation->setConnection($this->getI18nConnection());
312
        $translation->setTable($this->getI18nTable());
313
        $translation->setKeyName($this->getForeignKey());
314
        $translation->setLocaleKey($this->getLocaleKey());
315
316
        if ($attributes = $this->translatableAttributes()) {
317
            $translation->fillable(array_intersect($attributes, $this->getFillable()));
0 ignored issues
show
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

317
            $translation->fillable(array_intersect($attributes, $this->/** @scrutinizer ignore-call */ getFillable()));
Loading history...
318
        }
319
320
        return $translation;
321
    }
322
323
    /**
324
     * Return an array of translatable attributes.
325
     *
326
     * @return array
327
     */
328
    public function translatableAttributes()
329
    {
330
        if (!isset(static::$i18nAttributes[$this->getTable()])) {
331
            return [];
332
        }
333
334
        return static::$i18nAttributes[$this->getTable()];
335
    }
336
337
    /**
338
     * Return the name of the locale key.
339
     *
340
     * @return string
341
     */
342
    public function getLocaleKey()
343
    {
344
        return TranslatableConfig::dbKey();
345
    }
346
347
    /**
348
     * Set the current locale.
349
     *
350
     * @param $locale
351
     *
352
     * @return $this
353
     */
354
    public function setLocale($locale)
355
    {
356
        $this->overrideLocale = $locale;
357
        $this->localeChanged = true;
358
359
        return $this;
360
    }
361
362
    /**
363
     * Return the current locale.
364
     *
365
     * @return string
366
     */
367
    public function getLocale()
368
    {
369
        if ($this->overrideLocale) {
370
            return $this->overrideLocale;
371
        }
372
373
        if (property_exists($this, 'locale')) {
374
            return $this->locale;
375
        }
376
377
        return TranslatableConfig::currentLocale();
378
    }
379
380
    /**
381
     * Set the fallback locale.
382
     *
383
     * @param $locale
384
     *
385
     * @return $this
386
     */
387
    public function setFallbackLocale($locale)
388
    {
389
        $this->overrideFallbackLocale = $locale;
390
391
        return $this;
392
    }
393
394
    /**
395
     * Return the fallback locale.
396
     *
397
     * @return string
398
     */
399
    public function getFallbackLocale()
400
    {
401
        if ($this->overrideFallbackLocale) {
402
            return $this->overrideFallbackLocale;
403
        }
404
405
        if (property_exists($this, 'fallbackLocale')) {
406
            return $this->fallbackLocale;
407
        }
408
409
        return TranslatableConfig::fallbackLocale();
410
    }
411
412
    /**
413
     * Set if model should select only translated rows.
414
     *
415
     * @param bool $onlyTranslated
416
     *
417
     * @return $this
418
     */
419
    public function setOnlyTranslated($onlyTranslated)
420
    {
421
        $this->overrideOnlyTranslated = $onlyTranslated;
422
423
        return $this;
424
    }
425
426
    /**
427
     * Return only translated rows.
428
     *
429
     * @return bool
430
     */
431
    public function getOnlyTranslated()
432
    {
433
        if (!is_null($this->overrideOnlyTranslated)) {
434
            return $this->overrideOnlyTranslated;
435
        }
436
437
        if (property_exists($this, 'onlyTranslated')) {
438
            return $this->onlyTranslated;
439
        }
440
441
        return TranslatableConfig::onlyTranslated();
442
    }
443
444
    /**
445
     * Set if model should select only translated rows.
446
     *
447
     * @param bool $withFallback
448
     *
449
     * @return $this
450
     */
451
    public function setWithFallback($withFallback)
452
    {
453
        $this->overrideWithFallback = $withFallback;
454
455
        return $this;
456
    }
457
458
    /**
459
     * Return current locale with fallback.
460
     *
461
     * @return bool
462
     */
463
    public function getWithFallback()
464
    {
465
        if (!is_null($this->overrideWithFallback)) {
466
            return $this->overrideWithFallback;
467
        }
468
469
        if (property_exists($this, 'withFallback')) {
470
            return $this->withFallback;
471
        }
472
473
        return TranslatableConfig::withFallback();
474
    }
475
476
    /**
477
     * Return the i18n connection name associated with the model.
478
     *
479
     * @return string
480
     */
481
    public function getI18nConnection()
482
    {
483
        return $this->getConnectionName();
0 ignored issues
show
It seems like getConnectionName() 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

483
        return $this->/** @scrutinizer ignore-call */ getConnectionName();
Loading history...
484
    }
485
486
    /**
487
     * Return the i18n table associated with the model.
488
     *
489
     * @return string
490
     */
491
    public function getI18nTable()
492
    {
493
        return $this->getTable().$this->getTranslationTableSuffix();
494
    }
495
496
    /**
497
     * Return the i18n table suffix.
498
     *
499
     * @return string
500
     */
501
    public function getTranslationTableSuffix()
502
    {
503
        return TranslatableConfig::dbSuffix();
504
    }
505
506
    /**
507
     * Should fall back to a primary translation.
508
     *
509
     * @param string|null $locale
510
     *
511
     * @return bool
512
     */
513
    public function shouldFallback($locale = null)
514
    {
515
        if (!$this->getWithFallback() || !$this->getFallbackLocale()) {
516
            return false;
517
        }
518
519
        $locale = $locale ?: $this->getLocale();
520
521
        return $locale != $this->getFallbackLocale();
522
    }
523
524
    /**
525
     * Create a new Eloquent query builder for the model.
526
     *
527
     * @param \Illuminate\Database\Query\Builder $query
528
     *
529
     * @return \Illuminate\Database\Eloquent\Builder|static
530
     */
531
    public function newEloquentBuilder($query)
532
    {
533
        return new Builder($query);
534
    }
535
536
    /**
537
     * Return a new query builder instance for the connection.
538
     *
539
     * @return \Illuminate\Database\Query\Builder
540
     */
541
    protected function newBaseQueryBuilder()
542
    {
543
        $conn = $this->getConnection();
544
        $grammar = $conn->getQueryGrammar();
545
        $builder = new QueryBuilder($conn, $grammar, $conn->getPostProcessor());
546
547
        return $builder->setModel($this);
0 ignored issues
show
$this of type Laraplus\Data\Translatable is incompatible with the type Illuminate\Database\Eloquent\Model expected by parameter $model of Laraplus\Data\QueryBuilder::setModel(). ( Ignorable by Annotation )

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

547
        return $builder->setModel(/** @scrutinizer ignore-type */ $this);
Loading history...
548
    }
549
550
    /**
551
     * Return the attributes that have been changed since the last sync.
552
     *
553
     * @return array
554
     */
555
    public function getDirty()
556
    {
557
        $dirty = parent::getDirty();
558
559
        if (!$this->localeChanged) {
560
            return $dirty;
561
        }
562
563
        foreach ($this->translatableAttributes() as $key) {
564
            if (isset($this->attributes[$key])) {
565
                $dirty[$key] = $this->attributes[$key];
566
            }
567
        }
568
569
        return $dirty;
570
    }
571
572
    /**
573
     * Sync the original attributes with the current.
574
     *
575
     * @return $this
576
     */
577
    public function syncOriginal()
578
    {
579
        $this->localeChanged = false;
580
581
        return parent::syncOriginal();
582
    }
583
584
    /**
585
     * Prefix column names with the translation table instead of the model table if the given column is translated.
586
     *
587
     * @param $column
588
     *
589
     * @return string
590
     */
591
    public function qualifyColumn($column)
592
    {
593
        if (in_array($column, $this->translatableAttributes(), true)) {
594
            return sprintf('%s.%s', $this->getI18nTable(), $column);
595
        }
596
597
        return parent::qualifyColumn($column);
598
    }
599
}
600