Funzioni di callback in PHP moderno
Le funzioni di callback rappresentano uno dei pilastri della programmazione moderna in PHP. A partire dalla versione 5.3, con l'introduzione delle closure, e poi con i continui miglioramenti apportati nelle versioni successive fino a PHP 8.x, il linguaggio ha acquisito strumenti espressivi e potenti per trattare le funzioni come cittadini di prima classe. Questo articolo esplora in profondità il concetto di callback, le sue diverse forme, i pattern di utilizzo e le best practice da adottare nel codice PHP contemporaneo.
Che cos'è una callback
Una callback è una porzione di codice eseguibile che viene passata come argomento a un'altra funzione, la quale potrà invocarla in un momento successivo. Il meccanismo è semplice nella sua essenza: invece di eseguire direttamente un'operazione, la si delega a un blocco di codice fornito dall'esterno. Questo approccio consente di scrivere funzioni generiche e riutilizzabili, il cui comportamento specifico viene determinato al momento della chiamata.
In PHP il tipo che rappresenta una callback è callable. Qualsiasi valore che superi il controllo is_callable() può essere utilizzato dove è atteso un callable. Le forme principali sono le funzioni anonime (closure), i riferimenti a funzioni con nome, i metodi di classe e gli oggetti invocabili.
Funzioni anonime e closure
La forma più comune e idiomatica di callback in PHP moderno è la funzione anonima, introdotta in PHP 5.3. Una funzione anonima è una funzione senza nome, assegnabile a una variabile o passabile direttamente come argomento.
// Definizione di una funzione anonima assegnata a una variabile
$greet = function (string $name): string {
return "Ciao, {$name}!";
};
// Invocazione della callback
echo $greet('Marco'); // Stampa: Ciao, Marco!
Le closure in PHP possono catturare variabili dal contesto circostante tramite la parola chiave use. Questo le rende estremamente versatili, perché possono trasportare stato senza richiedere strutture aggiuntive.
$taxRate = 0.22;
// La closure cattura la variabile esterna $taxRate
$calculatePrice = function (float $netPrice) use ($taxRate): float {
return $netPrice * (1 + $taxRate);
};
echo $calculatePrice(100.00); // Stampa: 122
Per impostazione predefinita, le variabili catturate con use vengono copiate per valore. Se si desidera modificare la variabile originale dall'interno della closure, occorre catturarla per riferimento.
$counter = 0;
// Cattura per riferimento: la closure modifica la variabile originale
$increment = function () use (&$counter): void {
$counter++;
};
$increment();
$increment();
$increment();
echo $counter; // Stampa: 3
Arrow function
PHP 7.4 ha introdotto le arrow function, una sintassi abbreviata per le closure che consistono in una singola espressione. Le arrow function catturano automaticamente le variabili dal contesto circostante per valore, eliminando la necessità della clausola use.
$multiplier = 3;
// Arrow function: sintassi concisa con cattura automatica
$multiply = fn(int $value): int => $value * $multiplier;
echo $multiply(7); // Stampa: 21
Le arrow function sono particolarmente utili nelle operazioni su array e in tutti i casi in cui la logica della callback si esaurisce in un'unica espressione. La loro concisione migliora notevolmente la leggibilità del codice.
$prices = [29.99, 49.50, 12.00, 85.75];
// Filtraggio con arrow function: solo i prezzi superiori a 30
$expensiveItems = array_filter($prices, fn(float $price): bool => $price > 30.00);
// Trasformazione con arrow function: applica lo sconto del 10%
$discountRate = 0.10;
$discountedPrices = array_map(
fn(float $price): float => round($price * (1 - $discountRate), 2),
$prices
);
print_r($discountedPrices);
Callback con funzioni con nome
Non è sempre necessario definire una funzione anonima. PHP consente di passare il nome di una funzione esistente come stringa, e questo verrà trattato come un callable valido.
function toUpperCase(string $text): string {
// Converte il testo in maiuscolo
return mb_strtoupper($text);
}
$words = ['alfa', 'bravo', 'charlie'];
// Passaggio del nome della funzione come callback
$uppercasedWords = array_map('toUpperCase', $words);
print_r($uppercasedWords);
// Stampa: ['ALFA', 'BRAVO', 'CHARLIE']
Anche le funzioni native di PHP possono essere usate direttamente come callback, a patto che la loro firma sia compatibile con ciò che la funzione chiamante si aspetta.
$values = [' ciao ', ' mondo ', ' php '];
// Utilizzo diretto di una funzione nativa come callback
$trimmedValues = array_map('trim', $values);
print_r($trimmedValues);
// Stampa: ['ciao', 'mondo', 'php']
First-class callable syntax
PHP 8.1 ha introdotto la sintassi functionName(...), nota come first-class callable syntax. Questa notazione crea una closure a partire da qualsiasi funzione o metodo richiamabile, risolvendo i problemi legati all'uso delle stringhe e offrendo il pieno supporto dell'analisi statica da parte degli strumenti di sviluppo.
// Sintassi first-class callable: crea una closure dalla funzione strlen
$lengthCalculator = strlen(...);
echo $lengthCalculator('programmazione'); // Stampa: 15
$words = ['gatto', 'elefante', 'ape'];
// Ordinamento per lunghezza usando la sintassi first-class
usort($words, fn(string $a, string $b): int => strlen($a) - strlen($b));
print_r($words);
// Stampa: ['ape', 'gatto', 'elefante']
Questa sintassi funziona anche con i metodi statici e i metodi di istanza.
class TextFormatter
{
public function capitalize(string $text): string
{
// Rende maiuscola la prima lettera di ogni parola
return mb_convert_case($text, MB_CASE_TITLE);
}
public static function slugify(string $text): string
{
// Converte il testo in formato slug
$slug = mb_strtolower($text);
$slug = preg_replace('/[^a-z0-9]+/', '-', $slug);
return trim($slug, '-');
}
}
$formatter = new TextFormatter();
// First-class callable su metodo di istanza
$capitalizer = $formatter->capitalize(...);
// First-class callable su metodo statico
$slugifier = TextFormatter::slugify(...);
echo $capitalizer('ciao mondo'); // Stampa: Ciao Mondo
echo $slugifier('Funzioni di Callback'); // Stampa: funzioni-di-callback
Metodi come callback
PHP supporta diverse notazioni per utilizzare metodi di classe come callback. La più tradizionale impiega array con due elementi: l'oggetto (o il nome della classe per i metodi statici) e il nome del metodo come stringa.
class MathHelper
{
public function square(int $number): int
{
// Restituisce il quadrato del numero
return $number ** 2;
}
public static function cube(int $number): int
{
// Restituisce il cubo del numero
return $number ** 3;
}
}
$helper = new MathHelper();
// Callback con metodo di istanza: array con oggetto e nome del metodo
$squares = array_map([$helper, 'square'], [1, 2, 3, 4, 5]);
// Callback con metodo statico: array con nome della classe e nome del metodo
$cubes = array_map([MathHelper::class, 'cube'], [1, 2, 3, 4, 5]);
print_r($squares); // [1, 4, 9, 16, 25]
print_r($cubes); // [1, 8, 27, 64, 125]
Sebbene questa sintassi sia ancora valida e diffusa, la first-class callable syntax di PHP 8.1 è da preferire nei progetti moderni per la sua maggiore chiarezza e il supporto degli strumenti di analisi statica.
Oggetti invocabili
Qualsiasi oggetto che implementi il metodo magico __invoke può essere utilizzato come callback. Questo pattern è particolarmente utile quando la logica della callback richiede stato interno, configurazione o dipendenze iniettate.
class DiscountCalculator
{
// Il costruttore accetta la percentuale di sconto
public function __construct(
private readonly float $percentage
) {}
public function __invoke(float $originalPrice): float
{
// Calcola il prezzo scontato
return round($originalPrice * (1 - $this->percentage / 100), 2);
}
}
$tenPercentOff = new DiscountCalculator(10);
$twentyPercentOff = new DiscountCalculator(20);
$prices = [100.00, 59.99, 249.90];
// L'oggetto invocabile viene usato direttamente come callback
$discountedA = array_map($tenPercentOff, $prices);
$discountedB = array_map($twentyPercentOff, $prices);
print_r($discountedA); // [90.0, 53.99, 224.91]
print_r($discountedB); // [80.0, 47.99, 199.92]
Gli oggetti invocabili offrono un vantaggio strutturale rispetto alle closure per le callback complesse: possono essere testati unitariamente, serializzati, e beneficiano dell'intero ecosistema della programmazione a oggetti, inclusa l'ereditarietà e l'implementazione di interfacce.
Type hint e il tipo callable
PHP offre due meccanismi principali per tipizzare i parametri callback: il tipo callable e l'interfaccia Closure. Il tipo callable accetta qualsiasi forma valida di callback, mentre Closure restringe l'accettazione alle sole funzioni anonime e ai risultati della first-class callable syntax.
// Accetta qualsiasi tipo di callback
function applyTransformation(array $items, callable $transformer): array
{
return array_map($transformer, $items);
}
// Accetta solo closure, escludendo stringhe e array callable
function applyStrictTransformation(array $items, Closure $transformer): array
{
return array_map($transformer, $items);
}
$numbers = [1, 2, 3, 4, 5];
// Entrambe le chiamate funzionano con una closure
$doubled = applyTransformation($numbers, fn(int $n): int => $n * 2);
$tripled = applyStrictTransformation($numbers, fn(int $n): int => $n * 3);
// Questa funziona solo con callable, non con Closure
$absoluteValues = applyTransformation([-3, -1, 0, 2], 'abs');
A partire da PHP 8.0, è possibile utilizzare i tipi union per creare firme ancora più espressive. In contesti avanzati, è comune definire interfacce funzionali personalizzate che descrivono con precisione la firma attesa della callback.
// Interfaccia funzionale personalizzata per validatori
interface Validator
{
public function __invoke(mixed $value): bool;
}
class RangeValidator implements Validator
{
public function __construct(
private readonly float $min,
private readonly float $max
) {}
public function __invoke(mixed $value): bool
{
// Verifica che il valore sia compreso nell'intervallo
return is_numeric($value) && $value >= $this->min && $value <= $this->max;
}
}
function filterWithValidator(array $items, Validator $validator): array
{
// Filtra gli elementi usando il validatore
return array_filter($items, $validator);
}
$ageValidator = new RangeValidator(18, 65);
$ages = [12, 25, 17, 42, 70, 33, 8];
$validAges = filterWithValidator($ages, $ageValidator);
print_r($validAges); // [25, 42, 33]
Callback nelle funzioni native di PHP
Il linguaggio fa largo uso di callback nelle sue funzioni native, specialmente nelle operazioni su array. Le funzioni più utilizzate in questo contesto sono array_map, array_filter, array_reduce, usort, array_walk e preg_replace_callback.
$products = [
['name' => 'Tastiera', 'price' => 79.99, 'category' => 'periferiche'],
['name' => 'Monitor', 'price' => 349.00, 'category' => 'display'],
['name' => 'Mouse', 'price' => 29.99, 'category' => 'periferiche'],
['name' => 'Webcam', 'price' => 89.50, 'category' => 'periferiche'],
['name' => 'Altoparlante','price' => 149.00, 'category' => 'audio'],
];
// Filtraggio: solo i prodotti della categoria periferiche
$peripherals = array_filter(
$products,
fn(array $product): bool => $product['category'] === 'periferiche'
);
// Trasformazione: estrae solo i nomi
$productNames = array_map(
fn(array $product): string => $product['name'],
$products
);
// Riduzione: calcola il totale dei prezzi
$totalPrice = array_reduce(
$products,
fn(float $carry, array $product): float => $carry + $product['price'],
0.0
);
echo "Totale: " . number_format($totalPrice, 2) . " EUR";
// Stampa: Totale: 697.48 EUR
La funzione usort merita un'attenzione particolare. Accetta una callback di confronto che deve restituire un intero negativo, zero o positivo. A partire da PHP 7.4, l'operatore spaceship <=> semplifica enormemente la scrittura di queste callback.
$students = [
['name' => 'Elena', 'grade' => 28],
['name' => 'Marco', 'grade' => 30],
['name' => 'Anna', 'grade' => 25],
['name' => 'Luca', 'grade' => 28],
];
// Ordinamento per voto decrescente, a parità per nome crescente
usort($students, function (array $a, array $b): int {
// Prima confronta i voti in ordine decrescente
$gradeComparison = $b['grade'] <=> $a['grade'];
if ($gradeComparison !== 0) {
return $gradeComparison;
}
// A parità di voto, ordina per nome in ordine crescente
return $a['name'] <=> $b['name'];
});
print_r($students);
Composizione di callback
Uno dei vantaggi più significativi delle callback è la possibilità di comporle per creare pipeline di trasformazione. Questo approccio consente di costruire operazioni complesse a partire da blocchi semplici e riutilizzabili.
// Compone più callback in un'unica funzione che le esegue in sequenza
function compose(callable ...$functions): Closure
{
return function (mixed $value) use ($functions): mixed {
foreach ($functions as $function) {
$value = $function($value);
}
return $value;
};
}
// Definizione di trasformazioni elementari
$trim = fn(string $text): string => trim($text);
$lowercase = fn(string $text): string => mb_strtolower($text);
$removeAccents = fn(string $text): string => transliterator_transliterate(
'Any-Latin; Latin-ASCII', $text
);
$slugify = fn(string $text): string => preg_replace('/[^a-z0-9]+/', '-', $text);
// Composizione in una pipeline unica
$createSlug = compose($trim, $lowercase, $removeAccents, $slugify);
echo $createSlug(' Cafè e Cornetto a Milano ');
// Stampa: cafe-e-cornetto-a-milano
La composizione di funzioni è alla base di molti pattern funzionali. Un'altra tecnica correlata è il piping, che consente di applicare una sequenza di trasformazioni a un valore iniziale in modo dichiarativo.
// Applica una serie di trasformazioni a un valore iniziale
function pipe(mixed $value, callable ...$functions): mixed
{
foreach ($functions as $function) {
$value = $function($value);
}
return $value;
}
$rawInput = ' esempio@Email.COM ';
// Pipeline di sanificazione dell'input
$sanitizedEmail = pipe(
$rawInput,
trim(...),
mb_strtolower(...),
fn(string $email): string => filter_var($email, FILTER_SANITIZE_EMAIL)
);
echo $sanitizedEmail; // Stampa: esempio@email.com
Callback e gestione degli errori
Quando si lavora con le callback, la gestione degli errori richiede attenzione particolare. Le eccezioni lanciate all'interno di una callback si propagano normalmente attraverso lo stack di chiamate, ma è buona pratica documentare e gestire esplicitamente i casi di errore.
// Esegue una callback con gestione degli errori e un valore di fallback
function trySafely(callable $operation, mixed $fallback = null): mixed
{
try {
return $operation();
} catch (\Throwable $exception) {
// Registra l'errore per il debug
error_log(sprintf(
'Errore nella callback: %s in %s:%d',
$exception->getMessage(),
$exception->getFile(),
$exception->getLine()
));
return $fallback;
}
}
// Esempio di utilizzo con operazione che potrebbe fallire
$result = trySafely(
fn(): int => intdiv(10, 0),
-1
);
echo $result; // Stampa: -1 (valore di fallback)
Un pattern più sofisticato prevede l'uso di callback per la strategia di retry, utile nelle operazioni di rete o di accesso a risorse esterne.
// Ritenta l'esecuzione di una callback fino a un massimo di tentativi
function retry(callable $operation, int $maxAttempts = 3, int $delayMs = 100): mixed
{
$lastException = null;
for ($attempt = 1; $attempt <= $maxAttempts; $attempt++) {
try {
return $operation($attempt);
} catch (\Throwable $exception) {
$lastException = $exception;
if ($attempt < $maxAttempts) {
// Attesa esponenziale prima del prossimo tentativo
usleep($delayMs * 1000 * (2 ** ($attempt - 1)));
}
}
}
// Tutti i tentativi esauriti: rilancia l'ultima eccezione
throw new \RuntimeException(
"Operazione fallita dopo {$maxAttempts} tentativi: " . $lastException->getMessage(),
0,
$lastException
);
}
Callback nell'architettura a eventi
Le callback sono il fondamento dei sistemi basati su eventi. Un semplice event dispatcher può essere realizzato con poche righe di codice, dimostrando la potenza del pattern observer implementato tramite callable.
class EventDispatcher
{
// Registro dei listener organizzati per nome dell'evento
private array $listeners = [];
public function subscribe(string $eventName, callable $listener): void
{
// Aggiunge un listener per l'evento specificato
$this->listeners[$eventName][] = $listener;
}
public function dispatch(string $eventName, array $payload = []): void
{
// Esegue tutti i listener registrati per l'evento
foreach ($this->listeners[$eventName] ?? [] as $listener) {
$listener($payload);
}
}
}
$dispatcher = new EventDispatcher();
// Registrazione dei listener tramite callback
$dispatcher->subscribe('user.registered', function (array $data): void {
// Invio dell'email di benvenuto
echo "Email inviata a: {$data['email']}\n";
});
$dispatcher->subscribe('user.registered', function (array $data): void {
// Registrazione nel log di sistema
echo "Nuovo utente registrato: {$data['name']}\n";
});
$dispatcher->subscribe('user.registered', function (array $data): void {
// Notifica agli amministratori
echo "Notifica admin: nuovo utente {$data['name']}\n";
});
// Emissione dell'evento
$dispatcher->dispatch('user.registered', [
'name' => 'Giulia Rossi',
'email' => 'giulia@esempio.it',
]);
Callback e currying parziale
L'applicazione parziale consiste nel creare una nuova funzione fissando alcuni parametri di una funzione esistente. Questo pattern è molto utile per creare callback specializzate a partire da funzioni generiche.
// Crea una nuova funzione fissando il primo argomento
function partial(callable $function, mixed ...$partialArgs): Closure
{
return function (mixed ...$remainingArgs) use ($function, $partialArgs): mixed {
return $function(...$partialArgs, ...$remainingArgs);
};
}
// Funzione generica per il calcolo della potenza
function power(int $base, int $exponent): int
{
return $base ** $exponent;
}
// Creazione di funzioni specializzate tramite applicazione parziale
$square = partial('power', exponent: 2);
$cube = partial('power', exponent: 3);
echo $square(5); // Stampa: 25
echo $cube(3); // Stampa: 27
// Applicazione pratica: formattazione valuta
function formatCurrency(string $symbol, int $decimals, float $amount): string
{
// Formatta l'importo con il simbolo della valuta
return $symbol . number_format($amount, $decimals, ',', '.');
}
$formatEuro = partial('formatCurrency', 'EUR ', 2);
$formatDollar = partial('formatCurrency', '$ ', 2);
echo $formatEuro(1234.5); // Stampa: EUR 1.234,50
echo $formatDollar(1234.5); // Stampa: $ 1.234,50
Memoizzazione tramite callback
La memoizzazione è una tecnica di ottimizzazione che memorizza i risultati di chiamate costose per evitare ricalcoli. Si implementa elegantemente con le closure, sfruttando la cattura per riferimento.
// Avvolge una callback con una cache dei risultati
function memoize(callable $function): Closure
{
// Cache interna alla closure
$cache = [];
return function () use ($function, &$cache): mixed {
$arguments = func_get_args();
$cacheKey = serialize($arguments);
if (!array_key_exists($cacheKey, $cache)) {
// Risultato non in cache: calcola e memorizza
$cache[$cacheKey] = $function(...$arguments);
}
return $cache[$cacheKey];
};
}
// Funzione costosa simulata
$fibonacci = memoize(function (int $n) use (&$fibonacci): int {
if ($n <= 1) {
return $n;
}
// Calcolo ricorsivo con memoizzazione automatica
return $fibonacci($n - 1) + $fibonacci($n - 2);
});
echo $fibonacci(30); // Stampa: 832040 (calcolato efficientemente)
Callback come middleware
Il pattern middleware, ampiamente utilizzato nei framework HTTP come Laravel e Slim, si basa interamente sulle callback. Ogni middleware è una funzione che riceve la richiesta e un riferimento al middleware successivo nella catena.
// Tipo semplificato per la richiesta HTTP
class Request
{
public function __construct(
public readonly string $method,
public readonly string $path,
public array $attributes = []
) {}
}
// Costruisce una pipeline di middleware
function buildPipeline(callable $handler, callable ...$middlewares): Closure
{
// Compone i middleware in ordine inverso attorno all'handler finale
$pipeline = $handler;
foreach (array_reverse($middlewares) as $middleware) {
$pipeline = fn(Request $request) => $middleware($request, $pipeline);
}
return $pipeline;
}
// Middleware di logging
$loggingMiddleware = function (Request $request, callable $next): string {
echo "[LOG] {$request->method} {$request->path}\n";
// Passa la richiesta al middleware successivo
$response = $next($request);
echo "[LOG] Risposta generata\n";
return $response;
};
// Middleware di autenticazione
$authMiddleware = function (Request $request, callable $next): string {
if (!isset($request->attributes['authenticated'])) {
// Blocca la richiesta se non autenticata
return '401 Non autorizzato';
}
return $next($request);
};
// Handler finale
$handler = function (Request $request): string {
return "200 OK - Benvenuto su {$request->path}";
};
// Composizione della pipeline
$application = buildPipeline($handler, $loggingMiddleware, $authMiddleware);
// Esecuzione con richiesta autenticata
$request = new Request('GET', '/dashboard', ['authenticated' => true]);
echo $application($request);
Best practice e considerazioni finali
Nell'utilizzo delle callback in PHP moderno, alcune linee guida contribuiscono a mantenere il codice pulito, leggibile e manutenibile.
Preferire le arrow function per le callback brevi e le closure complete per quelle più articolate. Quando la logica supera le poche righe, valutare l'uso di oggetti invocabili o metodi dedicati. La chiarezza è sempre preferibile alla brevità.
Utilizzare la first-class callable syntax introdotta in PHP 8.1 al posto delle stringhe e degli array callable. La sintassi functionName(...) offre maggiore sicurezza a livello di tipo e pieno supporto per il refactoring automatizzato negli IDE.
Tipizzare sempre i parametri callback con callable o Closure. Quando la firma della callback è cruciale per il corretto funzionamento del codice, considerare la creazione di interfacce funzionali personalizzate che esplicitino il contratto atteso.
Prestare attenzione alla cattura delle variabili nelle closure. La cattura per riferimento con & può generare effetti collaterali inattesi se non gestita con consapevolezza. Le arrow function, che catturano per valore, eliminano questa classe di problemi.
Comporre le callback in pipeline quando si devono applicare trasformazioni sequenziali. Questo approccio produce codice dichiarativo, testabile e facilmente estensibile. Pattern come compose, pipe e middleware incarnano questo principio e si prestano a un'ampia varietà di scenari applicativi.
Le funzioni di callback, nelle loro molteplici incarnazioni, sono uno strumento indispensabile nel repertorio di ogni sviluppatore PHP. Dalla semplice trasformazione di un array alla costruzione di architetture complesse basate su eventi e middleware, le callback consentono di scrivere codice flessibile, componibile e aderente ai principi della programmazione moderna.