Jdi na navigaci předmětu

Výjimky a zpracování chyb (error handling)

K čemu je to dobré?

  • Error Handling
  • vědět, co se stalo - např. zaznamenat do logu
  • zotavit se z chyby, pokud je to možné
  • PHP má dva mechanismy
    • výjimky - objekty, které se vyhazují a zachytávají (throw, try/catch); stejný princip jako v C++
    • chyby (errors) - historický mechanismus vestavěných funkcí (warning, notice, …​); řeší se přes error handler

Výjimky

  • Referenční dokumentace
  • vyhození příkazem throw, od PHP 8 je throw výraz (lze použít např. za ??)
  • zachycení blokem try / catch, volitelně finally
  • každá výjimka je objekt třídy implementující rozhraní Throwable
  • nezachycená výjimka ukončí skript fatální chybou (příp. ji zpracuje set_exception_handler(), viz níže)
  • výjimka postupuje zásobníkem volání nahoru, dokud ji někdo nezachytí
function divide(float $a, float $b): float {
    if ($b == 0) {
        throw new InvalidArgumentException("Nelze dělit nulou");
    }
    return $a / $b;
}

try {
    echo divide(10, 2) . "\n"; // 5
    echo divide(1, 0) . "\n";  // vyhodí výjimku, další řádek se neprovede
    echo "Tohle se nevypíše\n";
} catch (InvalidArgumentException $e) {
    echo "Chyba: " . $e->getMessage() . "\n";
} finally {
    echo "Provede se vždy (úklid zdrojů apod.)\n";
}

Hierarchie výjimek

  • Throwable - kořenové rozhraní, nelze implementovat přímo (jen skrz Exception nebo Error)
    • Exception - výjimky aplikace a knihoven, základ pro vlastní výjimky
      • SPL výjimky
        • LogicException (InvalidArgumentException, OutOfRangeException, …​)
        • RuntimeException (OutOfBoundsException, UnexpectedValueException, …​)
      • výjimky PHP rozšíření: JsonException, PDOException, …​
    • Error - chyby interpretu, dříve fatální chyby
      • TypeError (špatný typ argumentu), ValueError (správný typ, ale nesmyslná hodnota), ArgumentCountError, DivisionByZeroError, ArithmeticError, …​
  • catch (Exception $e) vestavěné chyby nezachytí; pro obojí použijte catch (Throwable $e)
  • pro vlastní výjimky použijte nejbližší vhodnou SPL výjimku jako rodiče
try {
    intdiv(1, 0);
} catch (Exception $e) {
    echo "Sem se nedostaneme\n";
} catch (Error $e) {
    echo get_class($e) . ": " . $e->getMessage() . "\n"; // DivisionByZeroError: Division by zero
}

Zachycení výjimek

  • bloků catch může být více, vyhodnocují se v pořadí zápisu → od nejkonkrétnější k nejobecnější
  • jeden blok pro více typů: catch (A | B $e)
  • od PHP 8 lze proměnnou vynechat, pokud výjimku nepotřebujeme: catch (A)
  • výjimku lze v catch znovu vyhodit (throw $e;) nebo zabalit do jiné (viz níže)
try {
    $data = json_decode($input, flags: JSON_THROW_ON_ERROR);
    process($data);
} catch (JsonException $e) {
    echo "Neplatný JSON: " . $e->getMessage() . "\n";
} catch (InvalidArgumentException | UnexpectedValueException $e) {
    echo "Neplatná data: " . $e->getMessage() . "\n";
} catch (Throwable $e) {
    echo "Neočekávaná chyba: " . get_class($e) . "\n";
    throw $e; // znovu vyhodíme, ať se o ni postará někdo výš
}

Metody výjimky

  • getMessage() - text zprávy
  • getCode() - číselný kód (význam určuje ten, kdo výjimku vyhazuje; např. HTTP status, kód chyby databáze)
  • getFile(), getLine() - kde byla výjimka vytvořena
  • getTrace(), getTraceAsString() - zásobník volání
  • getPrevious() - předchozí výjimka (řetězení, viz níže)
  • __toString() - vše výše v jednom řetězci, vhodné do logu

