Condition Processor
The Condition Processor provides declarative authorization and validation for controller methods using PHP 8 attributes. It integrates with the DI container’s resolveMethod() to evaluate conditions before a method executes.
Overview
flowchart TD
Resolve["resolveMethod()"] --> Scan["Scan attributes"]
Scan --> Collect["Collect PreConditions<br/>remove SkipConditions"]
Collect --> Check{"Conditions<br/>remaining?"}
Check -->|No| Execute["Execute method"]
Check -->|Yes| Eval{"process(ctx)<br/>returns null?"}
Eval -->|All pass| Execute
Eval -->|Non-null| Interrupt["Return interrupt value"]
Attributes
#[PreCondition]
Marks a method (or class) with a condition that must pass before execution:
use Cubex\Attributes\PreCondition;
class AdminController extends Controller
{
#[PreCondition(RequiresAuth::class)]
#[PreCondition(RequiresRole::class, ['admin'])]
public function getUsers(): Response
{
return new JsonResponse($this->listUsers());
}
}
The attribute accepts:
$class— The fully qualified class name implementingConditionResult$args(optional) — Constructor arguments passed when instantiating the condition
#[SkipCondition]
Exempts a method from a specific PreCondition. This is useful when a class-level condition should not apply to certain methods:
#[PreCondition(RequiresAuth::class)]
class DashboardController extends Controller
{
public function getIndex(): Response
{
// RequiresAuth IS evaluated
return new TextResponse('Dashboard');
}
#[SkipCondition(RequiresAuth::class)]
public function getHealthCheck(): Response
{
// RequiresAuth is SKIPPED
return new TextResponse('OK');
}
}
Both attributes are repeatable (IS_REPEATABLE) and can target any element (TARGET_ALL).
Implementing ConditionResult
Create a condition by implementing the ConditionResult interface:
use Cubex\Attributes\ConditionResult;
use Packaged\Context\Context;
class RequiresAuth implements ConditionResult
{
public function process(Context $ctx): mixed
{
if ($ctx->request()->headers->has('Authorization'))
{
return null; // Allow execution to proceed
}
// Interrupt — this value becomes the method's return
return new Response('Unauthorized', 401);
}
}
The process() method:
- Returns
nullto allow execution to continue - Returns any non-null value to interrupt execution — the returned value replaces the method’s normal return value
Conditions with Arguments
Pass constructor arguments via the attribute:
class RequiresRole implements ConditionResult
{
public function __construct(private string $role)
{
}
public function process(Context $ctx): mixed
{
$user = $ctx->meta()->get('user');
if ($user && $user->hasRole($this->role))
{
return null;
}
return new Response('Forbidden', 403);
}
}
#[PreCondition(RequiresRole::class, ['admin'])]
public function getAdminPanel(): Response
{
// Only accessible to users with the 'admin' role
}
How It Works Internally
The ConditionProcessor class integrates with packaged/di-container’s reflection system:
ConditionProcessorextendsAttributeWatcherand implementsReflectionInterrupt- When
resolveMethod()is called on a controller method, the DI container’s reflection observers are notified ConditionProcessorscans the method (and class) for#[PreCondition]and#[SkipCondition]attributes- Skip conditions are collected first — any
PreConditionwhose class appears in the skip list is removed - Each remaining condition is instantiated (via DI if available, for constructor injection) and
process($context)is called - If any condition returns non-null,
shouldInterruptMethod()returnstrueand the interrupt value is used as the method’s return value
// This happens automatically inside Controller::_prepareHandler()
// when Cubex DI is available:
$conditionProcessor = new ConditionProcessor($cubex);
$result = $di->resolveMethod($controller, $methodName, [], [$conditionProcessor]);
Execution Flow
sequenceDiagram
participant DI as DI Container
participant CP as ConditionProcessor
participant Cond as ConditionResult
participant Method as Controller Method
Note over CP: Collect attributes,<br/>remove skipped conditions
loop Each condition
CP->>Cond: process(context)
alt pass (null)
Note over CP: Continue
else fail (non-null)
CP-->>DI: interrupt with result
Note over Method: Method NOT called
end
end
Note over CP: All conditions passed
DI->>Method: invoke with injected params
Method-->>DI: return value
Multiple Conditions
When multiple #[PreCondition] attributes are present, they are evaluated in order. The first condition to return a non-null value stops evaluation:
#[PreCondition(RequiresAuth::class)] // Checked first
#[PreCondition(RequiresRole::class, ['editor'])] // Checked second
#[PreCondition(RateLimiter::class)] // Checked third
public function postArticle(): Response
{
// All three conditions must pass (return null)
}
Class-Level vs Method-Level
Attributes on the class apply to all methods. Method-level attributes are additive. Use #[SkipCondition] to exempt specific methods:
#[PreCondition(RequiresAuth::class)]
class SecureController extends Controller
{
// RequiresAuth applies to all methods
#[PreCondition(RequiresRole::class, ['admin'])]
public function getAdmin(): Response
{
// Both RequiresAuth AND RequiresRole apply
}
#[SkipCondition(RequiresAuth::class)]
public function getPublic(): Response
{
// RequiresAuth is skipped, no conditions apply
}
}