Skip to content

Instantly share code, notes, and snippets.

@Jeff-Russ
Last active April 5, 2024 21:07
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save Jeff-Russ/e1f64273a471d440e8b4d9183f9a2667 to your computer and use it in GitHub Desktop.
Save Jeff-Russ/e1f64273a471d440e8b4d9183f9a2667 to your computer and use it in GitHub Desktop.
PHP: ArrayObject, IteratorAggregate , ArrayAccess , Serializable , Countable

Array Objects in PHP

Array are not object in PHP but PHP does give us some ways to make object that act like arrays. First, there is the ArrayObject class which is pretty close to what you have with a normal array (you can iterate it, use [] etc.) If you use it as a parent for your class you can add methods to it.

class ArrObj extends ArrayObject{
	// add methods here
}
$arracc = new ArrObj(['zero', 'one']);
$arracc[] = 'two';
$arracc->prop1 = 'prop 1';
$arracc->prop2 = 'prop 2';
foreach($arracc as $k=>$v) echo "$k=>$v\n"; # prints array but not -> style props

If you want to define the behavior of [], however, you'd be better off making something else that does what ArrayObject does rather than overriding it. Let's look at what ArrayObject did for us:

ArrayObject implements IteratorAggregate , ArrayAccess , Serializable , Countable { 
    // everything gets implemented + things added. 
}

More on the body of that later but for now focus on ArrayAccess.

ArrayAccess

To implement ArrayAccess you must define the following abstract methods:

ArrayAccess {
/* Methods */
abstract public boolean offsetExists ( mixed $offset )
abstract public mixed offsetGet ( mixed $offset )
abstract public void offsetSet ( mixed $offset , mixed $value )
abstract public void offsetUnset ( mixed $offset )
}

Here is an example implementation:

class ArrAcc implements ArrayAccess {
    protected $array = array();
    
    public function __construct($array=[]) {
        $this->array = $array;
    }
    public function offsetExists($index) {
        return isset($this->array[$index]);
    }
    public function offsetGet($index) {
        if($this->offsetExists($index)):return $this->array[$index];
        else: return false; endif;
    }
    public function offsetSet($index, $value) {
        if($index): $this->array[$index] = $value;
        else: $this->array[] = $value; endif;
        return true;
    }
    public function offsetUnset($index) {
        unset($this->array[$index]);
        return true;
    }
}
$arracc = new ArrAcc(['zero', 'one']);

$arracc[] = 'two';
$arracc[] = 'three';
$arracc[] = 'four';

$arracc->prop1 = 'prop 1';
$arracc->prop2 = 'prop 2';

foreach($arracc as $k=>$v) echo "$k=>$v\n";

DOH! that last line iterates prop1 and prop2, which were not added to $array like everything else was! It also fail to iteratate anything in the array!! For that we need the IteratorAggregate interface.

If you just want to have an array with methods added, extending ArrayObject is your answer. When you need to specify the behavior of [] you need to ask yourself is you need iteration and if you don't, implementing AarrayAccess is probably your best bet.

But if you need iteration and you need to specify the behavior of [] you have to take command of all the pertinent methods by either overriding them or implementing them. The ArrayObject class does the latter latter.

Roll Your Own Array Class

First let's zoom in a little more on what ArrayObject does. We won't need all of these methods, just the ones that implement methods from the interfaces:

