FilterableFilterable
Home
📦 Installation
  • Setting Up Filterable
  • Discover Command
  • Listing All Filters
  • Testing Filters
  • Inspecting Filterable Classes
  • Caching
GitHub
Home
📦 Installation
  • Setting Up Filterable
  • Discover Command
  • Listing All Filters
  • Testing Filters
  • Inspecting Filterable Classes
  • Caching
GitHub
  • Home
  • Introduction
  • Installation
  • Service Provider
  • How It Works
  • Engines

    • Invokable

      • Overview
      • Annotations

        • Overview
        • Authorize
        • SkipIf
        • Trim
        • Sanitize
        • Cast
        • DefaultValue
        • MapValue
        • Explode
        • Required
        • In
        • Between
        • Regex
        • Scope
    • Tree
    • Ruleset
    • Expression
  • Features

    • Lifecycle Hooks
    • Header-Driven Filter Mode
    • Auto Register Filterable Macro
    • Conditional Logic
    • Filter Aliases
    • Through callbacks
    • Auto Binding
    • Custom engines
    • Data Provisioning
  • Execution

    • Invoker
  • API Reference

    • Filterable
    • Filterable facade
    • Payload
    • Sorter
  • Caching

    • Overview
    • Getting Started
    • Strategies
    • Auto Invalidation
    • Cache Profiles
    • Scoping Cache
    • Monitoring Cached Items
    • API Reference
    • Examples
  • CLI

    • Setup Filterable
    • Discover Filters
    • Test Filter
    • List Filters
    • Inspect Filter
  • Exceptions
  • Event System
  • Profile Management
  • Profiler
  • Sorting
  • Authorization
  • Validation
  • Sanitization

Annotations (PHP Attributes)

The Invokable Engine supports PHP 8 Attributes as a powerful declarative way to control filter behavior. Instead of writing validation, transformation, and authorization logic inside your filter methods, you declare it with attributes directly on the method signature.


How Annotations Work

When the Invokable Engine processes a filter method, it runs all declared attributes through an Attribute Pipeline before executing the method itself. If any attribute throws a SkipExecution exception, the filter method is skipped entirely. If an attribute throws a StrictnessException, the error propagates up.

#[Trim]
#[Sanitize('lowercase')]
#[Required]
#[In('active', 'pending', 'archived')]
protected function status(Payload $payload)
{
    return $this->builder->where('status', $payload->value);
}

Execution Stages

Attributes are sorted by stage before execution, regardless of the order you declare them. This ensures a predictable pipeline:

OrderStageValuePurposeDescription
1CONTROL1Gate / SkipDecide whether the filter should run
2TRANSFORM2Modify PayloadClean, convert, or map the input value
3VALIDATE3Assert CorrectnessVerify the value meets constraints
4BEHAVIOR4Affect QueryModify query behavior directly

Pipeline Flow

Incoming Payload
    │
    ▼
┌─────────────────┐
│  CONTROL (1)    │  → #[Authorize], #[SkipIf]
│  Should we run? │  → Throws SkipExecution to abort
└────────┬────────┘
         │ ✓ Pass
         ▼
┌─────────────────┐
│  TRANSFORM (2)  │  → #[Trim], #[Sanitize], #[Cast], #[MapValue], #[DefaultValue], #[Explode]
│  Clean the data │  → Modifies payload.value in place
└────────┬────────┘
         │
         ▼
┌─────────────────┐
│  VALIDATE (3)   │  → #[Required], #[In], #[Between], #[Regex]
│  Is data valid? │  → Throws SkipExecution or StrictnessException
└────────┬────────┘
         │ ✓ Pass
         ▼
┌─────────────────┐
│  BEHAVIOR (4)   │  → #[Scope]
│  Affect query   │  → May apply scopes or modify builder
└────────┬────────┘
         │
         ▼
   Filter Method Executes

Available Annotations

Control Stage

AttributeDescription
#[Authorize]Require authorization before running the filter
#[SkipIf]Skip the filter based on a Payload condition

Transform Stage

AttributeDescription
#[Trim]Remove whitespace from string values
#[Sanitize]Apply sanitization rules (lowercase, strip_tags, etc.)
#[Cast]Cast the value to a specific type
#[MapValue]Map input values to different values
#[DefaultValue]Set a fallback value when input is empty
#[Explode]Split a string value into an array

Validate Stage

AttributeDescription
#[Required]Ensure the value is present and not empty
#[In]Validate the value is in an allowed set
#[Between]Validate the value is within a numeric range
#[Regex]Validate the value matches a regex pattern

Behavior Stage

AttributeDescription
#[Scope]Auto-apply an Eloquent scope with the payload value

Combining Attributes

You can stack multiple attributes on a single method. They always execute in stage order:

#[SkipIf('empty')]                              // Stage 1: Skip if empty
#[Trim]                                          // Stage 2: Remove whitespace
#[Sanitize('lowercase', 'strip_tags')]           // Stage 2: Clean the value
#[Cast('int')]                                   // Stage 2: Cast to integer
#[Required]                                      // Stage 3: Must have a value
#[Between(min: 1, max: 1000)]                    // Stage 3: Range check
protected function price(Payload $payload)
{
    return $this->builder->where('price', $payload->value);
}

Creating Custom Annotations

All annotations implement the MethodAttribute interface:

<?php

namespace Kettasoft\Filterable\Engines\Foundation\Attributes\Contracts;

use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributeContext;

interface MethodAttribute
{
    public static function stage(): int;
    public function handle(AttributeContext $context): void;
}

Example: Custom Annotation

<?php

namespace App\Filters\Annotations;

use Attribute;
use Kettasoft\Filterable\Engines\Foundation\Attributes\Contracts\MethodAttribute;
use Kettasoft\Filterable\Engines\Foundation\Attributes\AttributeContext;
use Kettasoft\Filterable\Engines\Foundation\Attributes\Enums\Stage;

#[Attribute(Attribute::TARGET_METHOD)]
class MinLength implements MethodAttribute
{
    public function __construct(public int $length) {}

    public static function stage(): int
    {
        return Stage::VALIDATE->value;
    }

    public function handle(AttributeContext $context): void
    {
        $payload = $context->payload;

        if (is_string($payload->value) && mb_strlen($payload->value) < $this->length) {
            throw new \Kettasoft\Filterable\Engines\Exceptions\SkipExecution(
                "Value must be at least {$this->length} characters."
            );
        }
    }
}

Usage:

#[MinLength(3)]
protected function search(Payload $payload)
{
    return $this->builder->where('title', 'like', $payload->asLike());
}

AttributeContext

The AttributeContext object passed to each annotation's handle() method contains:

PropertyTypeDescription
querymixedThe Eloquent query builder instance
payloadmixedThe Payload object with the filter value
statearrayShared state array (method, key, custom data)

You can read and write to state for inter-attribute communication:

$context->set('my_flag', true);
$context->get('my_flag'); // true
$context->has('my_flag'); // true
Edit this page
Last Updated:
Contributors: kettasoft
Next
Authorize