ArrayAccess многомерный (не) установлен - PullRequest
16 голосов
/ 21 мая 2010

У меня есть класс, реализующий ArrayAccess, и я пытаюсь заставить его работать с многомерным массивом.exists и get работают.set и unset вызывают у меня проблему.

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 можно просто сделать общедоступным, чтобы вы могли получить к нему доступ напрямую, но для моей реализации это нежелательнои является приватным.

Последние 2 строки выдают ошибку: Notice: Indirect modification of overloaded element.

Я знаю, ArrayAccess, как правило, не работает с многомерными массивами, но все равно естьвокруг этой или какой-нибудь более чистой реализации, которая обеспечит желаемую функциональность?

Лучшая идея, которую я мог бы придумать, это использовать символ в качестве разделителя и проверить его в set и unset и действовать соответственно,Хотя это очень уродливо и очень быстро, если вы имеете дело с переменной глубиной.

Кто-нибудь знает, почему exists и get работают так, чтобы, возможно, копировать функциональность?

Спасибо за любую помощь, которую может предложить каждый.

Ответы [ 7 ]

19 голосов
/ 21 мая 2010

Проблема может быть решена путем изменения public function offsetGet($name) на public function &offsetGet($name) (путем добавления возврата по ссылке), , но , что приведет к фатальной ошибке (" декларация ArrayTest :: offsetGet () должен быть совместим с ArrayAccess :: offsetGet ()").

Авторы PHP некоторое время назад облажались с этим классом, и теперь они не изменят его ради обратной совместимости :

Мы выяснили, что это не решаемо без взрыва интерфейса и создание БК или предоставление дополнительный интерфейс для поддержки ссылки и тем самым создавая внутренний кошмар - на самом деле я не увидеть способ, которым мы можем заставить эту работу когда-либо. Таким образом, мы решили обеспечить соблюдение оригинальный дизайн и запретить ссылки полностью.

Редактировать: Если вам все еще нужны эти функции, я бы предложил вместо этого использовать магический метод (__get(), __set() и т. Д.), Поскольку __get() возвращает значение по ссылке. Это изменит синтаксис на что-то вроде этого:

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

Конечно, не идеальное решение, но я не могу придумать лучшего.

Обновление: Эта проблема была исправлена ​​в PHP 5.3.4 , и ArrayAccess теперь работает должным образом:

Начиная с PHP 5.3.4, проверки прототипов были ослаблены, и реализации этого метода могут вернуться по ссылке. Это делает возможным косвенное изменение размеров перегруженного массива объектов ArrayAccess.

6 голосов
/ 20 ноября 2012

Эта проблема на самом деле решаема, полностью функциональна, как и должна быть.

Из комментария к документации ArrayAccess здесь :

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

3 голосов
/ 21 мая 2010

РЕДАКТИРОВАТЬ: См. Ответ Александра Константинова. Я думал о методе __get magic, который аналогичен, но на самом деле был реализован правильно. Так что вы не можете сделать это без внутренней реализации вашего класса.

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

ОРИГИНАЛ следует - это неверно:

Ваша реализация offsetGet должна возвращать ссылку, чтобы она работала.

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

Внутренний эквивалент см. здесь .

Поскольку нет аналога get_property_ptr_ptr, вы должны вернуть ссылку (в смысле Z_ISREF) или прокси-объект (см. Обработчик get) в контексте, подобном записи (типы BP_VAR_W, BP_VAR_RW и BP_VAR_UNSET), хотя это не так обязательный. Если read_dimension вызывается в контексте, подобном записи, например, в $ val = & $ obj ['prop'], и вы не возвращаете ни ссылку, ни объект, механизм выдаст уведомление. Очевидно, что для правильной работы этих операций возврата ссылки недостаточно, необходимо, чтобы изменение возвращаемого zval действительно имело некоторый эффект. Обратите внимание, что такие присваивания, как $ obj ['key'] = & $ a, по-прежнему невозможны - для этого нужно, чтобы измерения были фактически сохраняемыми как zval (что может иметь или не иметь место) и два уровня косвенности.

В целом, операции, которые включают запись или отмена субразмера вызова под-свойства, вызывают offsetGet, а не offsetSet, offsetExists или offsetUnset.

2 голосов
/ 01 ноября 2012

Решение:

<?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);
    }
}
1 голос
/ 10 августа 2011

Я решил, используя это:

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']);

Обратите внимание, что я не проверяю, является ли смещение нулевым, и если это массив, он должен иметь размер> 1.

0 голосов
/ 26 июля 2013
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"
0 голосов
/ 07 июля 2013

В основном в соответствии с решением Дакоты * Я хочу поделиться своим упрощением.

*) Дакота была для меня наиболее понятной, и результат довольно велик (другие кажутся очень похожими). ​​

Итак, для таких, как я, которым трудно понять, что здесь происходит:

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, так как она не имеет непосредственного назначения, если вы не хотите преобразовывать свой объект в реальный (в случае ассоциативного типа Дакоты) массив.
  • удалил вещь-клон, так как она не имеет непосредственной цели, если вы не хотите клонировать свой объект.
  • переименовал расширенный класс и те же самые переменные: мне кажется более понятным. особенно хочу подчеркнуть, что класс DimensionalArrayAccess предоставляет массивоподобный доступ к вашему объекту даже для 3-х или более-мерных (и, конечно, также неассоциативных) «массивов» - по крайней мере до тех пор, пока вы создаете его с массив, подсчитывающий количество нужных вам измерений.
  • В заключение мне кажется важным подчеркнуть, что, как вы можете видеть, сам класс Example не зависит от переменной конструктора, тогда как класс DimensionalArrayAccess (как он вызывает себя в функции offsetSet рекурсивно.

Как я уже говорил, этот пост скорее для таких не очень продвинутых, как я.

РЕДАКТИРОВАТЬ: это работает только для ячеек, которые установлены во время создания экземпляра, тогда как невозможно добавить новые ячейки впоследствии.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...