ArrayObject implements IteratorAggregate , ArrayAccess , Serializable , Countable
{
  /* Constants */
    const integer STD_PROP_LIST = 1 ;
    const integer ARRAY_AS_PROPS = 2 ;
  /* Methods */
    public __construct(
        [ mixed $input=[] 
        [,int $flags=0 
        [,string $iterator_class="ArrayIterator"]]])
    public void append ( mixed $value )
    public void asort  ( void )
    public int  count  ( void )            #impl. Countable
    public array exchangeArray(mixed $input)
    public array getArrayCopy (void)
    public int   getFlags     (void)
    public ArrayIterator getIterator(void) #impl. Traversable
    public string   getIteratorClass(void)
    public void ksort        ( void )
    public void natcasesort  ( void )
    public void natsort      ( void )
    public bool  offsetExists(mixed $index)               #impl. ArrayAccess
    public mixed offsetGet   (mixed $index)               #impl. ArrayAccess
    public void  offsetSet   (mixed $index,mixed $newval) #impl. ArrayAccess
    public void  offsetUnset (mixed $index)               #impl. ArrayAccess
    public string serialize  ( void )              #impl. Serializable
    public void setFlags     ( int $flags )
    public void setIteratorClass(string $iterator_class)
    public void uasort ( callable $cmp_function )
    public void uksort ( callable $cmp_function )
    public void unserialize ( string $serialized ) #impl. Serializable
}

Here is a class that implements everything ArrayObject does and in a very standard way. You could work with a modified version of this or use it as a base class and override the methods in another class:

<?php
class Arr implements Serializable, IteratorAggregate, ArrayAccess, Countable
{
	protected $data;

	public function __construct($data=null){
		if (method_exists($data, 'toArray')): $data = $data->toArray();
		elseif ($data===null): $data = array();
		elseif (! is_array($data)): $data = func_get_args();endif;
		$this->data = $data;
	}

	// Implement Serializable
	public function serialize(){return serialize($this->data);} # object to string
	public function unserialize($serialized){$this->data = unserialize($serialized);}
	
	// Implement IteratorAggregate
	public function getIterator(){ return new ArrayIterator($this->data); }

	// Implement Countable
	public function count() { return count( $this->data ); }
	public function length(){ return $this->count(); }
	
	// Implement ArrayAccess
	public function offsetSet($key, $value) {
		if ($key===null) $this->data[] = $value;
		else $this->data[$key] = $value;
	}
	public function offsetGet($key) {
		return array_key_exists($key,$this->data) ? $this->data[$key] : null;
	}
	public function offsetExists($key) {
		return array_key_exists($key, $this->data);
	}
	public function offsetUnset($key) {
		unset($this->data[$key]);
	}
}
$arr1 = new Arr(1, 2, 3);
$arr1["four"] = 4;
foreach ($arr1 as $k => $v) echo "$k=>$v\n";
$arr2 = ["i'm", "an", "array?"];
$arr2 = new Arr($arr2);
$arr2[] = 4;
foreach ($arr2 as $k => $v) echo "$k=>$v\n";

You may want to make wrapper classes for other types such as strings and numbers and at that point it would be wise to modularize your code so you can reuse common elements. After all, strings are basically arrays and should have the the ability to be iterated too. If this sounds like the plan then check out PHP-PhpTypes

Getting Fancier

Drawing from some work in PHP-PhpTypes, here is a more fleshed out verion of the Arr class:

<?php
class Arr implements Serializable, IteratorAggregate, ArrayAccess, Countable
{
	protected $data;

	public function __construct($data=null){
		if (method_exists($data, 'toArray')): $data = $data->toArray();
		elseif ($data===null): $data = array();
		elseif (! is_array($data)): $data = func_get_args();endif;
		$this->data = $data;
	}

	// Implement Serializable
	public function serialize(){return serialize($this->data);} # object to string
	public function unserialize($serialized){$this->data = unserialize($serialized);}
	
	// Implement IteratorAggregate
	public function getIterator(){ return new ArrayIterator($this->data); }

	// Implement Countable
	public function count() { return count( $this->data ); }
	public function length(){ return $this->count(); }
	
	// Implement ArrayAccess
	public function offsetSet($key, $value) {
		if ($key===null) $this->data[] = $value;
		else $this->data[$key] = $value;
	}
	public function offsetGet($key) {
		return array_key_exists($key,$this->data) ? $this->data[$key] : null;
	}
	public function offsetExists($key) {
		return array_key_exists($key, $this->data);
	}
	public function offsetUnset($key) {
		unset($this->data[$key]);
	}

