Mastering PHP Attributes: Custom Creation, Routing & Validation
description: Deep dive into PHP 8+ attributes - learn to build custom attributes, attribute-based routing, and powerful validation systems with real-world
PHP 8.0 introduced attributes - a game-changer for writing clean, declarative code. Let's explore how to create custom attributes, build routing systems, and implement validation frameworks.
🎯 What Are Attributes?
Attributes are metadata you attach to classes, methods, properties, and parameters. Think of them as sticky notes on your code that become powerful at runtime.
#[Route('/users/{id}')]
#[AuditLog('ViewUser')]
public function show(int $id) { }
🔨 Part 1: Creating Custom Attributes
Basic Custom Attribute
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class AuditLog
{
public function __construct(
public string $operation,
public LogLevel $level = LogLevel::INFO,
public array $sensitiveParams = []
) {}
}
enum LogLevel: string {
case INFO = 'info';
case WARNING = 'warning';
case ERROR = 'error';
}
Usage Example
class PaymentController
{
#[AuditLog('ProcessPayment', LogLevel::WARNING, ['cardNumber', 'cvv'])]
public function process(string $cardNumber, string $cvv): bool
{
// Payment logic
return true;
}
}
🔍 Part 2: Reading Attributes with Reflection
class AttributeProcessor
{
public static function processAuditLogs(object $controller, string $method): void
{
$reflection = new ReflectionMethod($controller, $method);
$attributes = $reflection->getAttributes(AuditLog::class);
foreach ($attributes as $attr) {
$audit = $attr->newInstance();
echo "Logging: {$audit->operation} at level {$audit->level->value}\n";
}
}
}
// Usage
AttributeProcessor::processAuditLogs(new PaymentController(), 'process');
// Output: Logging: ProcessPayment at level warning
🛣️ Part 3: Attribute-Based Routing
Route Attributes
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
#[Attribute(Attribute::TARGET_CLASS)]
class RoutePrefix
{
public function __construct(public string $prefix) {}
}
Simple Router Implementation
class Router
{
private array $routes = [];
public function register(string $controllerClass): void
{
$reflection = new ReflectionClass($controllerClass);
// Get class prefix
$prefixAttrs = $reflection->getAttributes(RoutePrefix::class);
$prefix = $prefixAttrs[0]->newInstance()->prefix ?? '';
// Register methods
foreach ($reflection->getMethods() as $method) {
$routeAttrs = $method->getAttributes(Route::class);
foreach ($routeAttrs as $attr) {
$route = $attr->newInstance();
$fullPath = $prefix . $route->path;
$this->routes[$route->method][$fullPath] = [
'controller' => $controllerClass,
'method' => $method->getName()
];
}
}
}
public function dispatch(string $httpMethod, string $uri): mixed
{
if (!isset($this->routes[$httpMethod][$uri])) {
throw new Exception("Route not found");
}
$route = $this->routes[$httpMethod][$uri];
$controller = new $route['controller']();
return $controller->{$route['method']}();
}
}
Controller Example
#[RoutePrefix('/api/users')]
class UserController
{
#[Route('/')]
public function index(): array
{
return ['users' => ['Alice', 'Bob']];
}
#[Route('/{id}', method: 'GET')]
public function show(int $id): array
{
return ['id' => $id, 'name' => 'Alice'];
}
#[Route('/', method: 'POST')]
public function store(): array
{
return ['message' => 'User created'];
}
}
// Usage
$router = new Router();
$router->register(UserController::class);
$result = $router->dispatch('GET', '/api/users/');
// Returns: ['users' => ['Alice', 'Bob']]
✅ Part 4: Validation Attributes
#[Attribute(Attribute::TARGET_PROPERTY)]
abstract class ValidationRule
{
public function __construct(public string $message = '') {}
abstract public function validate(mixed $value): bool;
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Required extends ValidationRule
{
public function validate(mixed $value): bool
{
return !empty(trim($value));
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Email extends ValidationRule
{
public function validate(mixed $value): bool
{
return filter_var($value, FILTER_VALIDATE_EMAIL) !== false;
}
}
#[Attribute(Attribute::TARGET_PROPERTY)]
class Length extends ValidationRule
{
public function __construct(
public int $min = 0,
public int $max = PHP_INT_MAX,
string $message = 'Invalid length'
) {
parent::__construct($message);
}
public function validate(mixed $value): bool
{
$len = mb_strlen($value);
return $len >= $this->min && $len <= $this->max;
}
}
Validator Engine
class Validator
{
private array $errors = [];
public function validate(object $object): bool
{
$reflection = new ReflectionClass($object);
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$value = $property->getValue($object);
$attributes = $property->getAttributes(
ValidationRule::class,
ReflectionAttribute::IS_INSTANCEOF
);
foreach ($attributes as $attr) {
$rule = $attr->newInstance();
if (!$rule->validate($value)) {
$this->errors[$property->getName()][] = $rule->message;
}
}
}
return empty($this->errors);
}
public function getErrors(): array
{
return $this->errors;
}
}
Model with Validation
class UserRequest
{
#[Required(message: 'Name is required')]
#[Length(min: 3, max: 50)]
public string $name;
#[Required]
#[Email(message: 'Invalid email address')]
public string $email;
#[Required]
#[Length(min: 8, message: 'Password must be at least 8 characters')]
public string $password;
}
// Usage
$request = new UserRequest();
$request->name = 'Jo';
$request->email = 'invalid-email';
$request->password = '123';
$validator = new Validator();
if (!$validator->validate($request)) {
print_r($validator->getErrors());
/* Output:
[
'name' => ['Invalid length'],
'email' => ['Invalid email address'],
'password' => ['Password must be at least 8 characters']
]
*/
}
🎯 Real-World Combined Example
#[RoutePrefix('/api')]
class OrderController
{
#[Route('/orders', method: 'POST')]
#[AuditLog('CreateOrder', LogLevel::INFO)]
public function create(): array
{
$request = new OrderRequest();
$request->amount = 50.00;
$request->email = 'customer@example.com';
$validator = new Validator();
if (!$validator->validate($request)) {
http_response_code(422);
return ['errors' => $validator->getErrors()];
}
return ['message' => 'Order created', 'id' => 123];
}
}
class OrderRequest
{
#[Required]
#[Range(min: 1, max: 99999)]
public float $amount;
#[Required]
#[Email]
public string $email;
}
🚀 Key Benefits
✅ Type-safe - Compile-time validation
✅ Clean syntax - No messy docblocks
✅ IDE support - Full autocomplete
✅ Maintainable - DRY principle
✅ Performance - Cached by OPcache
💡 Pro Tips
Cache reflection data in production
Use IS_INSTANCEOF flag for inheritance
Combine attributes for powerful patterns
Keep attributes simple and focused
Document custom attributes well
🎓 Conclusion
PHP attributes unlock powerful declarative programming patterns. Start with validation, add routing, then explore cross-cutting concerns like logging and caching.
What will you build with attributes? Share in the comments! 👇
Want to dive deeper? Check out the full version of this article on Medium with extended examples and production-ready code!
More tutorials and insights? Follow me on Medium for deep dives into PHP, architecture patterns, and modern web development!
Found this helpful? Drop a ❤️ and follow for more!
Top comments (0)