Les pièges PHP que même les experts évitent de justesse
Typage permissif, comparaisons floues, références résiduelles, erreurs silencieuses : un guide pratique des pièges PHP 8 qui coûtent cher en production, avec les réflexes pour les neutraliser.
Comparaisons lâches et vérité des valeurs
Les comparaisons lâches (`==`) appliquent des conversions surprenantes : `0 == "foo"` est vrai, `"1e3" == 1000` est vrai. Le typage strict (`declare(strict_types=1)`) corrige les appels de fonctions mais pas les comparaisons. Le réflexe : utiliser `===`, `match` plutôt que des cascades de `switch` relâchées, et valider les entrées avec des types précis avant toute comparaison.
declare(strict_types=1);
// Comparaison lâche : 0 == "foo" renvoie true
if ($status == 'active') { /* faux positif possible */ }
// Comparaison stricte + enum
if ($status === Status::Active) { /* sûr */ }Les frontières du typage : coercion et union types
Même en mode strict, PHP convertit les scalaires dans certains contextes (concaténation, opérations numériques) et les union types acceptent des coercitions implicites. Un paramètre `int|string` laisse passer `"42"` sans erreur. La défense : normaliser les entrées à la frontière HTTP (DTO validés), puis travailler avec des types précis à l’intérieur du domaine.
function route(int|string $id): string
{
return "resource/{$id}"; // "42" et 42 arrivent ici
}
// À la frontière, normaliser :
$id = filter_var($rawId, FILTER_VALIDATE_INT) ?: throw new InvalidArgumentException();Erreurs silencieuses et exceptions avalées
Les opérateurs `@`, les `try/catch` vides et les retours `null` « par convention » transforment des erreurs en comportements mystérieux. Un `catch (Throwable)` qui ne fait rien masque aussi les erreurs fatales. Le réflexe : laisser remonter les exceptions, logger avec contexte, et traiter les cas d’erreur comme des chemins de code à tester, pas comme des accidents.
try {
$score = $service->answer($payload);
} catch (QuizClosedException $e) {
// Cas métier connu : réponse explicite
throw new BadRequestHttpException($e->getMessage(), $e);
}
// Pas de catch vide : les erreurs imprévues remontent au gestionnaire globalRéférences résiduelles et fuites en boucle
`foreach ($items as &$value)` laisse une référence sur le dernier élément : le réutiliser ensuite corrompt le tableau. `unset($value)` après la boucle est obligatoire. Plus largement, les closures qui capturent des variables par référence prolongent la vie d’objets et peuvent transformer un cache en fuite. Préférez des itérations immuables et des valeurs copiées.
$items = [1, 2, 3];
foreach ($items as &$value) {
$value *= 2;
}
unset($value); // sans cela, $value référence $items[2]
$items[] = 4; // modifierait silencieusement $items[2] si la référence survivaitÉchappement, injection SQL et XSS
PHP n’échappe rien par défaut : chaque sortie doit être encodée pour son contexte (HTML, attribut, JSON, URL) et chaque requête doit passer par des requêtes préparées. Les erreurs classiques : concaténer dans les requêtes, désactiver `htmlspecialchars` « pour aller plus vite », ou valider côté client seulement. La règle : ne jamais faire confiance à une entrée, même après validation.
// SQL : toujours des requêtes préparées
$stmt = $pdo->prepare('SELECT * FROM quiz WHERE slug = ?');
$stmt->execute([$slug]);
// HTML : encoder la sortie
echo htmlspecialchars($userName, ENT_QUOTES | ENT_SUBSTITUTE, 'UTF-8');OpCache et stratégie de déploiement
En production, OpCache doit être dimensionné (mémoire, nombre de fichiers) et sa politique de revalidation alignée sur le déploiement. Avec `validate_timestamps=1`, chaque déploiement recompile les fichiers modifiés ; avec des timestamps désactivés, il faut purger le cache (`cachetool opcache:reset` ou un endpoint protégé) après chaque release. Un cache saturé recompile tout en continu : surveillez `opcache_get_status()`.
// php.ini (production)
opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000
opcache.validate_timestamps=0
// Purge après déploiement : cachetool opcache:resetEnums, readonly et la tentation du sur-typage
Les enums natives et les propriétés readonly rendent les états explicites, mais leur usage excessif crée du bruit : une enum à un seul cas ou des value objects sans invariants. Utilisez-les quand ils portent une sémantique (états de session, statuts, types de question), pas comme un réflexe esthétique. Le code lisible reste plus maintenable que le code « élégant » illisible.
enum QuizMode: string
{
case Free = 'free';
case Timed = 'timed';
case Exam = 'exam';
}
final class QuizSession
{
public function __construct(
public readonly QuizMode $mode,
public readonly int $userId,
) {}
}Mesurer avant d’optimiser : profiler, ne pas deviner
L’optimisation sans mesure produit des micro-optimisations qui compliquent le code sans effet mesurable. `memory_get_peak_usage()`, un profiler (Blackfire, Xdebug + cachegrind) et les logs de requêtes lentes donnent des faits. Optimisez d’abord l’architecture (requêtes, I/O, cache), puis le code chaud seulement, avec un benchmark avant/après.
$start = hrtime(true);
$result = $service->rebuildDashboard($userId);
$elapsedMs = (hrtime(true) - $start) / 1_000_000;
error_log(sprintf('rebuildDashboard: %.1f ms', $elapsedMs));