Preparazione al colloquio PHP

Domande di colloquio PHP

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.

Avvia un Colloquio PHP AINessuna carta di credito richiesta. 1 sessione gratuita disponibile.
Pratica per colloqui tecnici in ingleseUna modalità in cui chi non è madrelingua può esercitarsi a superare i colloqui tecnici.

Domande Junior

1Come distingue il sistema di tipi di PHP la tipizzazione debole da quella rigorosa, e che cosa cambia con declare(strict_types=1)?

PHP normalmente permette conversioni scalari compatibili per i tipi dichiarati di parametri e valori restituiti, per esempio una stringa numerica passata a un parametro int. declare(strict_types=1) abilita controlli scalari rigorosi: un argomento o un valore restituito incompatibile genera TypeError, anche se int resta accettato per float. Per gli argomenti delle funzioni definite dall'utente, il file chiamante determina il rigore dei controlli; il file che definisce la funzione determina i controlli sul valore restituito. Le chiamate effettuate da funzioni interne di PHP hanno particolarità proprie e non sono semplicemente governate da strict_types del chiamante esterno. La direttiva deve trovarsi all'inizio del relativo file PHP. Non rende PHP staticamente tipizzato e non disabilita le conversioni effettuate dagli operatori in tutto il linguaggio.

// THREE SEPARATE FILES: save each PHP block under its indicated filename.
// File: lib.php
<?php
function takesInt(int $x): int { return $x; }
?>

// File: weak.php
<?php
require __DIR__ . '/lib.php';
echo takesInt('123'); // 123: coercive caller
?>

// File: strict.php
<?php
declare(strict_types=1);
require __DIR__ . '/lib.php';
try {
    echo takesInt('123');
} catch (TypeError $e) {
    echo 'TypeError';
}
?>
Prova a rispondere a questa domanda con un coach IA

2Spiega la differenza tra == e === in PHP, comprese le insidie con stringhe numeriche, array, oggetti e in_array().

In PHP == effettua un confronto non rigoroso, con conversione dei tipi quando applicabile; === richiede tipi e valori corrispondenti senza conversione implicita degli scalari. Le stringhe numeriche possono essere confrontate numericamente: evita quindi confronti non rigorosi quando è richiesta una corrispondenza esatta sensibile al tipo. Gli array sono uguali con == se contengono le stesse coppie chiave/valore secondo le regole di confronto, indipendentemente dall'ordine delle coppie. === tra array richiede anche lo stesso ordine e tipi corrispondenti per chiavi e valori. == tra oggetti confronta le proprietà di oggetti della stessa classe; === richiede la stessa istanza. in_array usa per impostazione predefinita un confronto non rigoroso: passa true come terzo argomento per un controllo rigoroso. Il comportamento dei confronti non rigorosi tra numeri e stringhe è cambiato in PHP 8, quindi evita di affidarti a esempi storici di conversione senza specificare la versione.

<?php
var_dump(123 == '123');      // loose: numeric string is converted
var_dump(123 === '123');     // strict: int !== string

$values = [0, 1, 2];
var_dump(in_array('0', $values));       // loose
var_dump(in_array('0', $values, true)); // strict
Prova a rispondere a questa domanda con un coach IA

3Come si distinguono mixed, void e never come tipi restituiti in PHP e quali garanzie offrono?

mixed permette qualsiasi valore restituito, incluso null. void dichiara che una funzione non restituisce un valore significativo: può arrivare alla fine o usare return;, ma non può restituire un'espressione. Una normale chiamata a una funzione void produce null. never, disponibile da PHP 8.1, significa che la funzione non restituisce mai normalmente il controllo al chiamante. Può generare un'eccezione, terminare l'esecuzione con exit o restare in un ciclo infinito; non è obbligata a terminare prima o poi il processo. Una funzione never non può contenere un'istruzione return. La differenza fondamentale è che una funzione void può concludersi normalmente e lasciare proseguire il chiamante, mentre una funzione never non ha una continuazione normale.

<?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';
Prova a rispondere a questa domanda con un coach IA

4Che cos'è iterable in PHP e come si collega ad array, Traversable, Iterator e Generator?

