TranslationFieldExistRule
The TranslationFieldExistRule is a validation rule that ensures a translated field value is unique per locale across records of a given model that uses the HasTranslation trait. It is aware of the package's versioning semantics and supports optional scoping by parent constraints and update-safe exclusions by current object id.
Namespace
JobMetric\Translation\Rules\TranslationFieldExistRule
Overview
TranslationFieldExistRule validates that:
- A translated field value is unique per locale across model records
- The rule respects versioning semantics (active vs historical translations)
- Supports update-safe exclusion (exclude current record when updating)
- Allows parent scoping for hierarchical models
- Provides customizable error messages via translation keys
This rule is essential for:
- Data Integrity: Ensuring unique translated values per locale
- Form Validation: Validating translation uniqueness in form requests
- Update Safety: Excluding current record during updates
- Hierarchical Models: Scoping uniqueness within parent relationships
- Versioning Awareness: Respecting translation versioning policies
Constructor
public function __construct(
string $class_name,
string $field_name = 'title',
?string $locale = null,
?int $object_id = null,
?int $parent_id = -1,
array $parent_where = [],
string $field_name_trans = 'translation::base.rule.default_field'
)
Parameters
| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
$class_name | string | Yes | - | Fully-qualified parent model class (must use HasTranslation) |
$field_name | string | No | 'title' | Translation field to check (e.g., 'title', 'slug') |
$locale | string|null | No | app()->getLocale() | Locale code (e.g., 'en', 'fa') |
$object_id | int|null | No | null | Current parent id to exclude (for updates) |
$parent_id | int|null | No | -1 | Filter on parent table's parent_id (use -1 to ignore) |
$parent_where | array | No | [] | Additional where constraints on parent table |
$field_name_trans | string | No | 'translation::base.rule.default_field' | i18n key for field name in error message |
Parameter Details
$class_name (string, required)
Fully-qualified class name of the parent model that uses HasTranslation trait.
// Using class constant
new TranslationFieldExistRule(Post::class, 'title')
// Using string
new TranslationFieldExistRule('App\Models\Post', 'title')
Requirements:
- Class must exist
- Class must use
HasTranslationtrait - Throws
ModelHasTranslationNotFoundExceptionif requirements not met
$field_name (string, default: 'title')
The translation field name to check for uniqueness.
'title' // Check title uniqueness
'slug' // Check slug uniqueness
'name' // Check name uniqueness
$locale (string|null, default: app()->getLocale())
The locale code to check uniqueness within.
'en' // English
'fa' // Persian
'de' // German
null // Uses app()->getLocale()
$object_id (int|null, default: null)
Current parent model id to exclude from uniqueness check. Used when updating existing records.
null // No exclusion (for create operations)
$post->id // Exclude current post (for update operations)
$parent_id (int|null, default: -1)
Optional filter on parent table's parent_id column. Use -1 to ignore.
-1 // Ignore parent_id filter
5 // Only check within parent_id = 5
null // Ignore parent_id filter
$parent_where (array, default: [])
Additional where constraints on the parent table.
[] // No additional constraints
['status' => 'published'] // Only check published records
['category_id' => 1] // Only check within category 1
['status' => 'active', 'type' => 'post'] // Multiple constraints
$field_name_trans (string, default: 'translation::base.rule.default_field')
Translation key for rendering a human-friendly field name in error messages.
'translation::base.rule.default_field' // Default
'validation.attributes.title' // Custom field name
How It Works
Validation Process
The rule performs the following steps:
- Validates Model: Checks if class exists and uses
HasTranslationtrait - Builds Query: Constructs database query with constraints
- Applies Versioning: Respects versioning semantics (active vs historical)
- Applies Scoping: Applies parent_id and parent_where constraints
- Excludes Current: Excludes current record if
object_idprovided - Checks Uniqueness: Returns
trueif no conflict,falseif conflict exists
Versioning Awareness
The rule respects translation versioning:
Versioning ON (usesTranslationVersioning() returns true):
- Checks only active rows (
deleted_at IS NULL) - Represents latest version for each
(locale, field)combination
Versioning OFF:
- Checks
version = 1ANDdeleted_at IS NULL - Strict version 1 matching
Uniqueness Target
The rule checks uniqueness for:
translatable_type= model class namelocale= specified localefield= field namevalue= submitted value- Versioning constraints (as above)
- Optional parent constraints
- Optional current record exclusion
Basic Usage
In Form Request (Create)
Validate translation uniqueness when creating:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', app()->getLocale());
return [
'translation.' . $locale . '.title' => [
'required',
'string',
'max:255',
new TranslationFieldExistRule(
Post::class,
'title',
$locale,
null, // object_id (null on create)
-1, // parent_id (ignore)
[] // parent_where
),
],
];
}
}
In Form Request (Update)
Exclude current record when updating:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class UpdatePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', app()->getLocale());
$post = $this->route('post'); // Get current post
return [
'translation.' . $locale . '.title' => [
'required',
'string',
'max:255',
new TranslationFieldExistRule(
Post::class,
'title',
$locale,
$post->id, // Exclude current post
-1, // parent_id (ignore)
[] // parent_where
),
],
];
}
}
Direct Validation
use Illuminate\Support\Facades\Validator;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
$validator = Validator::make($data, [
'translation.en.title' => [
'required',
'string',
new TranslationFieldExistRule(Post::class, 'title', 'en'),
],
]);
if ($validator->fails()) {
return response()->json($validator->errors(), 422);
}
Complete Examples
Example 1: Basic Uniqueness Check
Ensure product titles are unique per locale:
namespace App\Http\Requests;
use App\Models\Product;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StoreProductRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
return [
'translation.' . $locale . '.title' => [
'required',
'string',
'max:255',
new TranslationFieldExistRule(
Product::class,
'title',
$locale
),
],
];
}
}
Example 2: Update with Exclusion
Exclude current record when updating:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class UpdatePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
$post = $this->route('post');
return [
'translation.' . $locale . '.title' => [
'required',
'string',
new TranslationFieldExistRule(
Post::class,
'title',
$locale,
$post->id // Exclude current post
),
],
];
}
}
Example 3: With Parent Scoping
Ensure uniqueness within a parent category:
namespace App\Http\Requests;
use App\Models\Category;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StoreCategoryRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
$parentId = $this->input('parent_id');
return [
'translation.' . $locale . '.name' => [
'required',
'string',
new TranslationFieldExistRule(
Category::class,
'name',
$locale,
null,
$parentId !== null ? $parentId : -1 // Scope by parent
),
],
];
}
}
Example 4: With Additional Parent Constraints
Add additional constraints on parent table:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
return [
'translation.' . $locale . '.slug' => [
'required',
'string',
new TranslationFieldExistRule(
Post::class,
'slug',
$locale,
null,
-1,
['status' => 'published'] // Only check published posts
),
],
];
}
}
Example 5: Multiple Locales
Validate uniqueness for multiple locales:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
$rules = [];
$locales = ['en', 'fa', 'de'];
foreach ($locales as $locale) {
$rules["translation.{$locale}.title"] = [
'required',
'string',
new TranslationFieldExistRule(
Post::class,
'title',
$locale
),
];
}
return $rules;
}
}
Example 6: Custom Error Message
Provide custom field name in error message:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
return [
'translation.' . $locale . '.title' => [
'required',
'string',
new TranslationFieldExistRule(
Post::class,
'title',
$locale,
null,
-1,
[],
'validation.attributes.title' // Custom field name
),
],
];
}
}
Example 7: Complex Scoping
Combine parent_id and parent_where constraints:
namespace App\Http\Requests;
use App\Models\Product;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StoreProductRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
$categoryId = $this->input('category_id');
return [
'translation.' . $locale . '.name' => [
'required',
'string',
new TranslationFieldExistRule(
Product::class,
'name',
$locale,
null,
$categoryId, // Scope by category
['status' => 'active', 'type' => 'product'] // Additional constraints
),
],
];
}
}
Example 8: Conditional Validation
Apply rule conditionally:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use Illuminate\Validation\Rule;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
$rules = [
'translation.' . $locale . '.title' => ['required', 'string'],
];
// Only validate uniqueness for published posts
if ($this->input('status') === 'published') {
$rules['translation.' . $locale . '.title'][] = new TranslationFieldExistRule(
Post::class,
'title',
$locale
);
}
return $rules;
}
}
Error Messages
Default Error Message
The rule uses a translation key for error messages:
trans('translation::base.rule.exist', [
'field' => trans($this->field_name_trans)
])
Translation Key: translation::base.rule.exist
Default Message: "The translation field already exists."
Custom Error Messages
Override error messages in form requests:
namespace App\Http\Requests;
use App\Models\Post;
use Illuminate\Foundation\Http\FormRequest;
use JobMetric\Translation\Rules\TranslationFieldExistRule;
class StorePostRequest extends FormRequest
{
public function rules(): array
{
$locale = $this->input('locale', 'en');
return [
'translation.' . $locale . '.title' => [
'required',
'string',
new TranslationFieldExistRule(Post::class, 'title', $locale),
],
];
}
public function messages(): array
{
return [
'translation.' . $this->input('locale', 'en') . '.title' =>
'This title already exists for this language.',
];
}
}
Error Response Format
When validation fails:
{
"errors": {
"translation.en.title": [
"The translation field already exists."
]
}
}
Versioning Behavior
Versioning Enabled
When usesTranslationVersioning() returns true:
// Only checks active translations (deleted_at IS NULL)
// Represents latest version per (locale, field)
Query Logic:
WHERE deleted_at IS NULL
Versioning Disabled
When versioning is disabled:
// Checks version = 1 AND deleted_at IS NULL
Query Logic:
WHERE version = 1 AND deleted_at IS NULL
When to Use
Use TranslationFieldExistRule when you need to:
- Unique Translations: Ensure translated field values are unique per locale
- Form Validation: Validate translation uniqueness in form requests
- Update Safety: Exclude current record when updating translations
- Hierarchical Models: Enforce uniqueness within parent relationships
- Versioning Awareness: Respect translation versioning policies
- Scoped Uniqueness: Apply additional constraints on parent table
When NOT to Use
Avoid using this rule when:
- Global Uniqueness: If you need global uniqueness (not per locale), use Laravel's
uniquerule - Non-Translatable Fields: For regular database columns, use standard validation rules
- Performance Critical: For high-frequency validations, consider caching or database indexes
Best Practices
1. Always Use Class Constants
// Good: Use class constants
new TranslationFieldExistRule(Post::class, 'title', 'en')
// Avoid: String literals
new TranslationFieldExistRule('App\Models\Post', 'title', 'en')
2. Exclude Current Record on Updates
// Good: Exclude current record
new TranslationFieldExistRule(Post::class, 'title', 'en', $post->id)
// Avoid: Not excluding on updates
new TranslationFieldExistRule(Post::class, 'title', 'en') // May fail on update
3. Combine with Type Validation
// Good: Combined with type validation
'translation.en.title' => [
'required',
'string',
'max:255',
new TranslationFieldExistRule(Post::class, 'title', 'en'),
]
// Avoid: Missing type validation
'translation.en.title' => [
new TranslationFieldExistRule(Post::class, 'title', 'en'),
]
4. Use Appropriate Scoping
// Good: Scope by parent when needed
new TranslationFieldExistRule(
Category::class,
'name',
'en',
null,
$parentId // Scope by parent
)
// Avoid: Global check when scoping is needed
new TranslationFieldExistRule(Category::class, 'name', 'en')
5. Provide Custom Field Names
// Good: Custom field name
new TranslationFieldExistRule(
Post::class,
'title',
'en',
null,
-1,
[],
'validation.attributes.title'
)
// Avoid: Generic field name
new TranslationFieldExistRule(Post::class, 'title', 'en')
Common Mistakes
Mistake 1: Not Excluding Current Record on Updates
// Bad: May fail when updating with same value
new TranslationFieldExistRule(Post::class, 'title', 'en')
// Good: Exclude current record
new TranslationFieldExistRule(Post::class, 'title', 'en', $post->id)
Mistake 2: Using Wrong Locale
// Bad: Hard-coded locale
new TranslationFieldExistRule(Post::class, 'title', 'en')
// Good: Dynamic locale
$locale = $this->input('locale', app()->getLocale());
new TranslationFieldExistRule(Post::class, 'title', $locale)
Mistake 3: Not Combining with Type Validation
// Bad: Missing type validation
'translation.en.title' => [
new TranslationFieldExistRule(Post::class, 'title', 'en'),
]
// Good: Combined with type validation
'translation.en.title' => [
'required',
'string',
new TranslationFieldExistRule(Post::class, 'title', 'en'),
]
Mistake 4: Incorrect Parent Scoping
// Bad: Wrong parent_id value
new TranslationFieldExistRule(
Category::class,
'name',
'en',
null,
0 // May match parent_id = 0, not ignore
)
// Good: Use -1 to ignore
new TranslationFieldExistRule(
Category::class,
'name',
'en',
null,
-1 // Ignore parent_id
)
Performance Considerations
Database Queries
The rule performs a database query for each validation:
// Each rule instance = 1 database query
$rules = [
'translation.en.title' => new TranslationFieldExistRule(Post::class, 'title', 'en'),
'translation.fa.title' => new TranslationFieldExistRule(Post::class, 'title', 'fa'),
];
// = 2 database queries
Optimization Tips
- Add Database Indexes:
// Migration
Schema::table('translations', function (Blueprint $table) {
$table->index(['translatable_type', 'locale', 'field', 'value']);
$table->index(['translatable_type', 'locale', 'field', 'deleted_at']);
});
- Cache Validation Results:
// Cache unique values to reduce database queries
- Batch Validation:
// Validate multiple fields in single request
// to reduce overall query count