	static function from($data) {
		$cl = get_called_class();
		if (is_array($data)) return new $cl($data);
		elseif (func_num_args()>1) return new $cl(func_get_args());
		elseif (method_exists($data, 'getArrayCopy'))
			return new $cl($data->getArrayCopy());
		else return new $cl(array($data));
	}
	public function unshift($data){array_unshift($this->data,$data);return $this;}
	public function shift(){ return array_shift($this->data); }
	public function pop(){ return array_pop($this->data); } #Stack
	public function push(){                                 #Stack
		$args = func_get_args();
		foreach ($args as $arg): $this->data[] = $arg; endforeach;
		return $this;
	}
	public function __invoke()   { return $this->toArray(); }
	public function   toString() { return '' . $this->data; }
	public function __toString() { return '[' . $this->join(', ') . ']'; }
	public function   toArray()  { return $this->data; }
	public function   toJSON()   { return json_encode($this->data); }

	public function keys()   { return static::from(array_keys($this->data));  }
	public function values() { return static::from(array_values($this->data)); }
	public function has($key){ return array_key_exists($key, $this->data); }
	public function contains($value, $strict=true){return in_array($value,$this->data,$strict);}
	protected static function getClassName(){return get_called_class();}
	public function indexOf($value, $strict = true){
		$index = array_search($value, $this->data, $strict);
		return $index !== false ? $index : -1;
	}

	public function join($separator = ','){ return implode($separator, $this->data); }
	public function clear(){ $this->data=[]; return $this; }
	public function append($array){
		return call_user_func_array(array($this,'push'),method_exists($array,'toArray') ? $array->toArray() : $array);
	}
	public function reverse($preserveKeys = false){
		return static::from(array_reverse($this->data, $preserveKeys));
	}
	public function slice($begin = 0, $end = false){
		if (func_num_args() < 2) $end = $this->length();
		return static::from(array_slice($this->data, $begin, $end));
	}
	public function remove($value){
		if (in_array($value, $this->data))
			$this->data = array_diff_key($this->data, array_flip(array_keys($this->data, $value, true)));
		return $this;
	}
	public function clean(){
		return $this->remove(null)->remove(false)->remove(0)->remove('');}
	
	public function each($cb){
		foreach ($this as $key => $value): $cb($value, $key, $this); endforeach;
		return $this;
	}
	public function map($cb){
		$results = array();
		foreach ($this as $key => $value): $results[$key] = $cb($value, $key, $this); endforeach;
		return static::from($results);
	}
	public function filter($cb, $preserveKeys = false){
		$results = array();
		foreach ($this as $key => $value)
			if ($cb($value, $key, $this))
				$preserveKeys ? $results[$key] = $value : $results[] = $value;
		return static::from($results);
	}
	public function every($cb){
		foreach($this as $key => $value): if (!$cb($value, $key, $this)) return false; endforeach;
		return true;
	}
	public function some($cb){
		foreach($this as $key => $value): if ($cb($value, $key, $this)) return true; endforeach;
		return false;
	}
	public function item($at){
		$length = count($this->data);
		if ($at < 0){
			$mod = $at % $length;
			if ($mod == 0) $at = 0;
			else $at = $mod + $length;
		}
		return ($at < 0 || $at >= $length || !array_key_exists($at, $this->data)) ? null : $this->data[$at];
	}
}


//// ARRAY EXAMPLE ////
$arr1 = new Arr(1, 2, 3);
$arr1["four"] = 4;
foreach ($arr1 as $k => $v) echo "$k=>$v\n";
var_dump($arr1->keys());
$arr1 = $arr1->map(function($value){ return $value * 2; });
var_dump($arr1);
count($arr1); 
echo $arr1; // [2,4,6,8,10]
var_dump($arr1->clear());

$arr2 = ["i'm", "an", "array?"];
$arr2 = new Arr($arr2);
$arr2[] = 4;
foreach ($arr2 as $k => $v) echo "$k=>$v\n";
var_dump($arr2->contains(4));
var_dump($arr2->values());

// Casting:
Arr::from($arr2); 

REALLY Fancy

Here is a bunch of classes similar to those in PHP-PhpTypes

