ArrayAccess多维(un)设置?

时间:2010-05-21 10:59:42

标签: php arrays interface multidimensional-array

我有一个实现ArrayAccess的类,我试图让它与多维数组一起工作。 existsget工作。 setunset给了我一个问题。

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

我知道$_arr可以公开,因此您可以直接访问它,但对于我的实施,它不是所希望的并且是私有的。

最后两行引发错误:Notice: Indirect modification of overloaded element

我知道ArrayAccess通常不能用于多维数组,但是在这个或任何有点干净的实现中是否存在允许所需功能?

我能想到的最好的想法是使用一个字符作为分隔符并在setunset中对其进行测试并采取相应的行动。虽然如果你处理的是深度变化,这会变得非常难看。

有谁知道为什么existsget有效,以便复制功能?

感谢任何人提供的帮助。

7 个答案:

答案 0 :(得分:18)

通过将public function offsetGet($name)更改为public function &offsetGet($name)(通过添加引用返回)来解决问题会导致致命错误( “ ArrayTest :: offsetGet()的声明必须与ArrayAccess :: offsetGet()”的声明兼容。

PHP作者不久前搞砸了这个课程,现在他们won't change it in sake of backwards compatibility

  

我们发现这是不可解决的   没有炸毁界面和   创建BC或提供   额外的支持接口   引用,从而创建一个   内心的噩梦 - 实际上我没有   看看我们可以做到这一点的方式。   因此我们决定执行   原创设计,不允许   引用完整。

修改:如果您仍然需要该功能,我建议您使用魔术方法(__get()__set()等),因为__get()通过引用返回值。这会将语法更改为以下内容:

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

当然不是理想的解决方案,但我想不出更好的解决方案。

更新:此问题为fixed in PHP 5.3.4,ArrayAccess现在按预期工作:

  

从PHP 5.3.4开始,放松了原型检查,并且可以通过引用返回此方法的实现。这样就可以间接修改ArrayAccess对象的重载数组维度。

答案 1 :(得分:6)

这个问题实际上是可以解决的,完全是功能性的。

来自对ArrayAccess文档的评论here

<?php

// sanity and error checking omitted for brevity
// note: it's a good idea to implement arrayaccess + countable + an
// iterator interface (like iteratoraggregate) as a triplet

class RecursiveArrayAccess implements ArrayAccess {

    private $data = array();

    // necessary for deep copies
    public function __clone() {
        foreach ($this->data as $key => $value) if ($value instanceof self) $this[$key] = clone $value;
    }

    public function __construct(array $data = array()) {
        foreach ($data as $key => $value) $this[$key] = $value;
    }

    public function offsetSet($offset, $data) {
        if (is_array($data)) $data = new self($data);
        if ($offset === null) { // don't forget this!
            $this->data[] = $data;
        } else {
            $this->data[$offset] = $data;
        }
    }

    public function toArray() {
        $data = $this->data;
        foreach ($data as $key => $value) if ($value instanceof self) $data[$key] = $value->toArray();
        return $data;
    }

    // as normal
    public function offsetGet($offset) { return $this->data[$offset]; }
    public function offsetExists($offset) { return isset($this->data[$offset]); }
    public function offsetUnset($offset) { unset($this->data); }

}

$a = new RecursiveArrayAccess();
$a[0] = array(1=>"foo", 2=>array(3=>"bar", 4=>array(5=>"bz")));
// oops. typo
$a[0][2][4][5] = "baz";

//var_dump($a);
//var_dump($a->toArray());

// isset and unset work too
//var_dump(isset($a[0][2][4][5])); // equivalent to $a[0][2][4]->offsetExists(5)
//unset($a[0][2][4][5]); // equivalent to $a[0][2][4]->offsetUnset(5);

// if __clone wasn't implemented then cloning would produce a shallow copy, and
$b = clone $a;
$b[0][2][4][5] = "xyzzy";
// would affect $a's data too
//echo $a[0][2][4][5]; // still "baz"

?>

然后您可以扩展该类,如下所示:

<?php

class Example extends RecursiveArrayAccess {
    function __construct($data = array()) {
        parent::__construct($data);
    }
}

$ex = new Example(array('foo' => array('bar' => 'baz')));

print_r($ex);

$ex['foo']['bar'] = 'pong';

print_r($ex);

?>

这将为您提供一个可以被视为数组的对象(主要是参见代码中的注释),它支持多维数组set / get / unset。

答案 2 :(得分:3)

编辑:见亚历山大康斯坦丁诺夫的回应。我在考虑__get魔术方法,它类似,但实际上是正确实现的。因此,如果没有课程的内部实施,你就无法做到这一点。

EDIT2:内部实施:

注意:你可能会认为这纯粹是自慰,但无论如何它都是:

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;
}