Vlastní výjimky

  • třída dědící od Exception (nebo od vhodné SPL výjimky)
  • konstruktor __construct(string $message = "", int $code = 0, ?Throwable $previous = null)
  • může mít vlastní atributy a metody (např. jaký záznam nebyl nalezen)
class ProductNotFoundException extends RuntimeException {
    public function __construct(
        public readonly int $productId,
        ?Throwable $previous = null,
    ) {
        parent::__construct("Produkt $productId nebyl nalezen", 404, $previous);
    }
}

function findProduct(int $id): array {
    $products = [1 => ['name' => 'Mitsubishi Lancer']];
    return $products[$id] ?? throw new ProductNotFoundException($id);
}

try {
    findProduct(42);
} catch (ProductNotFoundException $e) {
    echo $e->getMessage() . " (id " . $e->productId . ", kód " . $e->getCode() . ")\n";
}

Řetězení výjimek

  • nízkoúrovňovou výjimku (databáze, síť, JSON) zabalíme do výjimky s významem pro naši aplikaci
  • původní výjimka zůstane dostupná přes getPrevious() → nepřijdeme o informaci, kde chyba skutečně vznikla
function loadConfig(string $json): array {
    try {
        return json_decode($json, associative: true, flags: JSON_THROW_ON_ERROR);
    } catch (JsonException $e) {
        throw new RuntimeException("Konfiguraci nelze načíst", previous: $e);
    }
}

try {
    loadConfig('{"debug": tru}');
} catch (RuntimeException $e) {
    echo $e->getMessage() . "\n";                // Konfiguraci nelze načíst
    echo $e->getPrevious()->getMessage() . "\n"; // Syntax error
}

Chyby (errors)

  • starší mechanismus, používá ho většina vestavěných funkcí
  • funkce zpravidla vypíše warning nebo notice a vrátí false (příp. null), skript běží dál
    • např. file_get_contents() neexistujícího souboru, fopen(), mkdir()
  • od PHP 8 vestavěné funkce při špatných argumentech vyhazují TypeError / ValueError; některé funkce mají přepínač, který výjimky zapne
    • json_decode() - příznak JSON_THROW_ON_ERROR
    • PDO - PDO::ERRMODE_EXCEPTION (viz 7. cvičení)
  • úrovně chyb: E_ERROR, E_WARNING, E_NOTICE, E_DEPRECATED, uživatelské E_USER_* (vyvolání funkcí trigger_error())

Konfigurační hodnoty

  • error_reporting - jaké chyby se budou oznamovat
  • display_errors - zda se budou chyby vypisovat na (chybový) výstup
  • log_errors - záznam chyb (na produkci vhodnější než display_errors)

Vlastní error handler

Vlastní error handler

function myErrorHandler($errno, $errstr, $errfile, $errline) {
  if (!(error_reporting() & $errno)) {
      // This error code is not included in error_reporting
      // Return false to indicate "we did not handle this anyhow"
      return false;
  }

  switch ($errno) {
    case E_USER_ERROR:
    ...
  }
}

Převod chyb na výjimky

  • obvyklý postup v moderních aplikacích: error handler každou chybu převede na výjimku ErrorException
  • výsledkem je jednotné zpracování: try/catch funguje i pro warningy vestavěných funkcí
  • takto se chovají i frameworky a nástroje typu Tracy (viz 5. cvičení)
set_error_handler(function (int $errno, string $errstr, string $errfile, int $errline): bool {
    throw new ErrorException($errstr, 0, $errno, $errfile, $errline);
});

try {
    $content = file_get_contents('/neexistujici/soubor');
} catch (ErrorException $e) {
    echo "Soubor nelze načíst: " . $e->getMessage() . "\n";
}

Nezotavitelné chyby

nelze zpracovat vlastním error handlerem

  • obvyklé - E_ERROR, E_PARSE
  • speciální - E_CORE_ERROR, E_CORE_WARNING, E_COMPILE_ERROR, E_COMPILE_WARNING

Záznam chyb

  • konfigurační hodnota error_log
    • stderr (výchozí), cesta k souboru nebo speciální hodnota syslog
  • nebo pomocí knihoven např. Monolog