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]