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
- What Are Attributes?
- Why Use Attributes?
- Attribute Reference
- Combining Multiple Attributes
- Step-by-Step Tutorial
- How Attributes Work Internally
1. What Are Attributes?
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:
- The code itself does its job normally
- The sticky note tells other systems something extra about the code
- You can have multiple sticky notes on the same item
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?
| Benefit | Explanation | Example |
|---|---|---|
| Colocation | Configuration lives next to the code it affects | Route is defined on the method, not in a separate file |
| Discoverability | Look at a method, see everything about it | See the route, middleware, and tracing in one place |
| Type Safety | IDE autocompletion and error checking | Wrong parameter type? IDE shows error immediately |
| Refactoring | Move/rename code, config moves with it | Rename method? Route stays attached |
| Composability | Stack multiple attributes freely | Same method can have Route + Middleware + Monitor |
3. Attribute Reference
3.1 #[Route] — HTTP Routing
Purpose: Define HTTP routes directly on controller methods.
Location:App\Attributes\Route
| Parameter | Type | Default | Description |
|---|---|---|---|
path | string | required | URL path (e.g., /users/{id}) |
methods | array | ['GET'] | HTTP methods: GET, POST, PUT, PATCH, DELETE |
name | ?string | null | Named 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
| Parameter | Type | Default | Description |
|---|---|---|---|
filters | string... | required | List 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
| Parameter | Type | Default | Description |
|---|---|---|---|
roles | string... | required | List 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
Parameter Type Default Description 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
Parameter Type Default Description name?string nullCustom span name (defaults to Class::method) tagsarray []Key-value pairs attached to span recordExceptionbool trueRecord 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
Parameter Type Default Description sampleRatefloat 1.0Fraction to record (0.0 to 1.0). 0.1 = 10% thresholdint 0Min duration (ms) to record group?string nullCustom metric group name recordMemorybool trueTrack 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:
- Find all PHP files in Modules/ (~100+ files)
- Load each file and create Reflection objects
- Parse all attributes
- 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