diff --git a/.scrutinizer.yml b/.scrutinizer.yml index 748304d..ff9c7ac 100644 --- a/.scrutinizer.yml +++ b/.scrutinizer.yml @@ -22,7 +22,7 @@ checks: tools: external_code_coverage: timeout: 1200 - runs: 3 + runs: 4 php_code_sniffer: enabled: true config: diff --git a/.travis.yml b/.travis.yml index 2cff3b7..6aee27e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -6,6 +6,7 @@ php: - 7.1.3 - 7.1 - 7.2 + - 7.3 - nightly matrix: diff --git a/LICENSE.md b/LICENSE.md index 5f29328..ac02644 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,6 +1,6 @@ The MIT License (MIT) -Copyright (c) 2015-2017 | ARCANEDEV - Localization +Copyright (c) 2015-2019 | ARCANEDEV - Localization Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/composer.json b/composer.json index 73c1085..c976fe4 100644 --- a/composer.json +++ b/composer.json @@ -15,6 +15,7 @@ "license": "MIT", "require": { "php": ">=7.1.3", + "ext-json": "*", "arcanedev/support": "~4.4.0" }, "require-dev": { diff --git a/src/Exceptions/UntranslatableAttributeException.php b/src/Exceptions/UntranslatableAttributeException.php index ee9d8ff..34c54bb 100644 --- a/src/Exceptions/UntranslatableAttributeException.php +++ b/src/Exceptions/UntranslatableAttributeException.php @@ -6,4 +6,14 @@ * @package Arcanedev\Localization\Exceptions * @author ARCANEDEV */ -class UntranslatableAttributeException extends LocalizationException {} +class UntranslatableAttributeException extends LocalizationException +{ + public static function make(string $key, array $translatableAttributes) + { + $translatableAttributes = implode(', ', $translatableAttributes); + + return new static( + "The attribute `{$key}` is untranslatable because it's not available in the translatable array: `$translatableAttributes`" + ); + } +} diff --git a/src/Traits/HasTranslations.php b/src/Traits/HasTranslations.php index 95185d9..ee4d6c8 100644 --- a/src/Traits/HasTranslations.php +++ b/src/Traits/HasTranslations.php @@ -9,9 +9,31 @@ * * @package Arcanedev\Localization\Traits * @author ARCANEDEV + * + * @property array attributes + * @property-read array translations */ trait HasTranslations { + /* ----------------------------------------------------------------- + | Getters + | ----------------------------------------------------------------- + */ + + /** + * Get the translations. + * + * @return array + */ + public function getTranslationsAttribute(): array + { + return collect($this->getTranslatableAttributes()) + ->mapWithKeys(function (string $key) { + return [$key => $this->getTranslations($key)]; + }) + ->toArray(); + } + /* ----------------------------------------------------------------- | Main Methods | ----------------------------------------------------------------- @@ -34,10 +56,25 @@ abstract public function getTranslatableAttributes(); public function getAttributeValue($key) { return $this->isTranslatableAttribute($key) - ? $this->getTranslation($key, config('app.locale')) + ? $this->getTranslation($key, $this->getLocale()) : parent::getAttributeValue($key); } + /** + * Set a given attribute on the model. + * + * @param string $key + * @param mixed $value + * + * @return self + */ + public function setAttribute($key, $value) + { + return ( ! $this->isTranslatableAttribute($key) || is_array($value)) + ? parent::setAttribute($key, $value) + : $this->setTranslation($key, $this->getLocale(), $value); + } + /** * Get the translated attribute (alias). * @@ -56,13 +93,13 @@ public function trans($key, $locale = '') * * @param string $key * @param string $locale - * @param bool $useFallback + * @param bool $useFallbackLocale * * @return mixed */ - public function getTranslation($key, $locale, $useFallback = true) + public function getTranslation($key, $locale, $useFallbackLocale = true) { - $locale = $this->normalizeLocale($key, $locale, $useFallback); + $locale = $this->normalizeLocale($key, $locale, $useFallbackLocale); $translations = $this->getTranslations($key); $translation = $translations[$locale] ?? ''; @@ -74,15 +111,25 @@ public function getTranslation($key, $locale, $useFallback = true) /** * Get the translations for the given key. * - * @param string $key + * @param string|null $key * * @return array */ - public function getTranslations($key) + public function getTranslations($key = null) { - $this->guardAgainstUntranslatableAttribute($key); + if ($key !== null) { + $this->guardAgainstNonTranslatableAttribute($key); + + return array_filter(json_decode($this->getAttributeFromArray($key) ?? '' ?: '{}', true) ?: [], function ($value) { + return $value !== null && $value !== false && $value !== ''; + }); + } - return json_decode($this->getAttributeFromArray($key) ?: '{}', true); + return array_reduce($this->getTranslatableAttributes(), function ($result, $item) { + $result[$item] = $this->getTranslations($item); + + return $result; + }); } /** @@ -96,16 +143,18 @@ public function getTranslations($key) */ public function setTranslation($key, $locale, $value) { - $this->guardAgainstUntranslatableAttribute($key); + $this->guardAgainstNonTranslatableAttribute($key); $translations = $this->getTranslations($key); $oldValue = $translations[$locale] ?? ''; - if ($this->hasSetMutator($key)) - $value = $this->{'set'.Str::studly($key).'Attribute'}($value); + if ($this->hasSetMutator($key)) { + $this->{'set'.Str::studly($key).'Attribute'}($value); + $value = $this->attributes[$key]; + } $translations[$locale] = $value; - $this->attributes[$key] = json_encode($translations); + $this->attributes[$key] = $this->asJson($translations); event(new TranslationHasBeenSet($this, $key, $locale, $oldValue, $value)); @@ -122,7 +171,7 @@ public function setTranslation($key, $locale, $value) */ public function setTranslations($key, array $translations) { - $this->guardAgainstUntranslatableAttribute($key); + $this->guardAgainstNonTranslatableAttribute($key); foreach ($translations as $locale => $translation) { $this->setTranslation($key, $locale, $translation); @@ -145,7 +194,7 @@ public function forgetTranslation($key, $locale) unset($translations[$locale]); if ($this->hasSetMutator($key)) - $this->attributes[$key] = json_encode($this->mutateTranslations($key, $translations)); + $this->attributes[$key] = $this->asJson($translations); else $this->setAttribute($key, $translations); @@ -161,7 +210,7 @@ public function forgetTranslation($key, $locale) */ public function flushTranslations($locale) { - collect($this->getTranslatableAttributes())->each(function ($attribute) use ($locale) { + collect($this->getTranslatableAttributes())->each(function (string $attribute) use ($locale) { $this->forgetTranslation($attribute, $locale); }); @@ -175,11 +224,26 @@ public function flushTranslations($locale) * * @return array */ - public function getTranslatedLocales($key) + public function getTranslatedLocales($key): array { return array_keys($this->getTranslations($key)); } + /** + * Check if has a translation. + * + * @param string $key + * @param string|null $locale + * + * @return bool + */ + public function hasTranslation(string $key, string $locale = null): bool + { + $locale = $locale ?: $this->getLocale(); + + return isset($this->getTranslations($key)[$locale]); + } + /** * Check if the attribute is translatable. * @@ -197,21 +261,25 @@ public function isTranslatableAttribute($key) | ----------------------------------------------------------------- */ + /** + * Get the locale. + * + * @return string + */ + protected function getLocale(): string + { + return config('app.locale'); + } + /** * Guard against untranslatable attribute. * * @param string $key - * - * @throws \Arcanedev\Localization\Exceptions\UntranslatableAttributeException */ - protected function guardAgainstUntranslatableAttribute($key) + protected function guardAgainstNonTranslatableAttribute($key) { if ( ! $this->isTranslatableAttribute($key)) { - $translatable = implode(', ', $this->getTranslatableAttributes()); - - throw new UntranslatableAttributeException( - "The attribute `{$key}` is untranslatable because it's not available in the translatable array: `$translatable`" - ); + throw UntranslatableAttributeException::make($key, $this->getTranslatableAttributes()); } } @@ -229,24 +297,7 @@ protected function normalizeLocale($key, $locale, $useFallback) if (in_array($locale, $this->getTranslatedLocales($key)) || ! $useFallback) return $locale; - return is_null($fallbackLocale = config('app.fallback_locale')) ? $locale : $fallbackLocale; - } - - /** - * Mutate many translations. - * - * @param string $key - * @param array $translations - * - * @return string - */ - protected function mutateTranslations($key, array $translations) - { - $method = 'set'.Str::studly($key).'Attribute'; - - return array_map(function ($value) use ($method) { - return $this->{$method}($value); - }, $translations); + return config('app.fallback_locale') ?: $locale; } /* ----------------------------------------------------------------- @@ -262,7 +313,8 @@ protected function mutateTranslations($key, array $translations) public function getCasts() { return array_merge( - parent::getCasts(), array_fill_keys($this->getTranslatableAttributes(), 'array') + parent::getCasts(), + array_fill_keys($this->getTranslatableAttributes(), 'array') ); } @@ -294,16 +346,6 @@ abstract protected function mutateAttribute($key, $value); */ abstract protected function getAttributeFromArray($key); - /** - * Set a given attribute on the model. - * - * @param string $key - * @param mixed $value - * - * @return self - */ - abstract public function setAttribute($key, $value); - /** * Determine if a set mutator exists for an attribute. * diff --git a/tests/Stubs/Models/TranslatableModel.php b/tests/Stubs/Models/TranslatableModel.php index a6f62af..591f07c 100644 --- a/tests/Stubs/Models/TranslatableModel.php +++ b/tests/Stubs/Models/TranslatableModel.php @@ -65,11 +65,9 @@ public function getNameAttribute($name) * Set the slug attribute (mutator). * * @param string $slug - * - * @return string */ public function setSlugAttribute($slug) { - return Str::slug($slug); + $this->attributes['slug'] = Str::slug($slug); } } diff --git a/tests/TranslatableModelTest.php b/tests/TranslatableModelTest.php index 0c163f3..7dc886d 100644 --- a/tests/TranslatableModelTest.php +++ b/tests/TranslatableModelTest.php @@ -41,7 +41,8 @@ public function setUp() /** @test */ public function it_can_set_translated_values_when_creating_a_model() { - $model = TranslatableModel::create([ + /** @var \Arcanedev\Localization\Tests\Stubs\Models\TranslatableModel $model */ + $model = TranslatableModel::query()->create([ 'name' => [ 'en' => 'Name', ], @@ -143,6 +144,61 @@ public function it_can_get_all_translations_for_a_specific_attribute() ], $this->model->getTranslations('name')); } + /** @test */ + public function it_handle_null_value_from_database() + { + $testModel = (new class() extends TranslatableModel { + public function setAttributesExternally(array $attributes) + { + $this->attributes = $attributes; + } + }); + + $testModel->setAttributesExternally([ + 'name' => json_encode(null), + 'other_field' => null, + ]); + + $this->assertEquals('', $testModel->name); + $this->assertEquals('', $testModel->other_field); + } + + /** @test */ + public function it_can_get_all_translations() + { + $translations = ['fr' => 'Salut', 'en' => 'Hello']; + + $this->model->setTranslations('name', $translations) + ->setTranslations('slug', $translations) + ->save(); + + static::assertEquals([ + 'name' => ['fr' => 'Salut', 'en' => 'Hello'], + 'slug' => ['fr' => 'salut', 'en' => 'hello'], + ], $this->model->translations); + } + + /** @test */ + public function it_can_get_all_translations_for_all_translatable_attributes() + { + $this->model->setTranslation('name', 'en', 'Name') + ->setTranslation('name', 'fr', 'Nom') + ->setTranslation('slug', 'en', 'Name') + ->setTranslation('slug', 'fr', 'Nom') + ->save(); + + static::assertSame([ + 'name' => [ + 'en' => 'Name', + 'fr' => 'Nom', + ], + 'slug' => [ + 'en' => 'name', + 'fr' => 'nom', + ], + ], $this->model->getTranslations()); + } + /** @test */ public function it_can_get_all_locales_available_in_the_translated_attribute() { @@ -247,6 +303,21 @@ public function it_can_check_if_an_attribute_is_translatable() static::assertFalse($this->model->isTranslatableAttribute('untranslated')); } + /** @test */ + public function it_can_check_if_an_attribute_has_translation() + { + $this->model->setTranslation('name', 'en', 'Hello') + ->setTranslation('name', 'fr', 'Salut') + ->setTranslation('name', 'nl', null) + ->save(); + + static::assertTrue($this->model->hasTranslation('name', 'en')); + static::assertTrue($this->model->hasTranslation('name', 'fr')); + + static::assertFalse($this->model->hasTranslation('name', 'nl')); + static::assertFalse($this->model->hasTranslation('name', 'ar')); + } + /** @test */ public function it_will_fire_an_event_when_a_translation_has_been_set() { @@ -257,6 +328,51 @@ public function it_will_fire_an_event_when_a_translation_has_been_set() static::assertSame(['en' => 'Name'], $this->model->getTranslations('name')); } + /** @test */ + public function it_will_return_fallback_locale_translation_when_getting_an_empty_translation_from_the_locale() + { + $this->setFallbackLocale('en'); + + $this->model->setTranslation('name', 'en', 'Name') + ->setTranslation('name', 'nl', null) + ->save(); + + static::assertSame('Name', $this->model->getTranslation('name', 'nl')); + } + + /** @test */ + public function it_will_return_correct_translation_value_if_value_is_set_to_zero() + { + $this->model->setTranslation('name', 'en', '0') + ->save(); + + static::assertSame('0', $this->model->getTranslation('name', 'en')); + } + + /** @test */ + public function it_will_not_return_fallback_value_if_value_is_set_to_zero() + { + $this->setFallbackLocale('en'); + + $this->model->setTranslation('name', 'en', '1') + ->setTranslation('name', 'fr', '0') + ->save(); + + static::assertSame('0', $this->model->getTranslation('name', 'fr')); + } + + /** @test */ + public function it_will_not_remove_zero_value_of_other_locale_in_database() + { + $this->setFallbackLocale('en'); + + $this->model->setTranslation('name', 'en', '0') + ->setTranslation('name', 'fr', '1') + ->save(); + + static::assertSame('0', $this->model->getTranslation('name', 'en')); + } + /* ----------------------------------------------------------------- | Other Methods | -----------------------------------------------------------------