MidТеория8 min

Итерация объектов

Iterator, IteratorAggregate, ArrayAccess, Countable, JsonSerializable, Stringable

foreach по объекту

По умолчанию foreach итерирует по видимым (public) свойствам объекта.

<?php
declare(strict_types=1);

class User
{
    public string $name = 'Alice';
    public int $age = 30;
    public string $email = '[email protected]';
    protected string $password = 'secret';  // NOT visible
    private int $id = 1;                     // NOT visible
}

$user = new User();

// Iterates ONLY public properties
foreach ($user as $key => $value) {
    echo "{$key}: {$value}\n";
}
// name: Alice
// age: 30
// email: [email protected]
// password and id are NOT iterated (not public)

// Inside the class — iterates ALL properties
class InternalIterator
{
    public string $public = 'pub';
    protected string $protected = 'prot';
    private string $private = 'priv';

    public function iterateAll(): void
    {
        foreach ($this as $key => $value) {
            echo "{$key}: {$value}\n";
        }
    }
}

$obj = new InternalIterator();
$obj->iterateAll();
// public: pub
// protected: prot
// private: priv

Интерфейс Iterator

Интерфейс Iterator дает полный контроль над итерацией.

<?php
declare(strict_types=1);

/**
 * @implements Iterator<int, string>
 */
class FileLineIterator implements Iterator
{
    private int $lineNumber = 0;
    private ?string $currentLine = null;

    /** @var resource|false */
    private mixed $handle;

    public function __construct(
        private readonly string $filePath,
    ) {
        $this->handle = fopen($filePath, 'r');
        if ($this->handle === false) {
            throw new RuntimeException("Cannot open file: {$filePath}");
        }
        $this->readLine();
    }

    public function __destruct()
    {
        if (is_resource($this->handle)) {
            fclose($this->handle);
        }
    }

    // Return current element
    public function current(): string
    {
        return $this->currentLine ?? '';
    }

    // Return key of current element
    public function key(): int
    {
        return $this->lineNumber;
    }

    // Move forward to next element
    public function next(): void
    {
        $this->lineNumber++;
        $this->readLine();
    }

    // Rewind to first element
    public function rewind(): void
    {
        rewind($this->handle);
        $this->lineNumber = 0;
        $this->readLine();
    }

    // Check if current position is valid
    public function valid(): bool
    {
        return $this->currentLine !== null;
    }

    private function readLine(): void
    {
        $line = fgets($this->handle);
        $this->currentLine = $line !== false ? rtrim($line, "\n\r") : null;
    }
}

// Usage — reads file line by line (memory efficient)
$lines = new FileLineIterator('/etc/hosts');
foreach ($lines as $number => $line) {
    echo "{$number}: {$line}\n";
}

Порядок вызова методов Iterator

<?php
declare(strict_types=1);

/**
 * @implements Iterator<int, string>
 */
class DebugIterator implements Iterator
{
    private int $position = 0;
    private array $data = ['first', 'second', 'third'];

    public function rewind(): void
    {
        echo "rewind()\n";
        $this->position = 0;
    }

    public function valid(): bool
    {
        $valid = $this->position < count($this->data);
        echo "valid() → " . ($valid ? 'true' : 'false') . "\n";
        return $valid;
    }

    public function current(): string
    {
        echo "current()\n";
        return $this->data[$this->position];
    }

    public function key(): int
    {
        echo "key()\n";
        return $this->position;
    }

    public function next(): void
    {
        echo "next()\n";
        $this->position++;
    }
}

// foreach calls methods in this order:
$it = new DebugIterator();
foreach ($it as $key => $value) {
    echo "--- {$key}: {$value} ---\n";
}

// Output:
// rewind()
// valid() → true
// current()
// key()
// --- 0: first ---
// next()
// valid() → true
// current()
// key()
// --- 1: second ---
// next()
// valid() → true
// current()
// key()
// --- 2: third ---
// next()
// valid() → false

Запомни: Порядок вызовов: rewind() -> valid() -> current() -> key() -> тело цикла -> next() -> valid() -> ... Если valid() вернет false, цикл завершается.

Интерфейс IteratorAggregate

IteratorAggregate -- упрощенная альтернатива Iterator. Нужно реализовать только один метод getIterator().

<?php
declare(strict_types=1);

/**
 * @template T
 * @implements IteratorAggregate<int, T>
 */
class Collection implements IteratorAggregate
{
    /** @var list<T> */
    private array $items;

    /**
     * @param list<T> $items
     */
    public function __construct(array $items = [])
    {
        $this->items = array_values($items);
    }

    /**
     * @return ArrayIterator<int, T>
     */
    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator($this->items);
    }

    /**
     * @param T $item
     */
    public function add(mixed $item): void
    {
        $this->items[] = $item;
    }

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

$users = new Collection(['Alice', 'Bob', 'Charlie']);
$users->add('Diana');

// IteratorAggregate makes this work:
foreach ($users as $index => $name) {
    echo "{$index}: {$name}\n";
}
// 0: Alice
// 1: Bob
// 2: Charlie
// 3: Diana

yield в getIterator()

<?php
declare(strict_types=1);

/**
 * @implements IteratorAggregate<int, int>
 */
class NumberRange implements IteratorAggregate
{
    public function __construct(
        private readonly int $start,
        private readonly int $end,
    ) {
    }

    public function getIterator(): Generator
    {
        for ($i = $this->start; $i <= $this->end; $i++) {
            yield $i;
        }
    }
}

$range = new NumberRange(1, 5);
foreach ($range as $number) {
    echo $number;  // 12345
}

// Lazy filtering with Generator
/**
 * @implements IteratorAggregate<int, mixed>
 */
class FilteredCollection implements IteratorAggregate
{
    public function __construct(
        private readonly iterable $source,
        private readonly Closure $predicate,
    ) {
    }

    public function getIterator(): Generator
    {
        foreach ($this->source as $key => $value) {
            if (($this->predicate)($value, $key)) {
                yield $key => $value;
            }
        }
    }
}

$numbers = new NumberRange(1, 20);
$evens = new FilteredCollection($numbers, fn(int $n): bool => $n % 2 === 0);

foreach ($evens as $n) {
    echo "{$n} ";  // 2 4 6 8 10 12 14 16 18 20
}

Traversable

Traversable -- базовый интерфейс, который нельзя реализовать напрямую. Он объединяет Iterator и IteratorAggregate.

<?php
declare(strict_types=1);

// Traversable CANNOT be implemented directly
// class Bad implements Traversable {}  // Fatal error!

// Must implement Iterator OR IteratorAggregate
class Good implements IteratorAggregate
{
    public function getIterator(): ArrayIterator
    {
        return new ArrayIterator([1, 2, 3]);
    }
}

// Type checking
function processTraversable(Traversable $items): void
{
    foreach ($items as $item) {
        echo $item;
    }
}

// iterable = array | Traversable
function processIterable(iterable $items): void
{
    foreach ($items as $item) {
        echo $item;
    }
}

// Both accept Traversable objects
processTraversable(new Good());
processIterable(new Good());
processIterable([1, 2, 3]);  // Also accepts arrays

ArrayAccess

Интерфейс ArrayAccess позволяет обращаться к объекту как к массиву.

<?php
declare(strict_types=1);

/**
 * @template TKey of string
 * @template TValue
 * @implements ArrayAccess<TKey, TValue>
 */
class Config implements ArrayAccess
{
    /** @var array<TKey, TValue> */
    private array $data;

    /**
     * @param array<TKey, TValue> $data
     */
    public function __construct(array $data = [])
    {
        $this->data = $data;
    }

    public function offsetExists(mixed $offset): bool
    {
        return isset($this->data[$offset]);
    }

    public function offsetGet(mixed $offset): mixed
    {
        if (!$this->offsetExists($offset)) {
            throw new OutOfBoundsException("Key not found: {$offset}");
        }
        return $this->data[$offset];
    }

    public function offsetSet(mixed $offset, mixed $value): void
    {
        if ($offset === null) {
            throw new InvalidArgumentException('Key is required');
        }
        $this->data[$offset] = $value;
    }

    public function offsetUnset(mixed $offset): void
    {
        unset($this->data[$offset]);
    }
}

$config = new Config(['host' => 'localhost', 'port' => 5432]);

// Array-like access
echo $config['host'];       // 'localhost'
echo $config['port'];       // 5432

// Modify
$config['debug'] = true;

// Check existence
var_dump(isset($config['host']));   // true
var_dump(isset($config['missing'])); // false

// Delete
unset($config['debug']);

Countable

<?php
declare(strict_types=1);

class UserList implements Countable
{
    /** @var list<array{name: string, email: string}> */
    private array $users = [];

    public function add(string $name, string $email): void
    {
        $this->users[] = ['name' => $name, 'email' => $email];
    }

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

$list = new UserList();
$list->add('Alice', '[email protected]');
$list->add('Bob', '[email protected]');

// count() function works with Countable objects
echo count($list);  // 2

// Also works in conditions
if (count($list) > 0) {
    echo "Has users\n";
}

JsonSerializable

<?php
declare(strict_types=1);

class ApiResponse implements JsonSerializable
{
    public function __construct(
        private readonly bool $success,
        private readonly mixed $data,
        private readonly ?string $error = null,
    ) {
    }

    public function jsonSerialize(): mixed
    {
        $result = [
            'success' => $this->success,
            'data' => $this->data,
        ];

        if ($this->error !== null) {
            $result['error'] = $this->error;
        }

        return $result;
    }
}

$response = new ApiResponse(true, ['users' => ['Alice', 'Bob']]);
echo json_encode($response, JSON_PRETTY_PRINT);
// {
//     "success": true,
//     "data": {
//         "users": ["Alice", "Bob"]
//     }
// }

// Nested JsonSerializable
class Money implements JsonSerializable
{
    public function __construct(
        private readonly int $amount,
        private readonly string $currency,
    ) {
    }

    public function jsonSerialize(): mixed
    {
        return [
            'amount' => $this->amount / 100,
            'currency' => $this->currency,
        ];
    }
}

$order = new ApiResponse(true, [
    'total' => new Money(4999, 'USD'),
    'items' => 3,
]);

echo json_encode($order, JSON_PRETTY_PRINT);
// {
//     "success": true,
//     "data": {
//         "total": {
//             "amount": 49.99,
//             "currency": "USD"
//         },
//         "items": 3
//     }
// }

Stringable (PHP 8.0+)

<?php
declare(strict_types=1);

// Stringable is automatically implemented by any class with __toString
class Email implements Stringable
{
    public function __construct(
        private readonly 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]');

// Automatically works in string context
echo $email;                // [email protected]
echo "Contact: {$email}";  // Contact: [email protected]

// Type hint with Stringable
function notify(string|Stringable $recipient): void
{
    echo "Sending to: {$recipient}\n";
}

notify('[email protected]');   // String — OK
notify($email);           // Stringable — OK

// NOTE: Any class with __toString() implicitly implements Stringable
class Implicit
{
    public function __toString(): string
    {
        return 'implicit';
    }
}

var_dump(new Implicit() instanceof Stringable);  // true

Комбинирование интерфейсов

<?php
declare(strict_types=1);

/**
 * Full-featured collection class combining multiple interfaces.
 *
 * @template T
 * @implements IteratorAggregate<int, T>
 * @implements ArrayAccess<int, T>
 */
final class TypedCollection implements
    IteratorAggregate,
    ArrayAccess,
    Countable,
    JsonSerializable,
    Stringable
{
    /** @var list<T> */
    private array $items;

    /**
     * @param list<T> $items
     */
    public function __construct(array $items = [])
    {
        $this->items = array_values($items);
    }

    // IteratorAggregate
    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]);
    }

    public function offsetGet(mixed $offset): mixed
    {
        return $this->items[$offset] ?? throw new OutOfBoundsException(
            "Index {$offset} does not exist"
        );
    }

    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);  // Re-index
    }

    // JsonSerializable
    public function jsonSerialize(): array
    {
        return $this->items;
    }

    // Stringable
    public function __toString(): string
    {
        return sprintf('Collection(%d items)', $this->count());
    }

    // Additional helper methods

    /**
     * @param Closure(T): bool $predicate
     * @return static<T>
     */
    public function filter(Closure $predicate): static
    {
        return new static(array_values(array_filter($this->items, $predicate)));
    }

    /**
     * @template U
     * @param Closure(T): U $callback
     * @return static<U>
     */
    public function map(Closure $callback): static
    {
        return new static(array_map($callback, $this->items));
    }

    public function isEmpty(): bool
    {
        return $this->count() === 0;
    }
}

// Usage
$numbers = new TypedCollection([1, 2, 3, 4, 5]);

// As iterable
foreach ($numbers as $n) {
    echo $n;  // 12345
}

// As array
echo $numbers[0];      // 1
$numbers[] = 6;        // Add
echo count($numbers);  // 6

// As JSON
echo json_encode($numbers);  // [1,2,3,4,5,6]

// As string
echo $numbers;  // Collection(6 items)

// Chainable operations
$evens = $numbers
    ->filter(fn(int $n): bool => $n % 2 === 0)
    ->map(fn(int $n): int => $n * 10);

echo json_encode($evens);  // [20,40,60]

Проверь себя

5 из 10

Какой интерфейс позволяет использовать `$obj['key']` синтаксис?

Чем `IteratorAggregate` отличается от `Iterator`?

Что делает `JsonSerializable`?

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

Какой порядок вызова методов Iterator в `foreach`?