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 le typage faible et strict, et quels changements surviennent lorsque `declare(strict_types=1)` est activé ?
PHP est faible/coercitif par défaut pour les déclarations de types scalaires : lorsqu'une fonction déclare des types de paramètres scalaires ou de retour, PHP peut convertir des valeurs compatibles comme les chaînes numériques en nombres au lieu de les rejeter. `declare(strict_types=1)` rend la vérification des types scalaires stricte pour ce fichier, de sorte que les valeurs scalaires incompatibles ne sont pas forcées (coerced) et une `TypeError` est levée. La directive est limitée au fichier (file-scoped) : pour les arguments de fonctions définies par l'utilisateur, le fichier de l'appelant contrôle la stricte ; pour les instructions de retour, le fichier où la fonction est définie contrôle la stricte de la valeur de retour. Cela ne rend pas PHP globalement strict et ne désactive pas toutes les conversions dans le langage.
<?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
2Expliquez la différence entre `==` et `===` en PHP, y compris les pièges avec les chaînes numériques, les tableaux, les objets et `in_array()`.
`==` effectue une comparaison lâche, donc PHP peut convertir les types avant de comparer ; `===` effectue une comparaison stricte, exigeant à la fois la même valeur et le même type sans aucune coercition. Les chaînes numériques sont un piège courant car elles peuvent être traitées comme des nombres lors de comparaisons lâches. Les tableaux et les objets ont leurs propres règles de comparaison : les tableaux sont comparés par clés et valeurs, tandis que `==` pour les objets compare les objets de même classe par leurs propriétés et `===` pour les objets exige la même instance exacte. `in_array()` utilise par défaut une comparaison lâche, il faut donc passer `true` comme troisième argument lorsque le type doit correspondre.
3En quoi `mixed`, `void` et `never` diffèrent-ils en tant que types de retour PHP, et quelles garanties fournissent-ils ?
En tant que types de retour, `mixed`, `void` et `never` offrent des garanties différentes. `mixed` signifie que la fonction peut retourner n'importe quelle valeur, y compris `null`. `void` signifie que la fonction n'est pas destinée à retourner une valeur significative ; elle peut utiliser `return;` mais ne peut pas retourner une expression. `never` signifie que la fonction ne retourne pas normalement à son appelant : elle doit toujours lancer une exception, quitter ou autrement terminer le flux de contrôle. `never` est plus fort que `void` car l'exécution ne peut pas continuer après un appel normal à une fonction de type `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';
4Qu'est-ce qu'un `iterable` en PHP, et comment est-il lié à `array`, `Traversable`, `Iterator` et `Generator` ?
En PHP, `iterable` est une déclaration de type signifiant « un `array` ou un objet implémentant `Traversable` ». `Traversable` est l'interface marqueur de base pour les objets utilisables par `foreach` ; le code utilisateur implémente normalement `Iterator` ou `IteratorAggregate`, qui étendent `Traversable`. Un `Generator` est l'objet renvoyé par une fonction génératrice utilisant `yield` ; il implémente `Iterator`, il est donc également `Traversable` et accepté partout où un `iterable` est requis.
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 ?
En PHP, les membres `public` sont accessibles de partout, les membres `protected` sont accessibles depuis la classe déclarante et ses sous-classes, et les membres `private` ne sont accessibles que depuis la classe qui les déclare. Dans l'héritage, les sous-classes peuvent utiliser les membres `public` et `protected` hérités, mais les membres `private` d'un parent ne sont pas accessibles en tant qu'API héritée. Lors de la surcharge des méthodes, une classe enfant ne peut généralement pas réduire la visibilité ; par exemple, une méthode `public` du parent doit rester `public`. Les traits sont intégrés dans la classe utilisatrice, et les membres de trait suivent les règles de visibilité de PHP. La visibilité des méthodes de trait peut également être adaptée ou renommée avec `as` lors de l'importation du 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
6Comment les traits fonctionnent-ils en PHP, et comment les conflits sont-ils résolus lorsque plusieurs traits définissent la même méthode ?
Les traits sont le mécanisme de PHP pour la réutilisation horizontale du code : une classe peut utiliser un ou plusieurs traits, et les membres du trait sont incorporés dans la classe. Les traits ne sont pas des classes autonomes et ne peuvent pas être instanciés directement. Si plusieurs traits fournissent des méthodes avec le même nom, PHP exige une résolution explicite des conflits. L'opérateur `insteadof` choisit quelle méthode de trait est utilisée pour le nom en conflit. L'opérateur `as` peut créer un alias pour une méthode de trait et peut également modifier la visibilité de la méthode importée. Les méthodes de classe priment sur les méthodes de trait, et les méthodes de trait priment sur les méthodes héritées du parent.
<?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 d'API (Application Programming Interface) PHP, et quand chacun doit-il être utilisé ?
Les déclarations de type PHP décrivent les valeurs qu'une API accepte ou renvoie. Les types scalaires tels que `int`, `float`, `string` et `bool` sont utilisés lorsqu'un seul type primitif est attendu. Les types nullables, écrits `?T` ou `T|null`, autorisent soit une valeur de type `T` soit `null`. Les types union comme `A|B` autorisent l'une de plusieurs alternatives. Les types intersection comme `A&B` exigent qu'une valeur satisfasse tous les types listés, communément plusieurs interfaces. Les types DNF (Disjunctive Normal Form) combinent les unions et les intersections, par exemple `(A&B)|C`, pour exprimer des contrats plus complexes. Chacun doit être utilisé lorsqu'il correspond à la promesse réelle de l'API, en évitant les types inutilement larges ou déroutants.
<?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 appelables (callable arrays) en PHP ?
`callable` est un type/contrat large pour toute valeur que PHP peut invoquer, telle qu'une chaîne de caractères représentant un nom de fonction, une `Closure`, un objet invocable, ou un tableau de référence de méthode comme `[$object, 'method']` ou `[ClassName::class, 'method']`. Une `Closure` est un objet concret de fonction anonyme et est elle-même appelable. La syntaxe des callables de première classe, telle que `$obj->method(...)` ou `strlen(...)`, crée une `Closure` à partir d'une expression appelable. Un objet invocable est appelable car il définit `__invoke()`. Les tableaux appelables (callable arrays) sont la forme traditionnelle de référence de méthode à deux éléments. `is_callable()` peut être utilisé pour vérifier l'appelabilité (callability) à l'exécution.
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.
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.
14Comment géreriez-vous les répliques de lecture, la séparation lecture/écriture, le délai de réplication et le basculement de la base de données primaire en PHP?
La gestion des répliques de lecture et du basculement en PHP nécessite de séparer le trafic de lecture et d'écriture, d'atténuer le délai de réplication et de gérer les interruptions de connexion lors d'un basculement. Au niveau de l'application, la configuration de la base de données ou un middleware achemine les requêtes de lecture (`SELECT`) vers les répliques de lecture et les requêtes d'écriture (`INSERT`, `UPDATE`, `DELETE`) vers l'instance primaire (alternativement géré via des proxies de base de données comme ProxySQL ou AWS RDS Proxy).
Étant donné que la réplication est asynchrone, le délai de réplication peut entraîner des lectures obsolètes. Pour garantir la cohérence 'lire ses propres écritures', l'application doit acheminer les lectures au sein d'une transaction active vers le primaire, et temporairement forcer les lectures suivantes vers le primaire pour une session utilisateur spécifique ou un cycle de vie de requête immédiatement après une écriture.
Lors d'un basculement du primaire, les clusters de bases de données modernes promeuvent une réplique et mettent à jour les points d'accès DNS (Domain Name System)/proxy. En PHP, la couche de connexion doit intercepter les pertes de connexion et les erreurs de lecture seule, fermer les sockets PDO (PHP Data Objects) obsolètes et appliquer des tentatives contrôlées avec un backoff exponentiel.
15Comment les migrations de base de données et les modifications de schéma sont-elles déployées en toute sécurité avec un temps d'arrêt nul ou minimal dans les applications PHP ?
Le déploiement de migrations de base de données sans temps d'arrêt dans les applications PHP nécessite de découpler l'évolution du schéma des déploiements de code d'application en utilisant le modèle Étendre et Contracter (Expand and Contract - exécution parallèle) conjointement avec des pratiques DDL (Data Definition Language - langage de définition de données) en ligne non bloquantes. Dans la phase d'extension (Expand), des modifications de schéma rétrocompatibles sont appliquées (par exemple, l'ajout de colonnes nullables, de nouvelles tables, ou d'index concurrents à l'aide d'outils comme `pt-online-schema-change`, `gh-ost`, ou les DDL en ligne natifs dans MySQL/PostgreSQL). L'application est ensuite déployée pour prendre en charge la double écriture (écriture dans les anciennes et nouvelles structures de schéma) tout en lisant depuis l'une ou l'autre. Les enregistrements historiques sont ensuite remplis en arrière-plan de manière asynchrone par lots limités pour éviter le verrouillage des tables ou l'induction d'un délai de réplication.
Dans la phase de contraction (Contract), une fois la cohérence des données validée et les lectures complètement basculées vers la nouvelle structure, un déploiement de code supprime les références à l'ancien schéma. Enfin, une migration de nettoyage supprime les colonnes ou tables obsolètes. Un retour arrière sûr exige que chaque état de schéma intermédiaire reste entièrement compatible avec les versions d'application précédentes et à venir pendant les déploiements progressifs.
<?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