Pull to refresh

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

Reading time 5 min
Views 30K

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).
Tags:
Hubs:
+50
Comments 39
Comments Comments 39

Articles