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)