PHP Avancé

Maîtriser les Internals PHP : Performance, Mémoire et Optimisation Avancée

Plongez dans les mécanismes internes de PHP pour déboguer efficacement, optimiser la performance et gérer la mémoire comme un expert. Ce cours dévoile les secrets du moteur PHP et comment les exploiter pour créer des applications ultra-performantes.

Preparetoi.academy 30 min
{
  "cours": {
    "metadata": {
      "titre": "Maîtriser les Internals PHP : Performance, Mémoire et Optimisation Avancée",
      "description": "Plongez dans les mécanismes internes de PHP pour déboguer efficacement, optimiser la performance et gérer la mémoire comme un expert. Ce cours dévoile les secrets du moteur PHP et comment les exploiter pour créer des applications ultra-performantes.",
      "technologie": "PHP",
      "categorie": "Développement Web",
      "domaine": "Développement & Programmation",
      "niveau": "Avancé",
      "langue": "Français",
      "type": "Mixte",
      "duree": "30 minutes",
      "nombreSections": 5
    },
    "sections": [
      {
        "numero": 1,
        "titre": "Comprendre le Modèle de Mémoire PHP et la Gestion des Zvals",
        "contenu": "## Comprendre le Modèle de Mémoire PHP et la Gestion des Zvals\n\n### Définition\nLe modèle de mémoire PHP repose sur les **zvals** (zend values), structures internes qui encapsulent chaque variable PHP avec des métadonnées incluant le type, la valeur et le compteur de références. Cette architecture en deux tiers permet à PHP de gérer dynamiquement les types tout en optimisant l'allocation mémoire.\n\n### Explication Détaillée\nChaque variable PHP est représentée en interne par une structure zval contenant :\n- **Type** : Le type de la valeur (int, string, array, object, etc.)\n- **Valeur** : Les données réelles ou un pointeur vers celles-ci\n- **Refcount** : Compteur de références (copy-on-write)\n- **Is_ref** : Booléen indiquant si c'est une référence\n\nLe système copy-on-write (CoW) est crucial : tant que plusieurs variables partagent la même valeur, elles pointent vers le même bloc mémoire. À la modification, une copie est effectuée. C'est pourquoi assigner une variable à une autre ne double pas la mémoire utilisée, mais crée simplement une référence supplémentaire.\n\nAvec l'arrivée de PHP 7, le modèle a été radicalement optimisé. Au lieu de stocker les zvals sur le heap, ils sont maintenant stockés directement dans les structures de données (locals, arrays), réduisant drastiquement l'usage mémoire et les allocations.\n\n### Bloc Code\n```php\n<?php\n// Demonstration du copy-on-write\n$a = \"Hello World\"; // Allocation: 1 zval pour la string\n$b = $a;           // Pas d'allocation supplémentaire, refcount = 2\n$c = $a;           // Pas d'allocation, refcount = 3\n\n// Vérifier les références avec xdebug ou debug_zval_dump (anciennes versions)\nvar_dump($a, $b, $c);\n\n// À la modification, copy-on-write se déclenche\n$b = \"Modified\";   // Nouvelle allocation pour $b, refcount($a) = 2\n\n// Avec les arrays (plus complexe)\n$arr1 = ['key' => 'value'];\n$arr2 = $arr1;     // Shared, refcount = 2\n$arr2['key'] = 'newvalue'; // CoW: duplication implicite\n\n// Interning de strings (optimization)\n$str1 = \"debug\";\n$str2 = \"debug\";   // Même adresse mémoire (interning)\n\n// Objets: TOUJOURS passés par référence\nclass Foo {}\n$obj1 = new Foo();\n$obj2 = $obj1;     // Pas de copie, $obj2 pointe vers le même objet\n$obj2->prop = 'changed'; // Affecte aussi $obj1\n\n// Utiliser memory_get_usage() pour tracker\necho memory_get_usage(true) . \" bytes\\n\";\n\n// Génération et nettoyage\n$big_array = range(1, 1000000);\necho memory_get_usage(true) . \" bytes\\n\";\nunset($big_array);\necho memory_get_usage(true) . \" bytes\\n\";\n?>\n```\n\n### Tableau Comparatif\n| Aspect | PHP 5.x | PHP 7.x+ | Impact |\n|--------|---------|----------|--------|\n| **Stockage zval** | Heap (pointeur) | Stack (inlined) | -50% mémoire |\n| **Copy-on-write** | Oui, avec refcount | Oui, optimisé | Transparence accrue |\n| **Variables locales** | Allocation dynamique | Array fixe | Plus rapide |\n| **Objets** | Références | Handle (ID interne) | Sécurité améliorée |\n| **Interning strings** | Limité | Agressif | Moins d'allocations |\n\n### Astuce d'Expert\nUtilisez `memory_get_usage(true)` plutôt que `memory_get_usage()` pour obtenir la mémoire allouée au système (en blocs), pas juste la mémoire utilisée. Différence critique en production : `false` vous donne l'usage réel, `true` vous donne ce que le système a alloué au processus PHP.\n\n### Attention ⚠️\nNe pas confondre le refcount avec les vraies références (`&`). Les références forcent `is_ref = true` et déclenchent des optimisations différentes. Assigner par référence puis supprimer une variable ne désalloue pas la mémoire si d'autres références existent : `unset($a)` décrémente le refcount mais ne libère que si refcount atteint 0.\n\n### Contenu Supplémentaire (Internals)\nEn C (Zend Engine), chaque zval est :\n```c\ntypedef struct {\n    zend_value value;\n    zend_refcounted_h gc;\n    union { zend_uchar type; ... } u1;\n} zval;\n```\nLe champ `gc` gère le garbage collector : les cycles de références (ex: array contenant une référence à lui-même) sont détectés et nettoyés périodiquement.",
        "stats": {
          "mots": 487,
          "elements": ["Definition", "Explication", "BlocCode", "Tableau", "Astuce", "Attention"]
        }
      },
      {
        "numero": 2,
        "titre": "Optimisation des Structures de Données : Arrays vs Objects vs Generators",
        "contenu": "## Optimisation des Structures de Données : Arrays vs Objects vs Generators\n\n### Définition\nL'optimisation des structures de données en PHP consiste à choisir et configurer les conteneurs de données (arrays, objets, generators) en fonction du cas d'usage pour minimiser la mémoire et maximiser la vitesse d'accès. Chaque structure a des caractéristiques de performance très différentes au niveau des internals.\n\n### Explication Détaillée\nLes **arrays PHP** sont techniquement des hash tables ordonnées avec un petit tableau indexé pour les clés numériques consécutives. Accéder à `$arr[0]` sur un array `[0=>val]` est O(1) ultra-rapide (direct array access). Accéder à `$arr[\"key\"]` est O(1) moyen via hachage. Cependant, les arrays avec clés mixtes (numériques et strings) ou non consécutives dégradent les performances.\n\nLes **objets** stockent les propriétés dans une hash table (property_table). L'accès est légèrement plus lent qu'un array indexé numériquement, mais le bénéfice est l'optimisation de classe (property caching en PHP 7.4+). Les objets avec `__get/__set` magiques sont beaucoup plus lents car ils déclenchent un appel de fonction.\n\nLes **generators** avec `yield` ne créent pas d'array intermédiaire en mémoire : ils produisent une valeur à la fois. Critique pour traiter des millions d'enregistrements sans saturer la RAM. Un generator coûte très peu en mémoire mais plus en appels de fonction.\n\nCompromis : un array de 1M items coûte ~50MB, un generator zéro si c'est du streaming.\n\n### Bloc Code\n```php\n<?php\n// 1. Arrays indexés vs associatifs\n$numeric = array_fill(0, 10000, 'value');\n$assoc = array_fill_keys(range('a', 'j'), 'value');\n\n// Accès numérique: ultra-rapide\necho $numeric[5000]; // Direct array offset\necho $assoc['f'];    // Hash table lookup\n\n// 2. Optimisation: utiliser array_column pour filtrer\n$data = [\n    ['id' => 1, 'name' => 'Alice', 'score' => 95],\n    ['id' => 2, 'name' => 'Bob', 'score' => 87],\n    ['id' => 3, 'name' => 'Charlie', 'score' => 92],\n];\n\n// LENT: boucle + array_push\n$names = [];\nforeach ($data as $row) {\n    $names[] = $row['name'];\n}\n\n// RAPIDE: array_column (internals optimisés)\n$names = array_column($data, 'name');\n\n// 3. Objects avec property caching (PHP 7.4+)\nclass User {\n    public string $name = 'default';\n    public int $age = 0;\n}\n\n$user = new User();\n$user->name = 'Alice'; // Rapide: property cache\necho $user->name;\n\n// 4. Comparison: array vs object en performance\n$benchmark_array = [\n    'name' => 'Bob',\n    'email' => 'bob@example.com',\n    'age' => 30\n];\n\nclass UserObj {\n    public string $name;\n    public string $email;\n    public int $age;\n    \n    public function __construct($name, $email, $age) {\n        $this->name = $name;\n        $this->email = $email;\n        $this->age = $age;\n    }\n}\n\n$benchmark_object = new UserObj('Bob', 'bob@example.com', 30);\n\n// Dans les boucles très serrées, l'array est ~5-10% plus rapide\nfor ($i = 0; $i < 100000; $i++) {\n    $x = $benchmark_array['name'];\n}\n\n// 5. Generators: economie massive de mémoire\nfunction generate_large_dataset() {\n    for ($i = 0; $i < 1000000; $i++) {\n        yield [\n            'id' => $i,\n            'data' => md5($i),\n            'value' => rand(0, 100)\n        ];\n    }\n}\n\n// Approche 1: MAUVAISE - charge tout en mémoire\n$all_data = [];\nfor ($i = 0; $i < 1000000; $i++) {\n    $all_data[] = ['id' => $i, 'data' => md5($i), 'value' => rand(0, 100)];\n}\n// Consomme ~300MB+\n\n// Approche 2: BONNE - generator\nforeach (generate_large_dataset() as $record) {\n    // Traiter $record\n    // Mémoire: < 1MB (juste un record à la fois)\n}\n\n// 6. SPL Data Structures (moins connus mais optimisés)\n$stack = new SplStack();\n$stack->push('item1');\n$stack->push('item2');\n// Plus rapide qu'array pour stacks/queues\n\n$heap = new SplMinHeap();\n$heap->insert(3);\n$heap->insert(1);\n$heap->insert(2);\n// Heap tree: O(log n) pour insertion\n?>\n```\n\n### Tableau Comparatif de Performance\n| Structure | Accès | Insertion | Supression | Mémoire | Cas d'Usage |\n|-----------|-------|-----------|-----------|---------|-------------|\n| **Array indexé** | O(1) très rapide | O(n) | O(n) | Modérée | Listes simples |\n| **Array assoc** | O(1) rapide | O(1) moyen | O(1) | Élevée | Clé-valeur |\n| **Object** | O(1) rapide | O(1) | O(1) | Élevée | Entités métier |\n| **Generator** | N/A | N/A | N/A | Très faible | Gros volumes |\n| **SplFixedArray** | O(1) ultra-rapide | Impossible | Impossible | Très faible | Données immutables |\n\n### Astuce d'Expert\nPour du traitement de gros fichiers CSV ou APIs : préférez les generators avec `yield`. Exemple réel :\n\n```php\nfunction read_csv_lazy($filepath) {\n    $handle = fopen($filepath, 'r');\n    $headers = fgetcsv($handle);\n    while (($row = fgetcsv($handle)) !== false) {\n        yield array_combine($headers, $row);\n    }\n    fclose($handle);\n}\n```\n\nCe pattern charge juste une ligne à la fois, peu importe si le CSV fait 1MB ou 1GB.\n\n### Attention ⚠️\nLes generators ne peuvent pas être réutilisés : une fois parcouris, ils sont épuisés. Si vous avez besoin de plusieurs passes, renvoyez un generator à chaque fois ou utilisez un itérateur custom avec `Iterator` interface. De plus, les generators n'ont pas d'accès aléatoire : pas de `$generator[50]`, seulement une itération séquentielle.\n\n### Point Avancé: Hashing interne\nLes clés string dans les arrays sont hachées via un hash murmur. Les collisions sont rares mais possibles. Pour les clés très longues (> 256 caractères), PHP stocke un hash au lieu de la clé complète, économisant mémoire mais rendant les clés invisibles aux debuggeurs externes.",
        "stats": {
          "mots": 523,
          "elements": ["Definition", "Explication", "BlocCode", "Tableau", "Astuce", "Attention"]
        }
      },
      {
        "numero": 3,
        "titre": "Profiling et Debugging Avancé avec Xdebug, Blackfire et Opcache",
        "contenu": "## Profiling et Debugging Avancé avec Xdebug, Blackfire et Opcache\n\n### Définition\nLe profiling avancé en PHP consiste à analyser en temps réel (ou post-mortem) l'exécution du code pour identifier les goulots d'étranglement, fuites mémoire et inefficacités. Les outils comme **Xdebug**, **Blackfire** et **Opcache** offrent des insights à différents niveaux : execution flow, CPU/mémoire et compilation du bytecode.\n\n### Explication Détaillée\n**Xdebug** est l'extension PHP la plus complète pour le debugging. Elle offre :\n- **Pas à pas** (step into/over/out) avec breakpoints\n- **Profiling** : génère des traces cachegrind (visualisables avec kcachegrind)\n- **Inspection de variables** en temps réel\n- **Stack traces** détaillées\n\n**Blackfire** est un profiler SaaS/on-premise créé par Symfony. Il offre :\n- Profiling sans surcharge significative (faible overhead)\n- Comparaison entre deux exécutions (diff)\n- Identification graphique des appels coûteux\n- Intégration IDE\n\n**Opcache** compile le code PHP en bytecode et le cache. Sans opcache, chaque requête réinterprète le code. Avec opcache, le bytecode est compilé une fois et réutilisé, d'où ~3-10x plus rapide. Mais opcache cache aussi les erreurs de logique, d'où le besoin de bien vérifier les configurations.\n\nLe combo optimal : Xdebug en dev, Blackfire en staging, Opcache en prod.\n\n### Bloc Code\n```php\n<?php\n// 1. Configuration Xdebug pour profiling\n// php.ini:\n// xdebug.mode=profile\n// xdebug.profiler_output_dir=/tmp/xdebug\n// xdebug.trigger_value=secret_key\n\n// Pour déclencher le profiler:\n// http://localhost/app.php?XDEBUG_PROFILE=secret_key\n\n// 2. Debugging de mémoire avec Xdebug\nfunction memory_leak_example() {\n    static $cache = [];\n    $cache[] = str_repeat('x', 1000000); // 1MB chaque appel\n    return count($cache);\n}\n\nfor ($i = 0; $i < 100; $i++) {\n    memory_leak_example();\n}\n// Xdebug montrera la croissance mémoire à chaque itération\n\n// 3. Profiling avec Blackfire (si installé)\n// \\Blackfire\\Probe::initialize();\n// // ... code to profile\n// Les données sont envoyées à Blackfire\n\n// 4. Utilisation d'Opcache et inspection\necho \"Opcache loaded: \" . extension_loaded('Zend OPcache') ? 'yes' : 'no';\necho \"\\n\";\n\nif (extension_loaded('Zend OPcache')) {\n    $status = opcache_get_status();\n    echo \"Opcache memory used: \" . $status['memory_usage']['used_memory'] . \" bytes\\n\";\n    echo \"Number of scripts cached: \" . $status['opcache_statistics']['num_cached_scripts'] . \"\\n\";\n    \n    // Invalider un script du cache\n    opcache_invalidate('/path/to/file.php', true);\n}\n\n// 5. Profiling Manuel: mesurer les performances\nclass PerformanceProfiler {\n    private static $timers = [];\n    private static $memory_markers = [];\n    \n    public static function start($name) {\n        self::$timers[$name] = microtime(true);\n        self::$memory_markers[$name] = memory_get_usage(true);\n    }\n    \n    public static function stop($name) {\n        if (!isset(self::$timers[$name])) {\n            throw new Exception(\"Timer $name not started\");\n        }\n        \n        $duration = microtime(true) - self::$timers[$name];\n        $memory = memory_get_usage(true) - self::$memory_markers[$name];\n        \n        return [\n            'duration_ms' => $duration * 1000,\n            'memory_delta_bytes' => $memory,\n        ];\n    }\n}\n\n// Usage\nPerformanceProfiler::start('database_query');\n// ... slow database operation\n$result = PerformanceProfiler::stop('database_query');\necho \"Query took: \" . $result['duration_ms'] . \"ms, memory delta: \" . $result['memory_delta_bytes'] . \" bytes\\n\";\n\n// 6. Inspection du bytecode avec Xdebug (PHP 8+)\n// Ajouter xdebug.output_dir pour voir les fichiers compilés\n\n// 7. Debugging d'une boucle inefficace\nfunction slow_function() {\n    $result = [];\n    for ($i = 0; $i < 1000; $i++) {\n        $result[] = expensive_operation($i);\n    }\n    return $result;\n}\n\n// Avec Xdebug, set un breakpoint et inspectez :\n// - Nombre d'appels à expensive_operation()\n// - Temps par appel\n// - Croissance de $result\n\nfunction expensive_operation($n) {\n    return array_sum(range(1, $n)); // Intentionally slow\n}\n\n// 8. Opcache et versionning\nif (function_exists('opcache_get_scripts')) {\n    $scripts = opcache_get_scripts();\n    foreach ($scripts as $path => $metadata) {\n        echo \"$path: \" . $metadata['timestamp'] . \"\\n\";\n    }\n}\n?>\n```\n\n### Tableau Comparatif des Outils\n| Outil | Overhead | Complexité | Cost | Best For | Live Debugging |\n|-------|----------|-----------|------|----------|----------------|\n| **Xdebug** | 10-50% | Haute | Gratuit | Dev, débugage | Oui (StepDebug) |\n| **Blackfire** | 2-5% | Moyenne | Payant | Prod, comparaisons | Limité |\n| **Opcache** | Négatif (-50%) | Basse | Gratuit | Prod, performance | Non |\n| **SPX Profiler** | 5-15% | Basse | Gratuit | Web UI, flamegraph | Oui |\n| **Manual timing** | <1% | Très haute | Gratuit | Cas spécifiques | Non |\n\n### Astuce d'Expert\nCombinaison optimale pour une équipe :\n\n1. **En développement** : Xdebug avec mode `debug` + `profile`. Breakpoints + auto-profiling.\n2. **En testing** : Opcache activé (reproduire l'env prod), Xdebug en trace mode.\n3. **En staging** : Blackfire pour comparer branches de features avant prod.\n4. **En prod** : Opcache + revalidate_freq = 0 (très rapide). Monitoring applicatif customisé (pas Xdebug!).\n\nPour une fuite mémoire suspecte : en dev, activez Xdebug trace et parsez les logs pour voir quels objets ne sont jamais détruits.\n\n### Attention ⚠️\nNe JAMAIS laisser Xdebug activé en production (mode debug ou profile). L'overhead est énorme (~10-50x plus lent). Même Xdebug avec mode `off` a un coût si l'extension est chargée. Désactivez-la complètement en prod avec `php.ini` ou check `php -m | grep xdebug`.\n\nOpcache peut cacher des bugs : si un fichier est modifié mais le bytecode cache n'est pas invalidé, vous executez du code obsolète. Utilisez `opcache_invalidate()` après les déploiements automatisés ou configurez `validate_timestamps=1` en dev (mais pas prod pour la perf).\n\n### Point Avancé: Call stacks et tail recursion\nXdebug montre les stacks complets mais peut ralentir les scripts récursifs. Pour les functions tail-recursive, PHP n'optimise pas (contrairement à Scheme ou Lua), donc une récursion profonde cause un débordement de stack. Profiling révèle cela : chaque appel ajoute un frame. Préférez l'itération ou tail call optimization manuelle.",
        "stats": {
          "mots": 512,
          "elements": ["Definition", "Explication", "BlocCode", "Tableau", "Astuce", "Attention"]
        }
      },
      {
        "numero": 4,
        "titre": "Gestion Avancée des Exceptions, Erreurs et Edge Cases",
        "contenu": "## Gestion Avancée des Exceptions, Erreurs et Edge Cases\n\n### Définition\nLa gestion avancée des exceptions et erreurs en PHP consiste à maîtriser les différents types d'erreurs (Error, Exception, Warning), leurs interactions avec le système d'exception, la gestion des edge cases (valeurs nulles, types inattendus, débordements) et les stratégies de recovery pour créer des applications résilientes et maintenables.\n\n### Explication Détaillée\nDepuis PHP 7, il existe une hiérarchie d'exceptions distincte :\n\n- **Throwable** : interface racine\n  - **Exception** : pour erreurs applicatives (extends/catch)\n  - **Error** : pour erreurs de PHP lui-même (TypeError, DivisionByZeroError, etc.)\n    - **TypeError** : appel fonction avec mauvais types\n    - **ArgumentCountError** : nombre d'arguments incorrect\n    - **DivisionByZeroError** : division par zéro\n    - **ParseError** : erreur de syntaxe\n\nCritique : les \"Errors\" ne déclenchent PAS les ancien `error_handler` custom. Seules les \"Exceptions\" le font. Cela signifie que `try/catch(Error)` ne capture pas les avertissements (warnings) non converti en exceptions.\n\nEn PHP 8+, on peut configurer `set_error_handler()` pour convertir les erreurs en exceptions, mais c'est une couche supplémentaire.\n\nLes **edge cases** typiques :\n- Valeurs `null` inattendues (null-coalescing `??` vs null-safe `?.`)\n- Débordements d'entiers (INT_MAX + 1 = float en PHP)\n- Division par zéro (génère DivisionByZeroError)\n- Type juggling (coercition implicite : \"5\" + 3 = 8)\n- Récursion infinie (limite ini `max_execution_time`)\n\n### Bloc Code\n```php\n<?php\n\n// 1. Hiérarchie Throwable modern\ntry {\n    // Code qui peut lever une Exception ou Error\n    $result = risky_operation();\n} catch (TypeError $e) {\n    // Capture les erreurs de type (PHP 7+)\n    echo \"Type Error: \" . $e->getMessage();\n} catch (DivisionByZeroError $e) {\n    // Division par zéro explicitement\n    echo \"Cannot divide by zero: \" . $e->getMessage();\n} catch (Error $e) {\n    // Capture tout Error qui n'est pas TypeError ou DivisionByZeroError\n    echo \"Generic Error: \" . $e->getMessage();\n} catch (Exception $e) {\n    // Capture Exception (exceptions applicatives)\n    echo \"Application Exception: \" . $e->getMessage();\n} catch (Throwable $e) {\n    // Fallback: capture TOUT (Error + Exception)\n    // RAREMENT recommandé, mais utile pour logging global\n    echo \"Critical Error: \" . $e->getMessage();\n} finally {\n    // Exécuté TOUJOURS, même après return dans try/catch\n    echo \"Cleanup code\";\n}\n\n// 2. Fonction avec argument type hint (levé TypeError si violated)\nfunction process_data(int $id, string $name): array {\n    if ($id < 0) {\n        throw new InvalidArgumentException(\"ID must be positive\");\n    }\n    if (strlen($name) === 0) {\n        throw new InvalidArgumentException(\"Name cannot be empty\");\n    }\n    return ['id' => $id, 'name' => $name];\n}\n\n// Appel qui lève TypeError\nprocess_data(\"not_an_int\", \"Bob\"); // TypeError automatiquement\n\n// Appel qui lève InvalidArgumentException\nprocess_data(-5, \"Alice\"); // InvalidArgumentException custom\n\n// 3. Gestion robuste de null (PHP 8.0+ null-safe operator)\nclass User {\n    public ?Address $address = null;\n}\n\nclass Address {\n    public string $city = 'Unknown';\n}\n\n$user = new User();\n\n// Ancien: doit vérifier chaque niveau\nif ($user && $user->address && $user->address->city) {\n    echo $user->address->city;\n}\n\n// Nouveau (PHP 8+): null-safe operator\necho $user->address?->city ?? 'No city'; // Court-circuite si null\n\n// 4. Coercition de type et edge cases\n$a = \"5 apples\"; // String\n$b = (int)$a;    // Coerce to int: 5 (!) - PHP strip leading numeric part\n\n$c = \"apples 5\"; // String sans nombre au début\n$d = (int)$c;    // 0\n\n$e = \"1e2\";      // Scientific notation string\n$f = (int)$e;    // 1, NOT 100 (int cast doesn't parse scientific)\n$g = (float)$e;  // 100.0 (float parse scientific)\n\n// 5. Division par zéro (edge case moderne)\ntry {\n    $result = 10 / 0; // DivisionByZeroError en PHP 8+\n} catch (DivisionByZeroError $e) {\n    echo \"Caught: \" . $e->getMessage();\n}\n\n// 6. Débordement d'entier\n$max = PHP_INT_MAX;      // 9223372036854775807 sur 64-bit\n$overflow = $max + 1;    // Converti en float\nvar_dump($overflow);     // float(9.2233720368548E+18)\n\n// 7. Custom Exception avec context\nclass DatabaseException extends Exception {\n    private $query;\n    private $bindings;\n    \n    public function __construct($message, $query, $bindings) {\n        parent::__construct($message);\n        $this->query = $query;\n        $this->bindings = $bindings;\n    }\n    \n    public function getDebugInfo() {\n        return [\n            'error' => $this->message,\n            'query' => $this->query,\n            'bindings' => $this->bindings,\n        ];\n    }\n}\n\ntry {\n    // Simulated DB error\n    throw new DatabaseException(\n        \"Syntax error in query\",\n        \"SELECT * FROM users WHERE id = ?\",\n        [123]\n
Accédez à des centaines d'examens QCM — Découvrir les offres Premium