2010-05-21 3 views
15

Ich habe eine Klasse implementieren ArrayAccess und ich versuche, es mit einem multidimensionalen Array zu arbeiten. exists und get arbeiten. set und unset geben mir jedoch ein Problem.ArrayAccess mehrdimensional (un) gesetzt?

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 

Ich weiß $_arr nur öffentlich gemacht werden könnte, so dass Sie es direkt zugreifen können, aber für meine Implementierung ist es nicht erwünscht und ist privat.

Die letzten 2 Zeilen werfen einen Fehler: Notice: Indirect modification of overloaded element.

Ich weiß, ArrayAccess funktioniert im Allgemeinen nicht mit mehrdimensionalen Arrays, aber gibt es sowieso um diese oder irgendeine einigermaßen saubere Implementierung, die die gewünschte Funktionalität ermöglicht?

Die beste Idee, die ich finden könnte, ist ein Zeichen als Trennzeichen zu verwenden und in set und unset dafür zu testen und dementsprechend zu handeln. Obwohl es sehr schnell hässlich wird, wenn Sie mit einer variablen Tiefe zu tun haben.

Weiß jemand, warum exists und get arbeiten, um vielleicht über die Funktionalität zu kopieren?

Danke für jede Hilfe, die jemand anbieten kann.

+0

Sie verpassen ein schließendes ')' auf dem Unset ($ arrTest ['test'] ['bar']; // Fehlerzeile War das ein Problem im ursprünglichen Code? – 88ad

Antwort

18

Das Problem von public function offsetGet($name) zu public function &offsetGet($name) Wechsel aufgelöst werden konnte (durch Rückgabe durch Bezugnahme Zugabe), aber es Fatal Error verursachen wird (“Erklärung Klasse Array :: offsetGet() muss mit der Arrayaccess kompatibel sein :: offsetGet() ").

PHP Autoren vor einiger Zeit mit dieser Klasse geschraubt und jetzt sind sie 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: Wenn Sie noch diese Funktionalität benötigen, würde ich stattdessen verwenden magische Methode vorschlagen (__get(), __set(), usw.), weil __get() den Wert als Referenz liefert. Dies ändert die Syntax in etwa wie folgt:

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

Nicht eine ideale Lösung natürlich, aber ich kann nicht an eine bessere denken.

Update: Dieses Problem war fixed in PHP 5.3.4 und Arrayaccess funktioniert nun wie erwartet:

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

Sie haben recht, ich erinnere mich, dass ich kürzlich eine Diskussion darüber auf php-dev gesehen habe. – Artefacto

+0

Es ist hier: http://marc.info/?t=127235631000008&r=1&w=2 So können sie Hoffnung sein! – Artefacto

+0

Er könnte es immer noch mit einer internen Implementierung machen. Er könnte eine benutzerdefinierte Basisklasse erstellen, nicht ArrayAccess implementieren, dann den Handler read_property außer Kraft setzen (vielleicht dann nach einer Funktion mit dem Namen "offsetget" suchen oder einfach die gesamte Arbeit im Handler selbst erledigen). – Artefacto

3

EDIT: Siehe die Antwort von Alexander Konstantinov. Ich dachte an die __get magic-Methode, die analog ist, aber tatsächlich korrekt implementiert wurde. Sie können das also nicht ohne eine interne Implementierung Ihrer Klasse tun.

EDIT2: Interne Umsetzung:

Hinweis: Sie könnten argumentieren, diese rein masturbatory ist, aber trotzdem hier geht es:

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

jetzt dieses Skript:

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

gibt:

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

ORIGINAL folgt - das ist falsch:

Ihre offsetGet Implementierung muss eine Referenz zurückgeben, damit sie funktioniert.

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

Für die interne Äquivalent finden 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 der Summe Operationen, die eine Unter Dimension des Unter Eigenschaft Aufruf offsetGet beinhalten das Schreiben oder unseting, nicht offsetSet, offsetExists oder offsetUnset.

1

Ich löste es dies mit:

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

So können Sie es mit verwenden:

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

Bitte beachten Sie, dass ich nicht überprüfen, ob Offset ist null, und wenn es ein Array ist, muss es von Größe> 1.

2

Lösung sein:

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

Dieses Problem ist eigentlich auflösbar, vollständig funktionale, wie es sein sollte.

Von einem Kommentar zu der Arrayaccess Dokumentation 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" 

?> 

können Sie dann diese Klasse erweitern, etwa so:

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

?> 

Dieses Sie ein Objekt gibt, das wie ein Array behandelt werden können, (meistens, siehe Hinweis im Code), der mehrdimensionale Array-Set/Get/Unset unterstützt.

+0

Vielen Dank dafür :) Versuch, es jetzt zu implementieren ... –

0

Hauptsächlich nach Dakotas Lösung * Ich möchte meine Vereinfachung davon teilen.

*) Dakota war das verständlichste für mich und das Ergebnis ist ziemlich groß (- die anderen scheinen ziemlich ähnlich groß).

Also, für diejenigen wie mich, die ihre Schwierigkeiten zu verstehen, was hier vor sich geht:

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

ich einige Änderungen haben:

  • löschte die toArray-Funktion, da es kein sofortiger Zweck, solange Sie Ihr Objekt nicht in ein reales (in Dakotas Fall assoziatives) Array konvertieren wollen.
  • löschte das Klon-Ding, da es keinen unmittelbaren Zweck hat, solange Sie Ihr Objekt nicht klonen wollen.
  • umbenannt die erweiterte Klasse und gleiche Vars: scheint mir verständlicher.insbesondere möchte ich betonen, dass die DimensionalArrayAccess-Klasse einen Array-ähnlichen Zugriff auf Ihr Objekt auch für 3- oder mehrdimensionale (und natürlich auch nicht-assoziative) 'Arrays' bietet - zumindest solange Sie es mit einem installieren Array, das die Anzahl der benötigten Dimensionen zählt.
  • Zuletzt scheint es mir wichtig zu betonen, dass, wie Sie sehen können, die Example-Klasse selbst nicht von einer Konstruktorvariablen abhängig ist, während die DimensionalArrayAccess-Klasse (wie sie sich selbst in der offsetSet-Funktion rekursiv aufruft)

wie ich eingeführt wird, ist dieser Beitrag eher für die nicht so weit fortgeschritten, diejenigen wie mich

EDIT:. funktioniert dies nur für Zellen, die während der Instanziierung eingestellt sind, während es nicht möglich ist, danach, neue Zellen zu addieren

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