SIGNALS Documentation
API Reference

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:

  1. evaluate() runs after the Permission, Approval, and Business Rules stages
  2. precheck() runs in the same stage position for the dry-run available_actions path
  3. 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.