acme_tracking:AC from a distributed package, register the validation logic on the validation Factory. Consuming applications can then use it in their form requests with the same syntax as the built-in rules.
This page is based on the official source of Laravel Framework v13.40.0. It supplements the basic examples in Custom validation rules and covers registration, translation, and state lifetime when maintaining such rules as a package’s public API.
Choose between this and rule objects
TheValidationRule objects introduced in the official documentation can also be used in packages. Unless being specifiable as a string is itself a requirement, consider an object that can receive typed arguments through its constructor.
Even though both use the name
validate(), their calling contracts differ. If you pass a class implementing ValidationRule directly as the class string to Factory::extend(), the type of the third argument and the return value will not match.
Separate registration, creation, and validation
extend() on the Validator facade is a call to Illuminate\Validation\Factory. It is a different object from the Illuminate\Validation\Validator instance that validates input.
The diagram shows the path when a string rule is executed. Skipping due to missing input, nullable, and so on is covered later.
The Factory is registered as a singleton under the validator key, but the Validator returned by the default make() implementation is newly created every time. Registering on the Factory afterward does not update the extension arrays of Validators that have already been created.
Register a package-specific rule
As an example, we validate a fictional tracking code consisting of an uppercase prefix, a hyphen, and eight digits. A format likeAC-00000001 is just an example and not the number specification of any real carrier.
This assumes the package namespace Acme\Courier\ maps to src/ via Composer PSR-4 and is loaded as a regular non-deferred service provider. For provider loading, see Package auto-discovery.
Separate the validation logic into a class
Prepare a handler for the string rule insrc/Validation/TrackingCode.php. This class does not implement the ValidationRule interface.
acme_tracking:AC, the parameter array is ['AC']. The rule also checks the value’s type itself so that it does not cause a type error when used on its own.
An invalid prefix or parameter count throws an exception as a misconfiguration of the rule definition, not as a user input error. When the rule is skipped for empty values, this configuration check is not executed either, so you also need tests that use valid non-empty values.
Register after the Factory is resolved
Register the rule and the message replacer fromboot() in src/CourierServiceProvider.php.
callAfterResolving() waits for creation if the Factory has not yet been resolved, and registers immediately on a Factory that has already been resolved. The hook itself is explained in detail in Package service post-resolution hooks.
With the class string TrackingCode::class.'@validate', the class is resolved from the container at validation time and the method is called. Even if you pass only the class name without @validate, the default method name is validate. If the class has constructor dependencies, bind them so the container can resolve them.
This example does not register the handler as a singleton or include properties that store input. Handle validation-time data through the arguments, and design the closures held by the Factory so they do not capture request-specific state.
Specify it from the consuming application
bail stops subsequent rules for that attribute once a preceding rule fails. Split responsibilities so that required handles presence and acme_tracking handles format.
Use unique snake case names such as acme_tracking consistently for public names. When the same name is registered for the same kind of extension, the later registration overwrites the earlier one. Also, if a built-in method such as validateRequired() exists, that method is called directly, so you cannot replace built-in validation with an extension of the same name.
Preserve the user’s error messages
Messages for regular string rules are selected roughly in the following order of precedence. Special branches such as size rules are out of scope here.
The custom replacer is called after Laravel replaces placeholders such as
:attribute. The replacer receives five arguments: the selected message, the attribute name, the rule name, the parameter array, and the Validator. The earlier example omits the unused fifth argument.
To specify a Japanese default message on the application side, add acme_tracking to the array returned by the existing lang/ja/validation.php. The example below shows the entry to add; it does not remove the other built-in messages.
__('...') at registration time and store the result as the fallback, the string for the locale at that moment remains in the Factory. In applications that switch locales, use translations at the time the error is generated.
When using namespaced package translations
If you callloadTranslationsFrom(__DIR__.'/../lang', 'acme-courier') in boot(), you can reference the package’s lang/ja/validation.php and similar files as acme-courier::validation.tracking_code.
However, simply passing a translation key as the third argument of extend() does not translate it automatically. The Factory keeps it as a plain fallback string. If you replace the extend() and replacer() in the earlier hook with the following registration, the package translation is retrieved only when the fallback is selected.
tracking_code entry in the package’s validation.php for each locale. For Japanese, you can use the same text as the earlier acme_tracking entry.
Because the replacer retrieves a new translation string here, :attribute is also passed explicitly to the translation. Laravel’s standard replacements are not applied again to this return value. When the user’s own message is selected, that text is kept. For overriding distributed translations, see Overriding and updating package translations.
Understand the run conditions for Implicit and nullable
extendImplicit() registers a rule that becomes a validation candidate even for missing attributes or empty strings. It does not mean “always fail” or “ignore all other skip conditions.”
The table compares the run conditions for each rule. Cases where processing stops earlier due to
bail, attribute exclusion, stopOnFirstFailure(), and so on are separate. For HTTP input, middleware may convert empty strings to null, so test empty strings and null separately.
For the format rule here, combining a regular extend() with required is sufficient. Changing an already-published rule to Implicit can cause input that was previously not validated to fail, so treat it as a compatibility change.
Resolve references within arrays with Dependent rules
For rules that take another field’s name as a parameter, you can useextendDependent(). When validating items.*.confirmed_tracking, the parameter items.*.tracking is converted to the same row, such as items.0.tracking. A regular extend() does not perform this replacement.
The following is a matching rule for confirming how this mechanism works. If you only need a simple equality check, real applications can use the built-in same. Treat this as an example for applying the mechanism to package-specific cross-field validation.
Add it to the Factory received from the hook.
getData() on the fourth argument returns the data under validation, not only validated data. Define the necessary rules on both the referencing and referenced fields.
Also, making a rule Dependent does not make it Implicit. Specify presence requirements for missing or empty values separately. Because the implementation escapes dots and replaces * in the parameters, avoid APIs that mix arbitrary strings other than field names into them.
Test from registration to validation in an application
In a Laravel 13 application that loads the package and its service provider, createtests/Feature/TrackingRuleTest.php. In addition to unit tests of the class, verify registration and message handling through the actual Factory.
What to check for long-term maintenance
- Document the rule name, the meaning and number of parameters, presence requirements, and accepted value types as public API.
- Test invalid parameters, missing values, empty strings,
null, and combinations withnullable,sometimes, andbail. - Check the user’s
messages(), attribute display names, locale switching, and overrides of package translations. - For Dependent rules, confirm with multiple rows and nested arrays that values from other rows are not referenced.
- Verify registration on both unresolved and resolved Factories, and do not expect changes to be applied retroactively to already-created Validators.
- Avoid name collisions with other packages, and do not mix regular, Implicit, and Dependent registrations under the same name.
- Restart processes to apply changes to long-lived workers, and do not leave input or user information in shared handlers.
Primary sources consulted
- Laravel official documentation: Custom rules and implicit rules
- Laravel official documentation: Package service providers and translations
- Factory: make, extend, extendImplicit, extendDependent, replacer
- Validator: Run conditions, extension registration, and class resolution
- ValidationRuleParser: Parsing string rules
- FormatsMessages: Message precedence and replacement
- ValidationServiceProvider: Singleton registration of validator
- ServiceProvider: callAfterResolving
- Official tests: Forwarding extensions and fallbacks from the Factory
- Official tests: String, class, Implicit, and Dependent rules