master a2daae936dbc cached
18 files
50.9 KB
13.4k tokens
57 symbols
1 requests
Download .txt
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 (<ivan@codezero.be>)

> 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
<input name="slug">
```

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
<input name="slug[en]">
<input name="slug[nl]">
```

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
<input name="slug[en]" value="abc">
<input name="slug[nl]" value="abc">
```

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
<input name="slug">
```

```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
<input name="slug[en]">
```

```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
================================================
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         backupGlobals="false"
         bootstrap="vendor/autoload.php"
         colors="true"
         processIsolation="false"
         stopOnFailure="false"
         xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/10.5/phpunit.xsd"
         cacheDirectory=".phpunit.cache"
         backupStaticProperties="false">
    <testsuites>
        <testsuite name="CodeZero">
            <directory suffix="Test.php">./tests</directory>
        </testsuite>
    </testsuites>
    <php>
        <env name="APP_ENV" value="testing"/>
        <env name="CACHE_DRIVER" value="array"/>
        <env name="SESSION_DRIVER" value="array"/>
        <env name="QUEUE_DRIVER" value="sync"/>
        <env name="DB_CONNECTION" value="mysql"/>
        <env name="DB_DATABASE" value="testing"/>
        <env name="DB_COLLATION" value="utf8mb4_unicode_ci"/>
        <env name="DB_USERNAME" value="root"/>
        <env name="DB_PASSWORD" value=""/>
    </php>
    <source>
        <include>
            <directory suffix=".php">./src</directory>
        </include>
    </source>
</phpunit>


================================================
FILE: src/UniqueTranslationRule.php
================================================
<?php

namespace CodeZero\UniqueTranslation;

use Illuminate\Validation\Rules\DatabaseRule;

class UniqueTranslationRule
{
    use DatabaseRule;

    /**
     * The name of the validation rule.
     *
     * @var string
     */
    protected $rule = 'unique_translation';

    /**
     * The value of the the 'ignoreColumn' to ignore.
     *
     * @var mixed
     */
    protected $ignoreValue;

    /**
     * The name of the 'ignoreColumn'.
     *
     * @var string|null
     */
    protected $ignoreColumn;

    /**
     * Create a new rule instance.
     *
     * @param string $table
     * @param string|null $column
     *
     * @return static
     */
    public static function for($table, $column = null)
    {
        return new static($table, $column);
    }

    /**
     * Create a new rule instance.
     *
     * @param string $table
     * @param string|null $column
     */
    public function __construct($table, $column = null)
    {
        $this->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
================================================
<?php

namespace CodeZero\UniqueTranslation;

use Illuminate\Support\Facades\Validator;
use Illuminate\Support\ServiceProvider;

class UniqueTranslationServiceProvider extends ServiceProvider
{
    /**
     * Bootstrap any application services.
     *
     * @return void
     */
    public function boot()
    {
        Validator::extend('unique_translation', UniqueTranslationValidator::class.'@validate');
    }

    /**
     * Register any application services.
     *
     * @return void
     */
    public function register()
    {
        //
    }
}


================================================
FILE: src/UniqueTranslationValidator.php
================================================
<?php

namespace CodeZero\UniqueTranslation;

use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Str;
use Illuminate\Support\Arr;

class UniqueTranslationValidator
{
    /**
     * Check if the translated value is unique in the database.
     *
     * @param string $attribute
     * @param string $value
     * @param array $parameters
     * @param \Illuminate\Validation\Validator $validator
     *
     * @return bool
     */
    public function validate($attribute, $value, $parameters, $validator)
    {
        list ($name, $locale) = $this->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
================================================
<?php

namespace CodeZero\UniqueTranslation\Tests\Stubs;

use Illuminate\Database\Eloquent\Model as EloquentModel;
use Spatie\Translatable\HasTranslations;

class Model extends EloquentModel
{
    use HasTranslations;

    public $translatable = ['slug', 'name'];

    protected $table = 'test_models';

    protected $guarded = [];

    public $timestamps = false;
}


================================================
FILE: tests/TestCase.php
================================================
<?php

namespace CodeZero\UniqueTranslation\Tests;

use CodeZero\UniqueTranslation\UniqueTranslationServiceProvider;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\App;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Str;
use Orchestra\Testbench\TestCase as BaseTestCase;

abstract class TestCase extends BaseTestCase
{
    /**
     * Database table for the test models.
     *
     * @var string
     */
    protected $table = 'test_models';

    /**
     * Name of the validation rule.
     *
     * @var string
     */
    protected $rule = 'unique_translation';

    /**
     * Setup the test environment.
     *
     * @return void
     */
    protected function setUp(): void
    {
        parent::setUp();

        Config::set('app.key', Str::random(32));

        App::setLocale('en');

        $this->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
================================================
<?php

namespace CodeZero\UniqueTranslation\Tests;

use CodeZero\UniqueTranslation\Tests\Stubs\Model;
use CodeZero\UniqueTranslation\UniqueTranslationRule;
use Illuminate\Support\Facades\Config;
use Illuminate\Support\Facades\Validator;

class UniqueTranslationTest extends TestCase
{
    /** @test */
    public function it_checks_if_the_translation_for_the_current_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),
        ];

        // 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
================================================
<?php

namespace CodeZero\UniqueTranslation\Tests;

use CodeZero\UniqueTranslation\Tests\Stubs\Model;
use CodeZero\UniqueTranslation\UniqueTranslationRule;
use Illuminate\Support\Facades\Validator;

class ValidationMessageTest extends TestCase
{
    /** @test */
    public function it_returns_a_default_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'),
        ];

        $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
================================================
<?php

namespace CodeZero\UniqueTranslation\Tests;

use CodeZero\UniqueTranslation\Tests\Stubs\Model;
use CodeZero\UniqueTranslation\UniqueTranslationRule;
use Illuminate\Support\Facades\Validator;

// * * *
// You can use any method defined in the DatabaseRule
// trait, except the whereIn and whereNotIn methods.
//
// https://laravel.com/api/5.8/Illuminate/Validation/Rules/DatabaseRule.html
//
// This is because it uses a closure which cannot be converted into a string.
// We need to convert the rule into a string to use it with the UniqueTranslationValidator.
// The reason we use this kind of validator is because it has access to the Validator instance.
// We need that instance to add custom error messages.
// * * *

class WhereClauseTest extends TestCase
{
    /** @test */
    public function it_accepts_where_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,!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());
    }
}
Download .txt
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
Download .txt
SYMBOL INDEX (57 symbols across 8 files)

FILE: src/UniqueTranslationRule.php
  class UniqueTranslationRule (line 7) | class UniqueTranslationRule
    method for (line 40) | public static function for($table, $column = null)
    method __construct (line 51) | public function __construct($table, $column = null)
    method ignore (line 65) | public function ignore($value, $column = 'id')
    method __toString (line 78) | public function __toString()

FILE: src/UniqueTranslationServiceProvider.php
  class UniqueTranslationServiceProvider (line 8) | class UniqueTranslationServiceProvider extends ServiceProvider
    method boot (line 15) | public function boot()
    method register (line 25) | public function register()

FILE: src/UniqueTranslationValidator.php
  class UniqueTranslationValidator (line 11) | class UniqueTranslationValidator
    method validate (line 23) | public function validate($attribute, $value, $parameters, $validator)
    method setMissingErrorMessages (line 51) | protected function setMissingErrorMessages($validator, $name, $locale)
    method isNovaTranslation (line 76) | protected function isNovaTranslation($attribute)
    method getNovaAttributeNameAndLocale (line 88) | protected function getNovaAttributeNameAndLocale($attribute)
    method isFilamentTranslation (line 102) | protected function isFilamentTranslation($attribute)
    method getFilamentAttributeNameAndLocale (line 114) | protected function getFilamentAttributeNameAndLocale($attribute, $vali...
    method getArrayAttributeNameAndLocale (line 136) | protected function getArrayAttributeNameAndLocale($attribute)
    method getAttributeNameAndLocale (line 149) | protected function getAttributeNameAndLocale($attribute, $delimiter)
    method getAttributeLocale (line 165) | protected function getAttributeLocale($attribute, $delimiter)
    method getAttributeName (line 181) | protected function getAttributeName($attribute, $locale, $delimiter)
    method getConnectionAndTable (line 193) | protected function getConnectionAndTable($parameters)
    method getParameter (line 214) | protected function getParameter($parameters, $index)
    method convertNullValue (line 226) | protected function convertNullValue($value)
    method isUnique (line 241) | protected function isUnique($value, $name, $locale, $parameters)
    method findTranslation (line 269) | protected function findTranslation($connection, $table, $column, $loca...
    method ignore (line 295) | protected function ignore($query, $column = null, $value = null)
    method getUniqueExtra (line 316) | protected function getUniqueExtra($parameters)
    method getExtraConditions (line 333) | protected function getExtraConditions(array $segments)
    method addConditions (line 355) | protected function addConditions($query, $conditions)
    method addWhere (line 374) | protected function addWhere($query, $key, $extraValue)

FILE: tests/Stubs/Model.php
  class Model (line 8) | class Model extends EloquentModel

FILE: tests/TestCase.php
  class TestCase (line 12) | abstract class TestCase extends BaseTestCase
    method setUp (line 33) | protected function setUp(): void
    method getPackageProviders (line 51) | protected function getPackageProviders($app)
    method setupDatabase (line 63) | protected function setupDatabase()

FILE: tests/UniqueTranslationTest.php
  class UniqueTranslationTest (line 10) | class UniqueTranslationTest extends TestCase
    method it_checks_if_the_translation_for_the_current_locale_is_unique (line 13) | public function it_checks_if_the_translation_for_the_current_locale_is...
    method search_is_case_insensitive (line 49) | public function search_is_case_insensitive()
    method it_checks_if_the_translation_for_a_specific_locale_is_unique (line 70) | public function it_checks_if_the_translation_for_a_specific_locale_is_...
    method a_database_connection_can_be_specified (line 112) | public function a_database_connection_can_be_specified()
    method the_models_attribute_name_can_be_specified (line 142) | public function the_models_attribute_name_can_be_specified()
    method it_ignores_the_given_id (line 175) | public function it_ignores_the_given_id()
    method it_ignores_a_specific_attribute_with_the_given_value (line 208) | public function it_ignores_a_specific_attribute_with_the_given_value()
    method it_ignores_null_values (line 242) | public function it_ignores_null_values()
    method it_validates_nova_translations (line 263) | public function it_validates_nova_translations()
    method it_handles_backslashes_in_values (line 291) | public function it_handles_backslashes_in_values()
    method it_handles_arabic_language (line 327) | public function it_handles_arabic_language()

FILE: tests/ValidationMessageTest.php
  class ValidationMessageTest (line 9) | class ValidationMessageTest extends TestCase
    method it_returns_a_default_error_message_when_validating_a_single_translation (line 12) | public function it_returns_a_default_error_message_when_validating_a_s...
    method it_returns_a_default_error_message_when_validating_an_array (line 44) | public function it_returns_a_default_error_message_when_validating_an_...
    method it_returns_a_custom_error_message_when_validating_a_single_translation (line 76) | public function it_returns_a_custom_error_message_when_validating_a_si...
    method it_returns_a_custom_error_message_when_validating_an_array (line 110) | public function it_returns_a_custom_error_message_when_validating_an_a...
    method it_returns_a_default_error_message_when_validating_a_nova_translation (line 144) | public function it_returns_a_default_error_message_when_validating_a_n...

FILE: tests/WhereClauseTest.php
  class WhereClauseTest (line 21) | class WhereClauseTest extends TestCase
    method it_accepts_where_clause (line 24) | public function it_accepts_where_clause()
    method it_accepts_where_not_clauses (line 46) | public function it_accepts_where_not_clauses()
    method it_accepts_where_null_clause (line 68) | public function it_accepts_where_null_clause()
    method it_accepts_where_not_null_clause (line 90) | public function it_accepts_where_not_null_clause()
Condensed preview — 18 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (55K chars).
[
  {
    "path": ".editorconfig",
    "chars": 337,
    "preview": "; This file is for unifying the coding style for different editors and IDEs.\n; More information at http://editorconfig.o"
  },
  {
    "path": ".gitattributes",
    "chars": 361,
    "preview": "# Path-based git attributes\n# https://www.kernel.org/pub/software/scm/git/docs/gitattributes.html\n\n# Ignore all test and"
  },
  {
    "path": ".github/FUNDING.yml",
    "chars": 59,
    "preview": "ko_fi: ivanvermeyen\ncustom: https://paypal.me/ivanvermeyen\n"
  },
  {
    "path": ".github/SECURITY.md",
    "chars": 130,
    "preview": "# Security Policy\n\nIf you discover any security related issues, please email ivan@codezero.be instead of using the issue"
  },
  {
    "path": ".github/workflows/run-tests.yml",
    "chars": 2298,
    "preview": "name: Tests\n\non: [ push, pull_request ]\n\njobs:\n  test:\n    runs-on: ubuntu-latest\n    strategy:\n      fail-fast: true\n  "
  },
  {
    "path": ".gitignore",
    "chars": 79,
    "preview": "/vendor\n.idea\ncomposer.lock\nphpunit.xml\n/.phpunit.result.cache\n/.phpunit.cache\n"
  },
  {
    "path": "LICENSE.md",
    "chars": 1135,
    "preview": "# The MIT License (MIT)\n\nCopyright (c) 2017 Ivan Vermeyen (<ivan@codezero.be>)\n\n> Permission is hereby granted, free of "
  },
  {
    "path": "README.md",
    "chars": 9673,
    "preview": "# Laravel Unique Translation\n\n## IMPORTANT: March 2022\n\n[![Support Ukraine](https://raw.githubusercontent.com/hampusborg"
  },
  {
    "path": "composer.json",
    "chars": 1350,
    "preview": "{\n    \"name\": \"codezero/laravel-unique-translation\",\n    \"description\": \"Check if a translated value in a JSON column is"
  },
  {
    "path": "phpunit.xml.dist",
    "chars": 1178,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n         backupGlo"
  },
  {
    "path": "src/UniqueTranslationRule.php",
    "chars": 1805,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation;\n\nuse Illuminate\\Validation\\Rules\\DatabaseRule;\n\nclass UniqueTranslationRule"
  },
  {
    "path": "src/UniqueTranslationServiceProvider.php",
    "chars": 557,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation;\n\nuse Illuminate\\Support\\Facades\\Validator;\nuse Illuminate\\Support\\ServicePr"
  },
  {
    "path": "src/UniqueTranslationValidator.php",
    "chars": 10680,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation;\n\nuse Illuminate\\Support\\Facades\\App;\nuse Illuminate\\Support\\Facades\\Config;"
  },
  {
    "path": "tests/Stubs/Model.php",
    "chars": 368,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation\\Tests\\Stubs;\n\nuse Illuminate\\Database\\Eloquent\\Model as EloquentModel;\nuse S"
  },
  {
    "path": "tests/TestCase.php",
    "chars": 1801,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation\\Tests;\n\nuse CodeZero\\UniqueTranslation\\UniqueTranslationServiceProvider;\nuse"
  },
  {
    "path": "tests/UniqueTranslationTest.php",
    "chars": 10819,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation\\Tests;\n\nuse CodeZero\\UniqueTranslation\\Tests\\Stubs\\Model;\nuse CodeZero\\Uniqu"
  },
  {
    "path": "tests/ValidationMessageTest.php",
    "chars": 5992,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation\\Tests;\n\nuse CodeZero\\UniqueTranslation\\Tests\\Stubs\\Model;\nuse CodeZero\\Uniqu"
  },
  {
    "path": "tests/WhereClauseTest.php",
    "chars": 3546,
    "preview": "<?php\n\nnamespace CodeZero\\UniqueTranslation\\Tests;\n\nuse CodeZero\\UniqueTranslation\\Tests\\Stubs\\Model;\nuse CodeZero\\Uniqu"
  }
]

About this extraction

This page contains the full source code of the codezero-be/laravel-unique-translation GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 18 files (50.9 KB), approximately 13.4k tokens, and a symbol index with 57 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!