ArrayAccess multidimensional (un)set? - php

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"

Related

Is it possible to skip calling __set from class internal code

I wrote the __set in order to make sure something is done before setter anything
public function __setter($name, $value) {
if ($this->needDoSomething) {$this->doSomeThingNecessaryBeforeSetAnything();}
$this->needDoSomething = false;
$this->$name = $value;
}
However, the magic method will impact performance. In the same class, I have another function
private function loadData() {
if ($this->needDoSomething) {$this->doSomeThingNecessaryBeforeSetAnything();}
foreach ($data as $key=>$value) {
$this->$key = $value;
}
}
Since the doSomeThingNecessaryBeforeSetAnything() is already called, I don't need to call __set, but would like to set the property directly. This will largely help on performance.
However, I cannot remove the __set, because there are lot's of legacy code outside the class, and I need it to make the logic correct.
Seems with PHP I cannot add or remove methods to object on the fly. Any ideas?
Edit: The performance is caused by __set itself, because I have large number of objects and each have large number of properties to set. The code below shows __set is 6 times slower than set properties directly.
class Test {}
class Test2 {
public function __set($name, $value) {
$this->$name = $value;
}
}
function runOnObject($o) {
$t = microtime(true);
for ($i=0; $i<100000; $i++) {
$prop = "prop{$i}";
$o->$prop = $i;
}
echo "".(microtime(true) - $t)." second ";
}
echo runOnObject(new Test()). "(With out __set)<p>";
echo runOnObject(new Test2()). "(With __set)";
The result:
0.084139823913574 second (With out __set)
0.47258400917053 second (With __set)
If you add a __get, you can store the properties in a private data structure (eg. an array), allowing direct access to the data from within the class, while still maintaining the same public interface.
Like this:
class Container
{
private $properties = array();
public function __set($key, $value)
{
$this->doSomethingExpensive();
$this->properties[$key] = $value;
}
public function __get($key)
{
if (!isset($this->properties[$key])) {
throw new Exception('Invalid Property ' . $key);
}
return $this->properties[$key];
}
public function loadData($data)
{
$this->doSomethingExpensive();
foreach ($data as $key => $value) {
$this->properties[$key] = $value;
}
}
private function doSomethingExpensive()
{
echo 'Doing Work...' . PHP_EOL;
}
}
// Example
$c = new Container();
$c->loadData(array(
'alpha' => 'A',
'beta' => 'D',
'charlie' => 'C',
'delta' => 'D'
));
var_dump($c->alpha, $c->beta);
If this will be any faster, I don't know, it depends on your specific use case as you avoid running the "expensive" code repeatedly, but there will be some overhead from using __get.
Obviously not exactly what you want, but another thought... Use ArrayAccess for the dynamic set functionality instead of __set. Though you'll have to update the 'tons' of client code (not sure how unrealistic that may be).
<?php
class A implements ArrayAccess
{
private $_doThings = false;
public function __construct() {
$this->loadData();
}
public function offsetExists($k) { return isset($this->$k); }
public function offsetGet($k) { return $this->$k; }
public function offsetUnset($k) { unset($this->$k); }
public function offsetSet($k, $v) {
if($this->_doThings)
$this->doSomeThingNecessaryBeforeSetAnything();
$this->$k = $v;
}
private function doSomeThingNecessaryBeforeSetAnything() {
echo __METHOD__ . PHP_EOL;
}
private function loadData() {
$this->doSomeThingNecessaryBeforeSetAnything();
$this->_doThings = false;
foreach (array('a' => 1, 'b' => 2, 'c' => 3) as $key=>$value) {
$this->$key = $value;
}
}
}
Demo code
$a = new A();
// Need to change all calls like $a->c = 4 to $a['c'] = 4
$a['c'] = 4;
var_dump($a);
So there's a painful code change required, but you get the best of both worlds. The dynamic behavior, and the performance.

PHP mandatory function call

I understand that one can use interfaces to mandate the definition of a function, but I cannot find something that enables one to mandate function calls, such that e.g. if I create a class being a member of another class (via extends, etc), with a function, for that class to automatically ensure that mandatory functions are called in part with that function.
I mean, to clarify further:
class domain {
function isEmpty($input) {
//apply conditional logic and results
}
}
class test extends domain {
function addTestToDBTable($test) {
/**
* try to add but this class automatically makes it so that all rules of
* class domain must be passed before it can run
* - so essentially, I am no longer required to call those tests for each and
* every method
**/
}
}
Apologies if this appears incoherent by any means. Sure, it seems lazy but I want to be able to force context without having to concern abou
Update:
Okay, to clarify further: in PHP, if I extend and declare a __construct() for a child class, that child class will override the parent __construct(). I do not want this, I want the parent construct to remain and mandate whatever as it pleases just as the child class may do so also.
I guess it can be done in two different ways.
Aspect Oriented Programming
Have a look here https://github.com/AOP-PHP/AOP
Generate or write Proxy classes
A really simple example could be:
<?php
class A {
public function callMe() {
echo __METHOD__ . "\n";
}
}
class B extends A {
// prevents instantiation
public function __construct() {
}
public function shouldCallMe() {
echo __METHOD__ . "\n";
}
public static function newInstance() {
return new ABProxy();
}
}
class ABProxy {
private $b;
public function __construct() {
$this->b = new B();
}
public function __call($method, $args) {
$this->b->callMe();
return call_user_func_array(array($this->b, $method), $args);
}
}
// make the call
$b = B::newInstance();
$b->shouldCallMe();
// Outputs
// ------------------
// A::callMe
// B::shouldCallMe
Hopes this helps a bit.
Sounds like you want a Decorator.
See This answer for a detailed explanation on how to do it. Note that it does not require a class extension.
I would use a domain-validating decorator with some doc-block metaprogramming magic. But this is really a job for an entire library, which no doubt exists.
fiddle
<?php
class FooDomain {
public static function is_not_empty($input) {
return !empty($input);
}
}
class Foo {
/**
* #domain FooDomain::is_not_empty my_string
*/
public function print_string($my_string) {
echo $my_string . PHP_EOL;
}
}
$foo = new DomainValidator(new Foo());
$foo->print_string('Hello, world!');
try {
$foo->print_string(''); // throws a DomainException
} catch (\DomainException $e) {
echo 'Could not print an empty string...' . PHP_EOL;
}
// ---
class DomainValidator {
const DOMAIN_TAG = '#domain';
private $object;
public function __construct($object) {
$this->object = $object;
}
public function __call($function, $arguments) {
if (!$this->verify_domain($function, $arguments)) {
throw new \DomainException('Bad domain!');
}
return call_user_func_array(
array($this->object, $function),
$arguments
);
}
public function __get($name) {
return $this->object->name;
}
public function __set($name, $value) {
$this->object->name = $value;
}
private function verify_domain($function, $arguments) {
// Get reference to method
$method = new \ReflectionMethod($this->object, $function);
$domains = $this->get_domains($method->getDocComment());
$arguments = $this->parse_arguments(
$method->getParameters(),
$arguments
);
foreach ($domains as $domain) {
if (!call_user_func(
$domain['name'],
$arguments[$domain['parameter']]
)) {
return false;
}
}
return true;
}
private function get_domains($doc_block) {
$lines = explode("\n", $doc_block);
$domains = array();
$domain_tag = DomainValidator::DOMAIN_TAG . ' ';
foreach ($lines as $line) {
$has_domain = stristr($line, $domain_tag) !== false;
if ($has_domain) {
$domain_info = explode($domain_tag, $line);
$domain_info = explode(' ', $domain_info[1]);
$domains[] = array(
'name' => $domain_info[0],
'parameter' => $domain_info[1],
);
}
}
return $domains;
}
private function parse_arguments($parameters, $values) {
$ret = array();
for ($i = 0, $size = sizeof($values); $i < $size; $i++) {
$ret[$parameters[$i]->name] = $values[$i];
}
return $ret;
}
}
Output:
Hello, world!
Could not print an empty string...

PHP Iterator like mysql iterator?

Already I extended and implemented from SPL iterator.
But if I want to use it, I should use it on a foreach.
I tried to use it in a while like this:
$news = new testClass();
while( $row = $news )
echo $row["name"];
It will create an infinite loop !
But with foreach, it works fine!
Here is top of my class:
class testClass implements \Iterator
Where is the mistake ?
Fist, bravo on using the SPL classes for this type of 'standard' problem. Too often have I seen inexperienced/sloppy developers (or even good ones that simply don't think ahead) reinvent the wheel in these types of situations.
You're missing some very important details about the implementation of the iterator interface.
see PHP:Iterator - Manual for more information, and the reference implementation from below.
First, you need to implement the, rewind, current, key, next, and valid functions. the reference implementation looks like this:
class myIterator implements Iterator {
private $position = 0;
private $array = array(
"firstelement",
"secondelement",
"lastelement",
);
public function __construct() {
$this->position = 0;
}
function rewind() {
var_dump(__METHOD__);
$this->position = 0;
}
function current() {
var_dump(__METHOD__);
return $this->array[$this->position];
}
function key() {
var_dump(__METHOD__);
return $this->position;
}
function next() {
var_dump(__METHOD__);
++$this->position;
}
function valid() {
var_dump(__METHOD__);
return isset($this->array[$this->position]);
}
}
)
And the code for traversing that implementation looks like this:
$it = new myIterator;
foreach($it as $key => $value) {
var_dump($key, $value);
echo "\n";
}
foreach is language construct that iterates through all elements. while executes block of code until given condition is true. To make it work you have to use your own function that checks for valid key and returns current element.
Finally I created a simple example of this:
<?php
/**
* #author Soroush Khosravi
* #copyright 2013
*/
class _Iterator
{
private $array;
public function setArray(array $data)
{
$this->array = $data;
}
public function reader()
{
if (is_null($this->array))
return false;
$elem = array_shift($this->array);
if (count ($this->array) > 0)
return $elem;
return false;
}
}
Class child extends _Iterator
{
function execute()
{
$this->setArray(array(1,2,3,4,5,6));
return $this;
}
}
$obj = new child;
$obj = $obj->execute();
while($row = $obj->reader())
echo $row;
?>

