Pull to refresh
0
Rating

Расширение возможностей массива в PHP

Lamoda.ru corporate blog PHP *
Уровень статьи: начальный/средний

Массив в PHP — один из самых мощных типов данных. Он может работать как линейный массив (список), как ассоциативный (словарь) и как смешанный. В качестве ключа массива может использоваться либо целое число, либо строка, причем строка, если она представляет собой целое число (например, «5»), будет конвертирована в целое. Остальные типы, за исключением массивов и объектов, так же конвертируются в целое или строку — подробнее можно прочитать в документации.

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

$foo = isset($array['foo']) ? $array['foo'] : null;
$bar = isset($array['bar']) ? $array['bar'] : null;


Один из способов сделать этот код короче и элегантней — использовать короткую запись тернарного оператора:

$foo = $array['foo'] ? : null;
$bar = $array['bar'] ? : null;


Но такой код выкинет PHP Notice в случе, когда ключ не определен, а я стараюсь писать максимально чистый код — на сервере разработки выставлено error_reporting = E_ALL. И именно в подобных случаях на помощь приходит ArrayObject — класс, к объектам которого можно обращаться используя синтаксис массивов и позволяющий изменять поведение используя механизм наследования.

Рассмотрим несколько примеров изменения поведения.



В проекте, над которым я сейчас работаю, мы используем следующие базовые наследники ArrayObject:
  • DefaultingArrayObject — возвращает значение по умолчанию, если ключ не определен в массиве;
  • ExceptionArrayObject — бросает исключение, если ключ не определен в массиве;
  • CallbackArrayObject — значения массива являются функциями (замыканиями), которые возвращают некое значение.


DefaultingArrayObject



Этот тип массива ведет себя примерно как словарь в Python при вызове dict.get(key, default) — если ключ не определен в массиве — возвращается значение по умолчанию. Это отлично работает в случае, когда значения по умолчанию у всех элементов, к которым мы обращаемся одинаковые, и не так элегантно, когда мы хотим получать разные значения в случае отсутствия ключа. Полный листинг этого класса выглядит следующим образом:

Листинг класса DefaultingArrayObject
class DefaultingArrayObject extends \ArrayObject  
{
    protected $default = null;

    public function offsetGet($index)
    {
        if ($this->offsetExists($index)) {
            return parent::offsetGet($index);
        } else {
            return $this->getDefault();
        }
    }

    /**
     * @param mixed $default
     * @return $this
     */
    public function setDefault($default)
    {
        $this->default = $default;
        return $this;
    }

    /**
     * @return mixed
     */
    public function getDefault()
    {
        return $this->default;
    }
}



Используя этот класс, можно переписать код, который я использовал в качестве примера, следующим образом:

$array = new DefaultingArrayObject($array);
$foo = $array['foo'];
$bar = $array['bar'];


В случае разных значений по-умолчанию будет выглядеть не так красиво, и далеко не факт что эта запись лучше использования полной тернарной записи — просто покажу как это можно сделать (PHP 5.4+):

$array = new DefaultingArrayObject($array);
$foo = $array->setDefault('default for foo')['foo'];
$bar = $array->setDefault('default for bar')['bar'];


ExceptionArrayObject



Как я уже отмечал выше, PHP бросит Notice в случае отсутствия ключа в массиве, но иногда хочется контролировать это без использования кучи условных операторов, а логику выполнения контролировать при помощи исключений. Например, код вида:

if (isset($array['foo']) && isset($array['bar'] && isset($array['baz'])) {  
    // logic that uses foo, bar and baz array values
} else {
    // logic that does not use foo, bar and baz array values
}


Можно переписать следующим образом:

$array = new ExceptionArrayObject($array);
try {
    // logic that uses foo, bar and baz array values
} catch (UndefinedIndexException $e) {
    // logic that does not use foo, bar and baz array values
}


Листинг класса ExceptionArrayObject
class ExceptionArrayObject extends \ArrayObject  
{
    public function offsetGet($index)
    {
        if ($this->offsetExists($index)) {
            return parent::offsetGet($index);
        } else {
            throw new UndefinedIndexException($index);
        }
    }
}


class UndefinedIndexException extends \Exception  
{
    protected $index;

    public function __construct($index)
    {
        $this->index = $index;
        parent::__construct('Undefined index "' . $index . '"');
    }

    /**
     * @return string
     */
    public function getIndex()
    {
        return $this->index;
    }
}



CallbackArrayObject



И напоследок еще один массив с нестандартным поведением. Вообще, этот тип массива вполне можно считать фабрикой, ибо элементы массива являются замыканиями (анонимными функциями), которые вызываются при обращении к соответствующим элементам массива и результат их выполнения возвращается вместо самого элемента. Думаю, что проще будет показать это на примере:

$array = new CallbackArrayObject([
    'foo' => function() {
        return 'foo ' . uniqid();
    },
    'bar' => function() {
        return 'bar ' . time();
    },
]);

$foo = $array['foo']; // "foo 526afed12969d"
$bar = $array['bar']; //  "bar 1382743789" 


Листинг класса CallbackArrayObject
class CallbackArrayObject extends \ArrayObject  
{
    protected $initialized = array();

    public function __construct(array $values)
    {
        foreach ($values as $key => $value) {
            if (!($value instanceof \Closure)) {
                throw new \RuntimeException('Value for CallbackArrayObject must be callback for key ' . $key);
            }
        }
        parent::__construct($values);
    }

    public function offsetGet($index)
    {
        if (!isset($this->initialized[$index])) {
            $this->initialized[$index] = $this->getCallbackResult(parent::offsetGet($index));
        }
        return $this->initialized[$index];
    }

    protected function getCallbackResult(\Closure $callback)
    {
        return call_user_func($callback);
    }
}



В данном случае результат выполнения функции кэшируется и повторно функция вызвана не будет. Но сферические результаты в вакууме бывают интересны крайне редко, поэтому можно сделать их настраиваемыми, зависящими от какого-нибудь конфига:

$array = new ConfigurableCallbackArrayObject([
    'foo' => function($config) {
        return 'foo ' . $config['foo'];
    },
    'bar' => function($config) {
        return 'bar ' . $config['bar'];
    },
]);
$array->setConfig(['foo' => 123, 'bar' => 321]);

$foo = $array['foo']; // "foo 123"
$bar = $array['bar']; //  "bar 321" 


Листинг класса ConfigurableCallbackArrayObject
class ConfigurableCallbackArrayObject extends CallbackArrayObject
{
    protected $config;

    protected function getCallbackResult(\Closure $callback)
    {
        return call_user_func($callback, $this->getConfig());
    }

    public function setConfig($config)
    {
        $this->config = $config;
    }

    public function getConfig()
    {
        return $this->config;
    }
}



Это все, что я хотел рассказать о примерах использовании ArrayObject. Думаю необходимо упомянуть, что как и во всем, при использовании ArrayObject нужно знать меру и понимать, когда использование изменяющих поведение массива классов оправдано, а когда проще просто вставить дополнительную проверку или пару лишних строк логики непосредственно в основной алгоритм, а не инкапсулировать их во вспомогательные классы. Иными словами — не плодить дополнительные сущности без необходимости.
Tags:
Hubs:
Total votes 34: ↑25 and ↓9 +16
Views 16K
Comments Comments 30

Information

Founded
2011
Location
Россия
Website
www.lamoda.ru
Employees
Unknown
Registered