Расширение возможностей массива в 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 нужно знать меру и понимать, когда использование изменяющих поведение массива классов оправдано, а когда проще просто вставить дополнительную проверку или пару лишних строк логики непосредственно в основной алгоритм, а не инкапсулировать их во вспомогательные классы. Иными словами — не плодить дополнительные сущности без необходимости.
    Lamoda.ru 23,90
    Компания
    Поделиться публикацией
    Комментарии 30
    • 0
      $array = new CallbackArrayObject([
          'foo' => function() {
              return 'foo ' . uniqid();
          },
          'bar' => function() {
              return 'bar ' . time();
          },
      ]);
      
      $foo = $array['foo']; // "foo 526afed12969d"
      $bar = $array['bar']; //  "bar 1382743789" 
      

      Очень страшно!) Никак не ожидаешь что простой вывод данных из массива может потянуть за собой кучу действий, о которых я могу только гадать (запросы в БД, запись/удаление файлов и т.д.).

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


      Здесь если метод getCallbackResult вернет null, то повторно будет выполнен код, isset()
      Лучше использовать array_key_exists(), так точно отработает один раз.
      • +3
        Количество дополнительных проверок уже зивисит от конкретной задачи, в которой планируется применять эти структуры. В каких-то случаях может понадобиться объединить CallbackArrayObject и DefaultingArrayObject/ExceptionArrayObject. В статье показаны только возможности.
      • +8
        Если такое встретить в коде то совсем непонятно что тут происходит
        $array = new DefaultingArrayObject($array);
        $foo = $array->setDefault('default for foo')['foo'];
        

        Мне куда понятней было бы увидеть:
        $array = new DefaultingArrayObject($array);
        $foo = $array->get('foo', 'default for foo');
        
        • +2
          Согласен, и я указал что это не самая лучшая форма записи. То, что так можно делать, совсем не значит что так делать нужно. Насчет вашего варианта — это уже не работа с массивом, а работа с объектом и это уже немного другая история — такое можно организовать и не на базе ArrayObject, а просто классом, инкапсулирующим массив.
          • –1
            Каждый под себя что-то делает

            Я, например, использую что-то вроде:
            <?php
            
                function valueFromArray(array $data, $map, $default = '')
                    {
                    if(!is_array($map))
                        {
                        $map = array($map);
                        }
            
                    foreach($map as $m)
                        {
                        if(is_array($data))
                            {
                            if(array_key_exists($data[$m]))
                                {
                                $data = $data[$m];
            
                                continue;
                                }
                            else
                                {
                                $data = $default;
            
                                break;
                                }
                            }
                        break;
                        }
            
                    return $data;
                    }
            
            
            echo valueFromArray($array, 'name');
            
            echo valueFromArray($array, 'email', 'empty');
            
            echo valueFromArray($array, array('settings', 'auth'), 'default');
            ?>
            
          • 0
            Отличная статья! Ничего кроме первых двух примеров не знал. Очень полезно, спасибо.
            • +2
              думаю лучше будет так

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


              или так

              protected function getCallbackResult(callable $callback)
              {
                  return call_user_func($callback);
              }
              
              • 0
                Чем лучше?
                • +2
                  Если использовать тайп хинт Closure, то лаконичнее запись $callback().
                  Если использовать тайп хинт callable, то можно передавать не только анонимные функции, но и такие штуки:
                  call_user_func('my_callback_function');
                  call_user_func(array('MyClass', 'myCallbackMethod')); 
                  call_user_func(array($obj, 'myCallbackMethod'));
                  call_user_func('MyClass::myCallbackMethod');
                  
                  • 0
                    Ок, согласен. Спасибо.
                • 0
                  Хинт callable можно использовать только с 5.4+, а поскольку 5.3 будет официально поддерживаться еще до 2014-07, то, возможно, рановато отказываться от его поддержки в приложениях. Плюс к этому неплохо было бы проверить на то, а валидный ли колбек передали. С учетом сказанного, я бы переписал это вот так:

                  protected function getCallbackResult($callback)
                  {
                  if (is_callable($callback)) {
                  return call_user_func($callback);
                  }
                  throw new \Exception('Invalid callable provided');
                  }

                  Разумеется, что можно изменить Exception — на свой тип, или же просто возвращать null.

                  Кстати, этот метод имеет один, возможно, неприятный побочный эффект. А именно: внутри класса is_callable() будет true даже для private и protected методов. Соответственно существует возможность передать туда array($classInstance, 'privateOrProtectedMethod') и оно будет успешно выполнено. Так что нужно иметь ввиду данную особенность.
                • +1
                  Очень спорная реализация, но идея наследования от базовых классов для заточки их под себя оправдывает написание статьи.
                  • +2
                    К моему стыду крайнему удивлению, все мои эксперименты попытки внедрить SPL/ArrayObject в реальный проект заканчивались примерно таким вот образом:

                    <?php
                    $a = new ArrayObject();
                    echo (int)is_array($a);
                    

                    0
                    • +2
                      Да, есть такое, но оно и не удивительно — массив это отдельный тип данных.

                      Насчет внедрения — я считаю что переход от массивов к ArrayObject дело, в общем случае, не то чтобы не полезное, но даже вредное. Использовать объекты имеет смысл только в отдельных конкретных случаях, когда есть понимание, что с ними будет проще, чем без них.
                    • +1
                      Тут еще подводный камень в том, что стандартные array_XXXXX функции не будут работать с этим объектом. Нужно кастать к array, но и на выходе получим array, а не arrayObject. Так что если использовать arrayObject, то нужно использовать его везде.
                      • +1
                        Скопирую свой комментарий чуть выше:

                        Насчет внедрения — я считаю что переход от массивов к ArrayObject дело, в общем случае, не то чтобы не полезное, но даже вредное. Использовать объекты имеет смысл только в отдельных конкретных случаях, когда есть понимание, что с ними будет проще, чем без них.
                        • +1
                          Тут чем дальше под воду, тем толще камни.
                          is_array("scalar") // false;
                          is_array((array)"scalar") // true; счастливой отладки
                          

                          В результате — с SPL/ArrayObject приходится четко мониторить, чтобы порожденные объекты не выходили за пределы «своего» кода и не пытались использоваться каким-либо другим образом.
                          • 0
                            а что собственно в вашем примере «камни»?
                            (array)«scalar» в данном случае преобразуется в array(0 => 'scalar')
                            • +1
                              Предположим, что переменная не объявлена парой строчек выше, а приходит извне (и мы ожидаем массив), и нам ее нужно передать в функцию array_foo().

                              function foo1($bar){
                                return array_merge(array(1), $bar);
                              }
                              

                              До приведения:
                              * если $bar — массив, все ок
                              * если $bar is ArrayObject — получаем варнинг
                              * иначе — получаем варнинг

                              После приведения:
                              * если $bar — массив, все ок
                              * если $bar is ArrayObject — все ок
                              * иначе — счастливой отладки

                              В итоге код начинает пестрить костылями типа ($foo instanceof ArrayObject) || is_array($foo)
                          • 0
                            <?php
                                public function __call($func, $argv)
                                {
                                    if (!is_callable($func) || substr($func, 0, 6) !== 'array_')
                                    {
                                        throw new BadMethodCallException(__CLASS__.'->'.$func);
                                    }
                                    return call_user_func_array($func, array_merge(array($this->getArrayCopy()), $argv));
                                }
                            ?>
                            

                            это по поводу нерабочих array_* функций. Не панацея, но всё же)
                            • 0
                              Написать свои методы не проблема. Я про то, что array_values($obj) по прежнему не будет работать.
                              Нужно понимать, что в данный момент мы работаем с объектом, а не с массивом. Я это хотел сказать.
                              • 0
                                Если применить шаблон проектирования «Костылинг», то можно воспользоваться override_function и переопределить все нужные функции.
                                • 0
                                  При этом нужно учеть, что оно "(PECL apd >= 0.2)".
                          • 0
                            Что нужно чтобы ArrayObject использовать в реальных проектах?

                            1. Нужно чтобы функции работы с массивами работали с ArrayObject также, как и с массивами
                            2. Обычный массив должен иметь поведение идентичное простейшей реализации ArrayObject (http://www.php.net/manual/ru/class.arrayobject.php ). Вплоть до
                            ([] instanceof ArrayObject) === true;
                            

                            3. Было бы классно, если бы это не повлияло на производительность.
                            4. Единственный способ отличить обычнй массив от кастомизированного: $array instanceof MyArrayObject

                            Но меня терзают смутные сомнения по поводу совместимости таких нововведений с существующими расширениями, т.ч. это будет не скоро, если вообще будет.
                            • +1
                              Этот код (не прм в таком виде, в расширенном) используется в реальном проекте. Но я, пожалуй, процитирую еще раз свою мысль:

                              Насчет внедрения — я считаю что переход от массивов к ArrayObject дело, в общем случае, не то чтобы не полезное, но даже вредное. Использовать объекты имеет смысл только в отдельных конкретных случаях, когда есть понимание, что с ними будет проще, чем без них.
                              • +1
                                Полностью согласен. Мой комментарий не столько про код из статьи, сколько про ArrayObject в общем.
                                А вы бенчмарки делали? Насколько медленнее работает?
                            • 0
                              В perl есть похожий механизм — tying variables, позволяющий сделать поведение простых типов неочевидным. Механизм этот старый и за долгие годы нигде особо не прижился, а все потому что отлавливать последствия операции простого добавления в массив дело очень неблагодарное. Да почти в каждом языке слаботипизированном есть подобный механизм и нигде он широко не используется, за редким исключением.

                              И вообще это крайне неудобно с точки зрения поддержки кода — каждый новый программист вводимый в проект будет заново проходить по одним и тем же костылям на одних и тех же базовых операциях.

                              Впрочем каждый решает для себя с чем он сможет совладать.
                              • +1
                                Рекомендую не писать кучу иссетов, когда они в условии собираются через &&. Проще так:
                                if (isset($array['foo'], $array['bar'], $array['baz'])) {
                                }
                                
                                • 0
                                  Подумалось… а операторы сравнения можно как-то сократить?
                                  Вроде такого: if ($val=='one' || $val=='two' || $val=='three') {… }?
                                  • 0
                                    Не всегда короче получается, но явно нагляднее:
                                    in_array($val, array('one', 'two', 'three'))
                                    

                                    или
                                    switch ($val) {
                                    case 'one': 
                                    case 'two':
                                    case 'three':
                                    // ...
                                    break;
                                    default:
                                    // ...
                                    break;
                                    }
                                    

                                    в зависимости от того, есть ли elseif или else.

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое