DEV Community

Cover image for πŸš€ Mastering PHP Attributes: From Zero to Hero
Laravel Mastery
Laravel Mastery

Posted on

πŸš€ Mastering PHP Attributes: From Zero to Hero

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) { }
Enter fullscreen mode Exit fullscreen mode

πŸ”¨ 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';
}
Enter fullscreen mode Exit fullscreen mode

Usage Example

class PaymentController
{
    #[AuditLog('ProcessPayment', LogLevel::WARNING, ['cardNumber', 'cvv'])]
    public function process(string $cardNumber, string $cvv): bool
    {
        // Payment logic
        return true;
    }
}
Enter fullscreen mode Exit fullscreen mode

πŸ” 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
Enter fullscreen mode Exit fullscreen mode

πŸ›£οΈ 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) {}
}
Enter fullscreen mode Exit fullscreen mode

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']}();
    }
}
Enter fullscreen mode Exit fullscreen mode

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']]
Enter fullscreen mode Exit fullscreen mode

βœ… 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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

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']
    ]
    */
}
Enter fullscreen mode Exit fullscreen mode

🎯 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;
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ 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!

php #attributes #php8 #webdevelopment #programming

Top comments (0)