Иерархия Throwable
Throwable (interface)
├── Error
│ ├── TypeError
│ ├── ValueError
│ ├── ArithmeticError
│ │ └── DivisionByZeroError
│ ├── FiberError
│ ├── UnhandledMatchError
│ └── ...
└── Exception
├── RuntimeException
│ ├── OverflowException
│ ├── UnderflowException
│ ├── OutOfBoundsException
│ └── UnexpectedValueException
├── LogicException
│ ├── InvalidArgumentException
│ ├── BadMethodCallException
│ ├── DomainException
│ ├── LengthException
│ └── OutOfRangeException
├── BadFunctionCallException
└── ...
<?php
declare(strict_types=1);
// Throwable is the base interface (PHP 7+)
// You CANNOT implement Throwable directly!
// You MUST extend Exception or Error
// Exception — recoverable errors (your code should handle)
// Error — unrecoverable errors (bugs, type errors, etc.)
// Catch both:
try {
// risky code
} catch (\Throwable $e) {
// catches both Exception and Error
echo $e->getMessage();
}
Ловушка экзамена:
Throwable— это интерфейс, НЕ класс. Нельзя напрямую реализоватьThrowable— нужно наследоваться отExceptionилиError. ТолькоExceptionиErrorмогут быть базовыми классами для пользовательских исключений.
try/catch/finally
<?php
declare(strict_types=1);
// Basic try/catch
try {
$result = riskyOperation();
} catch (RuntimeException $e) {
echo "Runtime: {$e->getMessage()}";
} catch (InvalidArgumentException $e) {
echo "Invalid arg: {$e->getMessage()}";
} catch (Exception $e) {
echo "General: {$e->getMessage()}";
}
// Multi-catch (PHP 8.0+)
try {
$data = processData($input);
} catch (InvalidArgumentException | TypeError $e) {
echo "Bad input: {$e->getMessage()}";
}
// finally — ALWAYS executes
function readConfig(string $path): array
{
$handle = fopen($path, 'r');
try {
$data = fread($handle, filesize($path));
return json_decode($data, true, 512, JSON_THROW_ON_ERROR);
} catch (JsonException $e) {
return [];
} finally {
// Executes even after return!
fclose($handle);
}
}
// try without catch (only finally)
function cleanup(): void
{
$resource = acquireResource();
try {
useResource($resource);
} finally {
releaseResource($resource); // Always runs
}
}
Запомни: Блок
finallyвыполняется ВСЕГДА — даже послеreturn,throw, илиbreak. Если и вtry, и вfinallyестьreturn, то возвращается значение изfinally. Еслиfinallyсодержитthrow, исключение изtry/catchтеряется.
Создание пользовательских исключений
<?php
declare(strict_types=1);
// Basic custom exception
class OrderNotFoundException extends RuntimeException
{
public function __construct(
private readonly int $orderId,
?\Throwable $previous = null,
) {
parent::__construct(
message: "Order #{$orderId} not found",
code: 404,
previous: $previous,
);
}
public function getOrderId(): int
{
return $this->orderId;
}
}
// Exception with context data
class ValidationException extends RuntimeException
{
/** @param array<string, string[]> $errors */
public function __construct(
private readonly array $errors,
?\Throwable $previous = null,
) {
$message = 'Validation failed: ' . implode(', ', array_keys($errors));
parent::__construct($message, 422, $previous);
}
/** @return array<string, string[]> */
public function getErrors(): array
{
return $this->errors;
}
}
// Usage
try {
throw new ValidationException([
'email' => ['Invalid email format'],
'age' => ['Must be at least 18'],
]);
} catch (ValidationException $e) {
foreach ($e->getErrors() as $field => $messages) {
echo "{$field}: " . implode(', ', $messages) . "\n";
}
}
Exception chaining (Previous)
<?php
declare(strict_types=1);
// Chain exceptions to preserve original cause
class DatabaseException extends RuntimeException {}
class UserServiceException extends RuntimeException {}
function findUserInDb(int $id): array
{
try {
$pdo = new PDO('mysql:host=localhost;dbname=app', 'root', '');
$stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
$stmt->execute([$id]);
return $stmt->fetch(PDO::FETCH_ASSOC) ?: throw new RuntimeException('Not found');
} catch (PDOException $e) {
// Wrap low-level exception in domain exception
throw new DatabaseException(
message: "Failed to query user #{$id}",
code: 0,
previous: $e, // Preserve original exception
);
}
}
function getUser(int $id): array
{
try {
return findUserInDb($id);
} catch (DatabaseException $e) {
throw new UserServiceException(
message: "Cannot get user",
previous: $e,
);
}
}
// Traverse exception chain
try {
$user = getUser(42);
} catch (UserServiceException $e) {
$current = $e;
while ($current !== null) {
echo get_class($current) . ": {$current->getMessage()}\n";
$current = $current->getPrevious();
}
}
Ловушка экзамена: Всегда передавайте
$previousв пользовательские исключения! Это сохраняет цепочку ошибок для отладки.getPrevious()возвращаетnullесли предыдущего исключения нет.
SPL Exceptions
<?php
declare(strict_types=1);
// Logic errors — bugs that should be fixed in code
throw new InvalidArgumentException('Age must be positive');
throw new BadMethodCallException('Method not implemented');
throw new DomainException('Value outside allowed domain');
throw new LengthException('String too long');
throw new OutOfRangeException('Index 10 is out of range 0-5');
// Runtime errors — can occur during normal operation
throw new RuntimeException('Cannot connect to service');
throw new OverflowException('Queue is full');
throw new UnderflowException('Stack is empty');
throw new OutOfBoundsException('Key does not exist');
throw new UnexpectedValueException('Expected int, got string');
// Best practice: choose the most specific SPL exception
function divide(float $a, float $b): float
{
if ($b === 0.0) {
throw new InvalidArgumentException('Divisor cannot be zero');
}
return $a / $b;
}
function getElement(array $arr, int $index): mixed
{
if ($index < 0 || $index >= count($arr)) {
throw new OutOfRangeException("Index {$index} is out of range");
}
return $arr[$index];
}
| Тип | Когда использовать |
|---|---|
LogicException |
Ошибка в логике программы (баг) |
InvalidArgumentException |
Некорректный аргумент |
BadMethodCallException |
Вызов несуществующего метода |
RuntimeException |
Ошибка во время выполнения |
OverflowException |
Переполнение контейнера |
OutOfBoundsException |
Ключ/индекс не существует |
UnexpectedValueException |
Неожиданное значение |
set_exception_handler
<?php
declare(strict_types=1);
// Global exception handler for uncaught exceptions
set_exception_handler(function (\Throwable $e): void {
error_log(sprintf(
"[%s] %s in %s:%d\nStack trace:\n%s",
get_class($e),
$e->getMessage(),
$e->getFile(),
$e->getLine(),
$e->getTraceAsString(),
));
if (PHP_SAPI !== 'cli') {
http_response_code(500);
echo '<h1>Internal Server Error</h1>';
} else {
fwrite(STDERR, "Fatal: {$e->getMessage()}\n");
}
});
// Restore default handler
restore_exception_handler();
Запомни:
set_exception_handler()перехватывает НЕперехваченные исключения. После выполнения обработчика скрипт завершается. Это последний рубеж — основная обработка должна быть черезtry/catch.
Exception в PHP 8.0+
<?php
declare(strict_types=1);
// PHP 8.0: throw is an expression (not statement)
$value = $input ?? throw new InvalidArgumentException('Input required');
$name = match ($role) {
'admin' => 'Administrator',
'user' => 'User',
default => throw new UnexpectedValueException("Unknown role: {$role}"),
};
// PHP 8.0: non-capturing catch
try {
riskyOperation();
} catch (SpecificException) {
// No need for $e variable if not used
log('Operation failed');
}
// PHP 8.0: Union types in catch
try {
parseData($raw);
} catch (JsonException | InvalidArgumentException $e) {
handleParseError($e);
}
// PHP 8.1: never return type for functions that always throw
function fail(string $message): never
{
throw new RuntimeException($message);
}
// PHP 8.1: Enums + exceptions
enum HttpStatus: int
{
case NotFound = 404;
case ServerError = 500;
public function toException(): RuntimeException
{
return match ($this) {
self::NotFound => new RuntimeException('Not Found', 404),
self::ServerError => new RuntimeException('Server Error', 500),
};
}
}
Ловушка экзамена: В PHP 8.0
throwстал выражением (expression), а не инструкцией (statement). Это позволяет использовать его в??,?:,match, arrow functions и других контекстах выражений.
Паттерн: Exception → JSON Response
<?php
declare(strict_types=1);
// API exception handler
set_exception_handler(function (\Throwable $e): void {
$statusCode = match (true) {
$e instanceof InvalidArgumentException => 400,
$e instanceof AuthenticationException => 401,
$e instanceof AuthorizationException => 403,
$e instanceof NotFoundException => 404,
$e instanceof ValidationException => 422,
default => 500,
};
http_response_code($statusCode);
header('Content-Type: application/json');
$response = ['error' => $e->getMessage()];
// Include details in non-production
if (getenv('APP_ENV') !== 'production') {
$response['exception'] = get_class($e);
$response['file'] = $e->getFile() . ':' . $e->getLine();
$response['trace'] = $e->getTrace();
}
echo json_encode($response, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE);
});