Readonly multidimensional array property, PHP

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.

Can you create instance properties dynamically in PHP?

Is there any way to create all instance properties dynamically? For example, I would like to be able to generate all attributes in the constructor and still be able to access them after the class is instantiated like this: $object->property. Note that I want to access the properties separately, and not using an array; here's an example of what I don't want:
class Thing {
public $properties;
function __construct(array $props=array()) {
$this->properties = $props;
}
}
$foo = new Thing(array('bar' => 'baz');
# I don't want to have to do this:
$foo->properties['bar'];
# I want to do this:
//$foo->bar;
To be more specific, when I'm dealing with classes that have a large number of properties, I would like to be able to select all columns in a database (which represent the properties) and create instance properties from them. Each column value should be stored in a separate instance property.
Sort of. There are magic methods that allow you to hook your own code up to implement class behavior at runtime:
class foo {
public function __get($name) {
return('dynamic!');
}
public function __set($name, $value) {
$this->internalData[$name] = $value;
}
}
That's an example for dynamic getter and setter methods, it allows you to execute behavior whenever an object property is accessed. For example
print(new foo()->someProperty);
would print, in this case, "dynamic!" and you could also assign a value to an arbitrarily named property in which case the __set() method is silently invoked. The __call($name, $params) method does the same for object method calls. Very useful in special cases. But most of the time, you'll get by with:
class foo {
public function __construct() {
foreach(getSomeDataArray() as $k => $value)
$this->{$k} = $value;
}
}
...because mostly, all you need is to dump the content of an array into correspondingly named class fields once, or at least at very explicit points in the execution path. So, unless you really need dynamic behavior, use that last example to fill your objects with data.
This is called overloading
http://php.net/manual/en/language.oop5.overloading.php
It depends exactly what you want. Can you modify the class dynamically? Not really. But can you create object properties dynamically, as in one particular instance of that class? Yes.
class Test
{
public function __construct($x)
{
$this->{$x} = "dynamic";
}
}
$a = new Test("bar");
print $a->bar;
Outputs:
dynamic
So an object property named "bar" was created dynamically in the constructor.
Yes, you can.
class test
{
public function __construct()
{
$arr = array
(
'column1',
'column2',
'column3'
);
foreach ($arr as $key => $value)
{
$this->$value = '';
}
}
public function __set($key, $value)
{
$this->$key = $value;
}
public function __get($value)
{
return 'This is __get magic '.$value;
}
}
$test = new test;
// Results from our constructor test.
var_dump($test);
// Using __set
$test->new = 'variable';
var_dump($test);
// Using __get
print $test->hello;
Output
object(test)#1 (3) {
["column1"]=>
string(0) ""
["column2"]=>
string(0) ""
["column3"]=>
string(0) ""
}
object(test)#1 (4) {
["column1"]=>
string(0) ""
["column2"]=>
string(0) ""
["column3"]=>
string(0) ""
["new"]=>
string(8) "variable"
}
This is __get magic hello
This code will set dynamic properties in the constructor which can then be accessed with $this->column. It's also good practice to use the __get and __set magic methods to deal with properties that are not defined within the class. More information them can be found here.
http://www.tuxradar.com/practicalphp/6/14/2
http://www.tuxradar.com/practicalphp/6/14/3
You can use an instance variable to act as a holder for arbitrary values and then use the __get magic method to retrieve them as regular properties:
class My_Class
{
private $_properties = array();
public function __construct(Array $hash)
{
$this->_properties = $hash;
}
public function __get($name)
{
if (array_key_exists($name, $this->_properties)) {
return $this->_properties[$name];
}
return null;
}
}
Why is every example so complicated?
<?php namespace example;
error_reporting(E_ALL | E_STRICT);
class Foo
{
// class completely empty
}
$testcase = new Foo();
$testcase->example = 'Dynamic property';
echo $testcase->example;
Here is simple function to populate object members without making class members public.
It also leaves constructor for your own usage, creating new instance of object without invoking constructor! So, your domain object doesn't depend on database!
/**
* Create new instance of a specified class and populate it with given data.
*
* #param string $className
* #param array $data e.g. array(columnName => value, ..)
* #param array $mappings Map column name to class field name, e.g. array(columnName => fieldName)
* #return object Populated instance of $className
*/
function createEntity($className, array $data, $mappings = array())
{
$reflClass = new ReflectionClass($className);
// Creates a new instance of a given class, without invoking the constructor.
$entity = unserialize(sprintf('O:%d:"%s":0:{}', strlen($className), $className));
foreach ($data as $column => $value)
{
// translate column name to an entity field name
$field = isset($mappings[$column]) ? $mappings[$column] : $column;
if ($reflClass->hasProperty($field))
{
$reflProp = $reflClass->getProperty($field);
$reflProp->setAccessible(true);
$reflProp->setValue($entity, $value);
}
}
return $entity;
}
/******** And here is example ********/
/**
* Your domain class without any database specific code!
*/
class Employee
{
// Class members are not accessible for outside world
protected $id;
protected $name;
protected $email;
// Constructor will not be called by createEntity, it yours!
public function __construct($name, $email)
{
$this->name = $name;
$this->emai = $email;
}
public function getId()
{
return $this->id;
}
public function getName()
{
return $this->name;
}
public function getEmail()
{
return $this->email;
}
}
$row = array('employee_id' => '1', 'name' => 'John Galt', 'email' => 'john.galt#whoisjohngalt.com');
$mappings = array('employee_id' => 'id'); // Employee has id field, so we add translation for it
$john = createEntity('Employee', $row, $mappings);
print $john->getName(); // John Galt
print $john->getEmail(); // john.galt#whoisjohngalt.com
//...
P.S. Retrieving data from object is similar, e.g. use $reflProp->setValue($entity, $value);
P.P.S. This function is heavily inspired by Doctrine2 ORM which is awesome!
class DataStore // Automatically extends stdClass
{
public function __construct($Data) // $Data can be array or stdClass
{
foreach($Data AS $key => $value)
{
$this->$key = $value;
}
}
}
$arr = array('year_start' => 1995, 'year_end' => 2003);
$ds = new DataStore($arr);
$gap = $ds->year_end - $ds->year_start;
echo "Year gap = " . $gap; // Outputs 8
You can:
$variable = 'foo';
$this->$variable = 'bar';
Would set the attribute foo of the object it's called on to bar.
You can also use functions:
$this->{strtolower('FOO')} = 'bar';
This would also set foo (not FOO) to bar.
Extend stdClass.
class MyClass extends stdClass
{
public function __construct()
{
$this->prop=1;
}
}
I hope this is what you need.
This is really complicated way to handle this kind of rapid development. I like answers and magic methods but in my opinion it is better to use code generators like CodeSmith.
I have made template that connect to database, read all columns and their data types and generate whole class accordingly.
This way I have error free (no typos) readable code. And if your database model changes run generator again... it works for me.
If you really really must do it, the best way is to overload an ArrayObject, that allows to maintain iteration support (foreach) that will still loop through all your properties.
I note that you said "without using an array", and I just want to assure you that that while technically an array is being used in the background, you NEVER HAVE TO SEE IT. You access all properties via ->properyname or foreach ($class in $name => $value).
Here is a sample I was working on yesterday, note this is also STRONGLY TYPED. So properties that are marked "integer" will throw an error if you try and supply a "string".
You can remove that of course.
There is also an AddProperty() member function, although it is not demonstrated in the example. That will allow you to add properties later.
Sample usage:
$Action = new StronglyTypedDynamicObject("Action",
new StrongProperty("Player", "ActionPlayer"), // ActionPlayer
new StrongProperty("pos", "integer"),
new StrongProperty("type", "integer"),
new StrongProperty("amount", "double"),
new StrongProperty("toCall", "double"));
$ActionPlayer = new StronglyTypedDynamicObject("ActionPlayer",
new StrongProperty("Seat", "integer"),
new StrongProperty("BankRoll", "double"),
new StrongProperty("Name", "string"));
$ActionPlayer->Seat = 1;
$ActionPlayer->Name = "Doctor Phil";
$Action->pos = 2;
$Action->type = 1;
$Action->amount = 7.0;
$Action->Player = $ActionPlayer;
$newAction = $Action->factory();
$newAction->pos = 4;
print_r($Action);
print_r($newAction);
class StrongProperty {
var $value;
var $type;
function __construct($name, $type) {
$this->name = $name;
$this->type = $type;
}
}
class StronglyTypedDynamicObject extends ModifiedStrictArrayObject {
static $basic_types = array(
"boolean",
"integer",
"double",
"string",
"array",
"object",
"resource",
);
var $properties = array(
"__objectName" => "string"
);
function __construct($objectName /*, [ new StrongProperty("name", "string"), [ new StrongProperty("name", "string"), [ ... ]]] */) {
$this->__objectName = $objectName;
$args = func_get_args();
array_shift($args);
foreach ($args as $arg) {
if ($arg instanceof StrongProperty) {
$this->AddProperty($arg->name, $arg->type);
} else {
throw new Exception("Invalid Argument");
}
}
}
function factory() {
$new = clone $this;
foreach ($new as $key => $value) {
if ($key != "__objectName") {
unset($new[$key]);
}
}
// $new->__objectName = $this->__objectName;
return $new;
}
function AddProperty($name, $type) {
$this->properties[$name] = $type;
return;
if (in_array($short_type, self::$basic_types)) {
$this->properties[$name] = $type;
} else {
throw new Exception("Invalid Type: $type");
}
}
public function __set($name, $value) {
self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
$this->check($name, $value);
$this->offsetSet($name, $value);
}
public function __get($name) {
self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
$this->check($name);
return $this->offsetGet($name);
}
protected function check($name, $value = "r4nd0m") {
if (!array_key_exists($name, $this->properties)) {
throw new Exception("Attempt to access non-existent property '$name'");
}
$value__objectName = "";
if ($value != "r4nd0m") {
if ($value instanceof StronglyTypedDynamicObject) {
$value__objectName = $value->__objectName;
}
if (gettype($value) != $this->properties[$name] && $value__objectName != $this->properties[$name]) {
throw new Exception("Attempt to set {$name} ({$this->properties[$name]}) with type " . gettype($value) . ".$value__objectName");
}
}
}
}
class ModifiedStrictArrayObject extends ArrayObject {
static $debugLevel = 0;
/* Some example properties */
static public function StaticDebug($message) {
if (static::$debugLevel > 1) {
fprintf(STDERR, "%s\n", trim($message));
}
}
static public function sdprintf() {
$args = func_get_args();
$string = call_user_func_array("sprintf", $args);
self::StaticDebug("D " . trim($string));
}
protected function check($name) {
if (!array_key_exists($name, $this->properties)) {
throw new Exception("Attempt to access non-existent property '$name'");
}
}
//static public function sget($name, $default = NULL) {
/******/ public function get ($name, $default = NULL) {
self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
$this->check($name);
if (array_key_exists($name, $this->storage)) {
return $this->storage[$name];
}
return $default;
}
public function offsetGet($name) {
self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
$this->check($name);
return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
}
public function offsetSet($name, $value) {
self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
$this->check($name);
return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
}
public function offsetExists($name) {
self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
$this->check($name);
return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
}
public function offsetUnset($name) {
self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
$this->check($name);
return call_user_func_array(array(parent, __FUNCTION__), func_get_args());
}
public function __toString() {
self::sdprintf("%s(%s)\n", __FUNCTION__, $name);
foreach ($this as $key => $value) {
$output .= "$key: $value\n";
}
return $output;
}
function __construct($array = false, $flags = 0, $iterator_class = "ArrayIterator") {
self::sdprintf("%s(%s)\n", __FUNCTION__, implode(",", func_get_args()));
parent::setFlags(parent::ARRAY_AS_PROPS);
}
}
After reading #Udo 's answer. I've come up with the following pattern, that doesn't bloat a class instance with what-ever items that is in your constructor array argument but still let you type less and easily add new properties to the class.
class DBModelConfig
{
public $host;
public $username;
public $password;
public $db;
public $port = '3306';
public $charset = 'utf8';
public $collation = 'utf8_unicode_ci';
public function __construct($config)
{
foreach ($config as $key => $value) {
if (property_exists($this, $key)) {
$this->{$key} = $value;
}
}
}
}
Then you can pass arrays like:
[
'host' => 'localhost',
'driver' => 'mysql',
'username' => 'myuser',
'password' => '1234',
'charset' => 'utf8',
'collation' => 'utf8_unicode_ci',
'db' => 'key not used in receiving class'
]

Categories