Introduzione
Tra le novità più utili introdotte da PHP 8 ci sono gli attributi, chiamati anche attributes. Gli attributi permettono di aggiungere metadati direttamente a classi, proprietà, metodi e parametri, utilizzando una sintassi integrata nel linguaggio.
Prima di PHP 8, per descrivere informazioni aggiuntive sul codice si utilizzavano spesso annotazioni nei commenti PHPDoc, ad esempio /** @route("/users") */. Queste annotazioni erano molto diffuse, ma richiedevano strumenti esterni per essere interpretate. Gli attributi, invece, fanno parte della sintassi ufficiale di PHP e possono essere letti durante l’esecuzione tramite la classe Reflection.
In questo tutorial realizzeremo un semplice sistema di routing. Una classe potrà dichiarare quali URL gestisce attraverso un attributo personalizzato. Successivamente, un piccolo router leggerà questi metadati e assocerà una richiesta HTTP al metodo corretto.
L’esempio è utile perché mostra un caso reale: framework e librerie possono utilizzare gli attributi per configurare rotte, validazioni, serializzazione, autorizzazioni e dipendenze senza ricorrere a configurazioni separate.
Codice completo
<?php
declare(strict_types=1);
/*
* L´attributo può essere applicato soltanto ai metodi.
* TARGET_METHOD indica proprio questo vincolo.
*/
#[Attribute(Attribute::TARGET_METHOD)]
final class Route
{
public function __construct(
public string $method,
public string $path
) {
}
}
/*
* Controller di esempio.
* Gli attributi descrivono le rotte gestite dai metodi.
*/
final class UserController
{
#[Route(´GET´, ´/users´)]
public function list(): string
{
return ´Elenco degli utenti´;
}
#[Route(´GET´, ´/users/{id}´)]
public function show(int $id): string
{
return "Dettaglio dell´utente {$id}";
}
}
/*
* Router minimale basato sulla Reflection API.
*/
final class Router
{
public function __construct(
private object $controller
) {
}
public function dispatch(string $httpMethod, string $uri): string
{
$reflection = new ReflectionClass($this->controller);
foreach ($reflection->getMethods() as $method) {
$attributes = $method->getAttributes(Route::class);
foreach ($attributes as $attribute) {
/** @var Route $route */
$route = $attribute->newInstance();
if ($route->method !== $httpMethod) {
continue;
}
$pattern = $this->convertPathToRegex($route->path);
if (preg_match($pattern, $uri, $matches) === 1) {
$parameters = [];
if (isset($matches[´id´])) {
$parameters[] = (int) $matches[´id´];
}
return $method->invokeArgs(
$this->controller,
$parameters
);
}
}
}
return ´404 - Risorsa non trovata´;
}
private function convertPathToRegex(string $path): string
{
$pattern = preg_replace(
´#{([a-zA-Z_][a-zA-Z0-9_]*)}#´,
´(?P<$1>[^/]+)´,
$path
);
return ´#^´ . $pattern . ´$#´;
}
}
$router = new Router(new UserController());
echo $router->dispatch(´GET´, ´/users´) . PHP_EOL;
echo $router->dispatch(´GET´, ´/users/42´) . PHP_EOL;
echo $router->dispatch(´POST´, ´/users´) . PHP_EOL; Spiegazione
Definire un attributo personalizzato
Un attributo è una normale classe preceduta da #[Attribute]. Nel nostro esempio, la classe Route riceve due informazioni: il metodo HTTP e il percorso dell’URL.
Il parametro Attribute::TARGET_METHOD stabilisce che l’attributo può essere applicato solo ai metodi. PHP supporta anche altri obiettivi, come TARGET_CLASS, TARGET_PROPERTY, TARGET_PARAMETER e TARGET_FUNCTION. È possibile combinare più obiettivi usando l’operatore bitwise OR.
Applicare l’attributo
Nel controller scriviamo:
#[Route(´GET´, ´/users´)]
public function list(): string
{
return ´Elenco degli utenti´;
} La sintassi con parentesi quadre precedute da cancelletto è la forma ufficiale degli attributi. Un metodo può avere più attributi, se la classe dell’attributo lo consente e se l’architettura dell’applicazione lo richiede.
Leggere gli attributi con Reflection
La classe ReflectionClass consente di analizzare una classe durante l’esecuzione. Con getMethods() otteniamo i suoi metodi, mentre getAttributes(Route::class) seleziona soltanto gli attributi di tipo Route.
Il metodo getAttributes() restituisce oggetti descrittivi, non ancora istanze della classe. Per creare l’oggetto vero e proprio utilizziamo newInstance(). In questo modo possiamo leggere le proprietà pubbliche method e path.
Convertire il percorso in una regular expression
Il percorso /users/{id} contiene un parametro dinamico. Il metodo convertPathToRegex() lo trasforma in una regular expression con un gruppo nominato: (?P<id>[^/]+). La funzione preg_match() estrae quindi il valore 42, che viene convertito in intero prima di essere passato al metodo del controller.
Best practice
- Usa nomi espliciti: un attributo come
Route,CacheoValidatedeve descrivere chiaramente il suo scopo. - Limita gli obiettivi: specificare
TARGET_METHODevita che l’attributo venga applicato accidentalmente a elementi non previsti. - Valida i dati: il costruttore dell’attributo dovrebbe controllare valori non validi, come metodi HTTP inesistenti o percorsi vuoti.
- Non abusare della Reflection: la lettura riflessiva può essere costosa. In applicazioni reali è preferibile costruire una tabella delle rotte una sola volta e conservarla in memoria o nella cache.
- Usa tipi rigorosi:
declare(strict_types=1)e dichiarazioni comestringeintrendono il comportamento più prevedibile. - Separa responsabilità: l’attributo descrive la rotta, mentre il router deve occuparsi della scansione e dell’esecuzione.
Gli attributi non sostituiscono ogni forma di configurazione. Per configurazioni modificabili senza distribuire nuovo codice può essere più appropriato usare file YAML, JSON o variabili d’ambiente.
Riepilogo
Gli attributi di PHP 8 permettono di associare metadati strutturati agli elementi del programma. Rispetto ai commenti PHPDoc, sono integrati nel linguaggio e possono essere letti in modo standard tramite Reflection.
Nell’esempio abbiamo creato l’attributo Route, lo abbiamo applicato ai metodi di un controller e abbiamo realizzato un router capace di interpretarlo. Lo stesso approccio può essere esteso a middleware, permessi, validazione dei dati, mapping tra oggetti e colonne del database oppure serializzazione JSON.
La combinazione tra attributi, classi tipizzate e Reflection consente di costruire codice dichiarativo: il controller descrive ciò che gestisce, mentre un componente generico interpreta queste informazioni e applica il comportamento necessario.
