2010-05-21 5 views
15

Tengo una clase implementando ArrayAccess y estoy tratando de que funcione con una matriz multidimensional. exists y get trabajo. set y unset me están dando un problema.ArrayAccess multidimensional (un) set?

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 solo pueden hacerse públicas por lo que podría acceder a él directamente, pero para mi aplicación no es desear y es privado.

Las últimas 2 líneas arrojan un error: Notice: Indirect modification of overloaded element.

ArrayAccess Por lo general, no funciona con matrices multidimensionales, pero ¿hay alguna forma de esto o una implementación algo limpia que permita la funcionalidad deseada?

La mejor idea que podría surgir es usar un carácter como separador y probarlo en set y unset y actuar en consecuencia. Aunque esto se pone muy feo realmente rápido si se trata de una profundidad variable.

¿Alguien sabe por qué exists y get funcionan para tal vez copiar la funcionalidad?

Gracias por cualquier ayuda que cualquiera pueda ofrecer.

+0

Falta un ')' de cierre en el desarmado ($ arrTest ['test'] ['bar']; // línea de error ¿Era eso un problema en el código original? – 88ad

Respuesta

18

El problema podrían resuelve cambiando public function offsetGet($name) a public function &offsetGet($name) (mediante la adición de retorno por referencia), pero que hará que un error fatal (" Declaración de ArrayTest :: offsetGet() debe ser compatible con el de ArrayAccess :: offsetGet() ").

autores de PHP atornilladas con esta clase hace algún tiempo y ahora 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.

Editar: Si aún necesita esa funcionalidad, sugeriría el uso de método mágico lugar (__get(), __set(), etc.), porque __get() devuelve valor por referencia. Esto va a cambiar la sintaxis a algo como esto:

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

No es una solución ideal, por supuesto, pero no puedo pensar en una mejor.

Actualización: Este problema se fixed in PHP 5.3.4 y ArrayAccess ahora funciona como se espera:

Starting with PHP 5.3.4, the prototype checks were relaxed and it's possible for implementations of this method to return by reference. This makes indirect modifications to the overloaded array dimensions of ArrayAccess objects possible.

+0

Tienes razón, recuerdo haber visto una discusión sobre esto en php-dev recientemente. – Artefacto

+0

Está aquí: http://marc.info/?t=127235631000008&r=1&w=2 ¡Así que pueden ser una esperanza! – Artefacto

+0

Aún podría hacerlo con una implementación interna. Podía crear una clase base personalizada, no implementar ArrayAccess, luego anular el manejador read_property (tal vez luego buscar una función llamada "offsetget" o simplemente hacer todo el trabajo en el manejador). – Artefacto

3

EDIT: Vea la respuesta de Alexander Konstantinov. Estaba pensando en el método mágico __get, que es análogo, pero en realidad se implementó correctamente. Entonces no puedes hacer eso sin una implementación interna de tu clase.

Edit2: aplicación interna:

NOTA: Se podría argumentar que esto es puramente masturbatoria, pero de todos modos aquí va:

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

ahora este 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 

da:

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

ORIGINAL sigue: esto es incorrecto:

Su implementación offsetGet debe devolver una referencia para que funcione.

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

Por el equivalente interno, ver 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.

En suma, las operaciones que implican para escribir o unseting un sub-dimensión del sub-propiedad offsetGet llamada, no offsetSet, offsetExists o offsetUnset.

1

Lo resuelto utilizando la siguiente:

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

esta manera puede utilizar con:

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

Tenga en cuenta que no compruebo si la compensación es nulo, y si se trata de una matriz, debe ser de un tamaño> 1.

2

Solución:

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

Este problema es solucionable en realidad, totalmente funcional, como debe ser.

Desde un comentario en la documentación 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" 

?> 

A continuación, puede ampliar esa clase, así:

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

?> 

esto le dará un objeto que puede ser entendido como un array (sobre todo, vea la nota en el código), que es compatible con conjunto de matriz multidimensional/get/unset.

+0

Gracias por esto :) Intentando implementarlo ahora ... –

0

Principalmente según la solución de Dakota * Quiero compartir mi simplificación.

*) Dakota fue la más comprensible para mí y el resultado es bastante bueno (los otros parecen bastante similares).

lo tanto, para los que, como yo, que tienen sus dificultades para entender lo que está pasando aquí:

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

Me hicieron algunos cambios:

  • suprime la toArray-función, ya que tiene no tiene un propósito inmediato, siempre y cuando no desee convertir su objeto en una matriz real (en el caso asociativo de Dakota).
  • eliminado la clonación, ya que no tiene un propósito inmediato siempre y cuando no desee clonar su objeto.
  • renombré la clase extendida y los mismos vars: me parece más comprensible.especialmente quiero enfatizar que la clase DimensionalArrayAccess brinda acceso similar a un arreglo a su objeto incluso para 'arrays' de 3 o más dimensiones (y por supuesto también no asociativos), al menos mientras lo instancia con un objeto array que cuenta el número de dimensiones que necesita.
  • último, me parece importante recalcar que, como puede ver, la Clase-Ejemplo en sí misma no depende de una variable constructora, mientras que la clase DimensionalArrayAccess es (como se llama a sí misma en la función offsetSet recursivamente.

Como introduje, este post es más bien para los no tan avanzados como yo

EDIT:. esto sólo funciona para las células que se establecen durante la instanciación, mientras que no es posible añadir nuevas células después

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