PHP предоставляет набор предопределённых интерфейсов, которые формируют контракты для ключевых возможностей языка: итерация, доступ по индексу, подсчёт элементов, сериализация и обработка ошибок. Реализация этих интерфейсов позволяет объектам вашего класса работать с конструкциями foreach, count(), json_encode() и другими встроенными механизмами.
Traversable -- маркерный интерфейс
<?php
declare(strict_types=1);
// Traversable is an abstract marker interface
// You CANNOT implement it directly — must implement Iterator or IteratorAggregate
// This will cause a fatal error:
// class Broken implements Traversable {} // Error!
// Purpose: type-checking for anything that can be used in foreach
function processItems(\Traversable $items): void
{
foreach ($items as $item) {
echo $item . "\n";
}
}
// Works with Iterator, IteratorAggregate, Generator, etc.
// Also: array is iterable but NOT Traversable (it's a primitive)
// Use "iterable" type hint to accept both arrays and Traversable:
function processAll(iterable $items): void
{
foreach ($items as $item) {
echo $item . "\n";
}
}
processAll([1, 2, 3]); // array
processAll(new \ArrayIterator([1, 2, 3])); // Traversable
Iterator -- полный контроль итерации
<?php
declare(strict_types=1);
// Iterator provides 5 methods for full control over foreach behavior
/**
* Range iterator — generates numbers in range without storing them all in memory.
*/
final class Range implements \Iterator
{
private int $current;
public function __construct(
private readonly int $start,
private readonly int $end,
private readonly int $step = 1,
) {
if ($step <= 0) {
throw new \InvalidArgumentException('Step must be positive');
}
$this->current = $start;
}
// Return current value
public function current(): int
{
return $this->current;
}
// Return current key (position)
public function key(): int
{
return ($this->current - $this->start) / $this->step;
}
// Move to next element
public function next(): void
{
$this->current += $this->step;
}
// Reset to first element (called at the START of foreach)
public function rewind(): void
{
$this->current = $this->start;
}
// Check if current position is valid
public function valid(): bool
{
return $this->current <= $this->end;
}
}
// Usage
$range = new Range(1, 10, 2);
foreach ($range as $key => $value) {
echo "$key => $value\n";
}
// 0 => 1
// 1 => 3
// 2 => 5
// 3 => 7
// 4 => 9
// Can be reused (rewind is called automatically)
foreach ($range as $value) {
echo "$value ";
}
// 1 3 5 7 9
// How foreach calls Iterator internally:
// $it->rewind();
// while ($it->valid()) {
// $key = $it->key();
// $value = $it->current();
// // loop body
// $it->next();
// }
IteratorAggregate -- делегирование итерации
<?php
declare(strict_types=1);
// IteratorAggregate — simpler alternative to Iterator
// Requires only one method: getIterator()
// Returns a Traversable (usually ArrayIterator or Generator)
/**
* User collection — wraps array, delegates iteration.
*/
final class UserCollection implements \IteratorAggregate
{
/** @var array<int, User> */
private array $users = [];
public function add(User $user): void
{
$this->users[] = $user;
}
// Return an iterator over internal data
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->users);
}
}
final readonly class User
{
public function __construct(
public int $id,
public string $name,
) {}
}
$collection = new UserCollection();
$collection->add(new User(1, 'Alice'));
$collection->add(new User(2, 'Bob'));
$collection->add(new User(3, 'Charlie'));
// foreach works transparently
foreach ($collection as $user) {
echo "$user->id: $user->name\n";
}
// Can also use Generator for lazy iteration
final class FileLines implements \IteratorAggregate
{
public function __construct(
private readonly string $filePath,
) {}
public function getIterator(): \Generator
{
$handle = fopen($this->filePath, 'r');
if ($handle === false) {
throw new \RuntimeException('Cannot open file');
}
try {
while (($line = fgets($handle)) !== false) {
yield rtrim($line, "\r\n");
}
} finally {
fclose($handle);
}
}
}
$lines = new FileLines('/etc/hosts');
foreach ($lines as $line) {
echo $line . "\n";
}
ArrayAccess -- доступ как к массиву
<?php
declare(strict_types=1);
// ArrayAccess allows object to be used with [] syntax
/**
* Type-safe configuration container.
*/
final class Config implements \ArrayAccess
{
/** @var array<string, mixed> */
private array $data = [];
public function __construct(array $initial = [])
{
$this->data = $initial;
}
// isset($config['key'])
public function offsetExists(mixed $offset): bool
{
return array_key_exists($offset, $this->data);
}
// $config['key']
public function offsetGet(mixed $offset): mixed
{
if (!$this->offsetExists($offset)) {
throw new \OutOfRangeException(
sprintf('Config key "%s" does not exist', $offset)
);
}
return $this->data[$offset];
}
// $config['key'] = 'value'
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
throw new \InvalidArgumentException('Config key cannot be null');
}
$this->data[$offset] = $value;
}
// unset($config['key'])
public function offsetUnset(mixed $offset): void
{
unset($this->data[$offset]);
}
}
$config = new Config([
'db.host' => 'localhost',
'db.port' => 5432,
'debug' => true,
]);
// Array-like access
echo $config['db.host']; // "localhost"
echo $config['db.port']; // 5432
// Check existence
var_dump(isset($config['debug'])); // true
var_dump(isset($config['missing'])); // false
// Set
$config['app.name'] = 'MyApp';
// Unset
unset($config['debug']);
Countable -- подсчёт элементов
<?php
declare(strict_types=1);
// Countable allows count() to work with your objects
final class TodoList implements \Countable
{
/** @var array<int, string> */
private array $items = [];
public function add(string $item): void
{
$this->items[] = $item;
}
public function remove(int $index): void
{
unset($this->items[$index]);
$this->items = array_values($this->items);
}
// Required by Countable
public function count(): int
{
return count($this->items);
}
}
$todo = new TodoList();
echo count($todo); // 0
$todo->add('Buy groceries');
$todo->add('Write tests');
$todo->add('Deploy app');
echo count($todo); // 3
$todo->remove(1);
echo count($todo); // 2
// Also works with sizeof() (alias for count)
echo sizeof($todo); // 2
Stringable -- преобразование в строку (PHP 8.0+)
<?php
declare(strict_types=1);
// Stringable is auto-implemented when class has __toString()
// You can also implement it explicitly
final readonly class Email implements \Stringable
{
public function __construct(
private string $address,
) {
if (!filter_var($address, FILTER_VALIDATE_EMAIL)) {
throw new \InvalidArgumentException('Invalid email: ' . $address);
}
}
public function __toString(): string
{
return $this->address;
}
public function getDomain(): string
{
return substr($this->address, strpos($this->address, '@') + 1);
}
}
$email = new Email('[email protected]');
// Works in string contexts automatically
echo $email; // "[email protected]"
echo "Contact: $email"; // "Contact: [email protected]"
echo strtoupper((string) $email); // "[email protected]"
// Type hint accepts both strings and Stringable objects
function sendNotification(string|\Stringable $to): void
{
echo "Sending to: $to\n";
}
sendNotification('[email protected]'); // string
sendNotification($email); // Stringable
// Note: any class with __toString() automatically implements Stringable
// Even without explicit "implements \Stringable"
var_dump($email instanceof \Stringable); // true
JsonSerializable -- контроль JSON-сериализации
<?php
declare(strict_types=1);
// JsonSerializable controls what json_encode() outputs for your objects
final readonly class Product implements \JsonSerializable
{
public function __construct(
private int $id,
private string $name,
private string $price, // stored as BCMath string
private \DateTimeImmutable $createdAt,
private ?string $internalCode = null, // not for JSON!
) {}
public function jsonSerialize(): mixed
{
// Return only public-facing data
return [
'id' => $this->id,
'name' => $this->name,
'price' => $this->price,
'created_at' => $this->createdAt->format(\DateTimeInterface::ATOM),
];
// Note: internalCode is excluded from JSON
}
}
$product = new Product(
id: 42,
name: 'Widget',
price: '19.99',
createdAt: new \DateTimeImmutable('2025-01-15 10:30:00'),
internalCode: 'SECRET-123',
);
echo json_encode($product, JSON_PRETTY_PRINT);
// {
// "id": 42,
// "name": "Widget",
// "price": "19.99",
// "created_at": "2025-01-15T10:30:00+00:00"
// }
// Without JsonSerializable, json_encode would output nothing
// (private/protected properties are not serialized)
// Nested serialization works automatically
$products = [$product, $product];
echo json_encode($products); // array of product objects
BackedEnum и UnitEnum (PHP 8.1+)
<?php
declare(strict_types=1);
// UnitEnum interface — all enums implement it
// Methods: cases() — returns array of all enum cases
enum Color
{
case Red;
case Green;
case Blue;
}
$all = Color::cases();
// [Color::Red, Color::Green, Color::Blue]
// BackedEnum interface — backed enums (string|int) implement it
// Methods: from(value), tryFrom(value), cases()
enum Status: string
{
case Active = 'active';
case Inactive = 'inactive';
case Pending = 'pending';
}
// from() — throws ValueError if value not found
$status = Status::from('active'); // Status::Active
// Status::from('unknown'); // ValueError!
// tryFrom() — returns null if value not found
$status = Status::tryFrom('active'); // Status::Active
$status = Status::tryFrom('unknown'); // null
// Get backed value
echo Status::Active->value; // "active"
echo Status::Active->name; // "Active"
// Use in match
function statusLabel(Status $status): string
{
return match ($status) {
Status::Active => 'Active',
Status::Inactive => 'Disabled',
Status::Pending => 'Awaiting review',
};
}
// Integer-backed enum
enum HttpCode: int
{
case Ok = 200;
case NotFound = 404;
case ServerError = 500;
}
echo HttpCode::NotFound->value; // 404
$code = HttpCode::from(200); // HttpCode::Ok
Throwable -- корневой интерфейс ошибок
<?php
declare(strict_types=1);
// Throwable is the base interface for all Exceptions and Errors
// You CANNOT implement Throwable directly — extend Exception or Error
// Throwable interface methods:
// getMessage(): string
// getCode(): int
// getFile(): string
// getLine(): int
// getTrace(): array
// getTraceAsString(): string
// getPrevious(): ?Throwable
// Catch everything throwable
try {
// Some code that might throw
throw new \RuntimeException('Something went wrong', 42);
} catch (\Throwable $e) {
echo get_class($e); // "RuntimeException"
echo $e->getMessage(); // "Something went wrong"
echo $e->getCode(); // 42
echo $e->getFile(); // current file
echo $e->getLine(); // line of throw
echo $e->getTraceAsString(); // stack trace
}
// Catching specific types
try {
$result = json_decode('invalid', flags: JSON_THROW_ON_ERROR);
} catch (\JsonException $e) {
// Specific JSON error
} catch (\RuntimeException $e) {
// Any runtime exception
} catch (\Exception $e) {
// Any user exception
} catch (\Error $e) {
// Internal PHP error (TypeError, etc.)
} catch (\Throwable $e) {
// Absolutely everything
}
// Exception chaining with getPrevious()
try {
try {
throw new \RuntimeException('Original error');
} catch (\RuntimeException $e) {
throw new \DomainException('Domain error', 0, $e);
}
} catch (\DomainException $e) {
echo $e->getMessage(); // "Domain error"
echo $e->getPrevious()->getMessage(); // "Original error"
}
Практический пример: Collection класс
<?php
declare(strict_types=1);
/**
* Type-safe generic-like collection.
*
* @template T
* @implements \IteratorAggregate<int, T>
*/
final class Collection implements
\IteratorAggregate,
\Countable,
\ArrayAccess,
\JsonSerializable,
\Stringable
{
/** @var array<int, T> */
private array $items;
/**
* @param array<int, T> $items
*/
public function __construct(array $items = [])
{
$this->items = array_values($items);
}
// --- IteratorAggregate ---
/** @return \ArrayIterator<int, T> */
public function getIterator(): \ArrayIterator
{
return new \ArrayIterator($this->items);
}
// --- Countable ---
public function count(): int
{
return count($this->items);
}
// --- ArrayAccess ---
public function offsetExists(mixed $offset): bool
{
return isset($this->items[$offset]);
}
/** @return T */
public function offsetGet(mixed $offset): mixed
{
if (!isset($this->items[$offset])) {
throw new \OutOfRangeException("Index $offset out of range");
}
return $this->items[$offset];
}
/** @param T $value */
public function offsetSet(mixed $offset, mixed $value): void
{
if ($offset === null) {
$this->items[] = $value;
} else {
$this->items[$offset] = $value;
}
}
public function offsetUnset(mixed $offset): void
{
unset($this->items[$offset]);
$this->items = array_values($this->items);
}
// --- JsonSerializable ---
/** @return array<int, T> */
public function jsonSerialize(): array
{
return $this->items;
}
// --- Stringable ---
public function __toString(): string
{
return sprintf('Collection(%d items)', count($this->items));
}
// --- Collection methods ---
/**
* @template U
* @param callable(T): U $fn
* @return Collection<U>
*/
public function map(callable $fn): self
{
return new self(array_map($fn, $this->items));
}
/**
* @param callable(T): bool $fn
* @return Collection<T>
*/
public function filter(callable $fn): self
{
return new self(array_values(array_filter($this->items, $fn)));
}
/**
* @return T
*/
public function first(): mixed
{
if (count($this->items) === 0) {
throw new \UnderflowException('Collection is empty');
}
return $this->items[0];
}
/**
* @return T
*/
public function last(): mixed
{
if (count($this->items) === 0) {
throw new \UnderflowException('Collection is empty');
}
return $this->items[count($this->items) - 1];
}
public function isEmpty(): bool
{
return count($this->items) === 0;
}
/**
* @return array<int, T>
*/
public function toArray(): array
{
return $this->items;
}
}
// Usage
$numbers = new Collection([5, 3, 8, 1, 9, 2]);
// IteratorAggregate: foreach
foreach ($numbers as $n) {
echo "$n ";
}
// 5 3 8 1 9 2
// Countable: count()
echo count($numbers); // 6
// ArrayAccess: []
echo $numbers[0]; // 5
$numbers[] = 7; // append
echo count($numbers); // 7
// JsonSerializable: json_encode()
echo json_encode($numbers); // [5,3,8,1,9,2,7]
// Stringable: echo
echo $numbers; // "Collection(7 items)"
// Collection methods
$evens = $numbers->filter(fn(int $n): bool => $n % 2 === 0);
echo count($evens); // 2
$doubled = $numbers->map(fn(int $n): int => $n * 2);
echo $doubled[0]; // 10