Search code examples
phparrayobject

How to unset nested array with ArrayObject?


ideone

Sample Code:

<?php
$a = new ArrayObject();
$a['b'] = array('c'=>array('d'));
print_r($a);
unset($a['b']['c']);
print_r($a);

Output

ArrayObject Object
(
    [b] => Array
        (
            [c] => Array
                (
                    [0] => d
                )
        )
)
ArrayObject Object
(
    [b] => Array
        (
            [c] => Array
                (
                    [0] => d
                )
        )
)

You notice that $a['b']['c'] is still there, even after unsetting. I would expect $a to have just the one value left (b).

In my actual app, I get the following warning:

Indirect modification of overloaded element of MyClass has no effect

Where MyClass extends ArrayObject. I have a lot of code that depends on being able to unset nested elements like this, so how can I get this to work?


Solution

  • One way to do it

    <?php
    $a      = new ArrayObject();
    $a['b'] = array('c' => array('d'));
    $d      =& $a['b'];
    
    unset($d['c']);
    print_r($a['b']);
    

    prints:

    Array
    (
    )
    

    Would have to think a bit longer for an explanation as to why the syntax you've originally used doesn't remove the element.

    EDIT: Explanation of behavior

    What's happening is the call to unset($a['b']['c']); is translated into:

    $temp = $a->offsetGet('b');
    unset($temp['c']);
    

    since $temp is a copy of $a instead of a reference to it, PHP uses copy-on-write internally and creates a second array where $temp doesn't have ['b']['c'], but $a still does.

    ANOTHER EDIT: Reusable Code

    So, no matter which way you slice it, seems like trying to overload function offsetGet($index) to be function &offsetGet($index) leads to trouble; so here's the shortest helper method I came up w/ could add it as a static or instance method in a subclass of ArrayObject, whatever floats your boat:

    function unsetNested(ArrayObject $oArrayObject, $sIndex, $sNestedIndex)
    {
        if(!$oArrayObject->offSetExists($sIndex))
            return;
    
        $aValue =& $oArrayObject[$sIndex];
    
        if(!array_key_exists($sNestedIndex, $aValue))
            return;
    
        unset($aValue[$sNestedIndex]);
    }
    

    So the original code would become

    $a      = new ArrayObject();
    $a['b'] = array('c' => array('d'));
    
    // instead of unset($a['b']['c']);
    unsetNested($a, 'b', 'c');
    print_r($a['b']);
    

    YET ANOTHER EDIT: OO Solution

    OK - So I must have been scrambling this morning b/c I found an error in my code, and when revised, we can implement a solution, based on OO.

    Just so you know I tried it, extension segfaults..:

    /// XXX This does not work, posted for illustration only
    class BadMoxuneArrayObject extends ArrayObject
    {
        public function &offsetGet($index)
        {   
            $var =& $this[$index];
            return $var;
        }   
    }
    

    Implementing a Decorator on the other hand works like a charm:

    class MoxuneArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Countable
    {
        private $_oArrayObject;  // Decorated ArrayObject instance
    
        public function __construct($mInput=null, $iFlags=0, $sIteratorClass='')
        {
            if($mInput === null)
                $mInput = array();
    
            if($sIteratorClass === '')
                $this->_oArrayObject = new ArrayObject($mInput, $iFlags);
            else
                $this->_oArrayObject = new ArrayObject($mInput, $iFlags, $sIteratorClass);
        } 
    
        // -----------------------------------------
        // override offsetGet to return by reference
        // -----------------------------------------
        public function &offsetGet($index)
        {
            $var =& $this->_oArrayObject[$index];
            return $var;
        }
    
        // ------------------------------------------------------------
        // everything else is passed through to the wrapped ArrayObject
        // ------------------------------------------------------------
        public function append($value)
        {
            return $this->_oArrayObject->append($value);
        }
    
        public function asort()
        {
            return $this->_oArrayObject->asort();
        }
    
        public function count()
        {
            return $this->_oArrayObject->count();
        }
    
        public function exchangeArray($mInput)
        {
            return $this->_oArrayObject->exchangeArray($mInput);
        }
    
        public function getArrayCopy()
        {
            return $this->_oArrayObject->getArrayCopy();
        }
    
        public function getFlags()
        {
            return $this->_oArrayObject->getFlags();
        }
    
        public function getIterator()
        {
            return $this->_oArrayObject->getIterator();
        }
    
        public function getIteratorClass()
        {
            return $this->_oArrayObject->getIteratorClass();
        }
    
        public function ksort()
        {
            return $this->_oArrayObject->ksort();
        }
    
        public function natcassesort()
        {
            return $this->_oArrayObject->natcassesort();
        }
    
        public function offsetExists($index)
        {
            return $this->_oArrayObject->offsetExists($index);
        }
    
        public function offsetSet($index, $value)
        {
            return $this->_oArrayObject->offsetSet($index, $value);
        }
    
        public function offsetUnset($index)
        {
            return $this->_oArrayObject->offsetUnset($index);
        }
    
        public function serialize()
        {
            return $this->_oArrayObject->serialize();
        }
    
        public function setFlags($iFlags)
        {
            return $this->_oArrayObject->setFlags($iFlags);
        }
    
        public function setIteratorClass($iterator_class)
        {
            return $this->_oArrayObject->setIteratorClass($iterator_class);
        }
    
        public function uasort($cmp_function)
        {
            return $this->_oArrayObject->uasort($cmp_function);
        }
    
        public function uksort($cmp_function)
        {
            return $this->_oArrayObject->uksort($cmp_function);
        }
    
        public function unserialize($serialized)
        {
            return $this->_oArrayObject->unserialize($serialized);
        }
    }
    

    Now this code works as desired:

    $a      = new MoxuneArrayObject();
    $a['b'] = array('c' => array('d'));
    unset($a['b']['c']);
    var_dump($a);
    

    Still have to modify some code though..; I don't see any way round that.