MidТеория9 min

Встроенные интерфейсы

Iterator, IteratorAggregate, ArrayAccess, Countable, Stringable, JsonSerializable, Throwable, BackedEnum

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

Проверь себя

5 из 11

Можно ли напрямую реализовать интерфейс Throwable в пользовательском классе?

Какой метод требует интерфейс IteratorAggregate?

Какой метод BackedEnum позволяет безопасно создать enum из значения без исключения?

Что произойдёт при вызове Status::from('unknown') на BackedEnum?

Можно ли напрямую реализовать интерфейс Traversable?