diff --git a/.codeclimate.yml b/.codeclimate.yml index afbc22ee..1d955bb3 100644 --- a/.codeclimate.yml +++ b/.codeclimate.yml @@ -40,5 +40,9 @@ plugins: enabled: false CyclomaticComplexity: enabled: false + CleanCode/ElseExpression: + enabled: false + CleanCode/BooleanArgumentFlag: + enabled: false phan: enabled: false diff --git a/.php_cs b/.php_cs new file mode 100644 index 00000000..f310dd9b --- /dev/null +++ b/.php_cs @@ -0,0 +1,35 @@ +in('src/Translatable'); + +return PhpCsFixer\Config::create() + ->setRules([ + 'ordered_class_elements' => [ + 'order' => [ + 'use_trait', + 'constant_public', + 'constant_protected', + 'constant_private', + 'property_public', + 'property_protected', + 'property_private', + 'construct', + 'method_public_static', + 'method_public', + 'method_protected_static', + 'method_protected', + 'method_private_static', + 'method_private', + 'destruct', + 'magic', + ], + 'sortAlgorithm' => 'alpha', + ], + 'yoda_style' => [ + 'equal' => false, + 'identical' => false, + 'less_and_greater' => null, + ], + ]) + ->setFinder($finder); diff --git a/composer.json b/composer.json index 3bc6db6b..f7003fb2 100644 --- a/composer.json +++ b/composer.json @@ -48,5 +48,8 @@ }, "config": { "sort-packages": true + }, + "scripts": { + "csfix": "php-cs-fixer fix --using-cache=no" } } diff --git a/src/Translatable/Contracts/Translatable.php b/src/Translatable/Contracts/Translatable.php new file mode 100644 index 00000000..612cd19e --- /dev/null +++ b/src/Translatable/Contracts/Translatable.php @@ -0,0 +1,43 @@ +load(); } - public function load(): void + public function add(string $locale): void { - $localesConfig = (array) $this->config->get('translatable.locales', []); - - if (empty($localesConfig)) { - throw new LocalesNotDefinedException('Please make sure you have run "php artisan config:publish astrotomic/laravel-translatable" and that the locales configuration is defined.'); - } - - $this->locales = []; - foreach ($localesConfig as $key => $locale) { - if (is_string($key) && is_array($locale)) { - $this->locales[$key] = $key; - foreach ($locale as $country) { - $countryLocale = $this->getCountryLocale($key, $country); - $this->locales[$countryLocale] = $countryLocale; - } - } elseif (is_string($locale)) { - $this->locales[$locale] = $locale; - } - } + $this->locales[$locale] = $locale; } public function all(): array @@ -65,9 +48,9 @@ public function current() return $this->config->get('translatable.locale') ?: $this->translator->getLocale(); } - public function has(string $locale): bool + public function forget(string $locale): void { - return isset($this->locales[$locale]); + unset($this->locales[$locale]); } public function get(string $locale): ?string @@ -75,14 +58,14 @@ public function get(string $locale): ?string return $this->locales[$locale] ?? null; } - public function add(string $locale): void + public function getCountryLocale(string $locale, string $country): string { - $this->locales[$locale] = $locale; + return $locale.$this->getLocaleSeparator().$country; } - public function forget(string $locale): void + public function getLanguageFromCountryBasedLocale(string $locale): string { - unset($this->locales[$locale]); + return explode($this->getLocaleSeparator(), $locale)[0]; } public function getLocaleSeparator(): string @@ -90,9 +73,9 @@ public function getLocaleSeparator(): string return $this->config->get('translatable.locale_separator') ?: '-'; } - public function getCountryLocale(string $locale, string $country): string + public function has(string $locale): bool { - return $locale.$this->getLocaleSeparator().$country; + return isset($this->locales[$locale]); } public function isLocaleCountryBased(string $locale): bool @@ -100,14 +83,26 @@ public function isLocaleCountryBased(string $locale): bool return strpos($locale, $this->getLocaleSeparator()) !== false; } - public function getLanguageFromCountryBasedLocale(string $locale): string + public function load(): void { - return explode($this->getLocaleSeparator(), $locale)[0]; - } + $localesConfig = (array) $this->config->get('translatable.locales', []); - public function toArray(): array - { - return $this->all(); + if (empty($localesConfig)) { + throw new LocalesNotDefinedException('Please make sure you have run "php artisan config:publish astrotomic/laravel-translatable" and that the locales configuration is defined.'); + } + + $this->locales = []; + foreach ($localesConfig as $key => $locale) { + if (is_string($key) && is_array($locale)) { + $this->locales[$key] = $key; + foreach ($locale as $country) { + $countryLocale = $this->getCountryLocale($key, $country); + $this->locales[$countryLocale] = $countryLocale; + } + } elseif (is_string($locale)) { + $this->locales[$locale] = $locale; + } + } } public function offsetExists($key): bool @@ -133,4 +128,9 @@ public function offsetUnset($key) { $this->forget($key); } + + public function toArray(): array + { + return $this->all(); + } } diff --git a/src/Translatable/Traits/Relationship.php b/src/Translatable/Traits/Relationship.php new file mode 100644 index 00000000..a9b261da --- /dev/null +++ b/src/Translatable/Traits/Relationship.php @@ -0,0 +1,67 @@ +getTranslationRelationKey(); + } + + /** + * @internal will change to protected + */ + public function getTranslationModelName(): string + { + return $this->translationModel ?: $this->getTranslationModelNameDefault(); + } + + /** + * @internal will change to private + */ + public function getTranslationModelNameDefault(): string + { + $modelName = get_class($this); + + if ($namespace = $this->getTranslationModelNamespace()) { + $modelName = $namespace.'\\'.class_basename(get_class($this)); + } + + return $modelName.config('translatable.translation_suffix', 'Translation'); + } + + /** + * @internal will change to private + */ + public function getTranslationModelNamespace(): ?string + { + return config('translatable.translation_model_namespace'); + } + + /** + * @internal will change to protected + */ + public function getTranslationRelationKey(): string + { + if ($this->translationForeignKey) { + return $this->translationForeignKey; + } + + return $this->getForeignKey(); + } + + public function translations(): HasMany + { + return $this->hasMany($this->getTranslationModelName(), $this->getTranslationRelationKey()); + } +} diff --git a/src/Translatable/Traits/Scopes.php b/src/Translatable/Traits/Scopes.php new file mode 100644 index 00000000..58d933eb --- /dev/null +++ b/src/Translatable/Traits/Scopes.php @@ -0,0 +1,130 @@ +useFallback(); + $translationTable = $this->getTranslationsTable(); + $localeKey = $this->getLocaleKey(); + + $query + ->select($this->getTable().'.'.$this->getKeyName(), $translationTable.'.'.$translationField) + ->leftJoin($translationTable, $translationTable.'.'.$this->getTranslationRelationKey(), '=', $this->getTable().'.'.$this->getKeyName()) + ->where($translationTable.'.'.$localeKey, $this->locale()); + + if ($withFallback) { + $query->orWhere(function (Builder $q) use ($translationTable, $localeKey) { + $q + ->where($translationTable.'.'.$localeKey, $this->getFallbackLocale()) + ->whereNotIn($translationTable.'.'.$this->getTranslationRelationKey(), function (QueryBuilder $q) use ( + $translationTable, + $localeKey + ) { + $q + ->select($translationTable.'.'.$this->getTranslationRelationKey()) + ->from($translationTable) + ->where($translationTable.'.'.$localeKey, $this->locale()); + }); + }); + } + + return $query; + } + + public function scopeNotTranslatedIn(Builder $query, ?string $locale = null) + { + $locale = $locale ?: $this->locale(); + + return $query->whereDoesntHave('translations', function (Builder $q) use ($locale) { + $q->where($this->getLocaleKey(), '=', $locale); + }); + } + + public function scopeOrderByTranslation(Builder $query, string $translationField, string $sortMethod = 'asc') + { + $translationTable = $this->getTranslationsTable(); + $localeKey = $this->getLocaleKey(); + $table = $this->getTable(); + $keyName = $this->getKeyName(); + + return $query + ->join($translationTable, function (JoinClause $join) use ($translationTable, $localeKey, $table, $keyName) { + $join + ->on($translationTable.'.'.$this->getTranslationRelationKey(), '=', $table.'.'.$keyName) + ->where($translationTable.'.'.$localeKey, $this->locale()); + }) + ->orderBy($translationTable.'.'.$translationField, $sortMethod) + ->select($table.'.*') + ->with('translations'); + } + + public function scopeOrWhereTranslation(Builder $query, string $translationField, $value, ?string $locale = null) + { + return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'orWhereHas'); + } + + public function scopeOrWhereTranslationLike(Builder $query, string $translationField, $value, ?string $locale = null) + { + return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'orWhereHas', 'LIKE'); + } + + public function scopeTranslated(Builder $query) + { + return $query->has('translations'); + } + + public function scopeTranslatedIn(Builder $query, ?string $locale = null) + { + $locale = $locale ?: $this->locale(); + + return $query->whereHas('translations', function (Builder $q) use ($locale) { + $q->where($this->getLocaleKey(), '=', $locale); + }); + } + + public function scopeWhereTranslation(Builder $query, string $translationField, $value, ?string $locale = null, string $method = 'whereHas', string $operator = '=') + { + return $query->$method('translations', function (Builder $query) use ($translationField, $value, $locale, $operator) { + $query->where($this->getTranslationsTable().'.'.$translationField, $operator, $value); + + if ($locale) { + $query->where($this->getTranslationsTable().'.'.$this->getLocaleKey(), $operator, $locale); + } + }); + } + + public function scopeWhereTranslationLike(Builder $query, string $translationField, $value, ?string $locale = null) + { + return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'whereHas', 'LIKE'); + } + + public function scopeWithTranslation(Builder $query) + { + $query->with([ + 'translations' => function (Relation $query) { + if ($this->useFallback()) { + $locale = $this->locale(); + $countryFallbackLocale = $this->getFallbackLocale($locale); // e.g. de-DE => de + $locales = array_unique([$locale, $countryFallbackLocale, $this->getFallbackLocale()]); + + return $query->whereIn($this->getTranslationsTable().'.'.$this->getLocaleKey(), $locales); + } + + return $query->where($this->getTranslationsTable().'.'.$this->getLocaleKey(), $this->locale()); + }, + ]); + } + + private function getTranslationsTable(): string + { + return app()->make($this->getTranslationModelName())->getTable(); + } +} diff --git a/src/Translatable/Translatable.php b/src/Translatable/Translatable.php index f5548576..fa3b0985 100644 --- a/src/Translatable/Translatable.php +++ b/src/Translatable/Translatable.php @@ -4,12 +4,9 @@ use Illuminate\Support\Str; use Illuminate\Database\Eloquent\Model; -use Illuminate\Database\Eloquent\Builder; -use Illuminate\Database\Query\JoinClause; +use Astrotomic\Translatable\Traits\Scopes; use Illuminate\Database\Eloquent\Collection; -use Illuminate\Database\Eloquent\Relations\HasMany; -use Illuminate\Database\Eloquent\Relations\Relation; -use Illuminate\Database\Query\Builder as QueryBuilder; +use Astrotomic\Translatable\Traits\Relationship; /** * @property-read Collection|Model[] $translations @@ -22,6 +19,8 @@ */ trait Translatable { + use Scopes, Relationship; + protected static $autoloadTranslations = null; protected $defaultLocale; @@ -34,129 +33,82 @@ public static function bootTranslatable(): void }); } - public function translate(?string $locale = null, bool $withFallback = false): ?Model + public static function defaultAutoloadTranslations(): void { - return $this->getTranslation($locale, $withFallback); + self::$autoloadTranslations = null; } - public function translateOrDefault(?string $locale = null): ?Model + public static function disableAutoloadTranslations(): void { - return $this->getTranslation($locale, true); + self::$autoloadTranslations = false; } - public function translateOrNew(?string $locale = null): Model + public static function enableAutoloadTranslations(): void { - return $this->getTranslationOrNew($locale); + self::$autoloadTranslations = true; } - public function getTranslation(?string $locale = null, bool $withFallback = null): ?Model + public function attributesToArray() { - $configFallbackLocale = $this->getFallbackLocale(); - $locale = $locale ?: $this->locale(); - $withFallback = $withFallback === null ? $this->useFallback() : $withFallback; - $fallbackLocale = $this->getFallbackLocale($locale); + $attributes = parent::attributesToArray(); - if ($translation = $this->getTranslationByLocaleKey($locale)) { - return $translation; - } - if ($withFallback && $fallbackLocale) { - if ($translation = $this->getTranslationByLocaleKey($fallbackLocale)) { - return $translation; - } - if ( - is_string($configFallbackLocale) - && $fallbackLocale !== $configFallbackLocale - && $translation = $this->getTranslationByLocaleKey($configFallbackLocale) - ) { - return $translation; - } + if ( + (! $this->relationLoaded('translations') && ! $this->toArrayAlwaysLoadsTranslations() && is_null(self::$autoloadTranslations)) + || self::$autoloadTranslations === false + ) { + return $attributes; } - return null; - } - - public function hasTranslation(?string $locale = null): bool - { - $locale = $locale ?: $this->locale(); + $hiddenAttributes = $this->getHidden(); - foreach ($this->translations as $translation) { - if ($translation->getAttribute($this->getLocaleKey()) == $locale) { - return true; + foreach ($this->translatedAttributes as $field) { + if (in_array($field, $hiddenAttributes)) { + continue; } - } - return false; - } - - public function getTranslationModelName(): string - { - return $this->translationModel ?: $this->getTranslationModelNameDefault(); - } - - public function getTranslationModelNameDefault(): string - { - $modelName = get_class($this); - - if ($namespace = $this->getTranslationModelNamespace()) { - $modelName = $namespace.'\\'.class_basename(get_class($this)); + $attributes[$field] = $this->getAttributeOrFallback(null, $field); } - return $modelName.config('translatable.translation_suffix', 'Translation'); - } - - public function getTranslationModelNamespace(): ?string - { - return config('translatable.translation_model_namespace'); + return $attributes; } - public function getRelationKey(): string + /** + * @param string|array|null $locales The locales to be deleted + */ + public function deleteTranslations($locales = null): void { - if ($this->translationForeignKey) { - return $this->translationForeignKey; + if ($locales === null) { + $translations = $this->translations()->get(); + } else { + $locales = (array) $locales; + $translations = $this->translations()->whereIn($this->getLocaleKey(), $locales)->get(); } - return $this->getForeignKey(); - } - - public function getLocaleKey(): string - { - return $this->localeKey ?: config('translatable.locale_key', 'locale'); - } - - public function translations(): HasMany - { - return $this->hasMany($this->getTranslationModelName(), $this->getRelationKey()); - } + foreach ($translations as $translation) { + $translation->delete(); + } - private function usePropertyFallback(): bool - { - return $this->useFallback() && config('translatable.use_property_fallback', false); + // we need to manually "reload" the collection built from the relationship + // otherwise $this->translations()->get() would NOT be the same as $this->translations + $this->load('translations'); } - private function getAttributeOrFallback(?string $locale, string $attribute) + public function fill(array $attributes) { - $translation = $this->getTranslation($locale); - - if ( - ( - ! $translation instanceof Model - || $this->isEmptyTranslatableAttribute($attribute, $translation->$attribute) - ) - && $this->usePropertyFallback() - ) { - $translation = $this->getTranslation($this->getFallbackLocale(), false); - } - - if ($translation instanceof Model) { - return $translation->$attribute; + foreach ($attributes as $key => $values) { + if ($this->getLocalesHelper()->has($key)) { + $this->getTranslationOrNew($key)->fill($values); + unset($attributes[$key]); + } else { + [$attribute, $locale] = $this->getAttributeAndLocale($key); + if ($this->isTranslationAttribute($attribute) and $this->getLocalesHelper()->has($locale)) { + $this->getTranslationOrNew($locale)->fill([$attribute => $values]); + unset($attributes[$key]); + } + } } - return null; - } - - protected function isEmptyTranslatableAttribute(string $key, $value): bool - { - return empty($value); + return parent::fill($attributes); } public function getAttribute($key) @@ -183,129 +135,97 @@ public function getAttribute($key) return parent::getAttribute($key); } - public function setAttribute($key, $value) + public function getDefaultLocale(): ?string { - [$attribute, $locale] = $this->getAttributeAndLocale($key); - - if ($this->isTranslationAttribute($attribute)) { - $this->getTranslationOrNew($locale)->$attribute = $value; - - return $this; - } + return $this->defaultLocale; + } - return parent::setAttribute($key, $value); + /** + * @internal will change to protected + */ + public function getLocaleKey(): string + { + return $this->localeKey ?: config('translatable.locale_key', 'locale'); } - protected function getTranslationOrNew(?string $locale = null): Model + public function getNewTranslation(string $locale): Model { - $locale = $locale ?: $this->locale(); + $modelName = $this->getTranslationModelName(); - if (($translation = $this->getTranslation($locale, false)) === null) { - $translation = $this->getNewTranslation($locale); - } + /** @var Model $translation */ + $translation = new $modelName(); + $translation->setAttribute($this->getLocaleKey(), $locale); + $this->translations->add($translation); return $translation; } - public function fill(array $attributes) + public function getTranslation(?string $locale = null, bool $withFallback = null): ?Model { - foreach ($attributes as $key => $values) { - if ($this->isKeyALocale($key)) { - $this->getTranslationOrNew($key)->fill($values); - unset($attributes[$key]); - } else { - [$attribute, $locale] = $this->getAttributeAndLocale($key); - if ($this->isTranslationAttribute($attribute) and $this->isKeyALocale($locale)) { - $this->getTranslationOrNew($locale)->fill([$attribute => $values]); - unset($attributes[$key]); - } - } - } - - return parent::fill($attributes); - } + $configFallbackLocale = $this->getFallbackLocale(); + $locale = $locale ?: $this->locale(); + $withFallback = $withFallback === null ? $this->useFallback() : $withFallback; + $fallbackLocale = $this->getFallbackLocale($locale); - private function getTranslationByLocaleKey(string $key): ?Model - { - foreach ($this->translations as $translation) { - if ($translation->getAttribute($this->getLocaleKey()) == $key) { + if ($translation = $this->getTranslationByLocaleKey($locale)) { + return $translation; + } + if ($withFallback && $fallbackLocale) { + if ($translation = $this->getTranslationByLocaleKey($fallbackLocale)) { return $translation; } - } - - return null; - } - - private function getFallbackLocale(?string $locale = null): ?string - { - if ($locale && $this->isLocaleCountryBased($locale)) { - if ($fallback = $this->getLanguageFromCountryBasedLocale($locale)) { - return $fallback; + if ( + is_string($configFallbackLocale) + && $fallbackLocale !== $configFallbackLocale + && $translation = $this->getTranslationByLocaleKey($configFallbackLocale) + ) { + return $translation; } } - return config('translatable.fallback_locale'); - } - - private function isLocaleCountryBased(string $locale): bool - { - return $this->getLocalesHelper()->isLocaleCountryBased($locale); + return null; } - private function getLanguageFromCountryBasedLocale(string $locale): string + public function getTranslationOrNew(?string $locale = null): Model { - return $this->getLocalesHelper()->getLanguageFromCountryBasedLocale($locale); - } + $locale = $locale ?: $this->locale(); - private function useFallback(): bool - { - if (isset($this->useTranslationFallback) && $this->useTranslationFallback !== null) { - return $this->useTranslationFallback; + if (($translation = $this->getTranslation($locale, false)) === null) { + $translation = $this->getNewTranslation($locale); } - return (bool) config('translatable.use_fallback'); - } - - public function isTranslationAttribute(string $key): bool - { - return in_array($key, $this->translatedAttributes); + return $translation; } - protected function isKeyALocale(string $key): bool + public function getTranslationsArray(): array { - return $this->getLocalesHelper()->has($key); - } + $translations = []; - protected function getLocales(): array - { - return $this->getLocalesHelper()->all(); - } + foreach ($this->translations as $translation) { + foreach ($this->translatedAttributes as $attr) { + $translations[$translation->{$this->getLocaleKey()}][$attr] = $translation->{$attr}; + } + } - protected function getLocaleSeparator(): string - { - return $this->getLocalesHelper()->getLocaleSeparator(); + return $translations; } - protected function saveTranslations(): bool + public function hasTranslation(?string $locale = null): bool { - $saved = true; - - if (! $this->relationLoaded('translations')) { - return $saved; - } + $locale = $locale ?: $this->locale(); foreach ($this->translations as $translation) { - if ($saved && $this->isTranslationDirty($translation)) { - if (! empty($connectionName = $this->getConnectionName())) { - $translation->setConnection($connectionName); - } - - $translation->setAttribute($this->getRelationKey(), $this->getKey()); - $saved = $translation->save(); + if ($translation->getAttribute($this->getLocaleKey()) == $locale) { + return true; } } - return $saved; + return false; + } + + public function isTranslationAttribute(string $key): bool + { + return in_array($key, $this->translatedAttributes); } public function replicateWithTranslations(array $except = null): Model @@ -321,229 +241,140 @@ public function replicateWithTranslations(array $except = null): Model return $newInstance; } - protected function isTranslationDirty(Model $translation): bool - { - $dirtyAttributes = $translation->getDirty(); - unset($dirtyAttributes[$this->getLocaleKey()]); - - return count($dirtyAttributes) > 0; - } - - public function getNewTranslation(string $locale): Model + public function setAttribute($key, $value) { - $modelName = $this->getTranslationModelName(); - - /** @var Model $translation */ - $translation = new $modelName(); - $translation->setAttribute($this->getLocaleKey(), $locale); - $this->translations->add($translation); - - return $translation; - } + [$attribute, $locale] = $this->getAttributeAndLocale($key); - public function __isset($key) - { - return $this->isTranslationAttribute($key) || parent::__isset($key); - } + if ($this->isTranslationAttribute($attribute)) { + $this->getTranslationOrNew($locale)->$attribute = $value; - public function scopeTranslatedIn(Builder $query, ?string $locale = null) - { - $locale = $locale ?: $this->locale(); + return $this; + } - return $query->whereHas('translations', function (Builder $q) use ($locale) { - $q->where($this->getLocaleKey(), '=', $locale); - }); + return parent::setAttribute($key, $value); } - public function scopeNotTranslatedIn(Builder $query, ?string $locale = null) + public function setDefaultLocale(?string $locale) { - $locale = $locale ?: $this->locale(); + $this->defaultLocale = $locale; - return $query->whereDoesntHave('translations', function (Builder $q) use ($locale) { - $q->where($this->getLocaleKey(), '=', $locale); - }); + return $this; } - public function scopeTranslated(Builder $query) + public function translate(?string $locale = null, bool $withFallback = false): ?Model { - return $query->has('translations'); + return $this->getTranslation($locale, $withFallback); } - public function scopeListsTranslations(Builder $query, string $translationField) + public function translateOrDefault(?string $locale = null): ?Model { - $withFallback = $this->useFallback(); - $translationTable = $this->getTranslationsTable(); - $localeKey = $this->getLocaleKey(); - - $query - ->select($this->getTable().'.'.$this->getKeyName(), $translationTable.'.'.$translationField) - ->leftJoin($translationTable, $translationTable.'.'.$this->getRelationKey(), '=', $this->getTable().'.'.$this->getKeyName()) - ->where($translationTable.'.'.$localeKey, $this->locale()); - if ($withFallback) { - $query->orWhere(function (Builder $q) use ($translationTable, $localeKey) { - $q->where($translationTable.'.'.$localeKey, $this->getFallbackLocale()) - ->whereNotIn($translationTable.'.'.$this->getRelationKey(), function (QueryBuilder $q) use ( - $translationTable, - $localeKey - ) { - $q->select($translationTable.'.'.$this->getRelationKey()) - ->from($translationTable) - ->where($translationTable.'.'.$localeKey, $this->locale()); - }); - }); - } + return $this->getTranslation($locale, true); } - public function scopeWithTranslation(Builder $query) + public function translateOrNew(?string $locale = null): Model { - $query->with([ - 'translations' => function (Relation $query) { - if ($this->useFallback()) { - $locale = $this->locale(); - $countryFallbackLocale = $this->getFallbackLocale($locale); // e.g. de-DE => de - $locales = array_unique([$locale, $countryFallbackLocale, $this->getFallbackLocale()]); - - return $query->whereIn($this->getTranslationsTable().'.'.$this->getLocaleKey(), $locales); - } - - return $query->where($this->getTranslationsTable().'.'.$this->getLocaleKey(), $this->locale()); - }, - ]); + return $this->getTranslationOrNew($locale); } - public function scopeWhereTranslation(Builder $query, string $translationField, $value, ?string $locale = null, string $method = 'whereHas', string $operator = '=') + protected function getLocalesHelper(): Locales { - return $query->$method('translations', function (Builder $query) use ($translationField, $value, $locale, $operator) { - $query->where($this->getTranslationsTable().'.'.$translationField, $operator, $value); - if ($locale) { - $query->where($this->getTranslationsTable().'.'.$this->getLocaleKey(), $operator, $locale); - } - }); + return app(Locales::class); } - public function scopeOrWhereTranslation(Builder $query, string $translationField, $value, ?string $locale = null) + protected function isEmptyTranslatableAttribute(string $key, $value): bool { - return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'orWhereHas'); + return empty($value); } - public function scopeWhereTranslationLike(Builder $query, string $translationField, $value, ?string $locale = null) + protected function isTranslationDirty(Model $translation): bool { - return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'whereHas', 'LIKE'); - } + $dirtyAttributes = $translation->getDirty(); + unset($dirtyAttributes[$this->getLocaleKey()]); - public function scopeOrWhereTranslationLike(Builder $query, string $translationField, $value, ?string $locale = null) - { - return $this->scopeWhereTranslation($query, $translationField, $value, $locale, 'orWhereHas', 'LIKE'); + return count($dirtyAttributes) > 0; } - public function scopeOrderByTranslation(Builder $query, string $translationField, string $sortMethod = 'asc') + protected function locale(): string { - $translationTable = $this->getTranslationsTable(); - $localeKey = $this->getLocaleKey(); - $table = $this->getTable(); - $keyName = $this->getKeyName(); + if ($this->defaultLocale) { + return $this->defaultLocale; + } - return $query - ->join($translationTable, function (JoinClause $join) use ($translationTable, $localeKey, $table, $keyName) { - $join - ->on($translationTable.'.'.$this->getRelationKey(), '=', $table.'.'.$keyName) - ->where($translationTable.'.'.$localeKey, $this->locale()); - }) - ->orderBy($translationTable.'.'.$translationField, $sortMethod) - ->select($table.'.*') - ->with('translations'); + return $this->getLocalesHelper()->current(); } - public function attributesToArray() + protected function saveTranslations(): bool { - $attributes = parent::attributesToArray(); + $saved = true; - if ( - (! $this->relationLoaded('translations') && ! $this->toArrayAlwaysLoadsTranslations() && is_null(self::$autoloadTranslations)) - || self::$autoloadTranslations === false - ) { - return $attributes; + if (! $this->relationLoaded('translations')) { + return $saved; } - $hiddenAttributes = $this->getHidden(); + foreach ($this->translations as $translation) { + if ($saved && $this->isTranslationDirty($translation)) { + if (! empty($connectionName = $this->getConnectionName())) { + $translation->setConnection($connectionName); + } - foreach ($this->translatedAttributes as $field) { - if (in_array($field, $hiddenAttributes)) { - continue; + $translation->setAttribute($this->getTranslationRelationKey(), $this->getKey()); + $saved = $translation->save(); } - - $attributes[$field] = $this->getAttributeOrFallback(null, $field); } - return $attributes; + return $saved; } - public function getTranslationsArray(): array + private function getAttributeAndLocale(string $key): array { - $translations = []; - - foreach ($this->translations as $translation) { - foreach ($this->translatedAttributes as $attr) { - $translations[$translation->{$this->getLocaleKey()}][$attr] = $translation->{$attr}; - } + if (Str::contains($key, ':')) { + return explode(':', $key); } - return $translations; + return [$key, $this->locale()]; } - private function getTranslationsTable(): string + private function getAttributeOrFallback(?string $locale, string $attribute) { - return app()->make($this->getTranslationModelName())->getTable(); - } + $translation = $this->getTranslation($locale); - protected function locale(): string - { - if ($this->defaultLocale) { - return $this->defaultLocale; + if ( + ( + ! $translation instanceof Model + || $this->isEmptyTranslatableAttribute($attribute, $translation->$attribute) + ) + && $this->usePropertyFallback() + ) { + $translation = $this->getTranslation($this->getFallbackLocale(), false); } - return $this->getLocalesHelper()->current(); - } - - public function setDefaultLocale(?string $locale) - { - $this->defaultLocale = $locale; - - return $this; - } + if ($translation instanceof Model) { + return $translation->$attribute; + } - public function getDefaultLocale(): ?string - { - return $this->defaultLocale; + return null; } - /** - * @param string|array|null $locales The locales to be deleted - */ - public function deleteTranslations($locales = null) + private function getFallbackLocale(?string $locale = null): ?string { - if ($locales === null) { - $translations = $this->translations()->get(); - } else { - $locales = (array) $locales; - $translations = $this->translations()->whereIn($this->getLocaleKey(), $locales)->get(); - } - foreach ($translations as $translation) { - $translation->delete(); + if ($locale && $this->getLocalesHelper()->isLocaleCountryBased($locale)) { + if ($fallback = $this->getLocalesHelper()->getLanguageFromCountryBasedLocale($locale)) { + return $fallback; + } } - // we need to manually "reload" the collection built from the relationship - // otherwise $this->translations()->get() would NOT be the same as $this->translations - $this->load('translations'); + return config('translatable.fallback_locale'); } - private function getAttributeAndLocale(string $key): array + private function getTranslationByLocaleKey(string $key): ?Model { - if (Str::contains($key, ':')) { - return explode(':', $key); + foreach ($this->translations as $translation) { + if ($translation->getAttribute($this->getLocaleKey()) == $key) { + return $translation; + } } - return [$key, $this->locale()]; + return null; } private function toArrayAlwaysLoadsTranslations(): bool @@ -551,23 +382,22 @@ private function toArrayAlwaysLoadsTranslations(): bool return config('translatable.to_array_always_loads_translations', true); } - public static function enableAutoloadTranslations(): void + private function useFallback(): bool { - self::$autoloadTranslations = true; - } + if (isset($this->useTranslationFallback) && $this->useTranslationFallback !== null) { + return $this->useTranslationFallback; + } - public static function defaultAutoloadTranslations(): void - { - self::$autoloadTranslations = null; + return (bool) config('translatable.use_fallback'); } - public static function disableAutoloadTranslations(): void + private function usePropertyFallback(): bool { - self::$autoloadTranslations = false; + return $this->useFallback() && config('translatable.use_property_fallback', false); } - protected function getLocalesHelper(): Locales + public function __isset($key) { - return app(Locales::class); + return $this->isTranslationAttribute($key) || parent::__isset($key); } } diff --git a/src/Translatable/TranslatableServiceProvider.php b/src/Translatable/TranslatableServiceProvider.php index 05e9f163..520e9911 100644 --- a/src/Translatable/TranslatableServiceProvider.php +++ b/src/Translatable/TranslatableServiceProvider.php @@ -13,6 +13,14 @@ public function boot() ], 'translatable'); } + public function provides() + { + return [ + 'translatable.locales', + Locales::class, + ]; + } + public function register() { $this->mergeConfigFrom( @@ -22,17 +30,9 @@ public function register() $this->registerTranslatableHelper(); } - public function registerTranslatableHelper() + protected function registerTranslatableHelper() { $this->app->singleton('translatable.locales', Locales::class); $this->app->singleton(Locales::class); } - - public function provides() - { - return [ - 'translatable.locales', - Locales::class, - ]; - } } diff --git a/tests/models/City.php b/tests/models/City.php index 4ae80fae..ad621b59 100644 --- a/tests/models/City.php +++ b/tests/models/City.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class City extends Eloquent +class City extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/Continent.php b/tests/models/Continent.php index b33657c6..fe46487a 100644 --- a/tests/models/Continent.php +++ b/tests/models/Continent.php @@ -4,11 +4,12 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; /** * A test class that has no required properties. */ -class Continent extends Eloquent +class Continent extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/Country.php b/tests/models/Country.php index 9b2cd40e..bd89e6cd 100644 --- a/tests/models/Country.php +++ b/tests/models/Country.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class Country extends Eloquent +class Country extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/CountryGuarded.php b/tests/models/CountryGuarded.php index 2397aae7..36fa86cc 100644 --- a/tests/models/CountryGuarded.php +++ b/tests/models/CountryGuarded.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class CountryGuarded extends Eloquent +class CountryGuarded extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/CountryStrict.php b/tests/models/CountryStrict.php index 46f5af26..76787697 100644 --- a/tests/models/CountryStrict.php +++ b/tests/models/CountryStrict.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class CountryStrict extends Eloquent +class CountryStrict extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/CountryWithCustomLocaleKey.php b/tests/models/CountryWithCustomLocaleKey.php index db8a8925..b7a32515 100644 --- a/tests/models/CountryWithCustomLocaleKey.php +++ b/tests/models/CountryWithCustomLocaleKey.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class CountryWithCustomLocaleKey extends Eloquent +class CountryWithCustomLocaleKey extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/CountryWithCustomTranslationModel.php b/tests/models/CountryWithCustomTranslationModel.php index 81e94364..5cd22dec 100644 --- a/tests/models/CountryWithCustomTranslationModel.php +++ b/tests/models/CountryWithCustomTranslationModel.php @@ -3,8 +3,9 @@ namespace Astrotomic\Translatable\Test\Model; use Astrotomic\Translatable\Translatable; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class CountryWithCustomTranslationModel extends Country +class CountryWithCustomTranslationModel extends Country implements TranslatableContract { use Translatable; diff --git a/tests/models/Food.php b/tests/models/Food.php index 2f77a36f..88b235b5 100644 --- a/tests/models/Food.php +++ b/tests/models/Food.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class Food extends Eloquent +class Food extends Eloquent implements TranslatableContract { use Translatable; diff --git a/tests/models/Person.php b/tests/models/Person.php index 05367433..736440f5 100644 --- a/tests/models/Person.php +++ b/tests/models/Person.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class Person extends Eloquent +class Person extends Eloquent implements TranslatableContract { protected $table = 'people'; diff --git a/tests/models/Vegetable.php b/tests/models/Vegetable.php index 625205ae..95df6ab2 100644 --- a/tests/models/Vegetable.php +++ b/tests/models/Vegetable.php @@ -4,8 +4,9 @@ use Astrotomic\Translatable\Translatable; use Illuminate\Database\Eloquent\Model as Eloquent; +use Astrotomic\Translatable\Contracts\Translatable as TranslatableContract; -class Vegetable extends Eloquent +class Vegetable extends Eloquent implements TranslatableContract { use Translatable;