PHP Attributes - Complete Guide with Examples
PHP attributes, introduced in PHP 8.0, let developers attach structured metadata to classes, methods, properties, functions, parameters, and constants. They are conceptually similar to Java annotations and are widely used by Symfony and Doctrine.
Attributes describe metadata; they do not execute behavior automatically. A framework or application reads and interprets them, usually using Reflection.
1. What are PHP attributes?
#[Example]
class MyClass {}
Attributes can accept arguments:
#[Route('/users', methods: ['GET'])]
public function getUsers(): array { return []; }
The example assumes a Route attribute supported by a framework or defined by your application.
PHP attributes vs Java annotations
| Java | PHP |
|---|---|
@Override |
#[Override] |
@Deprecated |
#[Deprecated] |
@GetMapping("/users") |
#[Route('/users', methods: ['GET'])] |
@Entity |
#[ORM\Entity] |
| Java Reflection API | PHP Reflection API |
@interface |
#[Attribute] class |
2. Creating a custom attribute
Define the attribute
<?php
#[Attribute]
class Description
{
public function __construct(public string $text) {}
}
Apply it
#[Description('Represents a user')]
class User
{
#[Description("The user's email address")]
public string $email;
}
Read it using Reflection
$reflection = new ReflectionClass(User::class);
$attributes = $reflection->getAttributes(Description::class);
foreach ($attributes as $attribute) {
$instance = $attribute->newInstance();
echo $instance->text;
}
Output:
Represents a user
| Reflection method | Purpose |
|---|---|
getAttributes() |
Gets attributes attached to a declaration |
getName() |
Returns an attribute’s class name |
getArguments() |
Returns its arguments without instantiating it |
newInstance() |
Instantiates the attribute class |
3. Restricting where attributes can be used
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
class UserController
{
#[Route('/users', 'GET')]
public function index(): string { return 'List of users'; }
#[Route('/users', 'POST')]
public function create(): string { return 'Create a user'; }
}
| Constant | Target |
|---|---|
Attribute::TARGET_CLASS |
Classes, interfaces, traits and enums |
Attribute::TARGET_METHOD |
Methods |
Attribute::TARGET_PROPERTY |
Properties |
Attribute::TARGET_FUNCTION |
Functions |
Attribute::TARGET_PARAMETER |
Parameters |
Attribute::TARGET_CLASS_CONSTANT |
Class constants and enum cases |
Attribute::TARGET_ALL |
All supported targets |
Combine targets using bitwise OR:
#[Attribute(Attribute::TARGET_CLASS | Attribute::TARGET_METHOD)]
class Permission
{
public function __construct(public string $name) {}
}
An invalid target generally produces an error when Reflection instantiates the attribute, rather than when PHP first loads the declaration.
4. Attributes with multiple arguments
#[Attribute(Attribute::TARGET_PROPERTY)]
class Validate
{
public function __construct(
public bool $required = false,
public int $minLength = 0,
public int $maxLength = 255
) {}
}
class User
{
#[Validate(required: true, minLength: 3, maxLength: 50)]
public string $username;
#[Validate(required: true, maxLength: 100)]
public string $email;
}
Read the values:
$reflection = new ReflectionProperty(User::class, 'username');
$attributes = $reflection->getAttributes(Validate::class);
$validation = $attributes[0]->newInstance();
echo $validation->required; // 1
echo $validation->minLength; // 3
echo $validation->maxLength; // 50
5. Practical example: Building a simple routing system
Define the Route attribute
#[Attribute(Attribute::TARGET_METHOD)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
Create a controller
class UserController
{
#[Route('/api/users', 'GET')]
public function index(): array
{
return [
['id' => 1, 'name' => 'John'],
['id' => 2, 'name' => 'Alice']
];
}
#[Route('/api/users', 'POST')]
public function create(): array
{
return ['message' => 'User created'];
}
}
Discover routes automatically
$controller = new UserController();
$reflection = new ReflectionClass($controller);
$routes = [];
foreach ($reflection->getMethods(ReflectionMethod::IS_PUBLIC) as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
$routes[] = [
'path' => $route->path,
'httpMethod' => $route->method,
'handler' => [$controller, $method->getName()]
];
}
}
Execute the matching route
$requestPath = parse_url($_SERVER['REQUEST_URI'], PHP_URL_PATH);
$requestMethod = $_SERVER['REQUEST_METHOD'];
foreach ($routes as $route) {
if ($route['path'] === $requestPath &&
$route['httpMethod'] === $requestMethod) {
header('Content-Type: application/json');
echo json_encode(call_user_func($route['handler']));
exit;
}
}
http_response_code(404);
header('Content-Type: application/json');
echo json_encode(['error' => 'Route not found']);
This is a simplified demonstration. Production routing requires robust request handling, input validation, authentication, authorization, and error handling.
6. Practical example: Authorization
#[Attribute(Attribute::TARGET_METHOD)]
class RequiresRole
{
public function __construct(public string $role) {}
}
class AdminController
{
#[RequiresRole('ADMIN')]
public function deleteUser(): void { echo 'User deleted'; }
#[RequiresRole('USER')]
public function profile(): void { echo 'User profile'; }
}
function executeMethod(
object $controller,
string $methodName,
array $userRoles
): void {
$reflection = new ReflectionMethod($controller, $methodName);
$attributes = $reflection->getAttributes(RequiresRole::class);
foreach ($attributes as $attribute) {
$permission = $attribute->newInstance();
if (!in_array($permission->role, $userRoles, true)) {
throw new RuntimeException('Access denied');
}
}
$reflection->invoke($controller);
}
$controller = new AdminController();
executeMethod($controller, 'deleteUser', ['ADMIN']);
Output:
User deleted
Security note: This is an illustrative example, not a complete authorization solution. Enforce authorization centrally, obtain roles from a trusted authenticated identity, and establish a default policy for unannotated methods.
7. Practical example: Property validation
#[Attribute(Attribute::TARGET_PROPERTY)]
class NotBlank
{
public function __construct(
public string $message = 'Field cannot be blank'
) {}
}
class CreateUserRequest
{
#[NotBlank('Username is required')]
public string $username = '';
#[NotBlank('Email is required')]
public string $email = '';
}
function validate(object $object): array
{
$errors = [];
$reflection = new ReflectionClass($object);
foreach ($reflection->getProperties() as $property) {
$attributes = $property->getAttributes(NotBlank::class);
foreach ($attributes as $attribute) {
$validation = $attribute->newInstance();
$value = $property->getValue($object);
if (!is_string($value) || trim($value) === '') {
$errors[$property->getName()] = $validation->message;
}
}
}
return $errors;
}
$request = new CreateUserRequest();
$request->username = '';
$request->email = 'john@example.com';
print_r(validate($request));
Output:
Array
(
[username] => Username is required
)
8. Real-world example: Symfony 8
Routing
<?php
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Attribute\Route;
class UserController extends AbstractController
{
#[Route('/api/users', name: 'api_users', methods: ['GET'])]
public function index(): JsonResponse
{
return $this->json([
['id' => 1, 'name' => 'John'],
['id' => 2, 'name' => 'Alice']
]);
}
}
Validation
<?php
namespace App\DTO;
use Symfony\Component\Validator\Constraints as Assert;
class CreateUserDTO
{
#[Assert\NotBlank]
#[Assert\Length(min: 3, max: 50)]
public string $username = '';
#[Assert\NotBlank]
#[Assert\Email]
public string $email = '';
}
Security
use Symfony\Component\Security\Http\Attribute\IsGranted;
#[IsGranted('ROLE_ADMIN')]
public function adminDashboard(): JsonResponse
{
return $this->json(['message' => 'Welcome admin']);
}
Symfony integrates these attributes into its routing, validation, and authorization systems.
9. Real-world example: Doctrine ORM
<?php
namespace App\Entity;
use Doctrine\ORM\Mapping as ORM;
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User
{
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: 'integer')]
private ?int $id = null;
#[ORM\Column(type: 'string', length: 100)]
private string $name;
#[ORM\Column(type: 'string', length: 180, unique: true)]
private string $email;
}
| Attribute | Purpose |
|---|---|
#[ORM\Entity] |
Declares an entity |
#[ORM\Table] |
Defines the table name |
#[ORM\Id] |
Marks the primary key |
#[ORM\GeneratedValue] |
Configures generated IDs |
#[ORM\Column] |
Maps a property to a column |
10. Repeatable attributes
#[Attribute(Attribute::TARGET_METHOD | Attribute::IS_REPEATABLE)]
class Route
{
public function __construct(
public string $path,
public string $method = 'GET'
) {}
}
class ProductController
{
#[Route('/products', 'GET')]
#[Route('/api/products', 'GET')]
public function index(): array { return []; }
}
$reflection = new ReflectionMethod(ProductController::class, 'index');
$attributes = $reflection->getAttributes(Route::class);
foreach ($attributes as $attribute) {
$route = $attribute->newInstance();
echo $route->path . PHP_EOL;
}
Output:
/products
/api/products
11. Built-in PHP attributes
| Attribute | PHP version | Purpose |
|---|---|---|
#[Attribute] |
8.0 | Defines an attribute class |
#[Deprecated] |
8.4 | Marks a declaration as deprecated |
#[Override] |
8.3 | Verifies a method overrides or implements an inherited declaration |
#[SensitiveParameter] |
8.2 | Hides parameter values in generated stack traces |
#[AllowDynamicProperties] |
8.2 | Allows dynamic properties without deprecation warnings |
#[ReturnTypeWillChange] |
8.1 | Temporarily suppresses return-type compatibility warnings |
Override
class BaseController
{
public function index(): void {}
}
class UserController extends BaseController
{
#[Override]
public function index(): void { echo 'Users'; }
}
SensitiveParameter
function login(
string $username,
#[SensitiveParameter] string $password
): void {
throw new Exception('Login failed');
}
This protects the argument from exposure in PHP-generated stack trace arguments; it does not encrypt the value or prevent arbitrary application logging.
Deprecated
#[Deprecated(message: 'Use newMethod() instead', since: '2.0')]
function oldMethod(): void {}
Starting with PHP 8.4, invoking the function triggers a deprecation warning.
12. Attributes vs PHPDoc annotations
Older Doctrine-style PHPDoc annotations:
/**
* @ORM\Entity
* @ORM\Table(name="users")
*/
class User {}
Native attributes:
#[ORM\Entity]
#[ORM\Table(name: 'users')]
class User {}
| Feature | PHPDoc annotations | PHP attributes |
|---|---|---|
| Native PHP feature | No | Yes |
| Native Reflection access | No; parsing required | Yes |
| IDE autocomplete | Tool-dependent | Generally supported |
| Constructor type checking | No native enforcement | Yes, when instantiated |
| External annotation parser | Usually | No |
PHPDoc remains useful for documentation, static analysis, and generic types.
13. Do attributes affect performance?
Attributes do not execute automatically. Performance considerations arise when applications use Reflection to read them or newInstance() to construct attribute objects. Frameworks may cache processed metadata to reduce repeated work.
14. When should you use PHP attributes?
Common use cases include:
- Routing: Associate methods with HTTP paths and methods.
- Validation: Declare DTO or entity constraints.
- Authorization: Describe required roles or permissions.
- ORM mapping: Associate objects with database tables and columns.
- Dependency injection: Configure services and injection.
- Serialization: Control JSON or other representations.
- Event handling: Mark listener methods.
- Testing: Supply test metadata.
Avoid attributes that merely duplicate business logic or obscure execution flow.
15. Recommended approach for PHP projects
Keep metadata declarations separate from the logic interpreting them:
Controller / DTO / Entity
|
v
Reflection / Metadata Reader
|
v
Router / Validator / Security Handler
|
v
Application Behavior
For Symfony 8, prefer the framework’s built-in routing, validation, and security attributes instead of creating custom replacements without a specific need. In CodeIgniter 4, custom attributes can be useful for application-specific metadata, but you must implement the reader and behavior yourself unless a library already supports them.
Key takeaway: PHP attributes are structured metadata. The application or framework is responsible for reading and acting on that metadata.