Repository: codezero-be/laravel-unique-translation Branch: master Commit: a2daae936dbc Files: 18 Total size: 50.9 KB Directory structure: gitextract_gcilwl22/ ├── .editorconfig ├── .gitattributes ├── .github/ │ ├── FUNDING.yml │ ├── SECURITY.md │ └── workflows/ │ └── run-tests.yml ├── .gitignore ├── LICENSE.md ├── README.md ├── composer.json ├── phpunit.xml.dist ├── src/ │ ├── UniqueTranslationRule.php │ ├── UniqueTranslationServiceProvider.php │ └── UniqueTranslationValidator.php └── tests/ ├── Stubs/ │ └── Model.php ├── TestCase.php ├── UniqueTranslationTest.php ├── ValidationMessageTest.php └── WhereClauseTest.php ================================================ FILE CONTENTS ================================================ ================================================ FILE: .editorconfig ================================================ ; This file is for unifying the coding style for different editors and IDEs. ; More information at http://editorconfig.org root = true [*] charset = utf-8 indent_size = 4 indent_style = space end_of_line = lf insert_final_newline = true trim_trailing_whitespace = true [*.yml] indent_size = 2 [*.md] trim_trailing_whitespace = false ================================================ FILE: .gitattributes ================================================ # Path-based git attributes # https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html # Ignore all test and documentation with "export-ignore". /.gitattributes export-ignore /.gitignore export-ignore /.travis.yml export-ignore /phpunit.xml.dist export-ignore /.scrutinizer.yml export-ignore /tests export-ignore ================================================ FILE: .github/FUNDING.yml ================================================ ko_fi: ivanvermeyen custom: https://paypal.me/ivanvermeyen ================================================ FILE: .github/SECURITY.md ================================================ # Security Policy If you discover any security related issues, please email ivan@codezero.be instead of using the issue tracker. ================================================ FILE: .github/workflows/run-tests.yml ================================================ name: Tests on: [ push, pull_request ] jobs: test: runs-on: ubuntu-latest strategy: fail-fast: true matrix: php: [ 8.0, 8.1, 8.2, 8.3 ] laravel: [ 8.*, 9.*, 10.*, 11.* ] dependency-version: [ prefer-stable ] exclude: - laravel: 10.* php: 8.0 - laravel: 11.* php: 8.0 - laravel: 11.* php: 8.1 include: - laravel: 6.* php: 7.2 testbench: 4.* - laravel: 6.* php: 8.0 testbench: 4.* - laravel: 7.* php: 7.2 testbench: 5.* - laravel: 7.* php: 8.0 testbench: 5.* - laravel: 8.* php: 7.3 testbench: 6.* - laravel: 8.* testbench: 6.* - laravel: 9.* testbench: 7.* - laravel: 10.* testbench: 8.* - laravel: 11.* testbench: 9.* name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.dependency-version }} services: mysql: image: mysql:5.7 env: MYSQL_ALLOW_EMPTY_PASSWORD: yes MYSQL_DATABASE: testing ports: - 3306 options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3 steps: - name: Checkout code uses: actions/checkout@v2 - name: Cache dependencies uses: actions/cache@v2 with: path: ~/.composer/cache/files key: dependencies-laravel-${{ matrix.laravel }}-php-${{ matrix.php }}-composer-${{ hashFiles('composer.json') }} - name: Setup PHP uses: shivammathur/setup-php@v2 with: php-version: ${{ matrix.php }} extensions: dom, curl, libxml, mbstring, zip, pcntl, pdo, sqlite, pdo_sqlite, bcmath, soap, intl, gd, exif, iconv, imagick coverage: pcov - name: Install dependencies run: composer update --with="orchestra/testbench:${{ matrix.testbench }}" --prefer-dist --no-interaction --no-progress - name: Execute tests run: vendor/bin/phpunit --coverage-clover=coverage.xml env: DB_PORT: ${{ job.services.mysql.ports[3306] }} ================================================ FILE: .gitignore ================================================ /vendor .idea composer.lock phpunit.xml /.phpunit.result.cache /.phpunit.cache ================================================ FILE: LICENSE.md ================================================ # The MIT License (MIT) Copyright (c) 2017 Ivan Vermeyen () > Permission is hereby granted, free of charge, to any person obtaining a copy > of this software and associated documentation files (the "Software"), to deal > in the Software without restriction, including without limitation the rights > to use, copy, modify, merge, publish, distribute, sublicense, and/or sell > copies of the Software, and to permit persons to whom the Software is > furnished to do so, subject to the following conditions: > > The above copyright notice and this permission notice shall be included in > all copies or substantial portions of the Software. > > THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR > IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, > FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE > AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER > LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, > OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN > THE SOFTWARE. ================================================ FILE: README.md ================================================ # Laravel Unique Translation ## IMPORTANT: March 2022 [![Support Ukraine](https://raw.githubusercontent.com/hampusborgos/country-flags/main/png100px/ua.png)](https://github.com/hampusborgos/country-flags/blob/main/png100px/ua.png) It's horrible to see what is happening now in Ukraine, as Russian army is [bombarding houses, hospitals and kindergartens](https://twitter.com/DavidCornDC/status/1501620037785997316). Please [check out supportukrainenow.org](https://supportukrainenow.org/) for the ways how you can help people there. Spread the word. And if you are from Russia and you are against this war, please express your protest in some way. I know you can get punished for this, but you are one of the hopes of those innocent people. --- [![GitHub release](https://img.shields.io/github/release/codezero-be/laravel-unique-translation.svg?style=flat-square)](https://github.com/codezero-be/laravel-unique-translation/releases) [![Laravel](https://img.shields.io/badge/laravel-11-red?style=flat-square&logo=laravel&logoColor=white)](https://laravel.com) [![License](https://img.shields.io/packagist/l/codezero/laravel-unique-translation.svg?style=flat-square)](LICENSE.md) [![Build Status](https://img.shields.io/github/actions/workflow/status/codezero-be/laravel-unique-translation/run-tests.yml?style=flat-square&logo=github&logoColor=white&label=tests)](https://github.com/codezero-be/laravel-unique-translation/actions) [![Code Coverage](https://img.shields.io/codacy/coverage/bb5f876fb1a94aa0a426fd31a2656e5b/master?style=flat-square)](https://app.codacy.com/gh/codezero-be/laravel-unique-translation) [![Code Quality](https://img.shields.io/codacy/grade/bb5f876fb1a94aa0a426fd31a2656e5b/master?style=flat-square)](https://app.codacy.com/gh/codezero-be/laravel-unique-translation) [![Total Downloads](https://img.shields.io/packagist/dt/codezero/laravel-unique-translation.svg?style=flat-square)](https://packagist.org/packages/codezero/laravel-unique-translation) [![ko-fi](https://www.ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/R6R3UQ8V) #### Check if a translated value in a JSON column is unique in the database. Imagine you want store a `slug` for a `Post` model in different languages. The amazing [`spatie/laravel-translatable`](https://github.com/spatie/laravel-translatable) package makes this a cinch! But then you want to make sure each translation is unique for its language. That's where this package comes in to play. This package also supports [`spatie/nova-translatable`](https://github.com/spatie/nova-translatable/) in case you are using [Laravel Nova](https://nova.laravel.com/) and [`filamentphp/spatie-laravel-translatable-plugin`](https://github.com/filamentphp/spatie-laravel-translatable-plugin) in case you are using [Filament](https://filamentphp.com/). ## ✅ Requirements - PHP ^7.2 or PHP ^8.0 - MySQL >= 5.7 - [Laravel](https://laravel.com/) >= 6 - [spatie/laravel-translatable](https://github.com/spatie/laravel-translatable) ^4.4|^5.0|^6.0 - [spatie/nova-translatable](https://github.com/spatie/nova-translatable/) ^3.0 - [filamentphp/spatie-laravel-translatable-plugin](https://github.com/filamentphp/spatie-laravel-translatable-plugin) ^3.0 ## 📦 Installation Require the package via Composer: ``` composer require codezero/laravel-unique-translation ``` Laravel will automatically register the [ServiceProvider](https://github.com/codezero-be/laravel-unique-translation/blob/master/src/UniqueTranslationServiceProvider.php). ## 🛠 Usage For the following examples, I will use a `slug` in a `posts` table as the subject of our validation. ### ☑️ Validate a Single Translation Your form can submit a single slug: ```html ``` We can then check if it is unique **in the current locale**: ```php $attributes = request()->validate([ 'slug' => 'required|unique_translation:posts', ]); ``` You could also use the Rule instance: ```php use CodeZero\UniqueTranslation\UniqueTranslationRule; $attributes = request()->validate([ 'slug' => ['required', UniqueTranslationRule::for('posts')], ]); ``` ### ☑️ Validate an Array of Translations Your form can also submit an array of slugs. ```html ``` We need to validate the entire array in this case. Mind the `slug.*` key. ```php $attributes = request()->validate([ 'slug.*' => 'unique_translation:posts', // or... 'slug.*' => UniqueTranslationRule::for('posts'), ]); ``` ### ☑️ Specify a Column Maybe your form field has a name of `post_slug` and your database field `slug`: ```php $attributes = request()->validate([ 'post_slug.*' => 'unique_translation:posts,slug', // or... 'post_slug.*' => UniqueTranslationRule::for('posts', 'slug'), ]); ``` ### ☑️ Specify a Database Connection If you are using multiple database connections, you can specify which one to use by prepending it to the table name, separated by a dot: ```php $attributes = request()->validate([ 'slug.*' => 'unique_translation:db_connection.posts', // or... 'slug.*' => UniqueTranslationRule::for('db_connection.posts'), ]); ``` ### ☑️ Ignore a Record with ID If you're updating a record, you may want to ignore the post itself from the unique check. ```php $attributes = request()->validate([ 'slug.*' => "unique_translation:posts,slug,{$post->id}", // or... 'slug.*' => UniqueTranslationRule::for('posts')->ignore($post->id), ]); ``` ### ☑️ Ignore Records with a Specific Column and Value If your ID column has a different name, or you just want to use another column: ```php $attributes = request()->validate([ 'slug.*' => 'unique_translation:posts,slug,ignore_value,ignore_column', // or... 'slug.*' => UniqueTranslationRule::for('posts')->ignore('ignore_value', 'ignore_column'), ]); ``` ### ☑️ Use Additional Where Clauses You can add 4 types of where clauses to the rule. #### `where` ```php $attributes = request()->validate([ 'slug.*' => "unique_translation:posts,slug,null,null,column,value", // or... 'slug.*' => UniqueTranslationRule::for('posts')->where('column', 'value'), ]); ``` #### `whereNot` ```php $attributes = request()->validate([ 'slug.*' => "unique_translation:posts,slug,null,null,column,!value", // or... 'slug.*' => UniqueTranslationRule::for('posts')->whereNot('column', 'value'), ]); ``` #### `whereNull` ```php $attributes = request()->validate([ 'slug.*' => "unique_translation:posts,slug,null,null,column,NULL", // or... 'slug.*' => UniqueTranslationRule::for('posts')->whereNull('column'), ]); ``` #### `whereNotNull` ```php $attributes = request()->validate([ 'slug.*' => "unique_translation:posts,slug,null,null,column,NOT_NULL", // or... 'slug.*' => UniqueTranslationRule::for('posts')->whereNotNull('column'), ]); ``` ### ☑️ Laravel Nova If you are using [Laravel Nova](https://nova.laravel.com/) in combination with [`spatie/nova-translatable`](https://github.com/spatie/nova-translatable/), then you can add the validation rule like this: ```php Text::make(__('Slug'), 'slug') ->creationRules('unique_translation:posts,slug') ->updateRules('unique_translation:posts,slug,{{resourceId}}'); ``` ### ☑️ Filament If you are using [Filament](https://filamentphp.com/) in combination with [`filamentphp/spatie-laravel-translatable-plugin`](https://github.com/filamentphp/spatie-laravel-translatable-plugin), then you can add the validation rule like this: ```php TextInput::make('slug') ->title(__('Slug')) ->rules([ UniqueTranslationRule::for('posts', 'slug') ]) ``` ```php TextInput::make('slug') ->title(__('Slug')) ->rules([ fn (Get $get) => UniqueTranslationRule::for('posts', 'slug')->ignore($get('id')) ]) ``` ## 🖥 Example Your existing `slug` column (JSON) in a `posts` table: ```json { "en":"not-abc", "nl":"abc" } ``` Your form input to create a new record: ```html ``` Your validation logic: ```php $attributes = request()->validate([ 'slug.*' => 'unique_translation:posts', ]); ``` The result is that `slug[en]` is valid, since the only `en` value in the database is `not-abc`. And `slug[nl]` would fail, because there already is a `nl` value of `abc`. ## ⚠️ Error Messages You can pass your own error messages as normal. When validating a single form field: ```html ``` ```php $attributes = request()->validate([ 'slug' => 'unique_translation:posts', ], [ 'slug.unique_translation' => 'Your custom :attribute error.', ]); ``` In your view you can then get the error with `$errors->first('slug')`. Or when validation an array: ```html ``` ```php $attributes = request()->validate([ 'slug.*' => 'unique_translation:posts', ], [ 'slug.*.unique_translation' => 'Your custom :attribute error.', ]); ``` In your view you can then get the error with `$errors->first('slug.en')` (`en` being your array key). ## 🚧 Testing ``` vendor/bin/phpunit ``` ## ☕️ Credits - [Ivan Vermeyen](https://byterider.io) - [All contributors](../../contributors) ## 🔓 Security If you discover any security related issues, please [e-mail me](mailto:ivan@codezero.be) instead of using the issue tracker. ## 📑 Changelog A complete list of all notable changes to this package can be found on the [releases page](https://github.com/codezero-be/laravel-unique-translation/releases). ## 📜 License The MIT License (MIT). Please see [License File](https://github.com/codezero-be/laravel-unique-translation/blob/master/LICENSE.md) for more information. ================================================ FILE: composer.json ================================================ { "name": "codezero/laravel-unique-translation", "description": "Check if a translated value in a JSON column is unique in the database.", "keywords": [ "translation", "json", "mysql", "php", "laravel", "validation", "validator", "unique", "rule", "language", "database" ], "license": "MIT", "authors": [ { "name": "Ivan Vermeyen", "email": "ivan@codezero.be" } ], "require": { "php": "^7.2|^8.0" }, "require-dev": { "orchestra/testbench": "^4.0|^5.0|^6.0|^7.0|^8.0|^9.0", "phpunit/phpunit": "^8.0|^9.0|^10.0", "spatie/laravel-translatable": "^4.4|^5.0|^6.0" }, "autoload": { "psr-4": { "CodeZero\\UniqueTranslation\\": "src" } }, "autoload-dev": { "psr-4": { "CodeZero\\UniqueTranslation\\Tests\\": "tests" } }, "extra": { "laravel": { "providers": [ "CodeZero\\UniqueTranslation\\UniqueTranslationServiceProvider" ] } }, "config": { "preferred-install": "dist", "sort-packages": true, "optimize-autoloader": true }, "minimum-stability": "dev", "prefer-stable": true } ================================================ FILE: phpunit.xml.dist ================================================ ./tests ./src ================================================ FILE: src/UniqueTranslationRule.php ================================================ table = $table; $this->column = $column; } /** * Ignore any record that has a column with the given value. * * @param mixed $value * @param string $column * * @return $this */ public function ignore($value, $column = 'id') { $this->ignoreValue = $value; $this->ignoreColumn = $column; return $this; } /** * Generate a string representation of the validation rule. * * @return string */ public function __toString() { return rtrim(sprintf( '%s:%s,%s,%s,%s,%s', $this->rule, $this->table, $this->column ?: 'NULL', $this->ignoreValue ?: 'NULL', $this->ignoreColumn ?: 'NULL', $this->formatWheres() ), ','); } } ================================================ FILE: src/UniqueTranslationServiceProvider.php ================================================ isNovaTranslation($attribute) ? $this->getNovaAttributeNameAndLocale($attribute) : ( $this->isFilamentTranslation($attribute) ? $this->getFilamentAttributeNameAndLocale($attribute, $validator) : $this->getArrayAttributeNameAndLocale($attribute) ); if ($this->isUnique($value, $name, $locale, $parameters)) { return true; } $this->setMissingErrorMessages($validator, $name, $locale); return false; } /** * Set any missing (custom) error messages for our validation rule. * * @param \Illuminate\Validation\Validator $validator * @param string $name * @param string $locale * * @return void */ protected function setMissingErrorMessages($validator, $name, $locale) { $rule = 'unique_translation'; $keys = [ "{$name}.{$rule}", "{$name}.*.{$rule}", "{$name}.{$locale}.{$rule}", "translations_{$name}_{$locale}.{$rule}", ]; foreach ($keys as $key) { if ( ! array_key_exists($key, $validator->customMessages)) { $validator->customMessages[$key] = trans('validation.unique'); } } } /** * Check if the attribute is a Nova translation field name. * * @param string $attribute * * @return bool */ protected function isNovaTranslation($attribute) { return strpos($attribute, '.') === false && strpos($attribute, 'translations_') === 0; } /** * Get the attribute name and locale of a Filament translation field. * * @param string $attribute * * @return array */ protected function getNovaAttributeNameAndLocale($attribute) { $attribute = str_replace('translations_', '', $attribute); return $this->getAttributeNameAndLocale($attribute, '_'); } /** * Check if the attribute is a Filament translation field name. * * @param string $attribute * * @return bool */ protected function isFilamentTranslation($attribute) { return strpos($attribute, 'data.') === 0; } /** * Get the attribute name and locale of a Filament translation field. * * @param string $attribute * * @return array */ protected function getFilamentAttributeNameAndLocale($attribute, $validator) { $attribute = str_replace('data.', '', $attribute); $dataValidator = $validator->getData(); @list($name, $locale) = @explode('.', $attribute); if ($locale === null && Arr::exists($dataValidator, 'activeLocale')) { $locale = $dataValidator['activeLocale']; } return [$name, $locale]; } /** * Get the attribute name and locale of an array field. * * @param string $attribute * * @return array */ protected function getArrayAttributeNameAndLocale($attribute) { return $this->getAttributeNameAndLocale($attribute, '.'); } /** * Get the attribute name and locale. * * @param string $attribute * @param string $delimiter * * @return array */ protected function getAttributeNameAndLocale($attribute, $delimiter) { $locale = $this->getAttributeLocale($attribute, $delimiter); $name = $this->getAttributeName($attribute, $locale, $delimiter); return [$name, $locale ?: App::getLocale()]; } /** * Get the locale from the attribute name. * * @param string $attribute * @param string $delimiter * * @return string|null */ protected function getAttributeLocale($attribute, $delimiter) { $pos = strrpos($attribute, $delimiter); return $pos > 0 ? substr($attribute, $pos + 1) : null; } /** * Get the attribute name without the locale. * * @param string $attribute * @param string|null $locale * @param string $delimiter * * @return string */ protected function getAttributeName($attribute, $locale, $delimiter) { return $locale ? str_replace("{$delimiter}{$locale}", '', $attribute) : $attribute; } /** * Get the database connection and table name. * * @param array $parameters * * @return array */ protected function getConnectionAndTable($parameters) { $parts = explode('.', $this->getParameter($parameters, 0)); $connection = isset($parts[1]) ? $parts[0] : Config::get('database.default'); $table = $parts[1] ?? $parts[0]; return [$connection, $table]; } /** * Get the parameter value at the given index. * * @param array $parameters * @param int $index * * @return string|null */ protected function getParameter($parameters, $index) { return $this->convertNullValue($parameters[$index] ?? null); } /** * Convert any 'NULL' string value to null. * * @param string $value * * @return string|null */ protected function convertNullValue($value) { return strtoupper($value) === 'NULL' ? null : $value; } /** * Check if a translation is unique. * * @param mixed $value * @param string $name * @param string $locale * @param array $parameters * * @return bool */ protected function isUnique($value, $name, $locale, $parameters) { list ($connection, $table) = $this->getConnectionAndTable($parameters); $column = $this->getParameter($parameters, 1) ?? $name; $ignoreValue = $this->getParameter($parameters, 2); $ignoreColumn = $this->getParameter($parameters, 3); $query = $this->findTranslation($connection, $table, $column, $locale, $value); $query = $this->ignore($query, $ignoreColumn, $ignoreValue); $query = $this->addConditions($query, $this->getUniqueExtra($parameters)); $isUnique = $query->count() === 0; return $isUnique; } /** * Find the given translated value in the database. * * @param string $connection * @param string $table * @param string $column * @param string $locale * @param mixed $value * * @return \Illuminate\Database\Query\Builder */ protected function findTranslation($connection, $table, $column, $locale, $value) { // Properly escape backslashes to work with LIKE queries... // See: https://stackoverflow.com/questions/14926386/how-to-search-for-slash-in-mysql-and-why-escaping-not-required-for-wher $escaped = DB::getDriverName() === 'sqlite' ? '\\\\' : '\\\\\\\\'; $value = str_replace('\\', $escaped, $value); // Support PostgreSQL case insensitive queries with ILIKE $operator = DB::getDriverName() === 'pgsql' ? 'ILIKE' : 'LIKE'; return DB::connection($connection)->table($table) ->where(function ($query) use ($column, $operator, $locale, $value) { $query->where($column, $operator, "%\"{$locale}\": \"{$value}\"%") ->orWhere($column, $operator, "%\"{$locale}\":\"{$value}\"%"); }); } /** * Ignore the column with the given value. * * @param \Illuminate\Database\Query\Builder $query * @param string|null $column * @param mixed $value * * @return \Illuminate\Database\Query\Builder */ protected function ignore($query, $column = null, $value = null) { if ($value !== null && $column === null) { $column = 'id'; } if ($column !== null) { $query = $query->where($column, '!=', $value); } return $query; } /** * Get the extra conditions for a unique rule. * Taken From: \Illuminate\Validation\Concerns\ValidatesAttributes * * @param array $parameters * * @return array */ protected function getUniqueExtra($parameters) { if (isset($parameters[4])) { return $this->getExtraConditions(array_slice($parameters, 4)); } return []; } /** * Get the extra conditions for a unique / exists rule. * Taken from: \Illuminate\Validation\Concerns\ValidatesAttributes * * @param array $segments * * @return array */ protected function getExtraConditions(array $segments) { $extra = []; $count = count($segments); for ($i = 0; $i < $count; $i += 2) { $extra[$segments[$i]] = $segments[$i + 1]; } return $extra; } /** * Add the given conditions to the query. * Adapted from: \Illuminate\Validation\DatabasePresenceVerifier * * @param \Illuminate\Database\Query\Builder $query * @param array $conditions * * @return \Illuminate\Database\Query\Builder */ protected function addConditions($query, $conditions) { foreach ($conditions as $key => $value) { $this->addWhere($query, $key, $value); } return $query; } /** * Add a "where" clause to the given query. * Taken from: \Illuminate\Validation\DatabasePresenceVerifier * * @param \Illuminate\Database\Query\Builder $query * @param string $key * @param string $extraValue * * @return \Illuminate\Database\Query\Builder */ protected function addWhere($query, $key, $extraValue) { if ($extraValue === 'NULL') { return $query->whereNull($key); } if ($extraValue === 'NOT_NULL') { return $query->whereNotNull($key); } $isNegative = Str::startsWith($extraValue, '!'); $operator = $isNegative ? '!=' : '='; $value = $isNegative ? mb_substr($extraValue, 1) : $extraValue; return $query->where($key, $operator, $value); } } ================================================ FILE: tests/Stubs/Model.php ================================================ setupDatabase(); } /** * Get the packages service providers. * * @param \Illuminate\Foundation\Application $app * * @return array */ protected function getPackageProviders($app) { return [ UniqueTranslationServiceProvider::class, ]; } /** * Setup the test database. * * @return void */ protected function setupDatabase() { $this->app['db']->getSchemaBuilder()->dropIfExists($this->table); $this->app['db']->getSchemaBuilder()->create($this->table, function (Blueprint $table) { $table->increments('id'); $table->json('slug')->nullable(); $table->text('name')->nullable(); $table->string('other_field')->nullable(); }); $this->beforeApplicationDestroyed(function () { $this->app['db']->getSchemaBuilder()->drop($this->table); }); } } ================================================ FILE: tests/UniqueTranslationTest.php ================================================ ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], ]); $rules = [ 'slug' => "{$this->rule}:{$this->table}", 'name' => UniqueTranslationRule::for($this->table), ]; // The following validation fails, because the // current locale is "en", so we actually set // ['en' => 'existing-slug-en'] etc. $validation = Validator::make([ 'slug' => 'existing-slug-en', 'name' => 'existing-name-en', ], $rules); $this->assertTrue($validation->fails()); // The following validation passes, because the // current locale is "en", so we actually set // ['en' => 'existing-slug-nl'] etc. $validation = Validator::make([ 'slug' => 'existing-slug-nl', 'name' => 'existing-name-nl', ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function search_is_case_insensitive() { Model::create([ 'slug' => ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], ]); $rules = [ 'slug' => "{$this->rule}:{$this->table}", 'name' => UniqueTranslationRule::for($this->table), ]; $validation = Validator::make([ 'slug' => 'Existing-slug-en', 'name' => 'Existing-name-en', ], $rules); $this->assertTrue($validation->fails()); } /** @test */ public function it_checks_if_the_translation_for_a_specific_locale_is_unique() { Model::create([ 'slug' => ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table}", 'name.*' => UniqueTranslationRule::for($this->table), ]; $validation = Validator::make([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ], $rules); $this->assertTrue($validation->fails()); $validation = Validator::make([ 'slug' => ['en' => 'different-slug-en'], 'name' => ['en' => 'different-name-en'], ], $rules); $this->assertTrue($validation->passes()); $validation = Validator::make([ 'slug' => ['nl' => 'existing-slug-nl'], 'name' => ['nl' => 'existing-name-nl'], ], $rules); $this->assertTrue($validation->fails()); $validation = Validator::make([ 'slug' => ['nl' => 'different-slug-nl'], 'name' => ['nl' => 'different-name-nl'], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function a_database_connection_can_be_specified() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ]); $connection = Config::get('database.default'); $rules = [ 'slug' => "{$this->rule}:{$connection}.{$this->table}", 'name' => UniqueTranslationRule::for("{$connection}.{$this->table}"), ]; $validation = Validator::make([ 'slug' => 'existing-slug-en', 'name' => 'existing-name-en', ], $rules); $this->assertTrue($validation->fails()); $validation = Validator::make([ 'slug' => 'different-slug-en', 'name' => 'different-name-en', ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function the_models_attribute_name_can_be_specified() { Model::create([ 'slug' => ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], ]); $rules = [ 'form_slug' => "{$this->rule}:{$this->table},slug", 'form_name' => UniqueTranslationRule::for($this->table, 'name'), ]; $validation = Validator::make([ 'form_slug' => 'existing-slug-en', 'form_name' => 'existing-name-en', ], $rules); $this->assertTrue($validation->fails()); $rules = [ 'form_slug.*' => "{$this->rule}:{$this->table},slug", 'form_name.*' => UniqueTranslationRule::for($this->table, 'name'), ]; $validation = Validator::make([ 'form_slug' => ['nl' => 'existing-slug-nl'], 'form_name' => ['nl' => 'existing-name-nl'], ], $rules); $this->assertTrue($validation->fails()); } /** @test */ public function it_ignores_the_given_id() { $model = Model::create([ 'slug' => ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], ]); $rules = [ 'slug' => "{$this->rule}:{$this->table},null,{$model->id}", 'name' => UniqueTranslationRule::for($this->table)->ignore($model->id), ]; $validation = Validator::make([ 'slug' => 'existing-slug-en', 'name' => 'existing-name-en', ], $rules); $this->assertTrue($validation->passes()); $rules = [ 'slug.*' => "{$this->rule}:{$this->table},null,{$model->id}", 'name.*' => UniqueTranslationRule::for($this->table)->ignore($model->id), ]; $validation = Validator::make([ 'slug' => ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_ignores_a_specific_attribute_with_the_given_value() { $model = Model::create([ 'slug' => ['en' => 'existing-slug-en', 'nl' => 'existing-slug-nl'], 'name' => ['en' => 'existing-name-en', 'nl' => 'existing-name-nl'], 'other_field' => 'foobar', ]); $rules = [ 'slug' => "{$this->rule}:{$this->table},null,{$model->other_field},other_field", 'name' => UniqueTranslationRule::for($this->table)->ignore($model->other_field, 'other_field'), ]; $validation = Validator::make([ 'slug' => 'existing-slug-en', 'name' => 'existing-name-en', ], $rules); $this->assertTrue($validation->passes()); $rules = [ 'slug.*' => "{$this->rule}:{$this->table},null,{$model->other_field},other_field", 'name.*' => UniqueTranslationRule::for($this->table)->ignore($model->other_field, 'other_field'), ]; $validation = Validator::make([ 'slug' => ['nl' => 'existing-slug-nl'], 'name' => ['nl' => 'existing-name-nl'], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_ignores_null_values() { Model::create([ 'slug' => ['en' => null, 'nl' => 'existing-slug-nl'], 'name' => ['en' => null, 'nl' => 'existing-name-nl'], ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table}", 'name.*' => UniqueTranslationRule::for($this->table), ]; $validation = Validator::make([ 'slug' => ['en' => null], 'name' => ['en' => null], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_validates_nova_translations() { Model::create([ 'slug' => ['nl' => 'existing-slug-nl'], 'name' => ['nl' => 'existing-name-nl'], ]); $rules = [ 'translations_slug_nl' => "{$this->rule}:{$this->table},slug", 'translations_name_nl' => UniqueTranslationRule::for($this->table, 'slug'), ]; $validation = Validator::make([ 'translations_slug_nl' => 'existing-slug-nl', 'translations_name_nl' => 'existing-name-nl', ], $rules); $this->assertTrue($validation->fails()); $validation = Validator::make([ 'translations_slug_nl' => 'different-slug-nl', 'translations_name_nl' => 'different-name-nl', ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_handles_backslashes_in_values() { Model::create([ 'slug' => ['en' => '\existing-slug-en', 'nl' => '\existing-slug-nl'], 'name' => ['en' => '\existing-name-en', 'nl' => '\existing-name-nl'], ]); $rules = [ 'slug' => "{$this->rule}:{$this->table}", 'name' => UniqueTranslationRule::for($this->table), ]; // The following validation fails, because the // current locale is "en", so we actually set // ['en' => '\existing-slug-en'] etc. $validation = Validator::make([ 'slug' => '\existing-slug-en', 'name' => '\existing-name-en', ], $rules); $this->assertTrue($validation->fails()); // The following validation passes, because the // current locale is "en", so we actually set // ['en' => '\existing-slug-nl'] etc. $validation = Validator::make([ 'slug' => '\existing-slug-nl', 'name' => '\existing-name-nl', ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_handles_arabic_language() { Model::create([ 'slug' => ['ar' => 'جديد'], 'name' => ['ar' => 'جديد'], ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table}", 'name.*' => UniqueTranslationRule::for($this->table), ]; $validation = Validator::make([ 'slug' => ['ar' => 'جديد'], 'name' => ['ar' => 'جديد'], ], $rules); $this->assertTrue($validation->fails()); } } ================================================ FILE: tests/ValidationMessageTest.php ================================================ ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ]); $formAttributes = [ 'form_slug' => 'existing-slug-en', 'form_name' => 'existing-name-en', ]; $rules = [ 'form_slug' => "{$this->rule}:{$this->table},slug", 'form_name' => UniqueTranslationRule::for($this->table, 'name'), ]; $expectedSlugError = trans('validation.unique', ['attribute' => 'form slug']); $expectedNameError = trans('validation.unique', ['attribute' => 'form name']); $this->assertNotEmpty($expectedSlugError); $this->assertNotEmpty($expectedNameError); $validation = Validator::make($formAttributes, $rules); $this->assertEquals([ 'form_slug' => [$expectedSlugError], 'form_name' => [$expectedNameError], ], $validation->errors()->messages()); } /** @test */ public function it_returns_a_default_error_message_when_validating_an_array() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ]); $formAttributes = [ 'form_slug' => ['en' => 'existing-slug-en'], 'form_name' => ['en' => 'existing-name-en'], ]; $rules = [ 'form_slug.*' => "{$this->rule}:{$this->table},slug", 'form_name.*' => UniqueTranslationRule::for($this->table, 'name'), ]; $expectedSlugError = trans('validation.unique', ['attribute' => 'form_slug.en']); $expectedNameError = trans('validation.unique', ['attribute' => 'form_name.en']); $this->assertNotEmpty($expectedSlugError); $this->assertNotEmpty($expectedNameError); $validation = Validator::make($formAttributes, $rules); $this->assertEquals([ 'form_slug.en' => [$expectedSlugError], 'form_name.en' => [$expectedNameError], ], $validation->errors()->messages()); } /** @test */ public function it_returns_a_custom_error_message_when_validating_a_single_translation() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ]); $formAttributes = [ 'form_slug' => 'existing-slug-en', 'form_name' => 'existing-name-en', ]; $rules = [ 'form_slug' => "{$this->rule}:{$this->table},slug", 'form_name' => UniqueTranslationRule::for($this->table, 'name'), ]; $messages = [ "form_slug.{$this->rule}" => 'Custom slug message for :attribute.', "form_name.{$this->rule}" => 'Custom name message for :attribute.', ]; $expectedSlugError = 'Custom slug message for form slug.'; $expectedNameError = 'Custom name message for form name.'; $validation = Validator::make($formAttributes, $rules, $messages); $this->assertEquals([ 'form_slug' => [$expectedSlugError], 'form_name' => [$expectedNameError], ], $validation->errors()->messages()); } /** @test */ public function it_returns_a_custom_error_message_when_validating_an_array() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ]); $formAttributes = [ 'form_slug' => ['en' => 'existing-slug-en'], 'form_name' => ['en' => 'existing-name-en'], ]; $rules = [ 'form_slug.*' => "{$this->rule}:{$this->table},slug", 'form_name.*' => UniqueTranslationRule::for($this->table, 'name'), ]; $messages = [ "form_slug.*.{$this->rule}" => 'Custom slug message for :attribute.', "form_name.*.{$this->rule}" => 'Custom name message for :attribute.', ]; $expectedSlugError = 'Custom slug message for form_slug.en.'; $expectedNameError = 'Custom name message for form_name.en.'; $validation = Validator::make($formAttributes, $rules, $messages); $this->assertEquals([ 'form_slug.en' => [$expectedSlugError], 'form_name.en' => [$expectedNameError], ], $validation->errors()->messages()); } /** @test */ public function it_returns_a_default_error_message_when_validating_a_nova_translation() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ]); $formAttributes = [ 'translations_form_slug_en' => 'existing-slug-en', 'translations_form_name_en' => 'existing-name-en', ]; $rules = [ 'translations_form_slug_en' => "{$this->rule}:{$this->table},slug", 'translations_form_name_en' => UniqueTranslationRule::for($this->table, 'name'), ]; $expectedSlugError = trans('validation.unique', ['attribute' => 'translations form slug en']); $expectedNameError = trans('validation.unique', ['attribute' => 'translations form name en']); $this->assertNotEmpty($expectedSlugError); $this->assertNotEmpty($expectedNameError); $validation = Validator::make($formAttributes, $rules); $this->assertEquals([ 'translations_form_slug_en' => [$expectedSlugError], 'translations_form_name_en' => [$expectedNameError], ], $validation->errors()->messages()); } } ================================================ FILE: tests/WhereClauseTest.php ================================================ ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], 'other_field' => 'foobar', ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table},null,null,null,other_field,!foobar", 'name.*' => UniqueTranslationRule::for($this->table)->where('other_field', 'not foobar'), ]; $validation = Validator::make([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_accepts_where_not_clauses() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], 'other_field' => 'foobar', ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table},null,null,null,other_field,!foobar", 'name.*' => UniqueTranslationRule::for($this->table)->whereNot('other_field', 'foobar'), ]; $validation = Validator::make([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_accepts_where_null_clause() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], 'other_field' => 'foobar', ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table},null,null,null,other_field,NULL", 'name.*' => UniqueTranslationRule::for($this->table)->whereNull('other_field'), ]; $validation = Validator::make([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ], $rules); $this->assertTrue($validation->passes()); } /** @test */ public function it_accepts_where_not_null_clause() { Model::create([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], 'other_field' => null, ]); $rules = [ 'slug.*' => "{$this->rule}:{$this->table},null,null,null,other_field,NOT_NULL", 'name.*' => UniqueTranslationRule::for($this->table)->whereNotNull('other_field'), ]; $validation = Validator::make([ 'slug' => ['en' => 'existing-slug-en'], 'name' => ['en' => 'existing-name-en'], ], $rules); $this->assertTrue($validation->passes()); } }