现在这个脚本:

<?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

给出:

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

ORIGINAL如下 - 这是不正确的:

您的offsetGet实施必须返回一个参考,以便它发挥作用。

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

对于内部等效内容,请参阅here

  

由于没有类似于get_property_ptr_ptr,你应该在类似写入的上下文(类型BP_VAR_W,BP_VAR_RW和BP_VAR_UNSET)中返回引用(在Z_ISREF意义上)或代理对象(请参阅get处理程序),尽管它不是强制性的。如果在类似写入的上下文中调用read_dimension,例如$ val =&amp; $ obj ['prop'],并且既不返回引用也不返回对象,引擎会发出通知。显然,返回引用不足以使这些操作正常工作,修改返回的zval实际上有必要产生一些效果。请注意,$ obj ['key'] =&amp; $ a这样的分配仍然是不可能的 - 因为那个维度实际上可以存储为zvals(可能是也可能不是这种情况)和两个间接级别

总之,涉及编写或取消子属性子维度的操作调用offsetGet,而不是offsetSet,offsetExists或offsetUnset。

答案 3 :(得分:2)

解决方案:

<?php
/**
 * Cube PHP Framework
 * 
 * The contents of this file are subject to the Mozilla Public License
 * Version 1.1 (the "License"); you may not use this file except in
 * compliance with the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * @author Dillen / Steffen
 */

namespace Library;

/**
 * The application
 * 
 * @package Library
 */
class ArrayObject implements \ArrayAccess
{
    protected $_storage = array();

    // necessary for deep copies
    public function __clone() 
    {
        foreach ($this->_storage as $key => $value)
        {
            if ($value instanceof self)
            {
                $this->_storage[$key] = clone $value;
            }
        }
    }

    public function __construct(array $_storage = array()) 
    {
        foreach ($_storage as $key => $value)
        {
            $this->_storage[$key] = $value;
        }
    }

    public function offsetSet($offset, $_storage) 
    {
        if (is_array($_storage))
        {
            $_storage = new self($_storage);
        }

        if ($offset === null) 
        {
            $this->_storage[] = $_storage;
        } 
        else 
        {
            $this->_storage[$offset] = $_storage;
        }
    }

    public function toArray() 
    {
        $_storage = $this -> _storage;

        foreach ($_storage as $key => $value)
        {
            if ($value instanceof self)
            {
                $_storage[$key] = $value -> toArray();
            }
        }

        return $_storage;
    }

    // as normal
    public function offsetGet($offset) 
    {
        if (isset($this->_storage[$offset]))
        {
            return $this->_storage[$offset];
        }

        if (!isset($this->_storage[$offset]))
        {
            $this->_storage[$offset] = new self;
        }

        return $this->_storage[$offset];
    }

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

    public function offsetUnset($offset) 
    {
         unset($this->_storage);
    }
}

答案 4 :(得分:1)

我用这个解决了它:

class Colunas implements ArrayAccess {

    public $cols = array();

    public function offsetSet($offset, $value) {
        $coluna = new Coluna($value);

        if (!is_array($offset)) {
            $this->cols[$offset] = $coluna;
        } else {
            if (!isset($this->cols[$offset[0]])) $this->cols[$offset[0]] = array();
            $col = &$this->cols[$offset[0]];
            for ($i = 1; $i < sizeof($offset); $i++) {
                if (!isset($col[$offset[$i]])) $col[$offset[$i]] = array();
                $col = &$col[$offset[$i]];
            }
            $col = $coluna;
        }
    }

