Question

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?

Was it helpful?

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.

OTHER TIPS

It seems to me that the "overloaded" bracket operator of ArrayObject is returning a copy of the nested array, and not a reference to the original. Thus, when you call $a['b'], you are getting a copy of the internal array that ArrayObject is using to store the data. Further resolving it to $a['b']['c'] is just giving you the element "c" inside a copy, so calling unset() on it is not unsetting the element "c" in the original.

ArrayObject implements the ArrayAccess interface, which is what actually allows the bracket operator to work on an object. The documentation for ArrayAccess::offsetGet indicates that, as of PHP 5.3.4, references to the original data in ArrayObject's internal array can be acquired using the =& operator, as quickshiftin indicated in his example.

You can use unset($a->b['c']); instead of unset($a['b']['c']); in case if there won't be a huge problem to do a such replacement for all same situations within your project

I seem to have a partial solution. unset seems to work if all the nested arrays are instances of ArrayObject. In order to ensure all the nested arrays are ArrayObjects as well, we can derive instead from this class:

class ArrayWrapper extends ArrayObject {
    public function __construct($input=array(), $flags=ArrayObject::STD_PROP_LIST, $iterator_class='ArrayIterator') {
        foreach($input as $key=>$value) {
            if(is_array($value)) {
                $input[$key] = new self($value, $flags, $iterator_class);
            }
        }
        parent::__construct($input, $flags, $iterator_class);
    }

    public function offsetSet($offset, $value) {
        parent::offsetSet($offset, is_array($value) ? new ArrayWrapper($value) : $value);
    }
}

(updated for recursiveness; untested)

And then whenever you try to add a nested array, it will automatically get converted to an ArrayWrapper instead.

Unfortunately many of the other array functions, such as array_key_exists don't work on ArrayObjects.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top