8

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?

mpen
  • 272,448
  • 266
  • 850
  • 1,236
  • Sadly you won't be able to get those calls to `unset` to work since `ArrayObject::offsetGet` returns a copy. I've left you w/ a drop-in replacement for those calls below though, hopefully it saves you some time! – quickshiftin Apr 02 '12 at 19:41
  • OK, I take this back, must have been debugging hastily or something earlier today, have a look at my latest edit. – quickshiftin Apr 02 '12 at 22:05

4 Answers4

11

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.

quickshiftin
  • 66,362
  • 10
  • 68
  • 89
  • I thought I read somewhere that as long as we don't override `offsetGet` it will return a reference, as `ArrayObject` is implemented in C. Might have been Sam's comment here: http://www.php.net/manual/en/arrayobject.offsetget.php#79496 – mpen Apr 02 '12 at 21:06
  • This is really unfortunate though... I've got about 340 times where I perform an unset like this sprinkled across my project. I'm not sure it would be safe to try to update them all, plus the core framework at once. I was hoping to keep it backwards compatible with the old array syntax. – mpen Apr 02 '12 at 21:09
  • 1
    Still have to change calls like `$a = new ArrayObject()` to `$a = new MoxuneArrayObject()`; best I can do to minimize requisite changes. – quickshiftin Apr 02 '12 at 22:17
  • That's okay. I've only got one ArrayObject so far, and it's derived (MyClass extends ArrayObject), so I'd just have to extend MoxuneArrayObject instead presumably. – mpen Apr 02 '12 at 23:04
  • Nice; well that ought to do it I'd imagine. Might want to double check the code for typos, heh, I haven't tested every method :) – quickshiftin Apr 02 '12 at 23:11
  • 1
    I decided to abandon attempting to turn my array into a class. Too many of the `array_*` functions don't work with it. Instead, I'm creating a secondary object that wraps the array (keeping a reference to it). – mpen Apr 10 '12 at 05:48
4

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.

Community
  • 1
  • 1
inspector-g
  • 4,146
  • 2
  • 24
  • 33
1

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

Pavel Perminov
  • 1,506
  • 2
  • 10
  • 7
0

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.

mpen
  • 272,448
  • 266
  • 850
  • 1,236