Plugin Validator Registry
Contract for registering rental transition validators that run in the plugin-validator guard stage.
Overview
App\Guards\Rentals\PluginValidatorRegistry is the extension seam for plugin-supplied rental transition validators.
Core binds the registry as a singleton in AppServiceProvider, but it intentionally ships with no plugin validators. The current runtime consumer is App\Guards\Rentals\Stages\PluginValidatorStage, which runs after core business rules and before the final Verbs validate() hard-invariant layer.
Public surface
| Method | Purpose |
|---|---|
register() |
Store one validator under its key() |
has() |
Check whether a validator key is registered |
all() |
Return the complete keyed validator map |
applicableTo() |
Return the validators whose appliesTo() matches the current transition context |
There is no get() method and no separate lookup API. The stage consumes the registry as a filtered pipeline rather than resolving validators one-by-one by key.
Accepted contract and identity
Every registration implements App\Guards\Rentals\Contracts\TransitionRule:
public function key(): string;
public function appliesTo(TransitionContext $context): bool;
public function evaluate(TransitionContext $context): GuardResult;
public function precheck(TransitionContext $context): GuardResult;
key() is the stable registry identity. appliesTo() decides whether a validator participates in a given App\Guards\Rentals\TransitionContext. evaluate() is the write-time path and may perform the guard-layer work allowed by the contract. precheck() is the dry-run read-model path used when the application wants to know whether a transition would be allowed without executing it; it must be side-effect-free and must not throw.
Collisions, ordering, and failure behaviour
register() assigns directly into an associative map keyed by key(). Registering another validator with the same key replaces the earlier validator instead of creating a second pass and preserves that key's original map position; duplicate keys are not rejected.
The registry has no priority field and no sort step. applicableTo() filters the warehoused map in registration order, so the effective validator order is the current array iteration order of registered keys. Replacing an existing key therefore changes the object used at that position without moving the validator to the end.
The singleton registry retains the same registered validator object instances across stage invocations; it does not resolve fresh instances from the flightcase for each transition.
Because there is no lookup-by-key API, the registry itself has no missing-key exception path. evaluate() may throw as allowed by TransitionRule, but throwing from precheck() violates its dry-run contract. PluginValidatorStage does not catch validator exceptions.
Current core registrations
The framework currently ships with no plugin validators. The core registry starts as an empty map, and PluginValidatorStage therefore allows every transition unless a plugin registers one or more TransitionRule validators.
Context applicability and guard-pipeline consumption
applicableTo() is the context filter:
return array_values(array_filter(
$this->validators,
static fn (TransitionRule $validator): bool => $validator->appliesTo($context),
));
That means applicability is entirely controlled by each validator's appliesTo() implementation. The registry does not match transitions by status name, priority, or any separate manifest.
PluginValidatorStage consumes the filtered validators in both execution modes:
evaluate()runs after the Permission, Approval, and Business Rules stagesprecheck()runs in the same stage position for the dry-runavailable_actionspath- the first denying validator verdict stops the stage and returns its
GuardResult
The stage does not aggregate multiple denials. Once one applicable validator denies, later validators are not consulted.
Worked example
This is the current stage loop:
foreach ($this->validators->applicableTo($context) as $validator) {
$result = $validator->precheck($context);
if ($result->denied()) {
return $result;
}
}
With the shipped empty registry, the loop performs no validator calls and the stage returns GuardResult::allow(). When a plugin registers validators, they participate in registration order and only when their appliesTo() method says they are relevant to the current transition context.