phparraysiteratorphp-5.6arrayaccess

Implement Iterator interface for ArrayAccess without container array


This is my attempt at implementing https://www.php.net/manual/en/class.iterator.php for an ArrayAccess. Many examples use a container array as a private member variable; but I do not want to use a container array if possible. The main reason why I don't want a container array is because I'd like to access the property (array key) like this $DomainData->domainId all while having intellisense, etc.

Demo: https://ideone.com/KLPwwY

class DomainData implements ArrayAccess, Iterator
{
    private $position = 0;
    public $domainId;
    public $color;

    public function __construct($data = array())
    {
        $this->position = 0;
        foreach ($data as $key => $value) {
            $this[$key] = $value;
        }
    }

    public function offsetExists($offset)
    {
        return isset($this->$offset);
    }

    public function offsetSet($offset, $value)
    {
        $this->$offset = $value;
    }

    public function offsetGet($offset)
    {
        return $this->$offset;
    }

    public function offsetUnset($offset)
    {
        $this->$offset = null;
    }

    /*****************************************************************/
    /*                     Iterator Implementation                   */
    /*****************************************************************/

    public function rewind()
    {
        $this->position = 0;
    }

    public function current()
    {
        return $this[$this->position];
    }

    public function key()
    {
        return $this->position;
    }

    public function next()
    {
        ++$this->position;
    }

    public function valid()
    {
        return isset($this[$this->position]);
    }
}

Calling it:

$domainData = new DomainData([
    "domainId" => 1,
    "color" => "red"
]);

var_dump($domainData);

foreach($domainData as $k => $v){
    var_dump("domainData[$k] = $v");
}

actual:

object(DomainData)#1 (3) {
  ["position":"DomainData":private]=>
  int(0)
  ["domainId"]=>
  int(1)
  ["color"]=>
  string(3) "red"
}

desired:

object(DomainData)#1 (3) {
  ["position":"DomainData":private]=>
  int(0)
  ["domainId"]=>
  int(1)
  ["color"]=>
  string(3) "red"
}
string(24) "domainData[domainId] = 1"
string(23) "domainData[color] = red"

Solution

  • Let me describe a couple of ways of how you could do this.

    ArrayObject with custom code

    ArrayObject implements all of the interfaces that you want.

    class DomainData extends ArrayObject
    {
      public $domainId;
      public $color;
    
      public function __construct($data = array())
      {
        parent::__construct($data);
        foreach ($data as $key => $value) {
          $this->$key = $value;
        }
      }
    }
    

    This isn't very nice, though; it copies the keys and values twice, and changing a property doesn't change the underlying array.

    Implement IteratorAggregate on get_object_vars()

    If you don't mind giving up on ArrayAccess, you could get away with only implementing an aggregate iterator.

    class DomainData implements IteratorAggregate
    {
        public $domainId;
        public $color;
    
        public function __construct($data = [])
        {
            foreach ($data as $key => $value) {
                $this->$key = $value;
            }
        }
    
        public function getIterator()
        {
            return new ArrayIterator(get_object_vars($this));
        }
    }
    

    ArrayObject with property flag and doc blocks

    A better way would be to use doc blocks for describing your properties (described here), and then use the ARRAY_AS_PROPS flag to expose the array as properties.

    /**
     * Magic class
     * @property int $domainId
     * @property string $color
     */
    class DomainData extends ArrayObject
    {
      function __construct($data = []) {
        parent::__construct($data, parent::ARRAY_AS_PROPS);
      }
    }
    

    When loaded inside PhpStorm, you'd see this: phpstorm editor