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и потомки -- это ошибки окружения (файл не найден, сервис недоступен, невалидные данные от пользователя).