<?php
// PhpTypes! For PHP. Like JavaScript
// adapted from [PHP-PhpTypes](https://github.com/cpojer/php-type)
abstract class DataContainer implements Serializable {
	protected $data;
	protected function setData($data){ $this->data = $data; return $this; }
	public function __invoke()  { return $this->__toString(); } # default with toString
	public function __toString(){ return $this->data; }
	public function   toString(){ return '' . $this->data; }
	public function serialize(){return serialize($this->data);} # object to string
	public function unserialize($serialized){$this->data = unserialize($serialized);}
	// from() takes object and returns a copy of it
	static function from($data) {
		$cl = get_called_class();
		if (is_array($data)) return new $cl($data);
		elseif (func_num_args()>1) return new $cl(func_get_args());
		elseif (method_exists($data, 'getArrayCopy'))
			return new $cl($data->getArrayCopy());
		else return new $cl(array($data));
	}
	protected static function getClassName(){return get_called_class();}
}
abstract class Iterable extends DataContainer {
	public function length(){ return $this->count(); }
	
	public function each($cb){
		foreach ($this as $key => $value): $cb($value, $key, $this); endforeach;
		return $this;
	}
	public function map($cb){
		$results = array();
		foreach ($this as $key => $value): $results[$key] = $cb($value, $key, $this); endforeach;
		return static::from($results);
	}
	public function filter($cb, $preserveKeys = false){
		$results = array();
		foreach ($this as $key => $value)
			if ($cb($value, $key, $this))
				$preserveKeys ? $results[$key] = $value : $results[] = $value;
		return static::from($results);
	}
	public function every($cb){
		foreach($this as $key => $value): if (!$cb($value, $key, $this)) return false; endforeach;
		return true;
	}
	public function some($cb){
		foreach($this as $key => $value): if ($cb($value, $key, $this)) return true; endforeach;
		return false;
	}
}
class ArrOb extends Iterable implements IteratorAggregate, ArrayAccess, Countable {
	public function __construct($data=null){
		if (method_exists($data, 'toArray')) $data = $data->toArray();
		$this->setData($data ?: array());
	}
	public function __invoke()  { return $this->toArray(); }
	public function __toString(){ return '[' . $this->join(', ') . ']'; }
	// Misc. Methods:
	public function indexOf($value, $strict = true){
		$index = array_search($value, $this->data, $strict);
		return $index !== false ? $index : -1;
	}
	public function contains($value, $strict = true){
		return in_array($value, $this->data, $strict);
	}
	public function has($key){ return array_key_exists($key, $this->data); }
	public function item($at){
		$length = count($this->data);
		if ($at < 0){
			$mod = $at % $length;
			if ($mod == 0) $at = 0;
			else $at = $mod + $length;
		}
		return ($at < 0 || $at >= $length || !array_key_exists($at, $this->data)) ? null : $this->data[$at];
	}
	public function join($separator = ','){ return implode($separator, $this->data); }
	public function clear(){ return $this->setData(array()); }
	public function append($array){
		return call_user_func_array(array($this, 'push'), method_exists($array, 'toArray') ? $array->toArray() : $array);
	}
	public function reverse($preserveKeys = false){
		return static::from(array_reverse($this->data, $preserveKeys));
	}
	public function slice($begin = 0, $end = false){
		if (func_num_args() < 2) $end = $this->length();
		return static::from(array_slice($this->data, $begin, $end));
	}
	public function remove($value){
		if (in_array($value, $this->data))
			$this->data = array_diff_key($this->data, array_flip(array_keys($this->data, $value, true)));
		return $this;
	}
	public function clean(){
		return $this->remove(null)->remove(false)->remove(0)->remove('');
	}
	// Implement Methods:
	public function keys(){ return static::from(array_keys($this->data)); }   #Keys / Values
	public function values(){return static::from(array_values($this->data));} #Keys / Values
	public function pop(){ return array_pop($this->data); } #Stack
	public function push(){                                 #Stack
		$args = func_get_args();
		foreach ($args as $arg): $this->data[] = $arg; endforeach;
		return $this;
	}
	public function shift(){ return array_shift($this->data); }                    #Shift
	public function unshift($data){array_unshift($this->data,$data);return $this;} #Shift
	public function toArray(){ return $this->data; }              #Cast
	public function toJSON() { return json_encode($this->data); } #Cast
	public function getIterator(){ return new ArrayIterator($this->data); }#IteratorAggregate
	public function offsetSet($key, $value){        #ArrayAccess
		if ($key === null): $this->data[] = $value;
		else: $this->data[$key] = $value; endif;
	}
	public function offsetGet($key){                
		return array_key_exists($key,$this->data) ? $this->data[$key] : null;     #ArrayAccess
	}
	public function offsetExists($key){return array_key_exists($key,$this->data);}#ArrayAccess
	public function offsetUnset($key){ unset($this->data[$key]); }                #ArrayAccess
	public function count(){ return count($this->data); }         #Countable
}
class Arr extends ArrOb {
	public function __construct(){ parent::__construct(func_get_args());}
	public static function from($data=null){ $array = new Arr; return $array->setData($data);}
}
class Str extends Iterable implements IteratorAggregate, ArrayAccess, Countable {
	
