15 questions d'entretien PHP backend sélectionnées, groupées par niveau d'ancienneté. Utilisez-les pour réviser les fondamentaux, les compromis pratiques et le raisonnement de production de niveau senior.
1Comment le système de types de PHP distingue-t-il typage faible et typage strict, et que change declare(strict_types=1) ?
PHP autorise normalement les conversions scalaires compatibles pour les types déclarés des paramètres et des retours, par exemple une chaîne numérique passée à un paramètre int. declare(strict_types=1) active des contrôles scalaires stricts : un argument ou une valeur de retour incompatible provoque TypeError, bien qu'un int reste accepté pour float. Pour les arguments des fonctions définies par l'utilisateur, le fichier appelant détermine la rigueur du contrôle ; le fichier définissant la fonction détermine celle des retours. Les appels provenant des fonctions internes de PHP ont leurs propres particularités et ne sont pas simplement régis par le réglage strict_types de l'appelant extérieur. La directive doit apparaître au début de son fichier PHP. Elle ne rend pas PHP statiquement typé et ne désactive pas les conversions effectuées par les opérateurs dans tout le langage.
// 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';
}
?>
2Explique la différence entre == et === en PHP, notamment les pièges liés aux chaînes numériques, aux tableaux, aux objets et à in_array().
En PHP, == effectue une comparaison non stricte, avec conversion de type lorsqu'elle s'applique ; === exige des types et des valeurs identiques sans conversion implicite des scalaires. Les chaînes numériques peuvent être comparées numériquement : évite donc les comparaisons non strictes lorsqu'une correspondance exacte tenant compte du type est nécessaire. Deux tableaux sont égaux avec == s'ils contiennent les mêmes paires clé/valeur selon les règles de comparaison, quel que soit l'ordre des paires. === exige aussi le même ordre et les mêmes types de clés et de valeurs. Pour les objets, == compare les propriétés d'objets de la même classe ; === exige la même instance. in_array est non strict par défaut ; passe true comme troisième argument pour une vérification stricte. Le comportement des comparaisons non strictes entre nombres et chaînes a changé dans PHP 8 : évite de te fonder sur des exemples historiques de conversion sans préciser la version.
3En quoi mixed, void et never diffèrent-ils comme types de retour PHP, et quelles garanties donnent-ils ?
mixed autorise toute valeur de retour, y compris null. void déclare qu'une fonction ne renvoie pas de valeur significative : elle peut atteindre sa fin ou utiliser return;, mais ne peut pas renvoyer une expression. Un appel normal à une fonction void produit null. never, disponible depuis PHP 8.1, signifie que la fonction ne rend jamais normalement la main à son appelant. Elle peut lever une exception, terminer l'exécution avec exit ou rester dans une boucle infinie ; elle n'est pas obligée de finir par terminer le processus. Une fonction never ne peut contenir aucune instruction return. La différence essentielle est qu'une fonction void peut se terminer normalement et laisser l'appelant continuer, tandis qu'une fonction never n'a pas de continuation 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';
4Qu'est-ce que iterable en PHP et quel est son lien avec array, Traversable, Iterator et Generator ?
iterable signifie array|Traversable. Generator implémente Iterator, qui étend Traversable : il satisfait donc iterable. Les classes définies par l'utilisateur offrent un comportement Traversable par Iterator ou IteratorAggregate plutôt qu'en implémentant directement Traversable dans une classe concrète. Iterator expose les méthodes qui contrôlent l'itération ; IteratorAggregate fournit un itérateur. Une fonction génératrice contenant yield renvoie un Generator. Une distinction est importante : foreach peut aussi parcourir les propriétés visibles d'un objet ordinaire qui n'implémente pas Traversable, mais cela ne lui permet pas pour autant de satisfaire une déclaration de paramètre 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());
5Quelles sont les règles de visibilité public, protected et private en PHP, et comment interagissent-elles avec l'héritage et les traits ?
Les membres public sont accessibles partout où l'objet ou la classe est accessible. Les membres protected sont accessibles dans la classe qui les déclare et dans sa hiérarchie liée de classes parentes et enfants, mais pas depuis du code externe quelconque. Les membres private sont accessibles dans leur classe déclarante ; une sous-classe ne peut pas utiliser un membre privé du parent comme une interface héritée. Une méthode ordinaire qui en redéfinit une autre ne peut pas réduire la visibilité héritée. Les constructeurs ont toutefois des règles d'héritage particulières et ne doivent pas être présentés comme un exemple inconditionnel de cette règle. Les membres d'un trait sont incorporés à la classe utilisatrice et suivent ses règles de visibilité. L'adaptation as peut créer un alias, modifier la visibilité d'une méthode du trait, ou les deux ; l'ajout d'un alias ne supprime pas à lui seul le nom d'origine.
<?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
6Comment fonctionnent les traits en PHP, et comment résoudre les conflits lorsque plusieurs traits définissent la même méthode ?
Les traits permettent la réutilisation horizontale du code : une classe importe leurs membres avec use, et un trait ne peut pas être instancié seul. Une méthode déclarée dans la classe prévaut sur les méthodes des traits, qui prévalent à leur tour sur les méthodes héritées du parent. Si plusieurs traits fournissent la même méthode et qu'une méthode de la classe ne résout pas déjà le conflit, utilise insteadof pour choisir l'implémentation. Utilise as pour donner un nom supplémentaire à une méthode de trait et, éventuellement, une visibilité différente. Un alias ne renomme ni ne supprime la méthode d'origine. Résoudre les collisions et garder une interface publique claire sont deux responsabilités distinctes.
<?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
7Que sont les énumérations PHP (PHP enums), y compris les énumérations pures (pure enums) et adossées (backed enums), et comment améliorent-elles la modélisation de domaine ?
Les énumérations PHP (enums) définissent un ensemble fermé de cas nommés pour un type, tels que les états de commande ou les rôles d'utilisateur. Les énumérations pures (pure enums) n'ont que des cas nommés ; les énumérations adossées (backed enums) attribuent à chaque cas une valeur unique de type chaîne de caractères ou entier, utile pour le stockage, les API (Application Programming Interface) ou l'interopérabilité. Les énumérations améliorent la modélisation de domaine en remplaçant les chaînes de caractères brutes, les entiers ou les constantes fragiles par des valeurs typées et auto-documentées. Les énumérations PHP peuvent également avoir des méthodes et implémenter des interfaces ; toutes les énumérations prennent en charge la liste de leurs cas, et les énumérations adossées prennent en charge la conversion vers et depuis leurs valeurs scalaires.
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);
8Comment les types scalaires, nullables, union, intersection et DNF expriment-ils les contrats des interfaces PHP, et quand utiliser chacun d'eux ?
Les déclarations PHP décrivent les valeurs acceptées et renvoyées. Les déclarations scalaires comme int, float, string et bool sélectionnent une catégorie de valeurs ; l'autorisation de conversions scalaires compatibles dépend également de strict_types et du contexte. ?T ou T|null admet null. Les unions, disponibles depuis PHP 8.0, permettent des alternatives comme int|string. Les intersections pures, disponibles depuis PHP 8.1, imposent qu'un objet satisfasse tous les types de classes ou d'interfaces indiqués, comme Cacheable&JsonSerializable ; int&string n'est pas une intersection valide. Les types DNF (forme normale disjonctive), disponibles depuis PHP 8.2, combinent des alternatives et des intersections entre parenthèses, par exemple (Cacheable&JsonSerializable)|string. Choisis le contrat compréhensible le plus précis qui représente les entrées valides, sans ajouter d'alternatives inutiles.
<?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
}
9Quelle est la différence entre callable, Closure, les callables de première classe, les objets invocables et les tableaux callable en PHP ?
callable décrit des valeurs invocables, notamment les chaînes contenant des noms de fonctions, les objets Closure, les références à des méthodes accessibles et les objets dotés de __invoke. Closure est un objet invocable concret : il peut provenir d'une fonction anonyme ou d'une fonction ou méthode nommée convertie en Closure. La syntaxe des callables de première classe, comme strlen(...) ou $object->method(...), crée une Closure sans invoquer la cible. Un tableau callable [$object, 'method'] fait référence à une méthode d'instance accessible ; [ClassName::class, 'method'] nécessite une méthode statique appropriée et accessible dans les versions modernes de PHP. is_callable vérifie la possibilité d'appel dans la portée concernée. callable est un contrat large, tandis que Closure est une représentation spécifique.
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;
}
10Comment PHP gère-t-il le dépassement d'entier, la précision des nombres à virgule flottante et les calculs décimaux exacts ?
Les entiers PHP ont une taille dépendante de la plateforme, avec des limites exposées par des constantes telles que `PHP_INT_MAX`. PHP ne conserve pas automatiquement les résultats d'entiers natifs comme des entiers à précision arbitraire après un dépassement ; l'arithmétique d'entiers en cas de dépassement peut devenir un `float` ou perdre autrement sa sémantique d'entier exact. Les nombres à virgule flottante de PHP sont des valeurs binaires, de sorte que de nombreuses fractions décimales, comme `0.1`, sont approximatives. Pour les calculs décimaux exacts ou monétaires, évitez les nombres à virgule flottante et utilisez une approche exacte telle que des unités mineures entières lorsque c'est approprié, ou des outils décimaux à précision fixe/arbitraire comme BCMath. GMP est utile pour l'arithmétique d'entiers à précision arbitraire.
11Expliquez les références PHP (`&`), le mécanisme de copie à l'écriture (*copy-on-write*) et comment les références peuvent créer des effets secondaires inattendus.
Les variables PHP sont représentées en interne par des `zvals`. Pour des valeurs comme les tableaux et les chaînes de caractères, PHP utilise la copie à l'écriture (*copy-on-write*): l'assignation `$b = $a` partage généralement la même valeur sous-jacente jusqu'à ce que l'une des variables soit modifiée, moment auquel PHP la sépare/copie. Une référence créée avec `&` fait des variables des alias du même conteneur, de sorte que la modification de l'une modifie l'autre et peut contourner le comportement de séparation que l'on attend d'une assignation normale. Le passage par référence permet à une fonction de modifier la variable de l'appelant. Les références peuvent provoquer des effets secondaires surprenants, en particulier avec `foreach ($array as &$value)`: la variable de boucle reste une référence au dernier élément après la boucle, donc sa réutilisation ultérieure peut accidentellement écraser cet élément à moins que vous ne fassiez `unset($value)`.
12Comment fonctionnent les références d'objet, l'affectation, le clonage, `__clone`, la copie superficielle et la copie profonde en PHP ?
En PHP, une variable d'objet stocke une référence vers un objet, et non une copie complète de l'objet. L'affectation `$b = $a` copie la référence, de sorte que les deux variables font référence au même objet ; toute modification de l'objet via l'une ou l'autre variable est visible à travers l'autre, sans utiliser `&`. `clone $a` crée une nouvelle instance d'objet avec une copie superficielle des propriétés de l'objet original. Si une propriété est elle-même un objet, la propriété de l'objet cloné pointe toujours vers le même objet imbriqué, à moins que vous ne le cloniez explicitement. `__clone()` est appelée sur le nouvel objet après la copie superficielle et est l'endroit idéal pour réinitialiser les identifiants, détacher les ressources ou effectuer une copie profonde des objets imbriqués. Une copie profonde signifie la copie récursive des objets mutables imbriqués qui ne devraient pas être partagés.
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;
13Comment dépanneriez-vous l'épuisement des connexions à la base de données et concevriez-vous la gestion des connexions pour PHP-FPM et les workers de longue durée ?
Pour dépanner l'épuisement des connexions, je comparerais les limites de connexion à la base de données et les sessions actuelles avec la concurrence PHP totale : chaque processus enfant PHP-FPM actif peut maintenir sa propre connexion à la base de données, et plusieurs pools d'applications, hôtes, tâches cron, workers de file d'attente, migrations et déploiements multiplient la demande. J'inspecterais les vues de session de la base de données, les états de connexion, les sessions inactives en transaction, les requêtes lentes, les attentes de verrouillage, les journaux d'erreurs, les métriques de pool, et les changements récents de trafic ou de nombre de workers. Pour PHP-FPM, maintenez `max_children` et la concurrence par hôte dans les limites du budget de la base de données, utilisez des transactions/connexions de courte durée et limitées à la requête, effectuez des commits ou des rollbacks rapidement, et évitez de maintenir des connexions pendant des appels externes lents. Les connexions PDO persistantes peuvent réduire la surcharge de connexion mais peuvent maintenir de nombreuses connexions inactives par processus FPM et conserver l'état de la session, elles nécessitent donc de la prudence. Un pooler ou un proxy tel que PgBouncer peut multiplexer de nombreux clients PHP sur moins de connexions au serveur de base de données lorsque l'application est compatible. Les workers de longue durée nécessitent une gestion explicite du cycle de vie des connexions : vérifications de santé, reconnexion après des délais d'inactivité/redémarrages/basculement/forks, réinitialisation de l'état de la session, et ne réessayer que le travail sûr ou idempotent.
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.
14Comment gérerais-tu les réplicas de lecture, la séparation lectures/écritures, le retard de réplication et le basculement de la base principale en PHP ?
Sépare les connexions à la base principale et aux réplicas selon la sémantique des opérations, et pas seulement selon que le SQL commence par SELECT : les lectures verrouillantes, les écritures et les transactions exigeant la cohérence de la base principale doivent l'utiliser. Les transactions en lecture seule peuvent utiliser un réplica adapté si leurs exigences de cohérence le permettent. La réplication asynchrone peut être en retard. Le réglage sticky de Laravel dirige les lectures suivant une écriture vers la connexion d'écriture pendant la requête courante ; maintenir cette affinité pendant une durée fixe entre requêtes atténue seulement le problème, sans garantir inconditionnellement la lecture de ses propres écritures. Pour obtenir cette garantie, lis sur la base principale ou attends qu'un réplica ait appliqué la position d'écriture requise, avec un délai maximal et un repli sur la principale. Les proxys diffèrent : RDS Proxy utilise des points de connexion distincts pour lecteurs et écrivains et n'est pas un répartiteur automatique général de chaque instruction entre lecture et écriture. Lors d'un basculement, abandonne les connexions périmées et reconnecte-toi avec une temporisation progressive bornée. La perte d'une réponse de validation laisse le résultat incertain : ne rejoue pas aveuglément des écritures non idempotentes ; utilise des clés d'idempotence ou vérifie d'abord le résultat.
15Comment déployer en sécurité des migrations de base de données et des changements de schéma dans une application PHP avec une interruption nulle ou minimale ?
Utilise des changements d'expansion puis de contraction qui tolèrent les anciennes et les nouvelles versions de l'application pendant un déploiement progressif. Ajoute d'abord des structures compatibles, puis déploie du code capable d'utiliser les deux représentations. Si une double écriture est nécessaire, assure sa cohérence et conçois des reprises des données historiques à débit limité et reprenables, sans écraser les écritures plus récentes. Valide les données copiées et bascule les lectures avant de supprimer les anciens chemins de code. Le DDL (langage de définition de données) en ligne réduit certains risques de blocage, mais n'est pas universellement sans verrous : examine les verrous propres à la base et à sa version, les restrictions transactionnelles, la consommation de ressources et l'effet sur la réplication. Par exemple, CREATE INDEX CONCURRENTLY dans PostgreSQL a des restrictions et peut laisser un index invalide après un échec. Ne supprime les anciennes colonnes qu'après disparition des anciennes instances applicatives et expiration de la période requise de retour arrière. Après une contraction destructive, revenir à du code nécessitant les données supprimées n'est plus automatiquement sûr.
<?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