MidТеория9 min

Встроенные классы и исключения

stdClass, Closure, WeakReference, WeakMap, иерархия Exception и Error, SPL-исключения

PHP содержит ряд встроенных классов, которые используются повсеместно: от анонимных объектов (stdClass) до замыканий (Closure), слабых ссылок (WeakReference, WeakMap) и полной иерархии исключений. Знание этих классов необходимо для грамотного проектирования и обработки ошибок.

stdClass -- анонимные объекты

<?php
declare(strict_types=1);

// stdClass is the default "empty" class in PHP
// Used for anonymous objects and type casting

// Create empty object
$obj = new \stdClass();
$obj->name = 'Alice';
$obj->age = 30;
$obj->active = true;

echo $obj->name;   // "Alice"
echo $obj->age;    // 30

// Cast array to object
$data = ['name' => 'Bob', 'email' => '[email protected]'];
$obj = (object) $data;
echo $obj->name;   // "Bob"
echo $obj->email;  // "[email protected]"

// Cast object to array
$arr = (array) $obj;
echo $arr['name']; // "Bob"

// json_decode returns stdClass by default
$json = '{"id": 1, "title": "Hello", "tags": ["php", "dev"]}';
$decoded = json_decode($json);
echo get_class($decoded);    // "stdClass"
echo $decoded->id;           // 1
echo $decoded->title;        // "Hello"
echo $decoded->tags[0];      // "php"

// json_decode with associative array instead
$arr = json_decode($json, true); // array, not stdClass
echo $arr['id']; // 1

// Checking for stdClass
var_dump($decoded instanceof \stdClass); // true
var_dump(get_class($decoded));           // "stdClass"

// Nested stdClass
$nested = json_decode('{"user": {"name": "Alice", "address": {"city": "Moscow"}}}');
echo $nested->user->address->city; // "Moscow"

// NOTE: stdClass has no methods, no interfaces, no type safety
// Prefer DTOs (readonly classes) for structured data in production code

Closure -- замыкания и анонимные функции

<?php
declare(strict_types=1);

// Every anonymous function in PHP is an instance of Closure
$greet = function (string $name): string {
    return "Hello, $name!";
};

var_dump($greet instanceof \Closure); // true
echo $greet('World');                 // "Hello, World!"

// Arrow functions are also Closures
$double = fn(int $n): int => $n * 2;
var_dump($double instanceof \Closure); // true

// --- Closure::bind() and Closure::bindTo() ---
// Bind closure to specific object and class scope

final class Counter
{
    private int $count = 0;

    public function getCount(): int
    {
        return $this->count;
    }
}

$counter = new Counter();

// Closure that accesses private property
$increment = \Closure::bind(
    function (int $amount): void {
        $this->count += $amount; // accesses private $count
    },
    $counter,         // bind $this to $counter
    Counter::class    // scope for private access
);

$increment(5);
echo $counter->getCount(); // 5

// --- Closure::call() (PHP 7.0+) ---
// Bind and call in one step — more efficient

$getCount = function (): int {
    return $this->count;
};

$result = $getCount->call($counter); // bind and execute
echo $result; // 5

// --- Closure::fromCallable() ---
// Convert any callable to Closure

function myFunction(int $x): int
{
    return $x * 3;
}

$closure = \Closure::fromCallable('myFunction');
echo $closure(10); // 30

// First-class callable syntax (PHP 8.1+) — same thing, shorter
$closure = myFunction(...);
echo $closure(10); // 30

// Method references
$closure = strlen(...);
echo $closure('Hello'); // 5

// Static method
$closure = \DateTimeImmutable::createFromFormat(...);

// Instance method
$obj = new \DateTime();
$closure = $obj->format(...);
echo $closure('Y-m-d'); // "2025-..."

WeakReference (PHP 7.4+)

<?php
declare(strict_types=1);

// WeakReference holds a reference to an object without preventing garbage collection

final class HeavyResource
{
    public function __construct(
        public readonly string $name,
    ) {
        echo "Created: $this->name\n";
    }

    public function __destruct()
    {
        echo "Destroyed: $this->name\n";
    }
}

