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)