v1.0
Docs / Attributes

Attributes

This page provides detailed documentation on PHP 8 Attributes—how they work, why we use them, and practical examples with step-by-step guidance.

Table of Contents

  1. What Are Attributes?
  2. Why Use Attributes?
  3. Attribute Reference
  4. Combining Multiple Attributes
  5. Step-by-Step Tutorial
  6. How Attributes Work Internally

1. What Are Attributes?

Goal: Stop using `Config/Routes.php` and keep your configuration directly on your code.

Technical Definition

Attributes are a PHP 8 feature that allows you to add structured metadata to classes, methods, properties, and parameters. They are defined using the #[...] syntax.

Simple Analogy

Think of attributes like sticky notes you attach to your code:

Example: Without vs With Attributes

Without attributes: (separate configuration file)

// Config/Routes.php
$routes->get('/users', 'UserController::index');
$routes->get('/users/(:num)', 'UserController::show/$1');
$routes->post('/users', 'UserController::store');

// UserController.php
class UserController
{
    public function index() { }
    public function show($id) { }
    public function store() { }
}

With attributes: (configuration is on the code)

// UserController.php
class UserController
{
    #[Route('/users', methods: ['GET'])]
    public function index() { }

    #[Route('/users/{id}', methods: ['GET'])]
    public function show(int $id) { }

    #[Route('/users', methods: ['POST'])]
    public function store() { }
}

2. Why Use Attributes?

BenefitExplanationExample
ColocationConfiguration lives next to the code it affectsRoute is defined on the method, not in a separate file
DiscoverabilityLook at a method, see everything about itSee the route, middleware, and tracing in one place
Type SafetyIDE autocompletion and error checkingWrong parameter type? IDE shows error immediately
RefactoringMove/rename code, config moves with itRename method? Route stays attached
ComposabilityStack multiple attributes freelySame method can have Route + Middleware + Monitor

3. Attribute Reference

3.1 #[Route] — HTTP Routing

Task: Add a `#[Route]` to a method and remove the manual entry from `Config/Routes.php`.

Purpose: Define HTTP routes directly on controller methods.

Location:App\Attributes\Route

ParameterTypeDefaultDescription
pathstringrequiredURL path (e.g., /users/{id})
methodsarray['GET']HTTP methods: GET, POST, PUT, PATCH, DELETE
name?stringnullNamed route for URL generation

Examples:

// Basic GET route
#[Route('/products')]
public function index() { }

// Route with parameter
#[Route('/products/{id}', methods: ['GET'])]
public function show(int $id) { }

// Multiple HTTP methods
#[Route('/products/{id}', methods: ['PUT', 'PATCH'])]
public function update(int $id) { }

// Named route (for URL generation)
#[Route('/products/{id}', methods: ['GET'], name: 'products.show')]
public function show(int $id) { }
// Usage: $url = route_to('products.show', 42); // /products/42

// Multiple URL parameters
#[Route('/blog/{year}/{month}/{slug}')]
public function post(int $year, int $month, string $slug) { }

3.2 #[Middleware] — Attach Filters

Purpose: Attach CI4 filters (middleware) to routes.

Location:App\Attributes\Middleware

ParameterTypeDefaultDescription
filtersstring...requiredList of filter aliases (Variadic)

Examples:

// Single filter
#[Route('/admin/dashboard')]
#[Middleware('auth')]
public function dashboard() { }

// Multiple filters (applied in order)
#[Route('/admin/settings')]
#[Middleware('auth', 'admin', 'throttle')]
public function settings() { }

// Class-level (applies to ALL methods)
#[Middleware('auth')]
class AdminController extends BaseController
{
    #[Route('/admin/dashboard')]
    public function dashboard() { }  // Has 'auth'

    #[Route('/admin/users')]
    public function users() { }  // Has 'auth'
}

3.3 #[Roles] — Role-Based Access

Purpose: Require specific user roles to access a route.

Location:App\Attributes\Roles

ParameterTypeDefaultDescription
rolesstring...requiredList of allowed role names (Variadic)

Examples:

// User needs "admin" OR "manager" role (Any one matches)
#[Route('/reports/sales')]
#[Roles('admin', 'manager')]
public function salesReport() { }

// Note: To require MULTIPLE roles (AND logic), apply the attribute multiple times or check in code
#[Route('/reports/financial')]
#[Roles('admin')]
#[Roles('finance')]
// Note: To require MULTIPLE roles (AND logic), apply the attribute multiple times or check in code
#[Route('/reports/financial')]
#[Roles('admin')]
#[Roles('finance')]
public function financialReport() { }

Method-Level vs Class-Level Roles

You can mix class-level and method-level attributes. Method attributes are checked in addition to class attributes.

#[Roles('admin')] // All methods require 'admin'
class UserController extends BaseController
{
    // Requires 'admin' (inherited)
    public function index() { ... }
    
    // Requires 'admin' AND 'superadmin'
    #[Roles('superadmin')]
    public function delete() { ... }
}

3.4 #[RequireAuth] — Admin Authentication

Purpose: Enforce session-based authentication for Admin Panel routes. Redirects unauthenticated users to login.

Location:App\Attributes\RequireAuth

ParameterTypeDefaultDescription
redirectstring'/admin/login'URL to redirect to if not logged in
message?string'Please login...'Flash message to show

Examples:

// Secure an entire controller (Recommended)
#[RequireAuth]
class DashboardController extends BaseController
{
    // All methods require login
}

// Secure specific method with custom redirect
#[RequireAuth(redirect: '/member/login', message: 'Members only')]
public function memberProfile() { }

3.5 #[Trace] — Distributed Tracing

Purpose: Mark methods for distributed tracing. Creates a "span" that tracks execution.

Location:App\Attributes\Trace

ParameterTypeDefaultDescription
name?stringnullCustom span name (defaults to Class::method)
tagsarray[]Key-value pairs attached to span
recordExceptionbooltrueRecord exceptions in the span

Examples:

// Basic tracing (span name = ProcessPayment::execute)
#[Trace]
public function execute() { }

// Custom span name
#[Trace(name: 'payment.process')]
public function processPayment() { }

// With tags for filtering/grouping
#[Trace(name: 'order.create', tags: ['priority' => 'high', 'team' => 'backend'])]
public function createOrder() { }

// Disable exception recording
#[Trace(recordException: false)]
public function riskyOperation() { }

3.6 #[Monitor] — Performance Monitoring

Purpose: Track method performance (duration, memory) with optional filtering.

Location:App\Attributes\Monitor

ParameterTypeDefaultDescription
sampleRatefloat1.0Fraction to record (0.0 to 1.0). 0.1 = 10%
thresholdint0Min duration (ms) to record
group?stringnullCustom metric group name
recordMemorybooltrueTrack memory usage

Examples:

// Monitor everything (100% of calls)
#[Monitor]
public function processOrder() { }

// Only log if execution takes > 100ms
#[Monitor(threshold: 100)]
public function generateReport() { }

// Sample 10% of calls (for high-frequency methods)
#[Monitor(sampleRate: 0.1)]
public function checkPermission() { }

// Sample 1%, only if > 50ms, custom group
#[Monitor(sampleRate: 0.01, threshold: 50, group: 'cache')]
public function getFromCache() { }

// Disable memory tracking
#[Monitor(recordMemory: false)]
public function lightOperation() { }

4. Combining Multiple Attributes

One of the most powerful features is stacking multiple attributes on the same method. They all work together.

Example: Full Stack

<?php
namespace App\Modules\Api\Order\Controllers;

use App\Controllers\BaseController;
use App\Attributes\Route;
use App\Attributes\Middleware;
use App\Attributes\Roles;
use App\Attributes\Trace;
use App\Attributes\Monitor;

class OrderController extends BaseController
{
    /**
     * Create a new order
     * 
     * This method has ALL attributes stacked:
     * 1. #[Route]      → Defines URL and HTTP method
     * 2. #[Middleware] → Requires authentication + rate limiting
     * 3. #[Roles]      → User must be 'customer' or 'admin'
     * 4. #[Trace]      → Creates trace span for debugging
     * 5. #[Monitor]    → Logs performance if > 200ms
     */
    #[Route('/api/orders', methods: ['POST'])]
    #[Middleware('auth', 'throttle:60,1')]
    #[Roles('customer', 'admin')]
    #[Trace(name: 'api.order.create')]
    #[Monitor(threshold: 200)]
    public function store()
    {
        // Your order creation logic
    }
}

What Happens When Request Comes In

POST /api/orders │ ▼ 1. ROUTE MATCH └── Route matched: OrderController::store │ ▼ 2. MIDDLEWARE EXECUTION (before) ├── 'auth' filter runs → Checks JWT token └── 'throttle:60,1' filter → Checks rate limit (60 req/min) │ ▼ 3. ROLES CHECK └── Verifies user has 'customer' OR 'admin' role │ ▼ 4. TRACE START └── SpanManager creates span: "api.order.create" │ ▼ 5. MONITOR START └── Records start time and memory │ ▼ 6. YOUR METHOD EXECUTES └── store() runs, creates order │ ▼ 7. MONITOR END └── Calculates duration. If > 200ms, logs to Redis │ ▼ 8. TRACE END └── Closes span, records duration │ ▼ 9. RESPONSE SENT

Class-Level + Method-Level Combination

#[Middleware('auth')]          // ALL methods require auth
#[Trace]                          // ALL methods are traced
class AdminUserController extends BaseController
{
    #[Route('/admin/users')]
    public function index()
    {
        // Has: auth, trace
    }

