Expanding array capabilities in PHP

    Article level: Beginner / Intermediate

    An array in PHP is one of the most powerful data types. It can work as a linear array (list), as an associative (dictionary), and as a mixed one. Either an integer or a string can be used as an array key, and a string, if it is an integer (for example, “5”), will be converted to an integer. The remaining types, with the exception of arrays and objects, are also converted to an integer or a string - you can read more in the documentation .

    Despite the powerful capabilities of the base array type, sometimes I want to expand them. For example, a similar piece of code can be found, probably, in most php projects:

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


    One way to make this code shorter and more elegant is to use a short ternary statement:

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


    But such code will throw PHP Notice in the case when the key is not defined, and I try to write the most clean code - it is set on the development server error_reporting = E_ALL. And it is precisely in such cases that ArrayObject comes to the rescue - a class whose objects can be accessed using the syntax of arrays and which allows changing behavior using the inheritance mechanism.

    Consider a few examples of behavioral changes.



    In the project I'm working on now, we use the following basic heirs ArrayObject:
    • DefaultingArrayObject - returns the default value if the key is not defined in the array;
    • ExceptionArrayObject - throws an exception if the key is not defined in the array;
    • CallbackArrayObject - array values ​​are functions (closures) that return a certain value.


    DefaultingArrayObject



    This type of array behaves much like a dictionary in Python when called dict.get(key, default)- if the key is not defined in the array - the default value is returned. This works great when the default values ​​for all the elements that we refer to are the same, and not so elegant when we want to get different values ​​in the absence of a key. A full listing of this class is as follows:

    Listing of the DefaultingArrayObject Class
    classDefaultingArrayObjectextends \ArrayObject{
        protected $default = null;
        publicfunctionoffsetGet($index){
            if ($this->offsetExists($index)) {
                returnparent::offsetGet($index);
            } else {
                return$this->getDefault();
            }
        }
        /**
         * @param mixed $default
         * @return $this
         */publicfunctionsetDefault($default){
            $this->default = $default;
            return$this;
        }
        /**
         * @return mixed
         */publicfunctiongetDefault(){
            return$this->default;
        }
    }
    



    Using this class, you can rewrite the code that I used as an example, as follows:

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


    In the case of different values, the default will not look so pretty, and it’s far from the fact that this record is better than using a full ternary record - I’ll just show you how to do it (PHP 5.4+):

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


    ExceptionArrayObject



    As I noted above, PHP will throw Notice if there is no key in the array, but sometimes you want to control it without using a bunch of conditional statements, and control the execution logic using exceptions. For example, a code of the form:

    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
    }
    


    Can be rewritten as follows:

    $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
    }
    


    Listing the class ExceptionArrayObject
    classExceptionArrayObjectextends \ArrayObject{
        publicfunctionoffsetGet($index){
            if ($this->offsetExists($index)) {
                returnparent::offsetGet($index);
            } else {
                thrownew UndefinedIndexException($index);
            }
        }
    }
    


    classUndefinedIndexExceptionextends \Exception{
        protected $index;
        publicfunction__construct($index){
            $this->index = $index;
            parent::__construct('Undefined index "' . $index . '"');
        }
        /**
         * @return string
         */publicfunctiongetIndex(){
            return$this->index;
        }
    }
    



    CallbackArrayObject



    And finally, another array with non-standard behavior. In general, this type of array can be considered a factory, because the elements of the array are closures (anonymous functions) that are called when accessing the corresponding elements of the array and the result of their execution is returned instead of the element itself. I think it will be easier to show this with an example:

    $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 Listing
    classCallbackArrayObjectextends \ArrayObject{
        protected $initialized = array();
        publicfunction__construct(array $values){
            foreach ($values as $key => $value) {
                if (!($value instanceof \Closure)) {
                    thrownew \RuntimeException('Value for CallbackArrayObject must be callback for key ' . $key);
                }
            }
            parent::__construct($values);
        }
        publicfunctionoffsetGet($index){
            if (!isset($this->initialized[$index])) {
                $this->initialized[$index] = $this->getCallbackResult(parent::offsetGet($index));
            }
            return$this->initialized[$index];
        }
        protectedfunctiongetCallbackResult(\Closure $callback){
            return call_user_func($callback);
        }
    }
    



    In this case, the result of the function execution is cached and the function will not be called again. But spherical results in a vacuum are extremely rare, so you can make them customizable, depending on some kind of config:

    $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" 


    Class Listing ConfigurableCallbackArrayObject
    classConfigurableCallbackArrayObjectextendsCallbackArrayObject{
        protected $config;
        protectedfunctiongetCallbackResult(\Closure $callback){
            return call_user_func($callback, $this->getConfig());
        }
        publicfunctionsetConfig($config){
            $this->config = $config;
        }
        publicfunctiongetConfig(){
            return$this->config;
        }
    }
    



    This is all I wanted to talk about examples using ArrayObject. I think it is necessary to mention that, like in everything, when using ArrayObject, you need to know the measure and understand when the use of classes that change the behavior of an array is justified, and when it's easier to just insert an additional check or a couple of extra lines of logic directly into the main algorithm, rather than encapsulate them in the auxiliary classes. In other words, do not produce additional entities unnecessarily.

    Also popular now: