Уровень статьи: начальный/средний
Массив в PHP — один из самых мощных типов данных. Он может работать как линейный массив (список), как ассоциативный (словарь) и как смешанный. В качестве ключа массива может использоваться либо целое число, либо строка, причем строка, если она представляет собой целое число (например, «5»), будет конвертирована в целое. Остальные типы, за исключением массивов и объектов, так же конвертируются в целое или строку — подробнее можно прочитать в документации.
Несмотря на мощные возможности базового типа array иногда хочется их расширить. Например, подобный кусок кода можно встретить, наверное, в большинстве php-проектов:
Один из способов сделать этот код короче и элегантней — использовать короткую запись тернарного оператора:
Но такой код выкинет PHP Notice в случе, когда ключ не определен, а я стараюсь писать максимально чистый код — на сервере разработки выставлено
Рассмотрим несколько примеров изменения поведения.
В проекте, над которым я сейчас работаю, мы используем следующие базовые наследники
Этот тип массива ведет себя примерно как словарь в Python при вызове
Используя этот класс, можно переписать код, который я использовал в качестве примера, следующим образом:
В случае разных значений по-умолчанию будет выглядеть не так красиво, и далеко не факт что эта запись лучше использования полной тернарной записи — просто покажу как это можно сделать (PHP 5.4+):
Как я уже отмечал выше, PHP бросит Notice в случае отсутствия ключа в массиве, но иногда хочется контролировать это без использования кучи условных операторов, а логику выполнения контролировать при помощи исключений. Например, код вида:
Можно переписать следующим образом:
И напоследок еще один массив с нестандартным поведением. Вообще, этот тип массива вполне можно считать фабрикой, ибо элементы массива являются замыканиями (анонимными функциями), которые вызываются при обращении к соответствующим элементам массива и результат их выполнения возвращается вместо самого элемента. Думаю, что проще будет показать это на примере:
В данном случае результат выполнения функции кэшируется и повторно функция вызвана не будет. Но сферические результаты в вакууме бывают интересны крайне редко, поэтому можно сделать их настраиваемыми, зависящими от какого-нибудь конфига:
Это все, что я хотел рассказать о примерах использовании ArrayObject. Думаю необходимо упомянуть, что как и во всем, при использовании ArrayObject нужно знать меру и понимать, когда использование изменяющих поведение массива классов оправдано, а когда проще просто вставить дополнительную проверку или пару лишних строк логики непосредственно в основной алгоритм, а не инкапсулировать их во вспомогательные классы. Иными словами — не плодить дополнительные сущности без необходимости.
Массив в 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 нужно знать меру и понимать, когда использование изменяющих поведение массива классов оправдано, а когда проще просто вставить дополнительную проверку или пару лишних строк логики непосредственно в основной алгоритм, а не инкапсулировать их во вспомогательные классы. Иными словами — не плодить дополнительные сущности без необходимости.