	public function __construct($data){
		if (is_array($data)) $data = implode($data);
		$this->setData('' . (method_exists($data, 'toString') ? $data->toString() : $data));
	}
	
	// Misc. Methods:
	
	public function indexOf($value){
		$index = strpos($this->data, $value);
		return $index !== false ? $index : -1;
	}
	public function contains($value){ return $this->indexOf($value) == -1 ? false : true; }
	public function split($separator = null){
		$data = $this->data;
		if ($separator === null): $data = array($this->data);
		elseif ($separator === ''): $data = $this->toArray();
		else: $data = explode($separator, $this->data);
		endif;
		return new ArrOb($data);
	}
	public function clear()  { return $this->setData(''); }
	public function reverse(){ return static::from(strrev($this->data));}
	public function trim()   { return static::from(trim($this->data)); }
	
	public function camelCase(){
		return static::from(str_replace('-', '', preg_replace_callback('/-\D/', function($matches){
			return strtoupper($matches[0]);
		}, $this->data)));
	}
	public function substitute($data){
		$keys = array();
		foreach ($data as $key => $value)
			$keys[] = '{' . $key . '}';
		
		$string = str_replace($keys, array_values($data), $this->data);
		return static::from(preg_replace('/\{([^{}]+)\}/', '', $string));
	}
	
	// Implement Methods:
	public function toArray() { return str_split($this->data);  } #Cast
	public function toJSON()  { return json_encode($this->data);} #Cast
	public function getIterator(){
	    return new ArrayIterator($this->toArray()); }     #IteratorAggregate
	public function offsetSet($key, $value){ $this->data[$key] = $value; }  #ArrayAccess
	public function offsetGet($key){
	    return !empty($this->data[$key]) ? $this->data[$key] : null; }      #ArrayAccess
	public function offsetExists($key){ return !empty($this->data[$key]); } #ArrayAccess
	public function offsetUnset($key) { $this->data[$key] = null; }         #ArrayAccess
	public function count(){ return strlen($this->data); } #Countable
}

Test it out like this

//// ARRAY EXAMPLE ////

$array = new Arr(1, 2, 3);
$array2 = $array->map(function($value){ return $value * 2; });
$array instanceof Arr;      // true
foreach ($array2 as $value) // 2, 4, 6
$array2[] = 8;
$array2[] = 10;
$array2->contains(8); // true
count($array2);       // 5
echo $array2; // [2,4,6,8,10]

// Casting:
ArrOb::from($someArray); 
Arr::from($someArray);

//// STRING EXAMPLE ////

$string = new Str('abc');
$string->reverse();             // 'cba';
foreach ($string as $character);// a, b, c
$string->each(function($v, $k, $ob){echo "$k => $v";});
Str::from('Hello {name}')->substitute(array('name' => 'Banana')); // Hello Banana
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment