DEV Community

arasosman
arasosman

Posted on • Originally published at mycuriosity.blog

PHP Attributes in Laravel: Complete Guide 2025

Introduction

PHP attributes, introduced in PHP 8.0, have revolutionized how we write metadata and configuration in our applications. If you're working with Laravel and haven't explored attributes yet, you're missing out on a powerful feature that can make your code cleaner, more readable, and easier to maintain.

The traditional approach of using docblocks and configuration arrays is giving way to a more elegant solution. PHP attributes allow you to attach structured metadata directly to classes, methods, properties, and parameters using a clean, declarative syntax.

In this comprehensive guide, you'll learn everything about PHP attributes, from basic syntax to advanced Laravel implementations. We'll cover practical examples, best practices, and real-world use cases that will transform how you structure your Laravel applications.

What Are PHP Attributes?

PHP attributes are a native way to add metadata to code declarations. Think of them as structured comments that your application can read and act upon at runtime. Unlike traditional docblocks, attributes are first-class citizens in PHP with proper syntax validation and IDE support.

Key Benefits of PHP Attributes

  • Type Safety: Attributes are validated by PHP's parser
  • IDE Support: Full autocompletion and refactoring capabilities
  • Performance: No parsing overhead compared to docblock annotations
  • Readability: Clean, declarative syntax that's easy to understand
  • Reflection Integration: Seamless integration with PHP's Reflection API

Basic Attribute Syntax

Here's the fundamental syntax for PHP attributes:

<?php

#[Attribute]
class MyAttribute
{
    public function __construct(
        public string $value,
        public array $options = []
    ) {}
}

#[MyAttribute('example', ['key' => 'value'])]
class ExampleClass
{
    #[MyAttribute('property')]
    public string $property;

    #[MyAttribute('method')]
    public function method(): void {}
}
Enter fullscreen mode Exit fullscreen mode

How PHP Attributes Work

Attribute Classes

Every attribute must be backed by a class. This class defines the structure and behavior of your attribute:

<?php

use Attribute;

#[Attribute]
class Route
{
    public function __construct(
        public string $path,
        public array $methods = ['GET'],
        public string $name = ''
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Attribute Targets

You can control where attributes can be applied using target flags:

<?php

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_FUNCTION)]
class HttpMethod
{
    public function __construct(public string $method) {}
}
Enter fullscreen mode Exit fullscreen mode

Available targets include:

  • TARGET_CLASS
  • TARGET_FUNCTION
  • TARGET_METHOD
  • TARGET_PROPERTY
  • TARGET_CLASS_CONSTANT
  • TARGET_PARAMETER
  • TARGET_ALL (default)

Reading Attributes with Reflection

To access attributes at runtime, you use PHP's Reflection API:

<?php

$reflectionClass = new ReflectionClass(ExampleClass::class);
$attributes = $reflectionClass->getAttributes(MyAttribute::class);

foreach ($attributes as $attribute) {
    $instance = $attribute->newInstance();
    echo $instance->value; // Output: example
}
Enter fullscreen mode Exit fullscreen mode

PHP Attributes in Laravel: Current Reality

Important Clarification: Laravel's core framework currently does not provide native PHP attribute support for routing and middleware. However, PHP attributes can still be effectively used in Laravel through custom implementations and third-party packages.

Laravel's Current Routing Structure

Laravel routing still works through traditional methods:

<?php
// routes/web.php - Standard Laravel approach
use App\Http\Controllers\UserController;

Route::middleware(['auth'])->group(function () {
    Route::get('/users', [UserController::class, 'index'])->name('users.index');
    Route::get('/users/{user}', [UserController::class, 'show'])->name('users.show');
    Route::post('/users', [UserController::class, 'store'])->name('users.store');
});
Enter fullscreen mode Exit fullscreen mode

Third-Party Solution: Spatie Route Attributes

If you want to use attributes for routing, you can install Spatie's popular package:

composer require spatie/laravel-route-attributes
Enter fullscreen mode Exit fullscreen mode
<?php

