How to unset nested array with ArrayObject?
Asked Answered
C

4

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?

Clinker answered 2/4, 2012 at 18:36 Comment(2)
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!Mainspring
OK, I take this back, must have been debugging hastily or something earlier today, have a look at my latest edit.Mainspring
M
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.

Mainspring answered 2/4, 2012 at 18:48 Comment(6)
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: php.net/manual/en/arrayobject.offsetget.php#79496Clinker
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.Clinker
Still have to change calls like $a = new ArrayObject() to $a = new MoxuneArrayObject(); best I can do to minimize requisite changes.Mainspring
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.Clinker
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 :)Mainspring
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).Clinker
P
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.

Proboscis answered 2/4, 2012 at 19:6 Comment(0)
F
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

Freezedrying answered 13/3, 2014 at 21:33 Comment(0)
C
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.

Clinker answered 2/4, 2012 at 23:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.