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).