use Spatie\RouteAttributes\Attributes\Get;
use Spatie\RouteAttributes\Attributes\Post;
use Spatie\RouteAttributes\Attributes\Middleware;

class UserController extends Controller
{
    #[Get('/users', name: 'users.index')]
    #[Middleware('auth')]
    public function index(): JsonResponse
    {
        return response()->json(User::all());
    }

    #[Get('/users/{user}', name: 'users.show')]
    #[Middleware(['auth', 'verified'])]
    public function show(User $user): JsonResponse
    {
        return response()->json($user);
    }

    #[Post('/users', name: 'users.store')]
    #[Middleware(['auth', 'throttle:api'])]
    public function store(Request $request): JsonResponse
    {
        $user = User::create($request->validated());
        return response()->json($user, 201);
    }
}
Enter fullscreen mode Exit fullscreen mode

Creating Custom Attributes in Laravel

You can create powerful custom attributes in Laravel to improve code organization and maintainability. Here are practical real-world examples:

Building a Cache Attribute

Let's create a custom attribute for method-level caching:

<?php

namespace App\Attributes;

use Attribute;
use Illuminate\Support\Facades\Cache;

#[Attribute(Attribute::TARGET_METHOD)]
class CacheResult
{
    public function __construct(
        public int $ttl = 3600,
        public ?string $key = null,
        public array $tags = []
    ) {}

    public function getCacheKey(string $class, string $method, array $parameters = []): string
    {
        if ($this->key) {
            return $this->key;
        }

        $paramHash = md5(serialize($parameters));
        return "{$class}@{$method}:{$paramHash}";
    }

    public function shouldCache(): bool
    {
        return $this->ttl > 0;
    }
}
Enter fullscreen mode Exit fullscreen mode

Implementing Cache Logic with Aspect-Oriented Programming

<?php

namespace App\Services;

use App\Attributes\CacheResult;
use Illuminate\Support\Facades\Cache;
use ReflectionMethod;

class AttributeCacheService
{
    public function handleMethodCache($instance, string $method, array $parameters, callable $originalMethod)
    {
        try {
            $reflectionMethod = new ReflectionMethod($instance, $method);
            $attributes = $reflectionMethod->getAttributes(CacheResult::class);

            if (empty($attributes)) {
                return $originalMethod();
            }

            $cacheAttribute = $attributes[0]->newInstance();

            if (!$cacheAttribute->shouldCache()) {
                return $originalMethod();
            }

            $cacheKey = $cacheAttribute->getCacheKey(
                get_class($instance),
                $method,
                $parameters
            );

            return Cache::remember($cacheKey, $cacheAttribute->ttl, $originalMethod);

        } catch (\Exception $e) {
            logger()->error('Cache attribute error: ' . $e->getMessage());
            return $originalMethod();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Validation Attributes

Create custom validation attributes for cleaner request handling:

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Validation
{
    public function __construct(
        public array $rules = [],
        public array $messages = []
    ) {}
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Required extends Validation
{
    public function __construct(array $messages = [])
    {
        parent::__construct(['required'], $messages);
    }
}

#[Attribute(Attribute::TARGET_PROPERTY)]
class Email extends Validation
{
    public function __construct(array $messages = [])
    {
        parent::__construct(['email'], $messages);
    }
}
Enter fullscreen mode Exit fullscreen mode

Using Validation Attributes in DTOs

<?php

namespace App\DTOs;

use App\Attributes\Required;
use App\Attributes\Email;
use App\Attributes\Validation;
use Illuminate\Http\Request;

class CreateUserDTO
{
    #[Required]
    #[Validation(['string', 'max:255'])]
    public string $name;

    #[Required]
    #[Email]
    #[Validation(['unique:users,email'])]
    public string $email;

    #[Required]
    #[Validation(['string', 'min:8', 'confirmed'])]
    public string $password;

    #[Validation(['nullable', 'date', 'after:today'])]
    public ?string $birthDate = null;

    public static function fromRequest(Request $request): self
    {
        $dto = new self();
        $dto->name = $request->input('name');
        $dto->email = $request->input('email');
        $dto->password = $request->input('password');
        $dto->birthDate = $request->input('birth_date');

        return $dto;
    }

    public function getRules(): array
    {
        $rules = [];
        $reflection = new \ReflectionClass($this);

        foreach ($reflection->getProperties() as $property) {
            $propertyRules = [];
            $attributes = $property->getAttributes();

            foreach ($attributes as $attribute) {
                $instance = $attribute->newInstance();
                if ($instance instanceof Validation) {
                    $propertyRules = array_merge($propertyRules, $instance->rules);
                }
            }

            if (!empty($propertyRules)) {
                $rules[$this->getPropertyName($property->getName())] = $propertyRules;
            }
        }

        return $rules;
    }

    private function getPropertyName(string $propertyName): string
    {
        return strtolower(preg_replace('/(?<!^)[A-Z]/', '_$0', $propertyName));
    }
}
Enter fullscreen mode Exit fullscreen mode

Advanced Laravel Attribute Patterns

Event-Driven Attributes

Create attributes that trigger events:

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class FireEvent
{
    public function __construct(
        public string $eventClass,
        public array $eventData = []
    ) {}
}

// Usage in controller
class ProductController extends Controller
{
    #[FireEvent(ProductViewed::class)]
    public function show(Product $product): View
    {
        // Event will be fired automatically through middleware/aspect
        return view('products.show', compact('product'));
    }
}
Enter fullscreen mode Exit fullscreen mode

Authorization Attributes

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class RequirePermission
{
    public function __construct(
        public string|array $permissions,
        public string $guard = 'web'
    ) {}
}

#[Attribute(Attribute::TARGET_METHOD | Attribute::TARGET_CLASS)]
class RequireRole
{
    public function __construct(
        public string|array $roles,
        public string $guard = 'web'
    ) {}
}

// Usage with traditional routing
class AdminController extends Controller
{
    #[RequireRole(['admin', 'super-admin'])]
    #[RequirePermission('manage-users')]
    public function editUser(User $user): View
    {
        return view('admin.users.edit', compact('user'));
    }
}
Enter fullscreen mode Exit fullscreen mode

Rate Limiting Attributes

<?php

namespace App\Attributes;

use Attribute;

#[Attribute(Attribute::TARGET_METHOD)]
class RateLimit
{
    public function __construct(
        public int $maxAttempts = 60,
        public int $decayMinutes = 1,
        public ?string $key = null
    ) {}
}

class ApiController extends Controller
{
    #[RateLimit(maxAttempts: 100, decayMinutes: 1)]
    public function search(Request $request): JsonResponse
    {
        // Rate limiting logic would be handled by middleware
        return response()->json(['results' => []]);
    }
}
Enter fullscreen mode Exit fullscreen mode

Best Practices for PHP Attributes

1. Keep Attributes Simple and Focused

Each attribute should have a single responsibility:

<?php

// Good: Focused attribute
#[Attribute]
class CacheFor
{
    public function __construct(public int $seconds) {}
}

// Bad: Too many responsibilities
#[Attribute]
class ComplexAttribute
{
    public function __construct(
        public int $cacheSeconds,
        public array $permissions,
        public string $logLevel,
        public bool $validateInput
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

2. Use Type Hints and Default Values

<?php

#[Attribute]
class DatabaseTable
{
    public function __construct(
        public string $name,
        public string $connection = 'default',
        public bool $timestamps = true,
        public array $indexes = []
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

3. Validate Attribute Parameters

<?php

#[Attribute(Attribute::TARGET_METHOD)]
class RateLimit
{
    public function __construct(
        public int $maxAttempts = 60,
        public int $decayMinutes = 1
    ) {
        if ($this->maxAttempts <= 0) {
            throw new InvalidArgumentException('Max attempts must be positive');
        }

        if ($this->decayMinutes <= 0) {
            throw new InvalidArgumentException('Decay minutes must be positive');
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

4. Document Your Attributes

<?php

/**
 * Caches the result of a method for the specified duration.
 * 
 * @example #[Cache(3600)] // Cache for 1 hour
 * @example #[Cache(ttl: 1800, key: 'custom-key')] // Custom cache key
 */
#[Attribute(Attribute::TARGET_METHOD)]
class Cache
{
    public function __construct(
        public int $ttl = 3600,
        public ?string $key = null,
        public array $tags = []
    ) {}
}
Enter fullscreen mode Exit fullscreen mode

Performance Considerations

Reflection Caching

Since reflection operations can be expensive, implement caching:

<?php

namespace App\Services;

use Illuminate\Support\Facades\Cache;

class AttributeCache
{
    private const CACHE_PREFIX = 'attributes:';
    private const CACHE_TTL = 3600;

    public function getMethodAttributes(string $class, string $method): array
    {
        $cacheKey = self::CACHE_PREFIX . "{$class}@{$method}";

        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($class, $method) {
            $reflectionMethod = new \ReflectionMethod($class, $method);
            return $reflectionMethod->getAttributes();
        });
    }

    public function getClassAttributes(string $class): array
    {
        $cacheKey = self::CACHE_PREFIX . $class;

        return Cache::remember($cacheKey, self::CACHE_TTL, function () use ($class) {
            $reflectionClass = new \ReflectionClass($class);
            return $reflectionClass->getAttributes();
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Lazy Loading Attributes

Only instantiate attribute objects when needed:

<?php

class AttributeProcessor
{
    public function processMethodAttributes(object $instance, string $method): void
    {
        $reflectionMethod = new \ReflectionMethod($instance, $method);
        $attributes = $reflectionMethod->getAttributes();

        foreach ($attributes as $attribute) {
            // Only instantiate when needed
            if ($this->shouldProcess($attribute->getName())) {
                $attributeInstance = $attribute->newInstance();
                $this->handleAttribute($attributeInstance);
            }
        }
    }

    private function shouldProcess(string $attributeClass): bool
    {
        return in_array($attributeClass, $this->getProcessableAttributes());
    }
}
Enter fullscreen mode Exit fullscreen mode

Common Mistakes and Troubleshooting

Mistake 1: Forgetting the #[Attribute] Attribute

<?php

// Wrong: Missing #[Attribute]
class MyAttribute
{
    public function __construct(public string $value) {}
}

// Correct: Include #[Attribute]
#[Attribute]
class MyAttribute
{
    public function __construct(public string $value) {}
}
Enter fullscreen mode Exit fullscreen mode

Mistake 2: Using Attributes on Wrong Targets

<?php

// Wrong: Method-only attribute used on class
#[Attribute(Attribute::TARGET_METHOD)]
class MethodOnly {}

#[MethodOnly] // This will throw an error
class MyClass {}

// Correct: Check target compatibility
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class FlexibleAttribute {}
Enter fullscreen mode Exit fullscreen mode

Mistake 3: Not Handling Reflection Exceptions

<?php

// Wrong: No error handling
public function getAttributes(string $class, string $method): array
{
    $reflectionMethod = new ReflectionMethod($class, $method);
    return $reflectionMethod->getAttributes();
}

// Correct: Handle exceptions
public function getAttributes(string $class, string $method): array
{
    try {
        $reflectionMethod = new ReflectionMethod($class, $method);
        return $reflectionMethod->getAttributes();
    } catch (ReflectionException $e) {
        logger()->warning("Failed to get attributes for {$class}@{$method}: " . $e->getMessage());
        return [];
    }
}
Enter fullscreen mode Exit fullscreen mode

Debugging Attribute Issues

Use this helper to debug attribute problems:

<?php

namespace App\Debug;

class AttributeDebugger
{
    public static function dumpClassAttributes(string $class): void
    {
        try {
            $reflection = new \ReflectionClass($class);
            echo "=== Class: {$class} ===\n";

            foreach ($reflection->getAttributes() as $attribute) {
                $instance = $attribute->newInstance();
                echo "Attribute: " . get_class($instance) . "\n";
                var_dump($instance);
                echo "\n";
            }

            foreach ($reflection->getMethods() as $method) {
                $methodAttributes = $method->getAttributes();
                if (!empty($methodAttributes)) {
                    echo "=== Method: {$method->getName()} ===\n";
                    foreach ($methodAttributes as $attribute) {
                        $instance = $attribute->newInstance();
                        echo "Attribute: " . get_class($instance) . "\n";
                        var_dump($instance);
                        echo "\n";
                    }
                }
            }
        } catch (\Exception $e) {
            echo "Error: " . $e->getMessage() . "\n";
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Real-World Laravel Examples

API Resource Transformation

<?php

namespace App\Attributes;

#[Attribute(Attribute::TARGET_PROPERTY)]
class ApiField
{
    public function __construct(
        public ?string $name = null,
        public ?string $type = null,
        public bool $required = true,
        public $default = null
    ) {}
}

// Usage in model
class User extends Model
{
    #[ApiField(type: 'string', required: true)]
    public string $name;

    #[ApiField(name: 'email_address', type: 'email', required: true)]
    public string $email;

    #[ApiField(type: 'timestamp', required: false)]
    public ?Carbon $email_verified_at;
}
Enter fullscreen mode Exit fullscreen mode

Database Migration Attributes

<?php

namespace App\Attributes;

#[Attribute(Attribute::TARGET_PROPERTY)]
class Column
{
    public function __construct(
        public string $type = 'string',
        public ?int $length = null,
        public bool $nullable = false,
        public $default = null,
        public bool $unique = false,
        public bool $index = false
    ) {}
}

// Usage in migration class
class CreateUsersTable
{
    #[Column(type: 'id')]
    public $id;

    #[Column(type: 'string', length: 255, unique: true)]
    public $email;

    #[Column(type: 'timestamp', nullable: true)]
    public $email_verified_at;

    #[Column(type: 'timestamps')]
    public $timestamps;
}
Enter fullscreen mode Exit fullscreen mode

Frequently Asked Questions

What's the difference between PHP attributes and annotations?

PHP attributes are native to the language since PHP 8.0, while annotations (like Doctrine annotations) are implemented through docblock parsing. Attributes offer better performance, IDE support, and type safety compared to annotation libraries.

Can I use multiple attributes on the same element?

Yes, you can stack multiple attributes on classes, methods, or properties. They will all be available through reflection and can be processed independently.

Are PHP attributes available at runtime?

Yes, attributes are available at runtime through PHP's Reflection API. However, they don't automatically execute - you need to explicitly read and process them using reflection.

Do attributes impact performance?

Attributes themselves have minimal performance impact. The reflection operations used to read attributes can be expensive, but this can be mitigated through caching strategies.

Can I use attributes with older PHP versions?

No, PHP attributes require PHP 8.0 or higher. For older versions, you'll need to use alternative approaches like docblock annotations or configuration arrays.

How do attributes work with inheritance?

Child classes inherit attributes from parent classes by default. You can override or extend inherited attributes by applying new ones to child class methods or properties.

Conclusion

PHP attributes represent a significant evolution in how we structure and organize our Laravel applications. While Laravel doesn't yet provide native attribute support for routing and middleware, attributes can still be powerfully used through custom implementations and third-party packages.

Key takeaways from this guide:

  1. Native PHP Feature: Attributes are built into PHP 8.0+, offering better performance and IDE support than traditional alternatives
  2. Laravel Reality: Laravel core doesn't include native routing attributes, but third-party solutions like Spatie's package exist
  3. Custom Implementation: You can create powerful custom attributes for caching, validation, authorization, and more
  4. Best Practices: Keep attributes focused, use proper type hints, and implement caching for performance
  5. Future Potential: Attributes may become more integrated into Laravel's ecosystem in future versions

Start incorporating PHP attributes into your Laravel projects today. Begin with custom attributes for cross-cutting concerns like caching and validation, then explore third-party packages for routing if needed.

Ready to transform your Laravel code with PHP attributes? Share your experience and questions in the comments below, and don't forget to subscribe to our newsletter for more advanced Laravel tutorials and best practices.

Top comments (0)