├── .gitignore ├── .scrutinizer.yml ├── .travis.yml ├── LICENSE ├── README.md ├── composer.json ├── config └── translatable.php ├── phpunit.xml ├── src ├── Exceptions │ └── MissingTranslationsException.php ├── HasTranslations.php ├── Scopes │ └── JoinTranslationScope.php ├── Services │ └── TranslationSavingService.php └── TranslatableServiceProvider.php └── tests ├── Feature └── TranslationTest.php └── TestCase.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /.idea/ 3 | composer.lock 4 | 5 | -------------------------------------------------------------------------------- /.scrutinizer.yml: -------------------------------------------------------------------------------- 1 | build: 2 | nodes: 3 | analysis: 4 | project_setup: 5 | override: 6 | - 'true' 7 | tests: 8 | override: 9 | - php-scrutinizer-run 10 | - command: ./vendor/bin/phpunit --coverage-clover=coverage.clover 11 | coverage: 12 | file: coverage.clover 13 | format: php-clover 14 | checks: 15 | php: true 16 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.2 5 | - 7.3 6 | 7 | env: 8 | matrix: 9 | - COMPOSER_FLAGS="--prefer-lowest" 10 | - COMPOSER_FLAGS="" 11 | 12 | before_script: 13 | - travis_retry composer self-update 14 | - travis_retry composer update ${COMPOSER_FLAGS} --no-interaction --prefer-source 15 | 16 | script: 17 | - phpunit --coverage-text --coverage-clover=coverage.clover 18 | 19 | after_script: 20 | - php vendor/bin/ocular code-coverage:upload --format=php-clover coverage.clover 21 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 Koen Hoeijmakers 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Laravel Translatable 2 | [![Packagist](https://img.shields.io/packagist/v/koenhoeijmakers/laravel-translatable.svg?colorB=brightgreen)](https://packagist.org/packages/koenhoeijmakers/laravel-translatable) 3 | [![Build Status](https://scrutinizer-ci.com/g/koenhoeijmakers/laravel-translatable/badges/build.png?b=master)](https://scrutinizer-ci.com/g/koenhoeijmakers/laravel-translatable/build-status/master) 4 | [![Code Coverage](https://scrutinizer-ci.com/g/koenhoeijmakers/laravel-translatable/badges/coverage.png?b=master)](https://scrutinizer-ci.com/g/koenhoeijmakers/laravel-translatable/?branch=master) 5 | [![Scrutinizer Code Quality](https://scrutinizer-ci.com/g/koenhoeijmakers/laravel-translatable/badges/quality-score.png?b=master)](https://scrutinizer-ci.com/g/koenhoeijmakers/laravel-translatable/?branch=master) 6 | [![license](https://img.shields.io/github/license/koenhoeijmakers/laravel-translatable.svg?colorB=brightgreen)](https://github.com/koenhoeijmakers/laravel-translatable) 7 | [![Packagist](https://img.shields.io/packagist/dt/koenhoeijmakers/laravel-translatable.svg?colorB=brightgreen)](https://packagist.org/packages/koenhoeijmakers/laravel-translatable) 8 | 9 | A fresh new way to handle Model translations, the translations are joined into the Model 10 | instead of making you query a relation or get every single attribute's translation one by one. 11 | 12 | ## Installation 13 | Require the package. 14 | ```sh 15 | composer require koenhoeijmakers/laravel-translatable 16 | ``` 17 | 18 | ... and optionally publish the config. 19 | ```sh 20 | php artisan vendor:publish --provider="KoenHoeijmakers\LaravelTranslatable\TranslatableServiceProvider" 21 | ``` 22 | 23 | ## Usage 24 | ### Setting up a translatable Model. 25 | Start off by creating a migration and a Model, 26 | we'll go with the `Animal` Model and the corresponding `AnimalTranslation` Model. 27 | 28 | #### Migrations 29 | ```php 30 | Schema::create('animals', function (Blueprint $table) { 31 | $table->increments('id'); 32 | $table->timestamps(); 33 | }); 34 | ``` 35 | 36 | Always have a `locale` and a `foreign_key` to the original Model, in our case `animal_id`. 37 | 38 | ```php 39 | Schema::create('animal_translations', function (Blueprint $table) { 40 | $table->increments('id'); 41 | $table->unsignedInteger('animal_id'); 42 | $table->string('locale'); 43 | $table->string('name'); 44 | $table->timestamps(); 45 | 46 | $table->unique(['locale', 'animal_id']); 47 | $table->foreign('animal_id')->references('id')->on('animals'); 48 | }); 49 | ``` 50 | 51 | #### Models 52 | Register the trait on the Model, and add the columns that should be translated to the `$translatable` property, 53 | **But also make them fillable**, this is because the saving is handled through events, 54 | this way we don't have to change the `save` method and makes the package more interoperable. 55 | 56 | > So make sure the `$translatable` columns are also `$fillable` on both Models. 57 | 58 | ```php 59 | use Illuminate\Database\Eloquent\Model; 60 | use KoenHoeijmakers\LaravelTranslatable\HasTranslations; 61 | 62 | class Animal extends Model 63 | { 64 | use HasTranslations; 65 | 66 | protected $translatable = ['name']; 67 | 68 | protected $fillable = ['name']; 69 | } 70 | ``` 71 | 72 | ```php 73 | use Illuminate\Database\Eloquent\Model; 74 | 75 | class AnimalTranslation extends Model 76 | { 77 | protected $fillable = ['name']; 78 | } 79 | ``` 80 | 81 | This is pretty much all there is to it, but you can read more about the package down here. 82 | 83 | ## About 84 | What makes this package so special is the way it handles the translations, 85 | how it retrieves them, how it stores them, and how it queries them. 86 | 87 | ### Querying 88 | Due to how the package handles the translations, querying is a piece of cake, 89 | while for other packages you would have a `->whereTranslation('nl', 'column', '=', 'foo')` method. 90 | 91 | But in this package you can just do `->where('column', '=', 'foo')` and it'll know what to query, just query how you used to! 92 | 93 | ### Retrieving 94 | When you retrieve a Model from the database, the package will join the translation table with the translation of the current locale `config/app.php`. 95 | 96 | This makes it so that any translated column acts like it is "native" to the Model, 97 | due to this we don't have to override a lot of methods on the Model which is a big plus. 98 | 99 | Need the Model in a different language? Call `$model->translate('nl')` and you are done. Now you would like to save the `nl` translation? just call `->update()`. The Model knows in which locale it is loaded and it will handle it accordingly. 100 | 101 | ```php 102 | $animal = Animal::query()->find(1); 103 | 104 | $animal->translate('nl')->update(['name' => 'Aap']); 105 | ``` 106 | 107 | ### Storing 108 | You will be storing your translations as if they're attributes on the Model, so this will work like a charm: 109 | ```php 110 | Animal::query()->create(['name' => 'Monkey']); 111 | ``` 112 | 113 | But i hear you, you would like to store multiple translations in one request! In that so you can use the `->storeTranslation()` or the `->storeTranslations()` method. 114 | 115 | ```php 116 | $animal = Animal::query()->create(['name' => 'Monkey']); 117 | 118 | $animal->storeTranslation('nl', [ 119 | 'name' => 'Aap', 120 | ]); 121 | 122 | $animal->storeTranslations([ 123 | 'nl' => [ 124 | 'name' => 'Aap', 125 | ], 126 | 'de' => [ 127 | 'name' => 'Affe', 128 | ], 129 | ]); 130 | ``` 131 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "koenhoeijmakers/laravel-translatable", 3 | "description": "Laravel Translations", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Koen Hoeijmakers", 8 | "email": "koen@hoeijmakers.me" 9 | } 10 | ], 11 | "minimum-stability": "stable", 12 | "require": { 13 | "php": ">=7.2.5", 14 | "laravel/framework": "^7.4" 15 | }, 16 | "require-dev": { 17 | "phpunit/phpunit": "^8.5", 18 | "orchestra/testbench": "^5.0", 19 | "mockery/mockery": "^1.3.1" 20 | }, 21 | "autoload": { 22 | "psr-4": { 23 | "KoenHoeijmakers\\LaravelTranslatable\\": "src/" 24 | } 25 | }, 26 | "autoload-dev": { 27 | "psr-4": { 28 | "KoenHoeijmakers\\LaravelTranslatable\\Tests\\": "tests/" 29 | } 30 | }, 31 | "extra": { 32 | "laravel": { 33 | "providers": [ 34 | "KoenHoeijmakers\\LaravelTranslatable\\TranslatableServiceProvider" 35 | ] 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /config/translatable.php: -------------------------------------------------------------------------------- 1 | true, 13 | 14 | /* 15 | | 16 | | The locale key name, used in the translation tables. 17 | | 18 | */ 19 | 'locale_key_name' => 'locale', 20 | ]; 21 | -------------------------------------------------------------------------------- /phpunit.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | 14 | ./tests/ 15 | 16 | 17 | 18 | 19 | ./src 20 | 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/Exceptions/MissingTranslationsException.php: -------------------------------------------------------------------------------- 1 | rememberTranslationForModel($model); 36 | }); 37 | 38 | self::saved(function ($model) { 39 | app(TranslationSavingService::class)->storeTranslationOnModel($model); 40 | 41 | $model->refreshTranslation(); 42 | }); 43 | } 44 | 45 | self::deleting(function ($model) { 46 | $model->purgeTranslations(); 47 | }); 48 | 49 | self::addGlobalScope(new JoinTranslationScope()); 50 | } 51 | 52 | /** 53 | * @return \Illuminate\Database\Eloquent\Relations\HasMany 54 | */ 55 | public function translations(): HasMany 56 | { 57 | return $this->hasMany($this->getTranslationModel(), $this->getTranslationForeignKey()); 58 | } 59 | 60 | /** 61 | * Check if the translation by the given locale exists. 62 | * 63 | * @param string $locale 64 | * @return bool 65 | */ 66 | public function translationExists(string $locale): bool 67 | { 68 | return $this->translations()->where($this->getLocaleKeyName(), $locale)->exists(); 69 | } 70 | 71 | /** 72 | * Purge the translations. 73 | * 74 | * @return mixed 75 | */ 76 | public function purgeTranslations() 77 | { 78 | return $this->translations()->delete(); 79 | } 80 | 81 | /** 82 | * Get the translation model. 83 | * 84 | * @return string 85 | */ 86 | public function getTranslationModel(): string 87 | { 88 | return property_exists($this, 'translationModel') 89 | ? $this->translationModel 90 | : get_class($this) . $this->getTranslationModelSuffix(); 91 | } 92 | 93 | /** 94 | * Get the translation model suffix. 95 | * 96 | * @return string 97 | */ 98 | protected function getTranslationModelSuffix(): string 99 | { 100 | return 'Translation'; 101 | } 102 | 103 | /** 104 | * Get the translation table. 105 | * 106 | * @return string 107 | */ 108 | public function getTranslationTable(): string 109 | { 110 | $model = $this->getTranslationModel(); 111 | 112 | return (new $model())->getTable(); 113 | } 114 | 115 | /** 116 | * Get the translation foreign key. 117 | * 118 | * @return string 119 | */ 120 | public function getTranslationForeignKey() 121 | { 122 | return property_exists($this, 'translationForeignKey') ? $this->translationForeignKey : $this->getForeignKey(); 123 | } 124 | 125 | /** 126 | * Get the translatable. 127 | * 128 | * @return array 129 | * @throws \KoenHoeijmakers\LaravelTranslatable\Exceptions\MissingTranslationsException 130 | */ 131 | public function getTranslatable(): array 132 | { 133 | if (! isset($this->translatable)) { 134 | throw new MissingTranslationsException('Model "' . get_class($this) . '" is missing translations'); 135 | } 136 | 137 | return $this->translatable; 138 | } 139 | 140 | /** 141 | * Get the translatable attributes. 142 | * 143 | * @return array 144 | */ 145 | public function getTranslatableAttributes(): array 146 | { 147 | return Arr::only($this->getAttributes(), $this->translatable); 148 | } 149 | 150 | /** 151 | * @param string $locale 152 | * @param array $attributes 153 | * @return \Illuminate\Database\Eloquent\Model 154 | */ 155 | public function storeTranslation(string $locale, array $attributes = []) 156 | { 157 | if (! is_null($model = $this->translations()->where($this->getLocaleKeyName(), $locale)->first())) { 158 | $model->update($attributes); 159 | 160 | return $model; 161 | } 162 | 163 | $model = $this->translations()->make($attributes); 164 | $model->setAttribute($this->getLocaleKeyName(), $locale); 165 | $model->save(); 166 | 167 | return $model; 168 | } 169 | 170 | /** 171 | * Store many translations at once. 172 | * 173 | * @param array $translations 174 | * @return $this 175 | */ 176 | public function storeTranslations(array $translations) 177 | { 178 | foreach ($translations as $locale => $translation) { 179 | $this->storeTranslation($locale, $translation); 180 | } 181 | 182 | return $this; 183 | } 184 | 185 | /** 186 | * @param string $locale 187 | * @return \Illuminate\Database\Eloquent\Model|self 188 | */ 189 | public function getTranslation(string $locale) 190 | { 191 | return $this->translations()->where($this->getLocaleKeyName(), $locale)->first(); 192 | } 193 | 194 | /** 195 | * @param string $locale 196 | * @param string $name 197 | * @return mixed 198 | */ 199 | public function getTranslationValue(string $locale, string $name) 200 | { 201 | return $this->translations()->where($this->getLocaleKeyName(), $locale)->value($name); 202 | } 203 | 204 | /** 205 | * The locale key name. 206 | * 207 | * @return string 208 | */ 209 | public function getLocaleKeyName(): string 210 | { 211 | return property_exists($this, 'localeKeyName') 212 | ? $this->localeKeyName 213 | : config()->get('translatable.locale_key_name', 'locale'); 214 | } 215 | 216 | /** 217 | * Get the locale. 218 | * 219 | * @return string 220 | */ 221 | public function getLocale(): string 222 | { 223 | return null !== $this->currentLocale 224 | ? $this->currentLocale 225 | : app()->getLocale(); 226 | } 227 | 228 | /** 229 | * Refresh the translation (in the current locale). 230 | * 231 | * @return \Illuminate\Database\Eloquent\Model|null|HasTranslations 232 | * @throws \KoenHoeijmakers\LaravelTranslatable\Exceptions\MissingTranslationsException 233 | */ 234 | public function refreshTranslation() 235 | { 236 | if (! $this->exists) { 237 | return null; 238 | } 239 | 240 | $attributes = Arr::only( 241 | $this->newQuery()->findOrFail($this->getKey())->attributes, $this->getTranslatable() 242 | ); 243 | 244 | foreach ($attributes as $key => $value) { 245 | $this->setAttribute($key, $value); 246 | } 247 | 248 | $this->syncOriginal(); 249 | 250 | return $this; 251 | } 252 | 253 | /** 254 | * Translate the model to the given locale. 255 | * 256 | * @param string $locale 257 | * @return \Illuminate\Database\Eloquent\Model|null 258 | */ 259 | public function translate(string $locale) 260 | { 261 | if (! $this->exists) { 262 | return null; 263 | } 264 | 265 | $this->currentLocale = $locale; 266 | 267 | return $this->refreshTranslation(); 268 | } 269 | 270 | /** 271 | * Format the translated columns. 272 | * 273 | * @return array 274 | * @throws \KoenHoeijmakers\LaravelTranslatable\Exceptions\MissingTranslationsException 275 | */ 276 | public function formatTranslatableColumnsForSelect(): array 277 | { 278 | $table = $this->getTranslationTable(); 279 | 280 | return array_map(function ($item) use ($table) { 281 | return $table . '.' . $item; 282 | }, $this->getTranslatable()); 283 | } 284 | 285 | /** 286 | * Get a new query builder that doesn't have any global scopes (except the JoinTranslationScope). 287 | * 288 | * @return \Illuminate\Database\Eloquent\Builder 289 | */ 290 | public function newQueryWithoutScopes(): Builder 291 | { 292 | return parent::newQueryWithoutScopes() 293 | ->withGlobalScope(JoinTranslationScope::class, new JoinTranslationScope()); 294 | } 295 | 296 | /** 297 | * Retrieve the model for a bound value. 298 | * 299 | * @param mixed $value 300 | * @param null $field 301 | * @return \Illuminate\Database\Eloquent\Model|null 302 | */ 303 | public function resolveRouteBinding($value, $field = null) 304 | { 305 | $field = $field ?? $this->getRouteKeyName(); 306 | 307 | return $this->newQuery()->where($this->getTable() . '.' . $field, $value)->first(); 308 | } 309 | } 310 | -------------------------------------------------------------------------------- /src/Scopes/JoinTranslationScope.php: -------------------------------------------------------------------------------- 1 | leftJoin($model->getTranslationTable(), function (JoinClause $join) use ($model) { 24 | $join->on( 25 | $model->getTable() . '.' . $model->getKeyName(), 26 | $model->getTranslationTable() . '.' . $model->getForeignKey() 27 | )->where($model->getLocaleKeyName(), $model->getLocale()); 28 | })->addSelect($model->getTable() . '.*', ...$model->formatTranslatableColumnsForSelect()); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Services/TranslationSavingService.php: -------------------------------------------------------------------------------- 1 | getTranslatableAttributes(); 25 | 26 | $this->rememberTranslation($this->getModelIdentifier($model), $attributes); 27 | 28 | foreach (array_keys($attributes) as $attribute) { 29 | $model->offsetUnset($attribute); 30 | } 31 | } 32 | 33 | /** 34 | * Store the remembered translation for the given model. 35 | * 36 | * @param \Illuminate\Database\Eloquent\Model|\KoenHoeijmakers\LaravelTranslatable\HasTranslations $model 37 | * @return void 38 | */ 39 | public function storeTranslationOnModel(Model $model) 40 | { 41 | $identifier = $this->getModelIdentifier($model); 42 | 43 | $model->storeTranslation( 44 | $model->getLocale(), 45 | $this->pullRememberedTranslation($identifier) 46 | ); 47 | } 48 | 49 | /** 50 | * Remember the translation on the given key. 51 | * 52 | * @param string $key 53 | * @param array $attributes 54 | * @return void 55 | */ 56 | public function rememberTranslation(string $key, array $attributes) 57 | { 58 | $this->translations[$key] = $attributes; 59 | } 60 | 61 | /** 62 | * Pull the translation on the given key. 63 | * 64 | * @param string $key 65 | * @return mixed 66 | */ 67 | public function pullRememberedTranslation(string $key) 68 | { 69 | $value = $this->translations[$key]; 70 | 71 | unset($this->translations[$key]); 72 | 73 | return $value; 74 | } 75 | 76 | /** 77 | * Get an unique identifier for the given model. 78 | * 79 | * @param \Illuminate\Database\Eloquent\Model $model 80 | * @return string 81 | */ 82 | protected function getModelIdentifier(Model $model): string 83 | { 84 | return spl_object_hash($model); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/TranslatableServiceProvider.php: -------------------------------------------------------------------------------- 1 | publishes([ 18 | __DIR__ . ' /../config/translatable.php' => config_path('translatable.php'), 19 | ], 'config'); 20 | } 21 | 22 | /** 23 | * Registers the package's services. 24 | * 25 | * @return void 26 | */ 27 | public function register() 28 | { 29 | $this->app->singleton(TranslationSavingService::class, function () { 30 | return new TranslationSavingService(); 31 | }); 32 | 33 | $this->mergeConfigFrom(__DIR__ . '/../config/translatable.php', 'translatable'); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /tests/Feature/TranslationTest.php: -------------------------------------------------------------------------------- 1 | create([ 18 | 'name' => 'Monkey', 19 | ]); 20 | 21 | $this->assertTrue($model->getAttribute('name') === 'Monkey'); 22 | $this->assertDatabaseHas('test_models', ['id' => $model->getKey()]); 23 | $this->assertDatabaseHas('test_model_translations', ['test_model_id' => $model->getKey(), 'name' => 'Monkey']); 24 | } 25 | 26 | public function testTranslationsCanBeSavedViaStoreTranslation() 27 | { 28 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 29 | $model = TestModel::query()->create([ 30 | 'name' => 'Monkey', 31 | ]); 32 | 33 | $model->storeTranslation('nl', ['name' => 'Aap']); 34 | 35 | $this->assertDatabaseHas('test_models', ['id' => $model->getKey()]); 36 | $this->assertDatabaseHas('test_model_translations', ['test_model_id' => $model->getKey(), 'name' => 'Aap']); 37 | } 38 | 39 | public function testTranslationsCanBeSavedViaStoreTranslationMethod() 40 | { 41 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 42 | $model = TestModel::query()->create([ 43 | 'name' => 'Monkey', 44 | ]); 45 | 46 | $model->storeTranslation('nl', ['name' => 'Aap']); 47 | 48 | $this->assertDatabaseHas('test_models', ['id' => $model->getKey()]); 49 | $this->assertTrue($model->translationExists('nl')); 50 | } 51 | 52 | public function testTranslationsCanBeSavedViaStoreTranslationsMethod() 53 | { 54 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 55 | $model = TestModel::query()->create([ 56 | 'name' => 'Monkey', 57 | ]); 58 | 59 | $model->storeTranslations([ 60 | 'nl' => ['name' => 'Aap'], 61 | 'de' => ['name' => 'Affe'], 62 | ]); 63 | 64 | $this->assertDatabaseHas('test_models', ['id' => $model->getKey()]); 65 | $this->assertTrue($model->translationExists('nl')); 66 | $this->assertTrue($model->translationExists('de')); 67 | } 68 | 69 | public function testCanRetrieveADifferentTranslation() 70 | { 71 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 72 | $model = TestModel::query()->create([ 73 | 'name' => 'Monkey', 74 | ]); 75 | 76 | $model->storeTranslation('nl', ['name' => 'Aap']); 77 | 78 | $this->assertTrue($model->translationExists('nl')); 79 | 80 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 81 | $model = TestModel::query()->find(1); 82 | 83 | $model->translate('nl'); 84 | 85 | $this->assertEquals($model->getLocale(), 'nl'); 86 | $this->assertEquals($model->getAttribute('name'), 'Aap'); 87 | } 88 | 89 | public function testCanRetrieveADifferentTranslationAndItsUpdatedInThatLocale() 90 | { 91 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 92 | $model = TestModel::query()->create([ 93 | 'name' => 'Monkey', 94 | ]); 95 | 96 | $model->storeTranslation('nl', ['name' => 'Aap']); 97 | 98 | $this->assertTrue($model->translationExists('nl')); 99 | 100 | $model->translate('nl'); 101 | 102 | $this->assertEquals($model->getLocale(), 'nl'); 103 | $this->assertEquals($model->getAttribute('name'), 'Aap'); 104 | 105 | $model->update(['name' => 'Gorilla']); 106 | 107 | $this->assertEquals($model->getAttribute('name'), 'Gorilla'); 108 | $this->assertEquals(TestModel::query()->find(1)->getAttribute('name'), 'Monkey'); 109 | } 110 | 111 | public function testCanPurgeTranslations() 112 | { 113 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 114 | $model = TestModel::query()->create([ 115 | 'name' => 'Monkey', 116 | ]); 117 | 118 | $this->assertTrue($model->translationExists('en')); 119 | 120 | $model->purgeTranslations(); 121 | 122 | $this->assertFalse($model->translationExists('en')); 123 | } 124 | 125 | public function testModelGetsDeletedAndTranslationsArePurged() 126 | { 127 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 128 | $model = TestModel::query()->create([ 129 | 'name' => 'Monkey', 130 | ]); 131 | 132 | $this->assertTrue($model->translationExists('en')); 133 | 134 | $model->delete(); 135 | 136 | $this->assertFalse($model->translationExists('en')); 137 | $this->assertDatabaseMissing('test_models', [$model->getKeyName() => $model->getKey()]); 138 | } 139 | 140 | public function testCanGetTranslationModel() 141 | { 142 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 143 | $model = TestModel::query()->create([ 144 | 'name' => 'Monkey', 145 | ]); 146 | 147 | $this->assertInstanceOf(TestModelTranslation::class, $model->getTranslation('en')); 148 | } 149 | 150 | public function testCanGetTranslationValue() 151 | { 152 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 153 | $model = TestModel::query()->create([ 154 | 'name' => 'Monkey', 155 | ]); 156 | 157 | $this->assertEquals('Monkey', $model->getTranslationValue('en', 'name')); 158 | } 159 | 160 | public function testRefreshingTranslationsOnANonExistingModelReturnsNull() 161 | { 162 | $model = new TestModel(); 163 | 164 | $this->assertNull($model->refreshTranslation()); 165 | } 166 | 167 | public function testTranslatingANonExistingModelReturnsNull() 168 | { 169 | $model = new TestModel(); 170 | 171 | $this->assertNull($model->translate('nl')); 172 | } 173 | 174 | public function testResolvingRouteBindingsReturnsCorrectModel() 175 | { 176 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 177 | $model = TestModel::query()->create([ 178 | 'name' => 'Monkey', 179 | ]); 180 | 181 | $this->assertTrue($model->is($model->resolveRouteBinding($model->getKey()))); 182 | } 183 | 184 | public function testTranslationModelCanBeOverridden() 185 | { 186 | $model = new TestModelWithTranslationModelOverride(); 187 | 188 | $this->assertSame($model->getTranslationTable(), 'test_model_translation_differents'); 189 | } 190 | 191 | public function testModelIsMissingTranslations() 192 | { 193 | $model = new TestModelWithoutTranslations(); 194 | 195 | $this->expectException(MissingTranslationsException::class); 196 | 197 | $model->getTranslatable(); 198 | } 199 | 200 | public function testCanOverrideLocaleKey() 201 | { 202 | $model = new TestModelLocaleKey(); 203 | 204 | $this->assertEquals('lang', $model->getLocaleKeyName()); 205 | } 206 | 207 | public function testCanAddSelect() 208 | { 209 | /** @var \KoenHoeijmakers\LaravelTranslatable\Tests\Feature\TestModel $model */ 210 | $model = TestModel::query()->create([ 211 | 'name' => 'Monkey', 212 | ]); 213 | 214 | $result = TestModel::query()->select('test_model_translations.id AS translation_id')->first(); 215 | 216 | $this->assertEquals($model->getTranslation('en')->getKey(), $result->translation_id); 217 | } 218 | } 219 | 220 | class TestModel extends Model 221 | { 222 | use HasTranslations; 223 | 224 | protected $fillable = ['name']; 225 | 226 | protected $translatable = ['name']; 227 | } 228 | 229 | class TestModelTranslation extends Model 230 | { 231 | protected $fillable = ['name']; 232 | } 233 | 234 | class TestModelWithTranslationModelOverride extends Model 235 | { 236 | use HasTranslations; 237 | 238 | protected $table = 'test_models'; 239 | 240 | protected $fillable = ['name']; 241 | 242 | protected $translatable = ['name']; 243 | 244 | protected $translationModel = TestModelTranslationDifferent::class; 245 | } 246 | 247 | class TestModelTranslationDifferent extends Model 248 | { 249 | protected $fillable = ['name']; 250 | } 251 | 252 | class TestModelWithoutTranslations extends Model 253 | { 254 | use HasTranslations; 255 | 256 | protected $fillable = ['name']; 257 | 258 | protected $table = 'test_models'; 259 | } 260 | 261 | class TestModelLocaleKey extends Model 262 | { 263 | use HasTranslations; 264 | 265 | protected $localeKeyName = 'lang'; 266 | 267 | protected $fillable = ['name']; 268 | 269 | protected $table = 'test_models'; 270 | } 271 | -------------------------------------------------------------------------------- /tests/TestCase.php: -------------------------------------------------------------------------------- 1 | setUpDatabase(); 17 | } 18 | 19 | protected function getPackageProviders($app) 20 | { 21 | return [TranslatableServiceProvider::class]; 22 | } 23 | 24 | protected function setUpDatabase() 25 | { 26 | Schema::create('test_models', function (Blueprint $table) { 27 | $table->increments('id'); 28 | $table->timestamps(); 29 | }); 30 | 31 | Schema::create('test_model_translations', function (Blueprint $table) { 32 | $table->increments('id'); 33 | $table->unsignedInteger('test_model_id'); 34 | $table->string('locale'); 35 | $table->string('name'); 36 | $table->timestamps(); 37 | 38 | $table->unique(['locale', 'test_model_id']); 39 | $table->foreign('test_model_id')->references('id')->on('test_models'); 40 | }); 41 | } 42 | } 43 | --------------------------------------------------------------------------------