    public function offsetExists($offset) {
        if (!is_array($offset)) {
            return isset($this->cols[$offset]);
        } else {
            $key = array_shift($offset);
            if (!isset($this->cols[$key])) return FALSE;
            $col = &$this->cols[$key];
            while ($key = array_shift($offset)) {
                if (!isset($col[$key])) return FALSE;
                $col = &$col[$key];
            }
            return TRUE;
        }
    }


    public function offsetUnset($offset) {
        if (!is_array($offset)) {
            unset($this->cols[$offset]);
        } else {
            $col = &$this->cols[array_shift($offset)];
            while (sizeof($offset) > 1) $col = &$col[array_shift($offset)];
            unset($col[array_shift($offset)]);
        }
    }

    public function offsetGet($offset) {
        if (!is_array($offset)) {
            return $this->cols[$offset];
        } else {
            $col = &$this->cols[array_shift($offset)];
            while (sizeof($offset) > 0) $col = &$col[array_shift($offset)];
            return $col;
        }
    }
} 

所以你可以用它来:

$colunas = new Colunas();
$colunas['foo'] = 'Foo';
$colunas[array('bar', 'a')] = 'Bar A';
$colunas[array('bar', 'b')] = 'Bar B';  
echo $colunas[array('bar', 'a')];
unset($colunas[array('bar', 'a')]);
isset($colunas[array('bar', 'a')]);
unset($colunas['bar']);

请注意,我不检查offset是否为null,如果是数组,则其大小必须为&gt; 1。

答案 5 :(得分:0)

主要根据Dakota的解决方案*我想分享我对它的简化。

*)Dakota对我来说是最容易理解的,结果非常好(其他人看起来非常相似)。

所以,对于像我这样的人,他们很难理解这里发生了什么:

class DimensionalArrayAccess implements ArrayAccess {

    private $_arr;

    public function __construct(array $arr = array()) {

        foreach ($arr as $key => $value)
            {
                $this[$key] = $value;
            }
    }

    public function offsetSet($offset, $val) {
        if (is_array($val)) $val = new self($val);
        if ($offset === null) {
            $this->_arr[] = $val;
        } else {
            $this->_arr[$offset] = $val;
        }
    }

    // as normal
    public function offsetGet($offset) {
        return $this->_arr[$offset];
    }

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

    public function offsetUnset($offset) {
        unset($this->_arr);
    }
}

class Example extends DimensionalArrayAccess {
    function __construct() {
        parent::__construct([[["foo"]]]);
    }
}


$ex = new Example();

echo $ex[0][0][0];

$ex[0][0][0] = 'bar';

echo $ex[0][0][0];

我做了一些改变:

  • 删除了toArray函数,因为它没有直接目的,只要你不想将你的对象转换为真实的(在Dakota案例关联中)数组。
  • 删除了clone-thing,因为只要你不想克隆你的对象,它就没有直接的目的。
  • 重命名了扩展类和相同的vars:对我来说似乎更容易理解。特别是我想强调的是,DimensionalArrayAccess-class为你的对象提供了类似于数组的访问,即使对于三维或更多维(当然也包括非关联的)'数组' - 至少只要你用它实例化它数组计算您需要的维数。
  • 最后,我强调一点似乎很重要,正如你所看到的,Example-class本身不依赖于构造函数变量,而DimensionalArrayAccess-class是(因为它在递归中调用自己的offsetSet函数。) LI>

正如我所介绍的那样,这篇文章更适用于像我这样不那么先进的人。

编辑:这仅适用于在实例化期间设置的单元格,而之后无法添加新单元格。

答案 6 :(得分:0)

class Test implements \ArrayAccess {
    private
        $input = [];

    public function __construct () {
        $this->input = ['foo' => ['bar' => 'qux']];
    }

    public function offsetExists ($offset) {}
    public function offsetGet ($offset) {}
    public function offsetSet ($offset, $value) {}
    public function offsetUnset ($offset) {}
}

runkit_method_redefine ('Test', 'offsetGet', '&$offset', 'return $this->input[$offset];');

$ui = new Test;

var_dump($ui['foo']['bar']); // string(3) "qux"