Iterator, ArrayAccess, Countable: Объект как массив

    0. Intro.


    В стандартной поставке php имеются 2 интересных интерфейса, позволяющие значительно изменять поведение объектов в языке.
    Это Iterator и ArrayAccess. Первый позволяет итерировать объект через такие конструкции each, foreach, for. Второй же, в свою очередь, позволяет обращаться к объекту, как к массиву применяя привычное $array[] = 'newItem'. Соответственно, для полноценной эмуляции массива, объект обязан заимплементить оба интерфейса.

    1. Iterator.


    Iterator (он же Cursor) является поведенческим шаблоном проектирования. В php представлен интерфейсом Iterator и требует реализации следующих методов:
    • public function rewind() — сброс указателя на нулевую позицию;
    • public function current() — возврат текущего значения;
    • public function key() — возврат ключа текущего элемента;
    • public function next() — сдвиг к следующему элементу;
    • public function valid() — должен вызываться после Iterator::rewind() или Iterator::next() для проверки, является ли валидной текущая позиция.

    Соответственно, эти методы являются аналогами обычных reset(), current(), key(), next().

    Пример 1:
    
    class Iteratable implements Iterator 
    {
        protected $_position = 0;
        protected $_container = array (
            'item1', 'item2', 'item3'
        );  
    
        public function __construct() 
        {
            $this->_position = 0;
        }
    
        public function rewind() 
        {
            $this->_position = 0;
        }
    
        public function current() 
        {
            return $this->_container[$this->_position];
        }
    
        public function key() 
        {
            return $this->_position;
        }
    
        public function next() 
        {
            ++$this->_position;
        }
    
        public function valid() 
        {
            return isset($this->_container[$this->_position]);
        }
    }
    
    $iteratable = new Iteratable;
    foreach ($iteratable as $item) {
        var_dump($iteratable->key(), $item);
    }
    


    Но текущий класс все еще не является псевдомассивом. Сейчас он все еще не дает возможности изменять значения, которые он содержит.

    2. ArrayAccess.


    Реализация этого интерфейса позволит уже обратиться к объекту как к массиву любой из доступных функций. Интерфейс содержит 4 абстрактных метода:
    • abstract public boolean offsetExists(mixed $offset) — существует ли значение по заданному ключу;
    • abstract public mixed offsetGet(mixed $offset) — получить значение по индексу;
    • abstract public void offsetSet(mixed $offset, mixed $value) — установить значение с указанием индекса;
    • abstract public void offsetUnset(mixed $offset) — удалить значение.


    Пример 2:
    
    class ArrayAccessable implements ArrayAccess 
    {
        protected $_container = array();
    
        public function __construct($array = null)
        {
            if (!is_null($array)) {
                $this->_container = $array;
            }
        }
    
        public function offsetExists($offset)
        {
            return isset($this->_container[$offset]);
        }
    
        public function offsetGet($offset)
        {
            return $this->offsetExists($offset) ? $this->_container[$offset] : null;
        }
    
        public function offsetSet($offset, $value)
        {
            if (is_null($offset)) {
                $this->_container[] = $value;
            } else {
                $this->_container[$offset] = $value;
            }
        }
    
        public function offsetUnset($offset)
        {
            unset($this->_container[$offset]);
        }
    }
    
    $array = new ArrayAccessable(array('a', 'b', 'c', 'd', 'e' => 'assoc'));
    var_dump($array);
    
    unset($array['e']);
    var_dump('unset: ', $array);
    
    $array['meta'] = 'additional element';
    var_dump('set: ', $array);
    
    var_dump(count($array));
    


    Теперь экземпляр класса ArrayAccessable работает как массив. Но count() по прежнему возвращает 1 (почему так? см. http://www.php.net/manual/en/function.count.php).

    3. Countable.


    Интерфейс содержит всего-то один метод, который создан для использования с count().
    • abstract public int count ( void ) — количество элементов объекта.


    Пример 3.
    
    class CountableObject implements Countable
    {
        protected $_container = array('a', 'b', 'c', 'd');
    
        public function count()
        {
            return count($this->_container);
        }
    }
    
    $countable = new CountableObject;
    var_dump(count($countable));
    

    Но наш объект все еще сериализируется как объект, а не массив…

    4. Serializable.


    Интерфейс, позволяющий переопределять способ сериализации объекта.
    Содержит 2 метода с говорящими названиями:
    • abstract public string serialize(void);
    • abstract public mixed unserialize(string $serialized).


    Пример 4.
    
    class SerializableObject implements Serializable    
    {    
        protected $_container = array('a', 'b', 'c', 'd');    
        
        public function serialize()    
        {    
            return serialize($this->_container);    
        }    
        
        public function unserialize($data)    
        {    
            $this->_container = unserialize($data);    
        }    
    }    
        
    $serializable = new SerializableObject;    
    var_dump($serializable); // SerializableObject    
        
    file_put_contents('serialized.txt', serialize($serializable));    
        
    $unserialized = unserialize(file_get_contents('serialized.txt'));    
    var_dump($unserialized); // SerializableObject
    


    Теперь объект сериализирует только данные, а не самого себя.

    5. Итоги.


    Объединяя описанные выше классы в один, мы получаем уже объект, который ведет себя как массив.
    Единственный недостаток заключается в том, что функции типа array_pop() не будут с ним работать.
    В качестве решения можно использовать новый магический метод из php 5.3 __invoke(), который позволит вызвать объект как функцию и таким образом заставить эти функции работать.
    
    public function __invoke(array $data = null)    
    {    
    	if (is_null($data)) {    
    		return $this->_container;    
    	} else {    
    		$this->_container = $data;    
    	}    
    }    
        
    $array = new SemiArray(array('a', 'b', 'c', 'd', 'e' => 'assoc'));    
    $tmp = $array();    
    array_pop($tmp);    
    $array($tmp);    
    var_dump('array_pop', $array);
    


    Вариант подпорочный, другие варианты жду в ваших комментах.
    Полный листинг полученного класса:
    
    class SemiArray implements ArrayAccess, Countable, Iterator, Serializable    
    {    
        protected $_container = array();    
        protected $_position = 0;    
        
        public function __construct(array $array = null)    
        {    
            if (!is_null($array)) {    
                $this->_container = $array;    
            }    
        }    
        
        public function offsetExists($offset)    
        {    
            return isset($this->_container[$offset]);    
        }    
        
        public function offsetGet($offset)    
        {    
            return $this->offsetExists($offset) ? $this->_container[$offset] : null;    
        }    
        
        public function offsetSet($offset, $value)    
        {    
            if (is_null($offset)) {    
                $this->_container[] = $value;    
            } else {    
                $this->_container[$offset] = $value;    
            }    
        }    
        
        public function offsetUnset($offset)    
        {    
            unset($this->_container[$offset]);    
        }    
        
        public function rewind()    
        {    
            $this->_position = 0;    
        }    
        
        public function current()     
        {    
            return $this->_container[$this->_position];    
        }    
        
        public function key()     
        {    
            return $this->_position;    
        }    
        
        public function next()     
        {    
            ++$this->_position;    
        }    
        
        public function valid()     
        {    
            return isset($this->_container[$this->_position]);    
        }    
        
        public function count()    
        {    
            return count($this->_container);    
        }    
        
        public function serialize()    
        {    
            return serialize($this->_container);    
        }    
        
        public function unserialize($data)    
        {    
            $this->_container = unserialize($data);    
        }    
        
        public function __invoke(array $data = null)    
        {    
            if (is_null($data)) {    
                return $this->_container;    
            } else {    
                $this->_container = $data;    
            }    
        }    
    }
    

    Тестируем:
    
    $array = new SemiArray(array('a', 'b', 'c', 'd', 'e' => 'assoc'));
    var_dump($array);
    
    $array->next();
    var_dump('advanced key: ', $array->key());
    
    unset($array['e']);
    var_dump('unset: ', $array);
    
    $array['meta'] = 'additional element';
    var_dump('set: ', $array);
    
    echo 'count: ';
    var_dump(count($array));
    
    file_put_contents('serialized.txt', serialize($array));
    
    echo 'unserialized:';
    var_dump($array); // SemiArray
    $array = unserialize(file_get_contents('serialized.txt'));
    
    $tmp = $array();
    array_pop($tmp);
    $array($tmp);
    var_dump('array_pop', $array);
    

    6. Область применения.


    6.1. Например, в результатах выборки из БД.

    Warning! Псевдокод.
    
    class Rowset extends SemiArray    
    {    
        protected $_total;    
            
        public function __construct()    
        {    
            $this->_total = $db->query('SELECT FOUND_ROWS() as total')->total;    
        }    
        
        public function getTotal()    
        {    
            return $this->_total;    
        }    
    }    
        
    $rowset = new Rowset();    
    while ($obj = mysql_fetch_object($res)) {    
        $rowset[] = $obj;    
    }    
        
    foreach ($rowset as $row) {    
        // ...    
    }    
    $rowset->getTotal(); // total row count
    


    7. Outro.


    Статья была написана в образовательных целей, а реализация уже доступна в php в build-in классе ArrayObject (http://www.php.net/manual/en/class.arrayobject.php).
    Поделиться публикацией
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 39
    • +4
      Это лучше в доку по php запостить чем сюда, если, конечно, это кусок еще не перевели на русский.
      • +3
        Я бы хотел добавить пару комментариев к вашему коду:

        Класс Iteratable:

        public function __construct() 
        {
            $this->_position = 0;
        }
        
        public function rewind() 
        {
            $this->_position = 0;
        }

        Стоить заменить этот фрагмент на

        public function __construct() 
        {
            $this->rewind();
        }
        
        public function rewind() 
        {
            $this->_position = 0;
        }


        Это обеспечит единый интерфейс для сброса итератора. Да и вообще, код избыточен (так как задано свойство protected $_position = 0;).

        Класс ArrayAccessable

        public function __construct($array = null)
        {
            if (!is_null($array)) {
                $this->_container = $array;
            }
        }


        Мне больше нравится следующий подход:

        
        protected container;
        
        public function __construct() 
        {
            $argv = func_get_args();
            $argc = func_num_args();
        
            if (argc == 0)
            {
                $this->container = array();
                return;
            }
        
            if (        $argc == 1
                AND  is_array(argv[0])) 
            {
                $this->container = argv[0];
            } 
            else 
            {
                $this->container = argv;
            }
        }


        Это позволяет использовать более красивое создание объекта:

        // создадим контейнер из нескольких элементов
        $arrayObject = new ArrayAccessable('a', 'b', 'c', 'd', 'e' => 'assoc');
        // создадим контейнер из массива
        $array = array('a', 'b', 'c', 'd', 'e' => 'assoc');
        $arrayObject = new ArrayAccessable($array)


        Единственный недостаток заключается в том, что функции типа array_pop() не будут с ним работать.


        Я для себя эту проблему решил созданием методов, которые являются обертками над функциями для работы с массивами. Например:

                /**
                 * Метод возвращает контейнер со списком всех ключей объекта
                 *
                 * @return      Map             Контейнер со списком ключей объекта
                 */
                public function get_keys()
                {
                    return new self(array_keys($this->container));
                }
        • +1
          >>Я для себя эту проблему решил созданием методов, которые являются обертками над функциями для работы с массивами. Например:

          я стремился к более нативной реализации.
          • 0
            Ваш пример нельзя сократить с
            $tmp = $array();
            array_pop($tmp);
            $array($tmp);
            до
            $last = array_pop($array());
            это вызовет ошибку уровня E_STRICT. Данный подход, разумеется, имеет место, но методы-обертки позволяют использовать более короткий код
            $last = $array->pop();
            и цепочки методов
            $set_fields = $query_params->get_keys->implode(' = ?, ') . ' =? ';
            . Кроме того, вы вольны сами выбирать интерфейс объекта, что позволяет избавиться от legacy-кода SPL (который очень хорошо заметен в функциях работы со строками), и избавляетесь от одного из параметров вызова, который необходимо передавать в функцию.
          • 0
            Увы, но ассоциативные массивы так создать не получится. Я про код:
            • 0
              Извиняюсь, отправилось раньше, чем я успел дописать это:

              // создадим контейнер из нескольких элементов
              $arrayObject = new ArrayAccessable('a', 'b', 'c', 'd', 'e' => 'assoc');
            • 0
              а вы уверенны, что
              $arrayObject = new ArrayAccessable('a', 'b', 'c', 'd', 'e' => 'assoc');

              сработает?
              мне на
              var_dump('a' => 'b');
              выдает
              PHP Parse error: syntax error, unexpected T_DOUBLE_ARROW in Command line code on line 1
              Parse error: syntax error, unexpected T_DOUBLE_ARROW in Command line code on line 1
              • 0
                Да, не сработает. Это я скопировал пример массива из статьи. С одномерными массивами работает. Ждем сокращенного синтаксиса для объявления массивов в PHP 5.4
                • 0
                  в статье, кстати, new SemiArray(array('a', 'b', 'c', 'd', 'e' => 'assoc'));

                  многомерный массив сработает на ура.
                  • 0
                    пардон, $array = new ArrayAccessable(array('a', 'b', 'c', 'd', 'e' => 'assoc'));
                    • 0
                      Естественно, так как параметр конструктора напрямую передается в свойство класса. В моем примере для одномерных массивов можно использовать синтаксис
                      $array = new ArrayAccessable('a', 'b', 'c', 'd', 'e');
                      для многомерных
                      $array = new ArrayAccessable(array('a', 'b', 'c', 'd', 'e' => 'assoc'));
              • –3
                Кажется 100% обкатывалась эта тема. Много воды… лучше бы привели просто примеры использования.
                • 0
                  оно то как бы массив, но как только чуть глубже начать с этим работать начинается ад
                  $a = new ArrayAccessable;
                  $a['x'] = array('foo' => 'bar');
                  $a['x']['foo'] = 1;
                  это только то что сверху
                  очень полезного применения я этим интерфейсам не нашел, для чего то простого не плохо, что-то посерьезней — костыль на костыле, костылем погоняет
                  т.е. эти интерфейсы мало того что хуже по функциональности стандартных структур данных, так они даже не нормальное ООП
                • +2
                  ArrayObject — как вам мой вариант? :)
                  • –2
                    Outro-то я и не заметил :) Но образовательные цели тоже сомнительны, в php не так много стандартных интерфейсов чтобы посвящать им целые статьи.
                    • +1
                      Afaik, он значительно медлительнее, чем собственная реализация на php
                      • 0
                        Не, на самом деле это не так, «своя реализация» чего-бы то ни было на php — всегда медленнее чем встроенный вариант на уровне си, и последний раз когда я проверял — ArrayObject не был исключением, хотя это тоже может варьироваться от версии к версии, может и были с ним когда-то какие-нибудь проблемы.
                      • 0
                        Из того, что мне лично не нравится в это классе:
                        0. В NetBeans в отладчике не видно, что содержится в контейнере.
                        1. Слишком мало встроенных методов, при добавлении новых начинают возникать некоторые проблемы, приводящие к костылям:
                                /**
                                 * Метод возвращает первый элемент контейнера
                                 *
                                 * @return      mixed|null    Первый элемент контейнера. Если контейнер пуст - null
                                 */
                                public function get_first()
                                {
                                    foreach($this as $value)
                                    {
                                        return $value;
                                    }
                                }
                        
                                /**
                                 * Метод возвращает последний элемент контейнера
                                 *
                                 * @return      mixed|null    Последний элемент контейнера. Если контейнер пуст - null
                                 */
                                public function get_last()
                                {
                                    if ($this->count() == 0)
                                    {
                                        return;
                                    }
                        
                                    $iterator = $this->getIterator();
                        
                                    // перейдем к последнему элементу
                                    $iterator->seek($this->count() - 1);
                        
                                    return $iterator->current();
                                }
                        Замечу, что использование метода getArrayCopy() приводит к тормозам, в зависимости от размера контейнера.
                        2. Разработчики PHP не отходят от старых традиций. Метод, который стоило бы назвать push(), как в других языках, назвали append().
                        • 0
                          0. Не аргумент, в eclipse видно :)
                          1. Ну вот это конечно аргумент, добавить в него свои методы порой проблематично, действительно приходится дергать getArrayCopy(), с другой стороны те что уже есть работают быстрее.
                          2. Весь PHP такой, что уж поделать.
                      • 0
                        Уже достаточно долго пользуемся данной мулькой. В целом в некоторых моментах она очень полезна и удобна.

                        Очень сильно упрощает код во View скриптах:
                        — легко позволяет подкладывать фикстурные массивы вместо объекта, в тестах и отладке просто незаменим.
                        — удобная работа с кешами. В контроллере берем из кеша массив данных и отдаем вьюхе, если в кеше все устарело — берем объект, кешируем его в виде массива и отдаем вьюхе. Девелопер нафиг вырубает кеш и спокойно работает, на продакшене врубаем кеш и девелопер вообще не заморачивается.

                        В контроллерах и моделях редко этим делом пользуемся.
                        — так как offsetGet не позволяет нормально передавать ссылку на данные, а тупо копирует и возвращает данные (из-за этого возникают проблемы с $a['abc']['var'] = 1; Т.е. берутся данные по ключу 'abc', возвращается копия массива, а затем к этой копии применяется установка значения 'var' = 1. Можно лечит возвратом не массива как это в примере этой статьи, а объекта подложки, который внутри себя хранит ссылку на единый источник данных. Т.е. все вложенные массивы, необходимо подменить своим классом. Вобщем страшный костыль.)

                        Так что в целом MUST USE в задачах где объекты нужны в режиме READ ONLY.
                        • 0
                          ArrayObject не содержит Итераторов, замените в конце ссылку на ua2.php.net/manual/en/class.arrayiterator.php
                          • 0
                            Вы, кстати, почитайте другие статьи по теме, и немножечко поменяйте код, ато и недо массив и пере класс получается (http://habrahabr.ru/blogs/personal/48697/) =)
                            И еще одно замечание =) Фореч забирает при переборе эеземпляр отданого ему обьекта/массива, чтоб чтоб обьект ну уж точно вел себя как массив нужно еще переопределить метод getIterator (да, его нет в интерфейсах), тогда он будет поддерживать и вложенный проход по одному и тому же обьекту.
                            • 0
                              getIterator это из другого интерфейса просто — IteratorAggregate, который то же наследуется от Traversable
                              • 0
                                Traversable — пустой интерфейс (маркер), он лишь говорит о том что по обьекту можно пройтись, а каким способом уже зависит от примененного интерфейса. Если Вы зайдете в статью что я привел, там в каментах человек жалуется что вложенные циклы не работают с обьектом так как с массивом.
                                • 0
                                  Я к тому, что кроме Iterator есть IteratorAggregate.

                                  Если вы про комментарий «Не поддерживается несколько обходов одновременно», то это очевидно. Для такой поддержки можно использовать IteratorAggregate и каждый раз возвращать новый объект итератора. Тот же ArrayObject замечательно справляется с поставленной задачей (он как раз используется getIterator)
                          • 0
                            Странная реализация методов rewind() и next(). Как Вы полагаете заставит их работать на ассоциированных массивах? У меня не получилось:

                            $array = new SemiArray(array('a'=>'a', 'b'=>'b', 'c'=>'c', 'd'=>'d', 'e'=>'assoc'));
                            $array->rewind();
                            echo $array->current(); // Notice: Undefined offset: 0

                            // В массиве же нет элемента с индексом 0
                            • 0
                              Наверное, по этой причине встроенный ArrayObject не содержит итераторов. Но не смотря на отсутствие итераторов, его можно прогнать через foreach:

                              $array = new ArrayObject(array('a'=>'a', 'b'=>'b', 'c'=>'c', 'd'=>'d', 'e'=>'assoc'));

                              foreach ($array as $k=>$v) { // Отработает правильно
                              echo $k. '='. $v .'';
                              }
                              $array->current(); // Fatal error: Call to undefined method ArrayObject::current()

                              Либо это магия, либо костыль на костыле.
                              • 0
                                Как не содержит?
                                ArrayObject implements IteratorAggregate,...
                                • 0
                                  Я имел ввиду, что не содержит описанного в статье итератора Iterator и нет методов rewind(), current() и next(). И показанные методы не позволяют работать с ассоциированными массивами (а это даже не отмечено в статье).

                                  Вместо этого, получается некоторая магия когда я использую foreach. Например, если я использую описанные здесь класс semiArray добавив вот это:
                                  public function next()
                                  {
                                  echo 'next()'; // Это
                                  ++$this->_position;
                                  }

                                  Я вижу, что конструкция foreach для каждой итерации вызывает метод next(). Однако, встроенный ArrayObject не имеет метода next(), но foreach как-то работает! Получается, что foreach подстраивается под переданный ему тип вместо того, что бы использовать полиморфизм. Имеено такое поведение foreach мне кажется костылём.
                                  • 0
                                    Встроенный ArrayObject использует IteratorAggregate вместо Iterator, поэтому там нет всяких next.
                                    foreach умеет работать с 2мя типами: массив и объект реализующий Traversable (наследниками от которого являются Iterator & IteratorAggregate)
                                      • 0
                                        /*
                                        foreach умеет работать с 2мя типами: массив и объект реализующий Traversable (наследниками от которого являются Iterator & IteratorAggregate)
                                        */
                                        Прочитав этом, можно сделать вывод, что Traversable заставляет оба класса реализовывать одни и те же методы (предоставить api), которое потом используются в foreach. И это было бы логично.

                                        Но это не так. Traversable — интерфейс не содержащий ни одного метода. Это что же за интерфейс такой? А foreach использует разные методы для объектов $semiArray и $arrayObject. Т.е. нет никакого полиморфизма. Разве это не костыль?

                                        P.S. Я бы написал так:
                                        foreach умеет работать с 3мя типами: 1) массив, 2) объект реализующий Iterator и 3) объект реализующий IteratorAggregate.
                                      • +1
                                        Если корректно сделать обёртку, то будет работать и ассоциативный массив.
                                        • 0
                                          Это правильно. Но в «обучающей» статье почему-то этого нет. Ассоциированные массивы не работают и об этом в статье умалчивается, хотя в разделе «тестируем» есть ассоциированный массив:

                                          $array = new SemiArray(array('a', 'b', 'c', 'd', 'e' => 'assoc'));

                                          Не внимательный читатель может сделать вывод, будто описанный в статье класс может работать с ассоциированными массивами.
                                • 0
                                  Для сравнения примерно такая-же функциональность в Python:

                                  Итератор

                                  def Iterator():
                                      for elem in ['item1', 'item2', 'item3']:
                                          yield elem

                                  В простейшем случае даже
                                  iterator=(elem for elem in ['item1', 'item2', 'item3'])


                                  Доступ по индексу:

                                  class ArrayAccess(object):
                                  
                                      def __getitem__(self, key):
                                          return self.container[key]

                                  Доступ по ключу
                                  class ArrayAccess(object):
                                  
                                      def __getitem__(self, key):
                                          return self.container[key]
                                  
                                      def __setitem__(self, key, value):
                                          self.container[key]=value
                                  
                                      def __delitem__(self, key, value):
                                          del self.container[key]

                                  длина
                                  class Countable(object):
                                  
                                      def __len__(self):
                                          return len(self.container)
                                  • НЛО прилетело и опубликовало эту надпись здесь
                                    • 0
                                      Я тоже привык использовать isset и empty. Надо просто не забывать, что в PHP нет строгой типизации. А в большинстве случаев функциональности этих двух конструкций вполне хватает.
                                      Еще полезная функция array_filter без коллбэка, лишний цикл иногда можно не писать :)
                                      • 0
                                        И еще немного про isset и unset.
                                        Можно писать так isset($array[1]) && isset($array[1][2]) => isset($array[1], $array[1][2]) => isset($array[1][2])
                                        и аналогично unset($array[1]); unset($array[2]); => unset($array[1], $array[2]);

                                      Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.