// Normal reference keeps object alive
$resource = new HeavyResource('Resource A');
$strongRef = $resource; // strong reference
unset($resource);
// Object still alive! $strongRef holds it
echo $strongRef->name; // "Resource A"

// Weak reference does NOT prevent GC
$resource = new HeavyResource('Resource B');
$weakRef = \WeakReference::create($resource);

echo $weakRef->get()?->name; // "Resource B" — still alive

unset($resource);
// "Destroyed: Resource B" — object collected!

var_dump($weakRef->get()); // null — object is gone

// Practical use: cache that doesn't prevent GC
final class WeakCache
{
    /** @var array<string, \WeakReference<object>> */
    private array $cache = [];

    public function get(string $key): ?object
    {
        if (!isset($this->cache[$key])) {
            return null;
        }

        $obj = $this->cache[$key]->get();
        if ($obj === null) {
            // Object was garbage collected, clean up
            unset($this->cache[$key]);
        }

        return $obj;
    }

    public function set(string $key, object $value): void
    {
        $this->cache[$key] = \WeakReference::create($value);
    }
}

WeakMap (PHP 8.0+)

<?php
declare(strict_types=1);

// WeakMap uses objects as keys without preventing their garbage collection
// When an object key is destroyed, its entry is automatically removed

// Regular SplObjectStorage / array would keep objects alive
// WeakMap does NOT keep objects alive

$map = new \WeakMap();

$obj1 = new \stdClass();
$obj2 = new \stdClass();

$map[$obj1] = ['metadata' => 'Object 1 data'];
$map[$obj2] = ['metadata' => 'Object 2 data'];

echo count($map); // 2

// Access by object key
echo $map[$obj1]['metadata']; // "Object 1 data"

// Check existence
var_dump(isset($map[$obj1])); // true

// When object is destroyed, entry is removed automatically
unset($obj1);
echo count($map); // 1 — entry for $obj1 was removed!

// Practical: attach metadata to objects without modifying them
final class EventDispatcher
{
    private \WeakMap $listeners;

    public function __construct()
    {
        $this->listeners = new \WeakMap();
    }

    public function attach(object $target, callable $listener): void
    {
        if (!isset($this->listeners[$target])) {
            $this->listeners[$target] = [];
        }

        $callbacks = $this->listeners[$target];
        $callbacks[] = $listener;
        $this->listeners[$target] = $callbacks;
    }

    public function dispatch(object $target, string $event): void
    {
        $callbacks = $this->listeners[$target] ?? [];
        foreach ($callbacks as $callback) {
            $callback($event);
        }
    }

    public function count(): int
    {
        return count($this->listeners);
    }
}

// When $target object is destroyed, its listeners are automatically cleaned up

Иерархия исключений

Вся иерархия ошибок и исключений в PHP строится на интерфейсе Throwable:

Throwable (interface)
├── Error (internal PHP errors)
│   ├── ArithmeticError
│   │   └── DivisionByZeroError
│   ├── TypeError
│   ├── ValueError (PHP 8.0+)
│   ├── UnhandledMatchError (PHP 8.0+)
│   ├── FiberError (PHP 8.1+)
│   ├── CompileError
│   │   └── ParseError
│   └── AssertionError
│
└── Exception (user-level errors)
    ├── LogicException (programming errors, fix in code)
    │   ├── BadFunctionCallException
    │   │   └── BadMethodCallException
    │   ├── DomainException
    │   ├── InvalidArgumentException
    │   ├── LengthException
    │   └── OutOfRangeException
    │
    ├── RuntimeException (errors at runtime, not predictable)
    │   ├── OutOfBoundsException
    │   ├── OverflowException
    │   ├── RangeException
    │   ├── UnderflowException
    │   └── UnexpectedValueException
    │
    ├── JsonException (PHP 7.3+)
    ├── DateError (PHP 8.3+)
    │   ├── DateRangeError
    │   └── DateObjectError
    └── DateException (PHP 8.3+)
        ├── DateInvalidTimeZoneException
        ├── DateInvalidOperationException
        └── DateMalformedStringException

Error -- внутренние ошибки PHP

<?php
declare(strict_types=1);

// TypeError — wrong type passed
function add(int $a, int $b): int
{
    return $a + $b;
}

try {
    add('hello', 'world'); // TypeError in strict mode
} catch (\TypeError $e) {
    echo $e->getMessage();
    // "add(): Argument #1 ($a) must be of type int, string given"
}

// ValueError — correct type, wrong value (PHP 8.0+)
try {
    json_decode('', flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
    echo $e->getMessage(); // "Syntax error"
}

try {
    str_repeat('x', -1); // negative repeat
} catch (\ValueError $e) {
    echo $e->getMessage();
}

// ArithmeticError / DivisionByZeroError
try {
    $result = intdiv(PHP_INT_MIN, -1); // overflow
} catch (\ArithmeticError $e) {
    echo $e->getMessage();
}

try {
    $result = 1 % 0; // division by zero
} catch (\DivisionByZeroError $e) {
    echo $e->getMessage(); // "Division by zero"
}

// UnhandledMatchError (PHP 8.0+)
try {
    $value = match ('unknown') {
        'a' => 1,
        'b' => 2,
    }; // no match for 'unknown'
} catch (\UnhandledMatchError $e) {
    echo $e->getMessage();
    // "Unhandled match case"
}

// FiberError (PHP 8.1+)
try {
    $fiber = new \Fiber(function (): void {
        \Fiber::suspend('value');
    });
    $fiber->start();
    $fiber->resume();
    $fiber->resume(); // Fiber already terminated
} catch (\FiberError $e) {
    echo $e->getMessage();
}

Exception -- пользовательские исключения

<?php
declare(strict_types=1);

// --- LogicException and children ---
// Programming errors that should be fixed in code

// InvalidArgumentException — argument has wrong value
function setAge(int $age): void
{
    if ($age < 0 || $age > 150) {
        throw new \InvalidArgumentException(
            sprintf('Age must be between 0 and 150, got %d', $age)
        );
    }
}

// BadMethodCallException — calling non-existent or invalid method
final class ReadonlyCollection
{
    public function __set(string $name, mixed $value): never
    {
        throw new \BadMethodCallException(
            'Cannot set properties on ReadonlyCollection'
        );
    }
}

// DomainException — value is outside acceptable domain
function calculateSquareRoot(float $number): float
{
    if ($number < 0) {
        throw new \DomainException(
            'Cannot calculate square root of negative number'
        );
    }
    return sqrt($number);
}

// LengthException — length constraint violated
function createPassword(string $password): string
{
    if (strlen($password) < 8) {
        throw new \LengthException(
            'Password must be at least 8 characters'
        );
    }
    return password_hash($password, PASSWORD_ARGON2ID);
}

// OutOfRangeException — invalid index at compile-time / logic level
function getMonth(int $month): string
{
    if ($month < 1 || $month > 12) {
        throw new \OutOfRangeException(
            sprintf('Month must be 1-12, got %d', $month)
        );
    }

    return ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
            'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'][$month - 1];
}

// --- RuntimeException and children ---
// Errors that can only be detected at runtime

// OutOfBoundsException — invalid key/index at runtime
function getFromCache(string $key): string
{
    $cache = []; // pretend this is populated
    if (!isset($cache[$key])) {
        throw new \OutOfBoundsException(
            sprintf('Cache key "%s" not found', $key)
        );
    }
    return $cache[$key];
}

// OverflowException — adding to a full container
final class FixedSizeStack
{
    private array $items = [];

    public function __construct(
        private readonly int $maxSize,
    ) {}

    public function push(mixed $item): void
    {
        if (count($this->items) >= $this->maxSize) {
            throw new \OverflowException(
                sprintf('Stack is full (max %d items)', $this->maxSize)
            );
        }
        $this->items[] = $item;
    }

    public function pop(): mixed
    {
        if (count($this->items) === 0) {
            throw new \UnderflowException('Stack is empty');
        }
        return array_pop($this->items);
    }
}

// UnexpectedValueException — function returned unexpected value
function parseConfig(string $path): array
{
    $content = file_get_contents($path);
    if ($content === false) {
        throw new \RuntimeException("Cannot read file: $path");
    }

    $data = json_decode($content, true);
    if (!is_array($data)) {
        throw new \UnexpectedValueException(
            'Config file must contain a JSON object'
        );
    }

    return $data;
}

Создание собственной иерархии исключений

<?php
declare(strict_types=1);

// Base domain exception
abstract class AppException extends \RuntimeException
{
    public function __construct(
        string $message,
        public readonly ?string $errorCode = null,
        int $code = 0,
        ?\Throwable $previous = null,
    ) {
        parent::__construct($message, $code, $previous);
    }
}

// Specific domain exceptions
final class EntityNotFoundException extends AppException
{
    public static function byId(string $entity, int|string $id): self
    {
        return new self(
            message: sprintf('%s with ID "%s" not found', $entity, $id),
            errorCode: 'ENTITY_NOT_FOUND',
            code: 404,
        );
    }
}

final class ValidationException extends AppException
{
    /**
     * @param array<string, string[]> $errors
     */
    public function __construct(
        public readonly array $errors,
        string $message = 'Validation failed',
    ) {
        parent::__construct($message, 'VALIDATION_ERROR', 422);
    }
}

final class AccessDeniedException extends AppException
{
    public function __construct(string $resource)
    {
        parent::__construct(
            message: sprintf('Access denied to resource: %s', $resource),
            errorCode: 'ACCESS_DENIED',
            code: 403,
        );
    }
}

// Usage
try {
    throw EntityNotFoundException::byId('User', 42);
} catch (EntityNotFoundException $e) {
    echo $e->getMessage();   // "User with ID "42" not found"
    echo $e->errorCode;      // "ENTITY_NOT_FOUND"
    echo $e->getCode();      // 404
} catch (AppException $e) {
    // Catch any domain exception
}

SensitiveParameterValue (PHP 8.2+)

<?php
declare(strict_types=1);

// #[\SensitiveParameter] attribute redacts values in stack traces
// SensitiveParameterValue wraps the redacted value

function connectToDatabase(
    string $host,
    string $username,
    #[\SensitiveParameter] string $password, // redacted in traces
): void {
    throw new \RuntimeException('Connection failed');
}

try {
    connectToDatabase('localhost', 'root', 'secret123');
} catch (\RuntimeException $e) {
    // In stack trace, password will show as:
    // Object(SensitiveParameterValue) instead of "secret123"
    echo $e->getTraceAsString();
}

// SensitiveParameterValue class
// Used internally by PHP to wrap redacted values
// You typically don't create instances manually

DateError и DateException (PHP 8.3+)

<?php
declare(strict_types=1);

// PHP 8.3 introduced proper exception classes for date/time errors

// DateMalformedStringException — invalid date string
try {
    new \DateTimeImmutable('not a date');
} catch (\DateMalformedStringException $e) {
    echo $e->getMessage(); // describes the parsing error
}

// DateInvalidTimeZoneException — invalid timezone
try {
    new \DateTimeZone('Not/A/Timezone');
} catch (\DateInvalidTimeZoneException $e) {
    echo $e->getMessage();
}

// DateRangeError — date value out of range
try {
    new \DateInterval('P99999999Y');
} catch (\DateRangeError $e) {
    echo $e->getMessage();
}

// Before PHP 8.3, these were generic warnings/exceptions
// Now they have specific exception classes for better error handling

Когда использовать какое исключение

Ситуация Исключение
Неверный аргумент функции InvalidArgumentException
Запрашиваемый ресурс не найден OutOfRangeException / OutOfBoundsException
Нарушение ограничения длины LengthException
Выход за пределы допустимой области DomainException
Вызов несуществующего метода BadMethodCallException
Переполнение контейнера OverflowException
Операция над пустым контейнером UnderflowException
Неожиданное значение от внешнего кода UnexpectedValueException
Общая ошибка времени выполнения RuntimeException
Ошибка программиста (не должна быть в production) LogicException

Правило: LogicException и потомки -- это ошибки в коде (баги). Они должны быть исправлены разработчиком. RuntimeException и потомки -- это ошибки окружения (файл не найден, сервис недоступен, невалидные данные от пользователя).


Проверь себя

5 из 11

К какой ветке иерархии относится DivisionByZeroError?

Что делает Closure::bind()?

Какое исключение выбрасывается при match без подходящего case (PHP 8.0+)?

Что произойдёт с записью в WeakMap при уничтожении объекта-ключа?

Что такое stdClass в PHP?