iterable significa array|Traversable. Generator implementa Iterator, che estende Traversable, quindi soddisfa iterable. Le classi definite dall'utente forniscono il comportamento Traversable tramite Iterator o IteratorAggregate, invece di implementare direttamente Traversable in una classe concreta. Iterator espone metodi che controllano l'iterazione; IteratorAggregate fornisce un iteratore. Una funzione generatrice contenente yield restituisce un Generator. È importante distinguere questo caso da foreach, che può enumerare anche le proprietà visibili di un oggetto ordinario che non implementa Traversable: ciò non fa sì che l'oggetto soddisfi una dichiarazione di parametro 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());
Prova a rispondere a questa domanda con un coach IA

5Quali sono le regole di visibilità public, protected e private in PHP e come interagiscono con ereditarietà e trait?

I membri public sono accessibili ovunque sia possibile accedere all'oggetto o alla classe. I membri protected sono accessibili nella classe che li dichiara e nella relativa gerarchia di classi genitrici e derivate, non da codice esterno arbitrario. I membri private sono accessibili nella classe che li dichiara; una sottoclasse non può usare un membro privato del genitore come parte della propria interfaccia ereditata. Un normale metodo che ne ridefinisce un altro non può restringere la visibilità ereditata. I costruttori hanno però regole speciali di ereditarietà e non vanno considerati un esempio incondizionato di questa regola. I membri di un trait vengono incorporati nella classe utilizzatrice e ne seguono le regole di visibilità. L'adattamento as può creare un alias, modificare la visibilità di un metodo del trait o entrambe le cose; aggiungere un alias non elimina di per sé il nome originale.

<?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
Prova a rispondere a questa domanda con un coach IA

6Come funzionano i trait in PHP e come si risolvono i conflitti quando più trait definiscono lo stesso metodo?

I trait permettono il riutilizzo orizzontale del codice: una classe importa i loro membri con use e un trait non può essere istanziato autonomamente. Un metodo dichiarato nella classe ha precedenza sui metodi dei trait, che a loro volta prevalgono sui metodi ereditati dal genitore. Se più trait forniscono lo stesso metodo e il conflitto non è già risolto da un metodo della classe, usa insteadof per scegliere l'implementazione. Usa as per assegnare a un metodo di trait un nome aggiuntivo e, facoltativamente, una visibilità diversa. Un alias non rinomina né elimina il metodo originale. Risolvere le collisioni e mantenere chiara l'interfaccia pubblica risultante sono responsabilità distinte.

<?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
Prova a rispondere a questa domanda con un coach IA

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);
Prova a rispondere a questa domanda con un coach IA

Domande Middle

8Come esprimono i tipi scalari, nullable, unione, intersezione e DNF i contratti delle interfacce PHP e quando va usato ciascuno?

Le dichiarazioni PHP descrivono i valori accettati e restituiti. Le dichiarazioni scalari come int, float, string e bool selezionano una categoria di valori; l'ammissibilità delle conversioni scalari compatibili dipende anche da strict_types e dal contesto. ?T o T|null ammette null. Le unioni, disponibili da PHP 8.0, ammettono alternative come int|string. Le intersezioni pure, disponibili da PHP 8.1, richiedono che un oggetto soddisfi tutti i tipi di classe o interfaccia elencati, come Cacheable&JsonSerializable; int&string non è un'intersezione valida. I tipi DNF (forma normale disgiuntiva), disponibili da PHP 8.2, combinano alternative e intersezioni tra parentesi, per esempio (Cacheable&JsonSerializable)|string. Scegli il contratto più ristretto e comprensibile che rappresenti gli input validi, senza aggiungere alternative superflue.

<?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
}
Prova a rispondere a questa domanda con un coach IA

9Qual è la differenza tra callable, Closure, callable di prima classe, oggetti invocabili e array callable in PHP?

callable descrive valori invocabili, tra cui stringhe con nomi di funzioni, oggetti Closure, riferimenti a metodi accessibili e oggetti con __invoke. Closure è un oggetto invocabile concreto: può derivare da una funzione anonima oppure da una funzione o metodo con nome convertito in Closure. La sintassi dei callable di prima classe, come strlen(...) o $object->method(...), crea una Closure senza invocare la destinazione. Un array callable [$object, 'method'] fa riferimento a un metodo di istanza accessibile; [ClassName::class, 'method'] richiede un metodo statico appropriato e accessibile nelle versioni moderne di PHP. is_callable verifica l'invocabilità nell'ambito pertinente. callable è un contratto ampio, mentre Closure è una rappresentazione specifica.

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;
}
Prova a rispondere a questa domanda con un coach IA

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.

var_dump(0.1 + 0.2 === 0.3);
printf('%.17f\n', 0.1 + 0.2);
Prova a rispondere a questa domanda con un coach IA

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)`.

$a = [1, 2];
$b = $a;      // shared until modification
$b[0] = 99;   // separates; $a is unchanged
var_dump($a, $b);

$c =& $a;     // alias/reference
$c[0] = 42;   // modifies $a too
var_dump($a, $c);
Prova a rispondere a questa domanda con un coach IA

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;
Prova a rispondere a questa domanda con un coach IA

Domande Senior

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.

Connection-budget example:
- 6 application servers
- 2 PHP-FPM pools on EACH server
- pm.max_children = 40 for EACH pool
- Assume at most 1 database connection per child

Potential request connections = 6 * 2 * 40 = 480.
If 40 were the TOTAL children per server across both pools, it would be 240.
Budget separately for queue workers, cron, deployments and administrators.
A limit of 300 database connections cannot accommodate 480 simultaneous connections.
Prova a rispondere a questa domanda con un coach IA

14Come gestiresti repliche di lettura, separazione di letture e scritture, ritardo di replica e failover del database primario in PHP?

Separa le connessioni al primario e alle repliche in base alla semantica dell'operazione, non soltanto al fatto che l'SQL inizi con SELECT: letture con blocchi, scritture e transazioni che richiedono la coerenza del primario devono usarlo. Le transazioni di sola lettura possono usare una replica adatta quando i requisiti di coerenza lo consentono. La replica asincrona può essere in ritardo. L'impostazione sticky di Laravel instrada le letture successive a una scrittura sulla connessione di scrittura durante la richiesta corrente; mantenere l'affinità per un intervallo fisso tra richieste è solo una mitigazione, non una garanzia incondizionata di rilettura delle proprie scritture. Per garantirla, leggi dal primario oppure attendi che una replica abbia applicato la posizione di scrittura richiesta, con un tempo limite e ripiego sul primario. I proxy hanno comportamenti diversi: RDS Proxy usa endpoint separati per lettori e scrittori e non è un separatore automatico generale delle letture e scritture per singola istruzione. Durante il failover scarta le connessioni obsolete e riconnettiti con attese progressivamente maggiori ma limitate. Se si perde la risposta a un commit, l'esito è ambiguo: non ripetere alla cieca scritture non idempotenti; usa chiavi di idempotenza o verifica prima il risultato.

<?php
// config/database.php
return [
    'mysql' => [
        'read' => [
            'host' => [env('DB_REPLICA_HOST_1'), env('DB_REPLICA_HOST_2')],
        ],
        'write' => [
            'host' => [env('DB_PRIMARY_HOST')],
        ],
        'sticky'    => true,
        'driver'    => 'mysql',
        'database'  => env('DB_DATABASE'),
        'username'  => env('DB_USERNAME'),
        'password'  => env('DB_PASSWORD'),
        'charset'   => 'utf8mb4',
        'collation' => 'utf8mb4_unicode_ci',
    ],
];
Prova a rispondere a questa domanda con un coach IA

15Come si distribuiscono in sicurezza migrazioni del database e modifiche allo schema nelle applicazioni PHP senza interruzioni o con interruzioni minime?

Usa modifiche di espansione e contrazione che tollerino la vecchia e la nuova versione dell'applicazione durante una distribuzione graduale. Aggiungi prima strutture compatibili, poi distribuisci codice capace di usare entrambe le rappresentazioni. Se serve una doppia scrittura, mantienila coerente e progetta riempimenti dei dati preesistenti riprendibili e a velocità limitata, senza sovrascrivere scritture più recenti. Valida i dati copiati e sposta le letture prima di eliminare i vecchi percorsi di codice. Il DDL (linguaggio di definizione dei dati) online riduce determinati rischi di blocco, ma non è universalmente privo di lock: esamina i blocchi specifici del database e della versione, i vincoli sulle transazioni, l'uso delle risorse e l'impatto sulla replica. CREATE INDEX CONCURRENTLY di PostgreSQL, per esempio, ha restrizioni e può lasciare un indice non valido dopo un errore. Rimuovi le vecchie colonne solo dopo che non esistono più istanze della vecchia applicazione e che è trascorso il periodo richiesto per poter tornare indietro. Dopo modifiche distruttive di contrazione, tornare a codice che necessita dei dati eliminati non è più automaticamente sicuro.

<?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
Prova a rispondere a questa domanda con un coach IA