    #[Route('/admin/users/{id}')]
    #[Monitor(threshold: 50)]     // ADDITIONAL: monitoring
    public function show(int $id)
    {
        // Has: auth, trace, monitor
    }

    #[Route('/admin/users/{id}', methods: ['DELETE'])]
    #[Middleware('superadmin')] // ADDITIONAL: superadmin filter
    #[Roles('superadmin')]      // ADDITIONAL: role check
    public function destroy(int $id)
    {
        // Has: auth + superadmin, trace, roles check
    }
}

5. Step-by-Step Tutorial

Task: Follow this tutorial to add Monitoring and Tracing to an existing controller effectively.

Scenario: Add Monitoring to an Existing Controller

You have an existing ProductController and want to add tracing and monitoring.

Step 1: Current Code (No Attributes)

<?php
namespace App\Modules\Api\Product\Controllers;

use App\Controllers\BaseController;

class ProductController extends BaseController
{
    public function index()
    {
        $products = $this->productService->getAll();
        return $this->response->setJSON($products);
    }

    public function show($id)
    {
        $product = $this->productService->findById($id);
        return $this->response->setJSON($product);
    }
}

Step 2: Add Use Statements

<?php
namespace App\Modules\Api\Product\Controllers;

use App\Controllers\BaseController;
use App\Attributes\Route;         // Add this
use App\Attributes\Trace;         // Add this
use App\Attributes\Monitor;       // Add this

Step 3: Add Attributes to Methods

class ProductController extends BaseController
{
    #[Route('/api/products', methods: ['GET'])]
    #[Trace(name: 'api.products.list')]
    #[Monitor(sampleRate: 0.5)]  // Sample 50% (high-traffic endpoint)
    public function index()
    {
        $products = $this->productService->getAll();
        return $this->response->setJSON($products);
    }

    #[Route('/api/products/{id}', methods: ['GET'])]
    #[Trace(name: 'api.products.show')]
    #[Monitor(threshold: 100)]  // Only log if > 100ms
    public function show(int $id)
    {
        $product = $this->productService->findById($id);
        return $this->response->setJSON($product);
    }
}

Step 4: Test the Route

# Clear route cache and test
docker-compose exec php php spark routes | grep products

# Output:
# GET    /api/products        ProductController::index
# GET    /api/products/{id}   ProductController::show

Step 5: Generate Traffic

# Make some requests
curl http://localhost:81/api/products
curl http://localhost:81/api/products/1
curl http://localhost:81/api/products/2

Step 6: Verify Tracing and Monitoring

# Check Redis for stored metrics
docker-compose exec redis redis-cli KEYS "perf:*"

# Analyze performance
docker-compose exec php php spark monitor:analyze

6. How Attributes Work Internally

PHP Reflection API

Attributes are read using PHP's Reflection API:

<?php
// Reading attributes from a method

$reflectionClass = new ReflectionClass(ProductController::class);
$reflectionMethod = $reflectionClass->getMethod('index');

// Get all Route attributes on this method
$routeAttributes = $reflectionMethod->getAttributes(Route::class);

foreach ($routeAttributes as $attribute) {
    // Create instance of the attribute class
    $route = $attribute->newInstance();
    
    // Access the properties
    echo $route->path;      // "/api/products"
    echo $route->methods;   // ["GET"]
}

RouteScanner Process

RouteScanner::scan() called │ ▼ ┌─────────────────────────────────────────────────────┐ │ 1. Find all Controller files in Modules/ │ │ │ │ Scan: Modules/*/Controllers/*.php │ │ Scan: Modules/*/*/Controllers/*.php │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ 2. For each Controller class │ │ │ │ • Use Reflection to get all public methods │ │ • Check for #[Route] attributes │ │ • Check for #[Middleware] attributes │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ 3. Register routes in CI4 │ │ │ │ $routes->get('/api/products', 'Controller::method'); │ │ $routes->addFilter('/api/products', 'before', ['auth']); │ └─────────────────────────────────────────────────────┘ │ ▼ ┌─────────────────────────────────────────────────────┐ │ 4. Save to cache │ │ │ │ File: writable/cache/attribute_routes.php │ └─────────────────────────────────────────────────────┘

Why Cache Is Important

Without caching, every request would:

  1. Find all PHP files in Modules/ (~100+ files)
  2. Load each file and create Reflection objects
  3. Parse all attributes
  4. Register routes

This would add ~50-100ms to every request. With caching, this only happens once.

Important: For performance, we disable automatic cache invalidation in production-like environments.

# If you add/change attributes, run this to update routes:
php spark route:clear

# Or if using Docker:
docker-compose exec php php spark route:clear
ESC

Start typing to search the documentation