Dot Notation

PHP arrays are useful (and quirky) tools for data organisation. They can lead to verbose code, if you’re trying to program defensively, as in the following example:

$config["app"]["cache"]["provider"] = "redis";

// ...later

if (
isset($config["app"]) and
isset($config["app"]["cache"]) and
isset($config["app"]["cache"]["provider"]
) {

}

// ...or with ominous error-suppression

if (!empty($config["app"]["cache"]["provider"])) {

}

Developers often prefer to substitute this kind of behaviour with the use of dot notation. That is; being able to refer to deeply-nested keys similarly to:

$config["app.cache.provider"];

// ...or

$config->get("app.cache.provider", $default);

I’ve often tried to implement this sort of thing, and though I get there in the end, it feels like I’m always trying a different method which is somehow less ugly than the last. So here are my adaptations on the cleanest examples I could find (from the Laravel framework).

Laravel Implementation

The Laravel framework implements static methods to get and set array values, using dot notation:

public static function get($array, $key, $default = null)
{
if (is_null($key)) return $array;

if (isset($array[$key])) return $array[$key];

foreach (explode('.', $key) as $segment)
{
if ( ! is_array($array) ||
! array_key_exists($segment, $array))
{
return value($default);
}

$array = $array[$segment];
}

return $array;
}

// ...and

public static function set(&$array, $key, $value)
{
if (is_null($key)) return $array = $value;

$keys = explode('.', $key);

while (count($keys) > 1)
{
$key = array_shift($keys);

if ( ! isset($array[$key]) || ! is_array($array[$key]))
{
$array[$key] = array();
}

$array =& $array[$key];
}

$array[array_shift($keys)] = $value;

return $array;
}
This is from Illuminate/Support/Arr.php

These gives us a good idea of what we should be shooting for. I want to take it a step further and create a trait for any container-like class to have internal storage, accessed/modified using dot notation!

Trait Implementation

We can begin by creating a few skeleton classes, implementing the ArrayAccess interface:

<?php

namespace Connect;

use ArrayAccess;
use Connect\Traits\ArrayAccessTrait;

class Collection implements ArrayAccess
{
use ArrayAccessTrait;
}
This is from source/Collection.php
<?php

namespace Connect\Traits;

trait ArrayAccessTrait
{
/**
* @var array
*/
protected $data = [];

/**
* @param string $key
*
* @return bool
*/
public function offsetExists($key)
{
// TODO
}

/**
* @param string $key
*
* @return mixed
*/
public function offsetGet($key)
{
// TODO
}

/**
* @param string $key
* @param mixed $value
*/
public function offsetSet($key, $value)
{
// TODO
}

/**
* @param string $key
*/
public function offsetUnset($key)
{
// TODO
}
}
This is from source/Traits/ArrayAccessTrait.php

Now we can begin to create the dot notation methods. Dot notation is easily achieved through iteration or recursion. Iteration tends to be less expensive than recursion, so that’s the approach we’re going to take:

/**
* @param string $key
*
* @return bool
*/
public function offsetExists($key)
{
return $this->exists($this->data, $key);
}

/**
* @param array $array
* @param string $key
*
* @return bool
*/
protected function exists(array $array, $key)
{
if (isset($array[$key])) {
return true;
}

foreach (explode(".", $key) as $part) {
if (!is_array($array) or !isset($array[$part])) {
return false;
}

$array = $array[$part];
}

return true;
}
This is from source/Traits/ArrayAccessTrait.php

This also creates a good model for the offsetGet() method:

/**
* @param string $key
*
* @return mixed
*/
public function offsetGet($key)
{
return $this->get($this->data, $key);
}

/**
* @param array $array
* @param string $key
*
* @return null
*/
protected function get(array $array, $key)
{
if (isset($array[$key])) {
return $array[$key];
}

foreach (explode(".", $key) as $part) {
if (!is_array($array) or !isset($array[$part])) {
return null;
}

$array = $array[$part];
}

return $array;
}
This is from source/Traits/ArrayAccessTrait.php

Setting follows a similar pattern to the Laravel implementation, with a few reductions:

/**
* @param string $key
* @param mixed $value
*/
public function offsetSet($key, $value)
{
$this->set($this->data, $key, $value);
}

/**
* @param array $array
* @param string $key
* @param mixed $value
*/
protected function set(array &$array, $key, $value)
{
$parts = explode(".", $key);

while (count($parts) > 1) {
$part = array_shift($parts);

if (!isset($array[$part]) or !is_array($array[$part])) {
$array[$part] = [];
}

$array =& $array[$part];
}

$array[array_shift($parts)] = $value;
}
This is from source/Traits/ArrayAccessTrait.php

Finally, we need to add the offsetUnset() method:

/**
* @param string $key
*/
public function offsetUnset($key)
{
$this->ünset($this->data, $key);
}

/**
* @param array $array
* @param string $key
*/
protected function ünset(array &$array, $key)
{
$parts = explode(".", $key);

while (count($parts) > 1) {
$part = array_shift($parts);

if (isset($array[$part]) and is_array($array[$part])) {
$array =& $array[$part];
}
}

unset($array[array_shift($parts)]);
}
This is from source/Traits/ArrayAccessTrait.php

I love that I have to use a unicode character in that method name, thanks to unset being a reserved word no matter the context. Now, we just need to finish the Collection class off:

/**
* @param array $data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
This is from source/Collection.php
Show your support

Clapping shows how much you appreciated Christopher Pitt’s story.