15 domande selezionate per colloquio PHP backend, raggruppate per livello di seniority. Usale per ripassare i fondamenti, i compromessi pratici e il ragionamento di produzione a livello senior.
1In che modo il sistema di tipi di PHP distingue la tipizzazione debole da quella stretta, e cosa cambia quando `declare(strict_types=1)` è abilitato?
PHP è debole/coercitivo per impostazione predefinita per le dichiarazioni di tipo scalare: quando una funzione dichiara parametri o tipi di ritorno scalari, PHP può convertire valori compatibili come stringhe numeriche in numeri invece di rifiutarli. `declare(strict_types=1)` rende stretto il controllo dei tipi scalari per quel file, quindi i valori scalari incompatibili non vengono convertiti (coerced) e viene lanciato un `TypeError`. La direttiva ha *scope* (ambito) a livello di file: per gli argomenti delle funzioni definite dall'utente, il file del chiamante controlla la rigorosità; per le istruzioni `return`, il file in cui la funzione è definita controlla la rigorosità del valore di ritorno. Non rende PHP strettamente tipizzato a livello globale né disabilita tutte le conversioni nel linguaggio.
<?php
// lib.php
function takesInt(int $x): int {
return $x;
}
// weak.php, no strict_types
echo takesInt('123'); // coerces string to int
// strict.php
declare(strict_types=1);
echo takesInt('123'); // TypeError
2Spiega la differenza tra `==` e `===` in PHP, incluse le insidie con stringhe numeriche, array, oggetti e `in_array()`.
`==` esegue un confronto non rigoroso (loose comparison), quindi PHP può convertire i tipi prima di confrontare; `===` esegue un confronto rigoroso (strict comparison), richiedendo sia lo stesso valore che lo stesso tipo senza coercizione. Le stringhe numeriche sono un'insidia comune perché possono essere trattate come numeri nei confronti non rigorosi. Array e oggetti hanno le proprie regole di confronto: gli array vengono confrontati per chiavi e valori, mentre `==` per gli oggetti confronta oggetti della stessa classe per proprietà e `===` per gli oggetti richiede esattamente la stessa istanza. `in_array()` usa il confronto non rigoroso di default, quindi passa `true` come terzo argomento quando il tipo deve corrispondere.
3In che modo `mixed`, `void` e `never` differiscono come tipi di ritorno in PHP e quali garanzie offrono?
Come tipi di ritorno, `mixed`, `void` e `never` offrono garanzie diverse. `mixed` significa che la funzione può restituire qualsiasi valore, incluso `null`. `void` significa che la funzione non è destinata a restituire un valore significativo; può usare `return;` ma non può restituire un'espressione. `never` significa che la funzione non ritorna normalmente al suo chiamante: deve sempre lanciare un'eccezione, terminare l'esecuzione o altrimenti interrompere il flusso di controllo. `never` è più forte di `void` perché l'esecuzione non può continuare dopo una normale chiamata a una funzione `never`.
<?php
function parse(string $raw): mixed {
return json_decode($raw, true);
}
function logMessage(string $message): void {
error_log($message);
return; // allowed
}
function fail(string $message): never {
throw new RuntimeException($message);
}
logMessage('starting');
fail('stop');
echo 'unreachable';
4Cosa significa `iterable` in PHP, e come si relaziona con `array`, `Traversable`, `Iterator` e `Generator`?
In PHP, `iterable` è una dichiarazione di tipo che significa "un `array` o un oggetto che implementa `Traversable`". `Traversable` è l'interfaccia marcatore di base per gli oggetti utilizzabili da `foreach`; il codice utente normalmente implementa `Iterator` o `IteratorAggregate`, che estendono `Traversable`. Un `Generator` è l'oggetto restituito da una funzione generatrice che usa `yield`; esso implementa `Iterator`, quindi è anche `Traversable` e viene accettato ovunque sia richiesto un `iterable`.
function printItems(iterable $items): void {
foreach ($items as $item) {
echo $item, PHP_EOL;
}
}
printItems(['a', 'b']);
function letters(): Generator {
yield 'x';
yield 'y';
}
printItems(letters());
5Quali sono le regole di visibilità `public`, `protected` e `private` in PHP, e come interagiscono con l'ereditarietà e i trait?
In PHP, i membri `public` sono accessibili da qualsiasi punto, i membri `protected` sono accessibili dalla classe dichiarante e dalle sottoclassi, e i membri `private` sono accessibili solo dalla classe che li dichiara. Nell'ereditarietà, le sottoclassi possono usare i membri `public` e `protected` ereditati, ma i membri `private` di un genitore non sono accessibili come API ereditata. Quando si sovrascrivono i metodi, una classe figlia generalmente non può ridurre la visibilità; ad esempio, un metodo `public` del genitore deve rimanere `public`. I trait vengono composti nella classe che li utilizza, e i membri dei trait seguono le regole di visibilità di PHP. La visibilità dei metodi dei trait può anche essere adattata o aliassata con `as` quando si importa il trait.
<?php
class Base {
public function a() {}
protected function b() {}
private function c() {}
}
trait Logs {
public function log(string $message): void {}
}
class Child extends Base {
use Logs { log as protected writeLog; }
public function test(): void {
$this->a(); // OK
$this->b(); // OK
// $this->c(); // Not accessible: private to Base
$this->writeLog('ok'); // OK inside class, protected alias
}
}
$child = new Child();
$child->a();
// $child->b(); // Not accessible from outside
// $child->writeLog('x'); // Not accessible from outside
6Come funzionano i trait in PHP e come vengono risolti i conflitti quando più trait definiscono lo stesso metodo?
I trait sono il meccanismo di PHP per il riuso orizzontale del codice: una classe può usare uno o più trait, e i membri del trait vengono incorporati nella classe. I trait non sono classi autonome e non possono essere istanziati direttamente. Se più trait forniscono metodi con lo stesso nome, PHP richiede una risoluzione esplicita dei conflitti. L'operatore `insteadof` sceglie quale metodo del trait viene utilizzato per il nome in conflitto. L'operatore `as` può creare un alias per un metodo del trait e può anche modificare la visibilità del metodo importato. I metodi di classe sovrascrivono i metodi dei trait, e i metodi dei trait hanno la precedenza sui metodi genitore ereditati.
<?php
trait JsonLogger {
public function log(string $message): void {
echo "json:$message\n";
}
}
trait FileLogger {
public function log(string $message): void {
echo "file:$message\n";
}
}
class Service {
use JsonLogger, FileLogger {
JsonLogger::log insteadof FileLogger;
FileLogger::log as logToFile;
JsonLogger::log as protected logJson;
}
}
$service = new Service();
$service->log('hello');
$service->logToFile('hello');
// $service->logJson('hello'); // not accessible from outside; protected alias
7Cosa sono le enum PHP, incluse le enum pure e backed, e come migliorano la modellazione del dominio?
Le enum PHP definiscono un insieme chiuso di casi nominati per un tipo, come gli stati degli ordini o i ruoli utente. Le enum pure hanno solo casi nominati; le enum backed assegnano a ogni caso un valore stringa o int univoco, utile per lo storage, le API o l'interoperabilità. Le enum migliorano la modellazione del dominio sostituendo stringhe grezze, interi o costanti fragili con valori type-safe e auto-documentanti. Le enum PHP possono anche avere metodi e implementare interfacce; tutte le enum supportano l'elenco dei loro casi, e le enum backed supportano la conversione da e verso i loro valori scalari.
enum OrderState
{
case Draft;
case Paid;
case Shipped;
}
enum PaymentStatus: string
{
case Pending = 'pending';
case Complete = 'complete';
case Failed = 'failed';
}
$status = PaymentStatus::tryFrom('complete');
var_dump($status === PaymentStatus::Complete);
8Come i tipi scalari, nullabili, unione, intersezione e DNF esprimono i contratti dell'API di PHP, e quando dovrebbe essere utilizzato ciascuno?
Le dichiarazioni di tipo di PHP descrivono i valori che un'API accetta o restituisce. I tipi scalari come `int`, `float`, `string` e `bool` sono usati quando si prevede esattamente un tipo primitivo. I tipi nullabili, scritti `?T` o `T|null`, consentono un valore di tipo `T` o `null`. I tipi unione come `A|B` consentono una tra diverse alternative. I tipi intersezione come `A&B` richiedono che un valore soddisfi tutti i tipi elencati, comunemente più interfacce. I tipi DNF (Disjunctive Normal Form) combinano unioni e intersezioni, ad esempio `(A&B)|C`, per esprimere contratti più complessi. Ciascuno dovrebbe essere usato quando corrisponde alla reale promessa dell'API, evitando tipi inutilmente ampi o confusi.
<?php
interface Cacheable {}
class Report implements Cacheable, JsonSerializable {
public function jsonSerialize(): mixed { return []; }
}
function findName(int $id): ?string {
return $id > 0 ? 'Alice' : null;
}
function normalize(int|string $id): string {
return (string) $id;
}
function store(Cacheable&JsonSerializable $item): void {
// must satisfy both interfaces
}
function handle((Cacheable&JsonSerializable)|string $input): void {
// either an object satisfying both contracts, or a string
}
9Qual è la differenza tra `callable`, `Closure`, callable di prima classe, oggetti invocabili e array callable in PHP?
Il termine `callable` è un tipo/contratto ampio per qualsiasi valore che PHP può invocare, come una stringa contenente il nome di una funzione, una `Closure`, un oggetto invocabile o un array di riferimento a un metodo come `[$object, 'method']` o `[ClassName::class, 'method']`. Una `Closure` è un oggetto funzione anonima concreto ed è di per sé callable. La sintassi dei callable di prima classe, come `$obj->method(...)` o `strlen(...)`, crea una `Closure` da un'espressione callable. Un oggetto invocabile è callable perché definisce `__invoke()`. Gli array callable sono la tradizionale forma a due elementi per i riferimenti a metodi. La funzione `is_callable()` può essere usata per verificare l'invocabilità a runtime.
class Greeter {
public function hello(string $name): string { return "Hello $name"; }
public function __invoke(string $name): string { return "Hi $name"; }
}
$g = new Greeter();
$closure = fn(string $name) => "Hey $name";
$arrayCallable = [$g, 'hello'];
$firstClass = $g->hello(...);
$invokable = $g;
foreach ([$closure, $arrayCallable, $firstClass, $invokable] as $cb) {
echo $cb('Sam'), PHP_EOL;
}
10Come gestisce PHP l'overflow di interi, la precisione in virgola mobile e i calcoli decimali esatti?
Gli interi di PHP hanno dimensioni dipendenti dalla piattaforma, con limiti esposti da costanti come `PHP_INT_MAX`. PHP non mantiene automaticamente i risultati interi nativi come interi a precisione arbitraria dopo un overflow; l'aritmetica intera con overflow può diventare un `float` o perdere altrimenti la semantica esatta degli interi. I float di PHP sono valori in virgola mobile binari, quindi molte frazioni decimali, come `0.1`, sono approssimate. Per calcoli decimali esatti o monetari, evita i float e usa un approccio esatto come le unità intere minori (integer minor units) quando appropriato, o strumenti decimali a precisione fissa/arbitraria come BCMath. GMP è utile per l'aritmetica intera a precisione arbitraria.
11Spiega i riferimenti PHP (`&`), il copy-on-write e come i riferimenti possono creare effetti collaterali inattesi.
Le variabili PHP sono rappresentate internamente da `zval`. Per valori come array e stringhe, PHP utilizza il meccanismo *copy-on-write*: l'assegnazione `$b = $a` di solito condivide lo stesso valore sottostante finché una delle variabili non viene modificata, a quel punto PHP lo separa/copia. Un riferimento creato con `&` rende le variabili alias dello stesso contenitore, quindi la modifica di una modifica l'altra e può bypassare il comportamento di separazione che ci si aspetta da una normale assegnazione. Passare per riferimento permette a una funzione di modificare la variabile del chiamante. I riferimenti possono causare effetti collaterali sorprendenti, specialmente con `foreach ($array as &$value)`: la variabile del ciclo rimane un riferimento all'ultimo elemento dopo il ciclo, quindi riutilizzarla in seguito può sovrascrivere accidentalmente quell'elemento a meno che non si esegua `unset($value)`.
12Come funzionano i riferimenti agli oggetti (object handles), l'assegnazione, il `clone`, il metodo magico `__clone`, la copia superficiale (shallow copy) e la copia profonda (deep copy) in PHP?
In PHP, una variabile oggetto memorizza un riferimento (handle) a un oggetto, non una copia completa dell'oggetto. L'assegnazione `$b = $a` copia il riferimento, quindi entrambe le variabili fanno riferimento allo stesso oggetto; modificare l'oggetto tramite una delle due variabili sarà visibile tramite l'altra, senza usare `&`. `clone $a` crea una nuova istanza dell'oggetto con una copia superficiale (shallow copy) delle proprietà dell'oggetto originale. Se una proprietà è essa stessa un oggetto, la proprietà dell'oggetto clonato punterà comunque allo stesso oggetto annidato a meno che non lo si cloni esplicitamente. Il metodo `__clone()` viene chiamato sul nuovo oggetto dopo la copia superficiale ed è il luogo in cui reimpostare gli ID, scollegare le risorse o eseguire una copia profonda (deep copy) degli oggetti annidati. Una copia profonda significa copiare ricorsivamente gli oggetti mutabili annidati che non dovrebbero essere condivisi.
class Address {
public function __construct(public string $city) {}
}
class User {
public function __construct(public string $name, public Address $address) {}
public function __clone() {
$this->address = clone $this->address; // make nested object independent
}
}
$u1 = new User('Ann', new Address('Paris'));
$u2 = $u1; // same object handle
$u2->name = 'Beth';
echo $u1->name, PHP_EOL;
$u3 = clone $u1; // new User object; __clone deep-clones address
$u3->address->city = 'Rome';
echo $u1->address->city, PHP_EOL;
echo $u3->address->city, PHP_EOL;
13Come risolveresti i problemi di esaurimento delle connessioni al database e progetteresti la gestione delle connessioni per PHP-FPM (FastCGI Process Manager) e i worker a lunga esecuzione?
Per risolvere i problemi di esaurimento delle connessioni, confronterei i limiti di connessione al database e le sessioni correnti con la concorrenza totale di PHP: ogni child PHP-FPM attivo può mantenere la propria connessione al database, e più pool di app, host, cron job, worker di coda, migrazioni e deploy moltiplicano la domanda. Esaminerei le viste delle sessioni del database, gli stati delle connessioni, le sessioni idle-in-transaction, le query lente, le attese di blocco, i log degli errori, le metriche del pool e i recenti cambiamenti nel traffico o nel numero di worker.
Per PHP-FPM, mantieni `max_children` e la concorrenza per host all'interno del budget del database, usa transazioni/connessioni a breve termine (request-scoped), effettua il commit o il rollback prontamente, e evita di mantenere connessioni durante chiamate esterne lente. Le connessioni PDO (PHP Data Objects) persistenti possono ridurre l'overhead di connessione ma possono mantenere molte connessioni inattive per processo FPM e conservare lo stato della sessione, quindi richiedono cautela. Un pooler o proxy come PgBouncer può multiplexare molti client PHP su un numero inferiore di connessioni al server di database quando l'applicazione è compatibile.
I worker a lunga esecuzione necessitano di una gestione esplicita del ciclo di vita delle connessioni: health check, riconnessione dopo timeout di inattività/riavvii/failover/fork, ripristino dello stato della sessione e tentativi solo per lavoro sicuro o idempotente.
Example:
- 6 application servers
- 40 PHP-FPM max_children per server
- 2 separate FPM pools that can hit the database
Potential concurrent PHP request DB connections = 6 * 40 * 2 = 480
If PostgreSQL max_connections is 300 and queues/cron/admin tools also connect,
connection exhaustion is expected during traffic spikes.
14Come gestiresti le repliche di lettura, la separazione lettura/scrittura, il ritardo di replicazione e il failover del database primario in PHP?
La gestione delle repliche di lettura e del failover in PHP richiede di separare il traffico di lettura e scrittura, mitigare il ritardo di replicazione e gestire le interruzioni di connessione durante il failover. A livello di applicazione, la configurazione del database o un middleware instradano le query di lettura (`SELECT`) alle repliche di lettura e le query di scrittura (`INSERT`, `UPDATE`, `DELETE`) all'istanza primaria (alternativamente gestite tramite proxy di database come ProxySQL o AWS RDS Proxy). Poiché la replicazione è asincrona, il ritardo di replicazione può causare letture non aggiornate. Per garantire la consistenza 'read-your-writes', l'applicazione dovrebbe instradare le letture all'interno di una transazione attiva al primario e mantenere temporaneamente le letture successive sul primario per una sessione utente specifica o un ciclo di vita della richiesta immediatamente dopo una scrittura. Durante il failover del primario, i moderni cluster di database promuovono una replica e aggiornano gli endpoint DNS (Domain Name System)/proxy. In PHP, il livello di connessione deve intercettare la perdita di connessione e gli errori di sola lettura, eliminare i socket PDO (PHP Data Objects) obsoleti e applicare tentativi controllati con `backoff` esponenziale.
15Come vengono implementate in sicurezza le migrazioni di database e le modifiche allo schema con downtime nullo o minimo nelle applicazioni PHP?
L'implementazione di migrazioni di database con downtime nullo nelle applicazioni PHP richiede il disaccoppiamento dell'evoluzione dello schema dalle implementazioni del codice applicativo utilizzando il pattern Espansione e Contrazione (Esecuzione Parallela) insieme a pratiche di DDL (Data Definition Language) online non bloccanti. Nella fase di Espansione, vengono applicate modifiche allo schema retrocompatibili (ad esempio, l'aggiunta di colonne nullable, nuove tabelle o indici concorrenti utilizzando strumenti come `pt-online-schema-change`, `gh-ost` o DDL online nativo in MySQL/PostgreSQL). L'applicazione viene quindi implementata per supportare la scrittura duale (scrittura sia nelle vecchie che nelle nuove strutture di schema) mentre legge da una delle due. I record storici vengono poi retrocompilati in modo asincrono in batch in background con limitazione per prevenire il blocco delle tabelle o l'induzione di ritardi di replica. Nella fase di Contrazione, una volta convalidata la coerenza dei dati e i lettori sono stati completamente spostati sulla nuova struttura, un'implementazione del codice rimuove i riferimenti al vecchio schema. Infine, una migrazione di pulizia elimina colonne o tabelle legacy. Un rollback sicuro richiede che ogni stato intermedio dello schema rimanga pienamente compatibile con le versioni dell'applicazione precedenti e successive durante i rilasci rolling.
<?php
// Step 1: Migration (Expand) -> Add new column as nullable
Schema::table('users', function (Blueprint $table) {
$table->string('full_name')->nullable()->after('name');
});
// Step 2: Code deployment -> Write to both, read with fallback
class User extends Model {
public function setNameAttribute($value) {
$this->attributes['name'] = $value;
$this->attributes['full_name'] = $value; // Dual write
}
}
// Step 3: Background worker backfills existing rows in small batches
// Step 4: Code deployment -> Read/write exclusively from 'full_name'
// Step 5: Migration (Contract) -> Drop legacy 'name' column