📜 ⬆️ ⬇️

Extending array capabilities in PHP

Article level: beginner / intermediate

PHP array 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 the 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 into an integer or string - for more information, see the documentation .

Despite the powerful features of the base type of the array, sometimes you 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 recording of the ternary operator:

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


But such code will throw out PHP Notice in the case when the key is not defined, and I try to write the most clean code - error_reporting = E_ALL is set on the development server. 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 allowing you to change the behavior using the inheritance mechanism.

Consider a few examples of behavior change.



In the project I'm working on, we use the following base ArrayObject heirs:


DefaultingArrayObject



This type of array behaves approximately like a dictionary in Python when you call 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 ​​of all elements to which we access are the same, and not so elegant when we want to get different values ​​in the case of a missing key. The full listing of this class is as follows:

Listing of class 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; } } 



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, by default it will not look so beautiful, and far from being 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 heap of conditional statements, and control the execution logic with exceptions. For example, the 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 class 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



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


Listing class 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); } } 



In this case, the result of the function execution is cached and the function will not be called again. But spherical results in vacuum are extremely rarely interesting, so you can make them customizable, depending on any 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" 


Listing class 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; } } 



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

Source: https://habr.com/ru/post/199096/


All Articles