Поиск по сайту:

Как написать свои собственные итерируемые объекты на PHP


PHP позволяет создавать итерируемые объекты. Их можно использовать в циклах вместо скалярных массивов. Итерации обычно используются в качестве коллекций объектов. Они позволяют вам указывать этот объект, сохраняя при этом поддержку циклов.

Простая итерация

Для перебора массива в PHP используется цикл foreach:

foreach (["1", "2", "3"] as $i) {
    echo ($i . " ");
}

В этом примере выдается 1 2 3.

Вы также можете перебирать объект:

$cls = new StdClass();
$cls -> foo = "bar";
foreach ($cls as $i) {
    echo $i;
}

В этом примере будет выведен bar.

Проблема сбора

Для базовых классов с общедоступными свойствами хорошо работает простой foreach. Теперь рассмотрим другой класс:

class UserCollection {
 
    protected array $items = [];
 
    public function add(UserDomain $user) : void {
        $this -> items[] = $user;
    }
 
    public function containsAnAdmin() : bool {
        return (count(array_filter(
            $this -> items,
            fn (UserDomain $i) : bool => $i -> isAdmin()
        )) > 0);
    }
 
}

Этот класс представляет собой набор экземпляров UserDomain. Поскольку PHP не поддерживает типизированные массивы, подобные классы необходимы, когда вы хотите указать тип массива, который может содержать только один тип значения. Коллекции также помогают создавать служебные методы, такие как containsWithAdmin, которые облегчают естественное взаимодействие с элементами массива.

К сожалению, попытка перебрать эту коллекцию не даст желаемых результатов. В идеале итерация должна работать с массивом $items, а не с самим классом.

Реализация итератора

Естественную итерацию можно добавить с помощью интерфейса Iterator. Внедрив Iterator, вы можете определить поведение PHP, когда экземпляры вашего класса используются с foreach.

Iterator имеет пять методов, которые вам необходимо реализовать:

  • current() : смешанный — получить элемент в текущей позиции в итерации.
  • key() : скаляр — получить ключ в текущей позиции в итерации.
  • next() : void — перейти к следующей позиции в итерации.
  • rewind() : void — перемотать позицию назад к началу итерации.
  • valid() : bool — узнать, имеет ли текущая позиция значение.

Поначалу эти методы могут сбивать с толку. Однако реализовать их несложно — вы указываете, что делать на каждом этапе выполнения foreach.

Каждый раз, когда ваш объект используется с foreach, будет вызываться rewind(). Затем вызывается метод valid(), сообщающий PHP, есть ли значение в текущей позиции. Если есть, вызываются current() и key(), чтобы получить значение и ключ в этой позиции. Наконец, вызывается метод next() для перемещения указателя позиции. Цикл возвращается к вызову valid(), чтобы узнать, доступен ли другой элемент.

Вот типичная реализация Iterator:

class DemoIterator {
 
    protected int $position = 0;
 
    protected array $items = ["cloud", "savvy"];
 
    public function rewind() : void {
        echo "Rewinding";
        $this -> position = 0;
    }
 
    public function current() : string {
        echo "Current";
        return $this -> items[$this -> position];
    }
 
    public function key() : int {
        echo "Key";
        return $this -> position;
    }
 
    public function next() : void {
        echo "Next";
        ++$this -> position;
    }
 
    public function valid() : void {
        echo "Valid";
        return isset($this -> items[$this -> position]);
    }
 
}

Вот что произойдет при повторении DemoIterator:

$i = new DemoIterator();
foreach ($i as $key => $value) {
    echo "$key $value";
}
 
// EMITS:
// 
// Rewind
// Valid Current Key
// 0 cloud
// Next
// 
// Valid Current Key
// 1 savvy
// Next
// 
// Valid

Ваш итератор должен поддерживать запись позиции цикла, проверять, есть ли элемент в текущей позиции цикла (через valid()) и возвращать ключ и значение в текущей позиции.

PHP не будет пытаться получить доступ к ключу или значению, если позиция цикла неверна. Возврат false из valid() немедленно завершает цикл foreach. Как правило, это будет, когда вы доберетесь до конца массива.

Итераторагрегат

Написание итераторов быстро становится повторяющимся. Большинство следуют точному рецепту, показанному выше. IteratorAggregate — это интерфейс, который помогает быстро создавать итерируемые объекты.

Реализуйте метод getIterator() и верните Traversable (базовый интерфейс Iterator). Он будет использоваться как итератор, когда ваш объект используется с foreach. Обычно это самый простой способ добавить итерацию в класс коллекции:

class UserCollection implements IteratorAggregate {
 
    protected array $items = [];
 
    public function add(UserDomain $User) : void {
        $this -> items[] = $user;
    }
 
    public function getIterator() : Traversable {
        return new ArrayIterator($this -> items);
    }
 
}
 
$users = new UserCollection();
$users -> add(new UserDomain("James"));
$users -> add(new UserDomain("Demo"));
 
foreach ($users as $user) {
    echo $user -> Name;
}

При работе с коллекциями обычно достаточно трех строк кода для настройки итерации! ArrayIterator возвращается как Traversable. Это класс, который автоматически создает Iterator из массива. Теперь вы можете перебирать логические значения внутри вашего объекта, а не прямые свойства объекта.

Использование встроенных итераторов PHP

Вам нужно будет написать свои собственные итераторы, если у вас сложная логика. Редко приходится начинать с нуля, так как PHP поставляется с несколькими расширенными итераторами, предоставляемыми SPL.

Встроенные классы включают DirectoryIterator, FilesystemIterator, GlobIterator и различные рекурсивные итераторы. Вот некоторые из наиболее полезных универсальных итераторов.

Лимититератор

LimitIterator позволяет перебирать подмножество массива. Вам не нужно сначала объединять массив или вручную отслеживать позицию внутри вашего foreach.

$arr = new ArrayIterator(["a", "b", "c", "d"]);
foreach (new LimitIterator($arr, 0, 2) as $val) {
    echo $val;
}

В этом примере выдается a b. Обратите внимание, что LimitIterator принимает другой Iterator, а не массив. В примере используется ArrayIterator, встроенный класс, который создает Iterator из массива.

Бесконечный итератор

InfiniteIterator никогда не завершает цикл, поэтому вам нужно прервать его вручную. В противном случае итератор автоматически возвращается к началу массива, когда он достигает конца.

Этот итератор особенно полезен при работе со значениями, зависящими от времени. Вот простой способ создания трехлетнего календаря, в котором также используется LimitIterator, описанный выше:

$months = [
    "Jan", "Feb", "Mar", "Apr", "May", "Jun",
    "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"
];
$infinite = new InfiniteIterator(new ArrayIterator($months));
foreach (new LimitIterator($infinite, 0, 36) as $month) {
    echo $month;
}

Это испускает три года на месяцы.

Фильтритератор

FilterIterator — это абстрактный класс, который вы должны расширить. Реализуйте метод accept для фильтрации нежелательных значений, которые следует пропустить во время итерации.

class DemoFilterIterator extends FilterIterator {
 
    public function __construct() {
        parent::__construct(new ArrayIterator([1, 10, 4, 6, 3]));
    }
 
    public function accept() {
        return ($this -> getInnerIterator() -> current() < 5);
    }
 
}
 
$demo = new DemoFilterIterator();
 
foreach ($demo as $val) {
    echo $val;
}

В этом примере выдается 1 4 3.

Значения массива выше 5 отфильтровываются и не отображаются в foreach.

«Итерируемый» тип

Иногда вы можете написать универсальную функцию, которая использует цикл foreach, но ничего не знает о значениях, которые она будет перебирать. Одним из примеров является абстрактный обработчик ошибок, который просто выводит полученные значения.

Вы можете указать iterable в этих сценариях. Это псевдотип, который принимает либо массив, либо любой объект, реализующий Traversable:

function handleBadValues(iterable $values) : void {
    foreach ($values as $value) {
        var_dump($value);
    }
}

Тип iterable настолько расплывчатый, что вам следует хорошенько подумать, прежде чем его использовать. Тем не менее, он может быть полезен в качестве последнего средства, если вам нужно гарантировать, что входное значение будет работать с foreach.

Заключение

Использование итераторов помогает вам писать чистый, более модульный код. Вы можете переместить методы, которые воздействуют на массивы объектов, в специальные классы коллекций. Затем их можно указывать по отдельности, оставаясь при этом полностью совместимыми с foreach.

В большинстве случаев добавить поддержку итераций можно путем реализации IteratorAggregate и возврата ArrayIterator, сконфигурированного с элементами вашей коллекции. Другие типы итераторов PHP, которые, как правило, не используются разработчиками, могут значительно упростить более специфические циклы. Они предлагают сложное логическое поведение, не требуя ручного отслеживания указателя в операторе foreach.




Все права защищены. © Linux-Console.net • 2019-2024