views:

123

answers:

2

I have a class implementing ArrayAccess and I'm trying to get it to work with a multidimensional array. exists and get work. set and unset are giving me a problem though.

class ArrayTest implements ArrayAccess {
    private $_arr = array(
        'test' => array(
            'bar' => 1,
            'baz' => 2
        )
    );

    public function offsetExists($name) {
        return isset($this->_arr[$name]);
    }

    public function offsetSet($name, $value) {
        $this->_arr[$name] = $value;
    }

    public function offsetGet($name) {
        return $this->_arr[$name];
    }

    public function offsetUnset($name) {
        unset($this->_arr[$name]);
    }
}

$arrTest = new ArrayTest();


isset($arrTest['test']['bar']);  // Returns TRUE

echo $arrTest['test']['baz'];    // Echo's 2

unset($arrTest['test']['bar'];   // Error
$arrTest['test']['bar'] = 5;     // Error

I know $_arr could just be made public so you could access it directly, but for my implementation it's not desired and is private.

The last 2 lines throw an error: Notice: Indirect modification of overloaded element.

I know ArrayAccess just generally doesn't work with multidimensional arrays, but is there anyway around this or any somewhat clean implementation that will allow the desired functionality?

The best idea I could come up with is using a character as a separator and testing for it in set and unset and acting accordingly. Though this gets really ugly really fast if you're dealing with a variable depth.

Does anyone know why exists and get work so as to maybe copy over the functionality?

Thanks for any help anyone can offer.

+2  A: 

EDIT: See the response of Alexander Konstantinov. I was thinking of the __get magic method, which is analogous, but was actually implemented correctly. So you cannot do that without an internal implementation of your class.

EDIT2: Internal implementation:

NOTE: You might argue this is purely masturbatory, but anyway here it goes:

static zend_object_handlers object_handlers;

static zend_object_value ce_create_object(zend_class_entry *class_type TSRMLS_DC)
{
    zend_object_value zov;
    zend_object       *zobj;

    zobj = emalloc(sizeof *zobj);
    zend_object_std_init(zobj, class_type TSRMLS_CC);

    zend_hash_copy(zobj->properties, &(class_type->default_properties),
        (copy_ctor_func_t) zval_add_ref, NULL, sizeof(zval*));
    zov.handle = zend_objects_store_put(zobj,
        (zend_objects_store_dtor_t) zend_objects_destroy_object,
        (zend_objects_free_object_storage_t) zend_objects_free_object_storage,
        NULL TSRMLS_CC);
    zov.handlers = &object_handlers;
    return zov;
}

/* modification of zend_std_read_dimension */
zval *read_dimension(zval *object, zval *offset, int type TSRMLS_DC) /* {{{ */
{
    zend_class_entry *ce = Z_OBJCE_P(object);
    zval *retval;
    void *dummy;

    if (zend_hash_find(&ce->function_table, "offsetgetref",
        sizeof("offsetgetref"), &dummy) == SUCCESS) {
        if(offset == NULL) {
            /* [] construct */
            ALLOC_INIT_ZVAL(offset);
        } else {
            SEPARATE_ARG_IF_REF(offset);
        }
        zend_call_method_with_1_params(&object, ce, NULL, "offsetgetref",
            &retval, offset);

        zval_ptr_dtor(&offset);

        if (!retval) {
            if (!EG(exception)) {
                /* ought to use php_error_docref* instead */
                zend_error(E_ERROR,
                    "Undefined offset for object of type %s used as array",
                    ce->name);
            }
            return 0;
        }

        /* Undo PZVAL_LOCK() */
        Z_DELREF_P(retval);

        return retval;
    } else {
        zend_error(E_ERROR, "Cannot use object of type %s as array", ce->name);
        return 0;
    }
}

ZEND_MODULE_STARTUP_D(testext)
{
    zend_class_entry ce;
    zend_class_entry *ce_ptr;

    memcpy(&object_handlers, zend_get_std_object_handlers(),
        sizeof object_handlers);
    object_handlers.read_dimension = read_dimension;

    INIT_CLASS_ENTRY(ce, "TestClass", NULL);
    ce_ptr = zend_register_internal_class(&ce TSRMLS_CC);
    ce_ptr->create_object = ce_create_object;

    return SUCCESS;
}

now this script:

<?php

class ArrayTest extends TestClass implements ArrayAccess {
    private $_arr = array(
        'test' => array(
            'bar' => 1,
            'baz' => 2
        )
    );

    public function offsetExists($name) {
        return isset($this->_arr[$name]);
    }

    public function offsetSet($name, $value) {
        $this->_arr[$name] = $value;
    }

    public function offsetGet($name) {
        throw new RuntimeException("This method should never be called");
    }

    public function &offsetGetRef($name) {
        return $this->_arr[$name];
    }

    public function offsetUnset($name) {
        unset($this->_arr[$name]);
    }
}

$arrTest = new ArrayTest();


echo (isset($arrTest['test']['bar'])?"test/bar is set":"error") . "\n";

echo $arrTest['test']['baz'];    // Echoes 2
echo "\n";

unset($arrTest['test']['baz']);
echo (isset($arrTest['test']['baz'])?"error":"test/baz is not set") . "\n";
$arrTest['test']['baz'] = 5;

echo $arrTest['test']['baz'];    // Echoes 5

gives:

test/bar is set
2
test/baz is not set
5

ORIGINAL follows -- this is incorrect:

Your offsetGet implementation must return a reference for it to work.

public function &offsetGet($name) {
    return $this->_arr[$name];
}

For the internal equivalent, see here.

Since there's no analogous to get_property_ptr_ptr, you ought to return a reference (in the sense of Z_ISREF) or a proxy object (see the get handler) in write-like contexts (types BP_VAR_W, BP_VAR_RW and BP_VAR_UNSET), though it's not mandatory. If read_dimension is being called in a write-like context such as in $val =& $obj['prop'], and you return neither a reference nor an object, the engine emit a notice. Obviously, returning a reference is not enough for those operations to work correctly, it is necessary that modifying the returned zval actually has some effect. Note that assignments such as $obj['key'] = &$a are still not possible – for that one would need the dimensions to actually be storable as zvals (which may or may not be the case) and two levels of indirection.

In sum, operations that involve writing or unseting a sub-dimension of sub-property call offsetGet, not offsetSet, offsetExists or offsetUnset.

Artefacto
+3  A: 

The problem could be resolved by changing public function offsetGet($name) to public function &offsetGet($name) (by adding return by reference), but it will cause Fatal Error ("Declaration of ArrayTest::offsetGet() must be compatible with that of ArrayAccess::offsetGet()").

PHP authors screwed up with this class some time ago and now they won't change it in sake of backwards compatibility:

We found out that this is not solvable without blowing up the interface and creating a BC or providing an additional interface to support references and thereby creating an internal nightmare - actually i don't see a way we can make that work ever. Thus we decided to enforce the original design and disallow references completley.

Edit: If you still need that functionality, I'd suggest using magic method instead (__get(), __set(), etc.), because __get() returns value by reference. This will change syntax to something like this:

$arrTest->test['bar'] = 5;

Not an ideal solution of course, but I can't think of a better one.

Alexander Konstantinov
You're right, I recall seeing a discussion on this on php-dev recently.
Artefacto
Artefacto
He could still do it with an internal implementation. He could create a custom base class, not implement ArrayAccess, then override the read_property handler (maybe then looking for a function named "offsetget" or just doing all the work in the handler itself).
Artefacto
@Artefacto do you know of any links with some reading on what you're talking about, or could you whip up a quick and simple example for me to check out?
anomareh
@anomareh I've updated my answer.
Artefacto
Note that I think you should accept this answer. Unless you're OK with deploying an extension together with your application, you shouldn't do an internal class implementation.
Artefacto
@Artefacto aye I suppose you're right. I thank you very much though for the effort you went through to toss together that code. I was really just curious about the underlying mechanics. Wish I could at least upvote your answer more than once heh. Thanks again.
anomareh