Расширение возможностей массива в 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 нужно знать меру и понимать, когда использование изменяющих поведение массива классов оправдано, а когда проще просто вставить дополнительную проверку или пару лишних строк логики непосредственно в основной алгоритм, а не инкапсулировать их во вспомогательные классы. Иными словами — не плодить дополнительные сущности без необходимости.

ссылка на оригинал статьи http://habrahabr.ru/post/199096/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *