I've been fooling with ArrayAccess and PHP's magic (__get, __set) for awhile now, and I'm stuck.
I'm trying to implement a class in which some properties, which are arrays, are read only. They will be set initially by the constructor, but should not be modifiable thereafter.
Using __get magic by reference, I can access array elements arbitrarily deep in the properties, and I was thinking I can throw exceptions when those properties are targeted via __set.
The problem is though, when I'm accessing the value of an array element, PHP is calling __get to return that part of the array by reference, and I have no knowledge of whether or not its a read or write action.
(The worst part is I knew this going in, but have been fooling with ArrayAccess as a possible workaround solution, given the properties were instances of an implemented object)
Simple example:
class Test{
public function &__get($key){
echo "[READ:{$key}]\n";
}
public function __set($key, $value){
echo "[WRITE:{$key}={$value}]\n";
}
}
$test = new Test;
$test->foo;
$test->foo = 'bar';
$test->foo['bar'];
$test->foo['bar'] = 'zip';
And the output:
[READ:foo]
[WRITE:foo=bar]
[READ:foo]
[READ:foo] // here's the problem
Realistically, I only need the value foo (as per my example) anyways, but I need to know it's a write action, not read.
I've already half accepted that this cannot be achieved, but I'm still hopeful. Does anyone have any idea how what I'm looking to accomplish can be done?
I was considering some possible workarounds with ArrayAccess, but so far as I can tell, I'll end up back at this spot, given I'm going to use the property notation that invokes __get.
Update: Another fun day with ArrayAccess.
(This is a different issue, but I suppose it works in. Posting just for kicks.)
class Mf_Params implements ArrayAccess{
private $_key = null;
private $_parent = null;
private $_data = array();
private $_temp = array();
public function __construct(Array $data = array(), $key = null, self $parent = null){
$this->_parent = $parent;
$this->_key = $key;
foreach($data as $key => $value){
$this->_data[$key] = is_array($value)
? new self($value, $key, $this)
: $value;
}
}
public function toArray(){
$array = array();
foreach($this->_data as $key => $value){
$array[$key] = $value instanceof self
? $value->toArray()
: $value;
}
return $array;
}
public function offsetGet($offset){
if(isset($this->_data[$offset])){
return $this->_data[$offset];
}
// if offset not exist return temp instance
return $this->_temp[$offset] = new self(array(), $offset, $this);
}
public function offsetSet($offset, $value){
$child = $this;
// copy temp instances to data after array reference chain
while(!is_null($parent = $child->_parent) && $parent->_temp[$child->_key] === $child){
$parent->_data[$child->_key] = $parent->_temp[$child->_key];
$child = $parent;
}
// drop temp
foreach($child->_temp as &$temp){
unset($temp);
}
if(is_null($offset)){
$this->_data[] = is_array($value)
? new self($value, null, $this)
: $value;
}else{
$this->_data[$offset] = is_array($value)
? new self($value, $offset, $this)
: $value;
}
}
public function offsetExists($offset){
return isset($this->_data[$offset]);
}
public function offsetUnset($offset){
unset($this->_data[$offset]);
}
}
You need to use a second class, implementing ArrayAccess, to use instead of your arrays. Then you will be able to control what is added to the array with the offsetSet() method:
class ReadOnlyArray implements ArrayAccess {
private $container = array();
public function __construct(array $array) {
$this->container = $array;
}
public function offsetSet($offset, $value) {
throw new Exception('Read-only');
}
public function offsetExists($offset) {
return isset($this->container[$offset]);
}
public function offsetUnset($offset) {
unset($this->container[$offset]);
}
public function offsetGet($offset) {
if (! array_key_exists($offset, $this->container)) {
throw new Exception('Undefined offset');
}
return $this->container[$offset];
}
}
You can then initialize your ReadOnlyArray with your original array:
$readOnlyArray = new ReadOnlyArray(array('foo', 'bar'));
You could not return by ref, which would solve the problem of changability, but would not allow changing of some values that are allowed to be changed.
Alternatively you need to wrap every returned array in ArrayAccess, too - and forbid write access there.
Related
I'm trying to JSON encode some objects in PHP, but I'm facing a problem: I want to encode data which is kept by a class private members.
I found this piece of code to encode this object by calling an encode function like:
public function encodeJSON()
{
foreach ($this as $key => $value)
{
$json->$key = $value;
}
return json_encode($json);
}
However, this only works if the object I want to encode does not contain other objects inside, which is the case. How can I do to encode not only the "outer" object, but encode as well any members that are objects too?
The best method to serialize an object with private properties is to implement the \JsonSerializable interface and then implement your own JsonSerialize method to return the data you require to be serialized.
<?php
class Item implements \JsonSerializable
{
private $var;
private $var1;
private $var2;
public function __construct()
{
// ...
}
public function jsonSerialize()
{
$vars = get_object_vars($this);
return $vars;
}
}
json_encode will now serialize your object correctly.
If you're using php 5.4 you can use the JsonSerializable interface: http://www.php.net/manual/en/class.jsonserializable.php
You just implement a jsonSerialize method in your class which returns whatever you want to be encoded.
Then when you pass your object into json_encode, it'll encode the result of jsonSerialize.
Anyway. You need create public method in your class to return all their fields json encoded
public function getJSONEncode() {
return json_encode(get_object_vars($this));
}
I think #Petah's got the best approach, but that way you lose properties that are array or object. So I added a function wich do that recursively:
function json_encode_private($object) {
function extract_props($object) {
$public = [];
$reflection = new ReflectionClass(get_class($object));
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$value = $property->getValue($object);
$name = $property->getName();
if(is_array($value)) {
$public[$name] = [];
foreach ($value as $item) {
if (is_object($item)) {
$itemArray = extract_props($item);
$public[$name][] = $itemArray;
} else {
$public[$name][] = $item;
}
}
} else if(is_object($value)) {
$public[$name] = extract_props($value);
} else $public[$name] = $value;
}
return $public;
}
return json_encode(extract_props($object));
}
EDIT: Added is_object() check inside the array loop to avoid a get_class() exception in the next extract_props() call when the array elements are not objects, like strings or numbers.
I think this may be a great case for the Usage of Traits
using the below guist I implemented jsonSerializable interface in multiple points of my app while keeping the code manageable
https://gist.github.com/zburgermeiszter/7dc5e65b06bb34a325a0363726fd8e14
trait JsonSerializeTrait
{
function jsonSerialize()
{
$reflect = new \ReflectionClass($this);
$props = $reflect->getProperties(\ReflectionProperty::IS_STATIC | \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_PRIVATE);
$propsIterator = function() use ($props) {
foreach ($props as $prop) {
yield $prop->getName() => $this->{$prop->getName()};
}
};
return iterator_to_array($propsIterator());
}
}
then you just have to do
class YourClass implements JsonSerializable
{
use JsonSerializeTrait;
... normal encapsulated code...
}
public function jsonSerialize()
{
$objectArray = [];
foreach($this as $key => $value) {
$objectArray[$key] = $value;
}
return json_encode($objectArray);
}
I personally think this is a way of doing it. It is similar to Petah's, except It keeps in line with encapsulation well, because the array is populated from the object.
Put this function in either your object or as a trait to be used by your object. To each their own though.
This would print a JSON with all of the properties (public, private and protected) of class foo:
$reflection = new ReflectionClass('Foo');
$properties = $reflection->getdefaultProperties();
echo json_encode($properties);
It would work from any context.
You can only encode an object's private members from within the class. As a side note though, does the json_enocde function not work for you? http://php.net/manual/en/function.json-encode.php
Using reflection you can json_encode private properties, although its not considered best practice:
function json_encode_private($object) {
$public = [];
$reflection = new ReflectionClass($object);
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$public[$property->getName()] = $property->getValue($object);
}
return json_encode($public);
}
E.g.
class Foo {
public $a = 1;
public $b = 2;
}
class Bar {
private $c = 3;
private $d = 4;
}
var_dump(json_encode(new Foo()));
var_dump(json_encode_private(new Bar()));
Outputs:
string(13) "{"a":1,"b":2}"
string(13) "{"c":3,"d":4}"
http://codepad.viper-7.com/nCcKYW
Here is the way I do:
$aNewObject = new MyObj();
$aNewObject->set_id($row->id);
$aNewObject->set_user_id($row->user_id);
$aNewObject->set_title($row->title);
$aNewObject->set_url($row->url);
$aNewObject->set_description($row->description);
$aNewObject->set_status($row->status);
as you can see, I follow a name convention, which the object and the data base field is 100% match, I think there should have a way to help me to do it lazier, any recommendation?
You can get even lazier by only writing
$aNewObject = new MyObj($row);
and having a constructor that sets the object's properties based on the contents of $row
You could do the setting dynamically by iterating over the fields (if that is meant by lazier):
$fields = array('user_id', ...);
foreach($fields as $field)
{
$setter = "set_{$field}";
$aNewObject->$setter($row->{$field});
}
It depends then where you want to place that code. Either just inline, as part of a function of MyObj (importRow($row)) or in a global helper function that always calls all setters matching object properties.
Within the class constructor:
$aNewObject = new MyObj($row);
class MyObj
{
public function __construct($row = null)
{
...
$this->importRow($row);
}
public function importRow($row = null)
{
if (null === $row)
return;
foreach($row as $field => $value)
{
$setter = "set_{$field}";
$this->$setter($value);
}
}
...
}
To prevent duplicate code across different classes (missing traits support in PHP < 5.4), a global static function or object can do it:
$aNewObject = new MyObj();
new Setter($aNewObject, $row);
# or
$aNewObject = Setter::fill('MyObj', $row);
class Setter
{
private $object;
public function __construct($class, $data)
{
// works on classnames or objects
if (is_string($class))
$object = new $class();
else
$object = $class;
$this->object = $this->import($object, $data);
}
private function import($object, $data)
{
foreach($data as $field => $value)
{
$setter = "set_{$field}";
$object->$setter($value);
}
return $object;
}
public function getObject()
{
return $this->object;
}
public static function fill($class, $data)
{
$self = new __CLASS__($class, $data);
return $self->getObject();
}
}
Lazier, it is not recommended, because it will make it hard to maintain by other programmers.
If you still want to do that, you should do with PHP reflection: http://www.php.net/manual/en/intro.reflection.php
or as #hakre answered.
For example, I have a object like this:
class myObj{
private $a;
private $b;
//getter , setter
}
And I would like to do something like:
$myObj = initWitharray(array('a'=> 'myavalue',
'b'=> 'mybvalue'));
And the myObj will have all the a value and b value. How can I do so ? Thank you.
As NullUserException suggested:
<?php
class myObj {
private $a;
private $b;
public function initWithArray(array $arr) {
foreach ($arr as $k => $v) {
$this->$k = $v;
}
return $this;
}
public function get($name) {
return $this->$name;
}
}
// usage
$myObj = new myObj();
echo $myObj->initWithArray(array(
'a' => 'myavalue',
'b' => 'mybvalue'))
->get('a');
function initWithArray(array $a){
$myObj = new myObj();
foreach($a as $k => $v){
$myObj->$k = $v;
}
return $myObj;
}
class myObj {
private $a;
private $b;
public function __set($name, $value) {
$this->$name = $value;
}
public function __get($name){
if($this->$name != null)
return $this->$name;
return null;
}
}
Or, as said in the comments, it's better if init function would be a member of a class.
Try the following:
class myObj {
private $a;
private $b;
function __construct($passedArray){
$this->a = array_key_exists('a', $passedArray) ? $passedArray['a'] : 'default_value_for_a';
$this->b = array_key_exists('b', $passedArray) ? $passedArray['b'] : 'default_value_for_b';
}
//Rest of the code
}
Then:
newObj = new myObj(array('a'=> 'myavalue', 'b'=> 'mybvalue'))
You could use the class constructor to pass in options when you create a new object. Doing it this way, you should also separate out the setOptions method so you can update the options after init as well.
Use this class like this: (shows both ways to set options)
$object = new myClass(array('a'=>'foo'));
$object->setOptions(array('b'=>'bar'));
Also, try not to confuse object with class. An object is an instance of a class.
class myClass
{
private $a;
private $b;
public function __construct(array $options = null)
{
if (null !== $options) {
$this->setOptions($options);
}
}
public function setOptions(array $options)
{
foreach ($options as $key => $value) {
if (isset($this->$key)) {
$this->$key = $value;
}
}
return $this;
}
}
I usually adopts the approach which gives me total control over the object, like allowing someone to access the property. denying the permission, allowing access to only those which i think is appropriate according to application etc. and that's the purpose of object.
Have a look at the example below.
Example
class MyObj {
private $data = array('one' => null, 'two' => null);
public function __set($property, $value) {
//Only allow to set those properties which is declared in $this->data array
if(array_key_exists($property, $this->data)) {
return $this->data[$property] = $value;
} else {
//if you want to throw some error.
}
}
//you can allow or disallow anyone from accessing the class property directly.
public function __get($property) {
//To deny the access permission, simply throw an error and return false.
$error = 'access denied to class property {' . $property . '}';
return false;
//Or Else Allow permission to access class property
//return $this->data[$property];
}
}
the above example demonstrates on how you can gain more control over the class property, by declaring class property $data as private you are basically disallowing anyone to do any sort of manipulation on the class property directly. whatever operation is to be carried out is done through PHP's getter __get() and setter __set() method. of course you can modify the above code according to your need, you just new a very few line of changes and it will behave the way you want it to.
I'm trying to JSON encode some objects in PHP, but I'm facing a problem: I want to encode data which is kept by a class private members.
I found this piece of code to encode this object by calling an encode function like:
public function encodeJSON()
{
foreach ($this as $key => $value)
{
$json->$key = $value;
}
return json_encode($json);
}
However, this only works if the object I want to encode does not contain other objects inside, which is the case. How can I do to encode not only the "outer" object, but encode as well any members that are objects too?
The best method to serialize an object with private properties is to implement the \JsonSerializable interface and then implement your own JsonSerialize method to return the data you require to be serialized.
<?php
class Item implements \JsonSerializable
{
private $var;
private $var1;
private $var2;
public function __construct()
{
// ...
}
public function jsonSerialize()
{
$vars = get_object_vars($this);
return $vars;
}
}
json_encode will now serialize your object correctly.
If you're using php 5.4 you can use the JsonSerializable interface: http://www.php.net/manual/en/class.jsonserializable.php
You just implement a jsonSerialize method in your class which returns whatever you want to be encoded.
Then when you pass your object into json_encode, it'll encode the result of jsonSerialize.
Anyway. You need create public method in your class to return all their fields json encoded
public function getJSONEncode() {
return json_encode(get_object_vars($this));
}
I think #Petah's got the best approach, but that way you lose properties that are array or object. So I added a function wich do that recursively:
function json_encode_private($object) {
function extract_props($object) {
$public = [];
$reflection = new ReflectionClass(get_class($object));
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$value = $property->getValue($object);
$name = $property->getName();
if(is_array($value)) {
$public[$name] = [];
foreach ($value as $item) {
if (is_object($item)) {
$itemArray = extract_props($item);
$public[$name][] = $itemArray;
} else {
$public[$name][] = $item;
}
}
} else if(is_object($value)) {
$public[$name] = extract_props($value);
} else $public[$name] = $value;
}
return $public;
}
return json_encode(extract_props($object));
}
EDIT: Added is_object() check inside the array loop to avoid a get_class() exception in the next extract_props() call when the array elements are not objects, like strings or numbers.
I think this may be a great case for the Usage of Traits
using the below guist I implemented jsonSerializable interface in multiple points of my app while keeping the code manageable
https://gist.github.com/zburgermeiszter/7dc5e65b06bb34a325a0363726fd8e14
trait JsonSerializeTrait
{
function jsonSerialize()
{
$reflect = new \ReflectionClass($this);
$props = $reflect->getProperties(\ReflectionProperty::IS_STATIC | \ReflectionProperty::IS_PUBLIC | \ReflectionProperty::IS_PROTECTED | \ReflectionProperty::IS_PRIVATE);
$propsIterator = function() use ($props) {
foreach ($props as $prop) {
yield $prop->getName() => $this->{$prop->getName()};
}
};
return iterator_to_array($propsIterator());
}
}
then you just have to do
class YourClass implements JsonSerializable
{
use JsonSerializeTrait;
... normal encapsulated code...
}
public function jsonSerialize()
{
$objectArray = [];
foreach($this as $key => $value) {
$objectArray[$key] = $value;
}
return json_encode($objectArray);
}
I personally think this is a way of doing it. It is similar to Petah's, except It keeps in line with encapsulation well, because the array is populated from the object.
Put this function in either your object or as a trait to be used by your object. To each their own though.
This would print a JSON with all of the properties (public, private and protected) of class foo:
$reflection = new ReflectionClass('Foo');
$properties = $reflection->getdefaultProperties();
echo json_encode($properties);
It would work from any context.
You can only encode an object's private members from within the class. As a side note though, does the json_enocde function not work for you? http://php.net/manual/en/function.json-encode.php
Using reflection you can json_encode private properties, although its not considered best practice:
function json_encode_private($object) {
$public = [];
$reflection = new ReflectionClass($object);
foreach ($reflection->getProperties() as $property) {
$property->setAccessible(true);
$public[$property->getName()] = $property->getValue($object);
}
return json_encode($public);
}
E.g.
class Foo {
public $a = 1;
public $b = 2;
}
class Bar {
private $c = 3;
private $d = 4;
}
var_dump(json_encode(new Foo()));
var_dump(json_encode_private(new Bar()));
Outputs:
string(13) "{"a":1,"b":2}"
string(13) "{"c":3,"d":4}"
http://codepad.viper-7.com/nCcKYW
I have a class implementing ArrayAccess and I'm trying to get it to work with a multidimensional array. exists and get work. set and unset are giving me a problem though.
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
I know $_arr could just be made public so you could access it directly, but for my implementation it's not desired and is private.
The last 2 lines throw an error: Notice: Indirect modification of overloaded element.
I know ArrayAccess just generally doesn't work with multidimensional arrays, but is there anyway around this or any somewhat clean implementation that will allow the desired functionality?
The best idea I could come up with is using a character as a separator and testing for it in set and unset and acting accordingly. Though this gets really ugly really fast if you're dealing with a variable depth.
Does anyone know why exists and get work so as to maybe copy over the functionality?
Thanks for any help anyone can offer.
The problem could be resolved by changing public function offsetGet($name) to public function &offsetGet($name) (by adding return by reference), but it will cause Fatal Error ("Declaration of ArrayTest::offsetGet() must be compatible with that of ArrayAccess::offsetGet()").
PHP authors screwed up with this class some time ago and now they 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: If you still need that functionality, I'd suggest using magic method instead (__get(), __set(), etc.), because __get() returns value by reference. This will change syntax to something like this:
$arrTest->test['bar'] = 5;
Not an ideal solution of course, but I can't think of a better one.
Update: This problem was fixed in PHP 5.3.4 and ArrayAccess now works as expected:
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.
This issue is actually solvable, entirely functional how it should be.
From a comment on the ArrayAccess documentation 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"
?>
You can then extend that class, like 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);
?>
This will give you an object that can be treated like an array (mostly, see note in code), which supports multi-dimensional array set/get/unset.
EDIT: See the response of Alexander Konstantinov. I was thinking of the __get magic method, which is analogous, but was actually implemented correctly. So you cannot do that without an internal implementation of your class.
EDIT2: Internal implementation:
NOTE: You might argue this is purely masturbatory, but anyway here it goes:
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;
}
now this 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
gives:
test/bar is set
2
test/baz is not set
5
ORIGINAL follows -- this is incorrect:
Your offsetGet implementation must return a reference for it to work.
public function &offsetGet($name) {
return $this->_arr[$name];
}
For the internal equivalent, see 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 sum, operations that involve writing or unseting a sub-dimension of sub-property call offsetGet, not offsetSet, offsetExists or offsetUnset.
Solution:
<?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);
}
}
I solved it using this:
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 you can use it with:
$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']);
Please note that I don't check if offset is null, and if it's an array, it must be of size > 1.
Mainly according to Dakota's solution* I want to share my simplification of it.
*) Dakota's was the most understandable one to me and the outcome is quite great (- the others seem quite similar great).
So, for the ones like me, who have their difficulties in understanding what's going on here:
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];
I did some changes:
deleted the toArray-function, as it has no immediate purpose as long as you don't want to convert your object into an real (in Dakota's case associative) array.
deleted the clone-thing, as it has no immediate purpose as long as you don't want to clone your object.
renamed the extended class and same vars: seems more understandable to me. especially I want to emphasize, that the DimensionalArrayAccess-class gives array-like access to your object even for 3- or more-dimensional (and of course also non-associative) 'arrays' - at least as long as you instanciate it with an array counting the number of dimensions you need.
last it seems important to me to emphasize, that as you can see the Example-class itself is not dependent on a constructor variable, whereas the DimensionalArrayAccess-class is (as it calls itself in the offsetSet-function recursively.
As I introduced, this post is rather for the not so advanced ones like me.
EDIT: this only works for cells which are set during instantiation, whereas it is not possible to add new cells afterwards.
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"