Подтвердить что ты не робот

ArrayAccess многомерный (un) набор?

У меня есть класс, реализующий 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 2

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

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

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

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

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

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

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

4b9b3361

Ответ 1

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

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

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

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

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

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

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

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

Ответ 2

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

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

<?php

// sanity and error checking omitted for brevity
// note: it 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 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

ИЗМЕНИТЬ: см. ответ Александра Константинова. Я думал о методе __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;
}

теперь этот 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

дает:

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

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

Ответ 4

Решение:

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

Ответ 5

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

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.

Ответ 6

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

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

EDIT: это работает только для ячеек, которые устанавливаются во время создания экземпляра, тогда как после этого невозможно добавить новые ячейки.

Ответ 7

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"