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
|
|||||
45 | |||||
46 | if ($model->save() && is_array($translations)) { |
||||
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
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. ![]() |
|||||
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()); |
||||
124 | |||||
125 | (new TranslatableScope)->apply($query, $this); |
||||
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; |
||||
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()])) { |
||||
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())) { |
||||
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)]); |
||||
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(); |
||||
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); |
||||
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())); |
||||
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(); |
||||
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); |
||||
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 |
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.