I'm trying to iterate an array with next() current() and rewind():
is there a simple way to know when current pointer is arrived at the end ?
I just seen that when i'm at the end next() and current() returns both FALSE, but this also append when the array cells contain the FALSE boolean value.
edit:
of course sorry to not have specified that it's a numerical array.
I use it to and i'm trying to do a wordpress like
while ( have_post() )
do_post()
but the origin can be both an array or a PDOStatement, like the follow:
class Document implements ContentGetter{
private $array_or_rs;
private $currentval;
public function have_content() { //part of interface
if( is_a($this->$array_or_rs, 'PDOStatement') ) {
$result = PDOStatement-fetch();
_do_things_
} else {
$result = next($this->$array_or_rs);
}
$this->$currentval = $result;
return $result === FALSE;
}
public function get_content() { //part of interface
return $this->$currentval;
}
}
Can I iterate over an array without using a foreach loop in PHP?
Here you go:
$array = array(NULL, FALSE, 0);
while(list($key, $value) = each($array)) {
var_dump($value);
}
Output:
NULL
bool(false)
int(0)
next() and current() can't be used because there is no way to determine if a FALSE return value means a FALSE element or the end of the array. (As you've observed). However, you can use the function each(), as it will return an array containing the current key and value or FALSE at the end of the array. After it has been executed, the current-pointer will be set to the next element.
I must admit that I've not used that since > 10 years. :) However it is a basic PHP function and it still works.
How to implement the WordPres-like ContentGetter interface?
I would make usage of PHP's Iterator concept. You can wrap either the array in to an ArrayIterator or the PDOStatement into a PDOStatementIterator. While this first is a built-in class of PHP, the latter has to be written. Alternatively you can use this one (looks good, but contains more functionality than required for this task)
Based on that, the Document class should look like this:
class Document implements ContentGetter {
/**
* #var Iterator
*/
protected $iterator;
/**
* #param array|PDOStatement $stmtOrArray
*/
public function __construct($stmtOrArray) {
if(is_a($stmtOrArray, 'PDOStatement')) {
$this->iterator = new PDOStatementIterator($stmtOrArray);
} else if(is_array($stmtOrArray)) {
$this->iterator = new ArrayIterator($stmtOrArray);
} else {
throw new Exception('Expected array or PDOStatement');
}
}
/**
* Wrapper for Iterator::valid()
*/
public function have_content() {
return $this->iterator->valid();
}
/**
* Wrapper for Iterator::current() + Iterator::next()
*/
public function get_content() {
$item = $this->iterator->current();
$this->iterator->next();
return $item;
}
}
Tests:
// Feed Document with a PDOStatement
$pdo = new PDO('mysql:host=localhost', 'user', 'password');
$result = $pdo->query('SELECT 1 UNION SELECT 2'); // stupid query ...
$doc = new Document($result);
while($doc->have_content()) {
var_dump($doc->get_content());
}
.
// Feed Document with an array
$doc = new Document(array(1, 2, 3));
while($doc->have_content()) {
var_dump($doc->get_content());
}
you could compare key() to sizeof($my_array)
Related
I've been wondering for a while, is it efficient (in a manner of resources and speed) to use associative arrays as translators?
For example, let's say we have a calculator class which receives an operation object and then calls a function to execute, run(). In order to check what operation the user is trying to use, we can either do a if() or switch() and then set the right operation or, we can do something like new $operations[$operation] where $operation represents a string like + or - and the $operations array values for these keys (correspondingly) are Addition and Subtraction. By doing that we can instantiate the operation we need without writing a long switch() or if() statement.
Code demonstration:
Calculator.php
class Calculator {
protected $result = null,
$operands = [],
$operation;
public function setOperands() {
$this->operands = func_get_args();
}
public function setOperation(Operation $operation)
{
$this->$operation = $operation;
}
public function calculate() {
foreach($this->operands as $num) {
if( ! is_number($num))
throw new \InvalidArgumentException;
$this->result = $this->operation->run($num, $this->result);
}
}
}
Operation.php
interface Operation {
public function run($num, $current);
}
Addition.php
class Addition implements Operation{
public function run($num, $current) {
return $num + $current;
}
}
Subtraction.php
class Subtraction implements Operation {
public function run($num, $current) {
return $num - $current;
}
}
Now, when we use these classes:
$calculator = new Calculator();
// input from user
$operation = "+";
$calculator->setOperands(1,2,3);
/**
* One way to set the operation
*/
// set the operation
switch($operation) {
case '+':
$calculator->setOperation(new Addition);
break;
case '-':
$calculator->setOpration(new Subtraction());
break;
// and so on..
}
$calculator->calculate();
/**
* Second way to set the operation
*/
$operations = ['+' => 'Addition', '-' => 'Subtraction']; // can add more
$calculator->setOperation(new $operations[$operation]);
$calculator->calculate();
Is the second way of setting the operation is efficient (in a manner of resources and speed)?
Lets think I have an array
$acronyms = ['AFAIK' => 'As far as I know', 'MVP' => 'Most valuable player'];
And the String to get Translated
$string = "AFAIK I do the MVP";
Now I will do this
echo strtr($string,$acronyms);
Now My Output will be
As far as I know I do the Most valuable player
I'm trying to write a memoization function, and I just realized this solution does not work when the callback is not a simple string. For example, PHP can accept a callback in the form array($this,'memberFunc'), which is not amenable to serialization.
Then I realized that we don't really want to hash/serialize the whole callback function/object anyway, we just need a unique ID for it so we can check for reference equality. I thought spl_object_hash would do the trick, but it doesn't work on arrays.
Is there another way to generate a unique reference ID for a callable?
You can always cast to object for hashing purposes:
<?php
class Foo{
public function __construct(){
$foo = array($this,'memberFunc');
var_dump( spl_object_hash((object)$foo) );
var_dump( spl_object_hash((object)$foo) );
}
}
new Foo;
string(32) "00000000532ba9fd000000000cc9b0a5"
string(32) "00000000532ba9fd000000000cc9b0a5"
I wrote this function to get a hash for callables specifically:
/**
* #param callable $func
* #return string Unique string identifier for callable instance
* #throws Exception
*/
private static function callable_hash($func) {
if(!is_callable($func)) throw new Exception("Function must be a callable");
if(is_string($func)) {
return $func;
} elseif(is_array($func)) {
if(count($func) !== 2) throw new Exception("Array-callables must have exactly 2 elements");
if(!is_string($func[1])) throw new Exception("Second element of array-callable must be a string function name");
if(is_object($func[0])) return spl_object_hash($func[0]).'::'.$func[1];
elseif(is_string($func[0])) return implode('::',$func);
throw new Exception("First element of array-callable must be a class name or object instance");
} elseif(is_object($func)) {
return spl_object_hash($func);
}
throw new Exception("Unhandled callable type");
}
But if Alvaro's solution works... that's much simpler and more flexible.
Based on Alvaro's answer, I came up with these two functions:
function memoized() {
static $cache = array();
$args = func_get_args();
$func = array_shift($args);
$key = sha1(serialize(array_merge(array(spl_object_hash((object)$func)),$args)));
if(!isset($cache[$key])) {
$cache[$key] = call_user_func_array($func, $args);
}
return $cache[$key];
}
function memoized_func($func) {
return function() use ($func) {
static $cache = array();
$args = func_get_args();
$key = sha1(serialize($args));
if(!isset($cache[$key])) {
$cache[$key] = call_user_func_array($func, $args);
}
return $cache[$key];
};
}
Usage:
$f = function($n) {
for($i=0; $i<$n; ++$i);
return $n;
};
$result = memoized($f,50000);
$func = memoized_func($f);
$result2 = $func(50000);
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;
?>
What is an elegant way to remove an object from an array of objects in PHP?
class Data{
private $arrObservers;
public add(Observer $o) {
array_push($this->arrObservers, $o);
}
public remove(Observer $o) {
// I NEED THIS CODE to remove $o from $this->arrObservers
}
}
You can do
function unsetValue(array $array, $value, $strict = TRUE)
{
if(($key = array_search($value, $array, $strict)) !== FALSE) {
unset($array[$key]);
}
return $array;
}
You can also use spl_object_hash to create a hash for the objects and use that as array key.
However, PHP also has a native Data Structure for Object collections with SplObjectStorage:
$a = new StdClass; $a->id = 1;
$b = new StdClass; $b->id = 2;
$c = new StdClass; $c->id = 3;
$storage = new SplObjectStorage;
$storage->attach($a);
$storage->attach($b);
$storage->attach($c);
echo $storage->count(); // 3
// trying to attach same object again
$storage->attach($c);
echo $storage->count(); // still 3
var_dump( $storage->contains($b) ); // TRUE
$storage->detach($b);
var_dump( $storage->contains($b) ); // FALSE
SplObjectStorage is Traversable, so you can foreach over it as well.
On a sidenote, PHP also has native interfaces for Subject and Observer.
I agree with the answers above, but for the sake of completeness (where you may not have unique IDs to use as a key) my preferred methods of removing values from an array are as follows:
/**
* Remove each instance of a value within an array
* #param array $array
* #param mixed $value
* #return array
*/
function array_remove(&$array, $value)
{
return array_filter($array, function($a) use($value) {
return $a !== $value;
});
}
/**
* Remove each instance of an object within an array (matched on a given property, $prop)
* #param array $array
* #param mixed $value
* #param string $prop
* #return array
*/
function array_remove_object(&$array, $value, $prop)
{
return array_filter($array, function($a) use($value, $prop) {
return $a->$prop !== $value;
});
}
Which are used in the following way:
$values = array(
1, 2, 5, 3, 5, 6, 7, 1, 2, 4, 5, 6, 6, 8, 8,
);
print_r(array_remove($values, 6));
class Obj {
public $id;
public function __construct($id) {
$this->id = $id;
}
}
$objects = array(
new Obj(1), new Obj(2), new Obj(4), new Obj(3), new Obj(6), new Obj(4), new Obj(3), new Obj(1), new Obj(5),
);
print_r(array_remove_object($objects, 1, 'id'));
I recommend using the ID (if you have one, anything that will be unique to that object should work within reason) of the object as the array key. This way you can address the object within the array without having to run through a loop or store the ID in another location. The code would look something like this:
$obj_array[$obj1->getId()] = $obj1;
$obj_array[$obj2->getId()] = $obj2;
$obj_array[$obj3->getId()] = $obj3;
unset($obj_array[$object_id]);
UPDATE:
class Data{
private $arrObservers;
public add(Observer $o) {
$this->arrObservers[$o->getId()] = $o;
}
public remove(Observer $o) {
unset($this->arrObservers[$o->getId()]);
}
}
unset($myArray[$index]); where $index is the index of the element you want to remove. If you wan't a more specific answer, show some code or describe what you're trying to do.
$obj_array['obj1'] = $obj1;
$obj_array['obj2'] = $obj2;
$obj_array['obj3'] = $obj3;
unset($obj_array['obj3']);
For remove an object from a multi dimensional array you can use this:
$exampleArray= [
[
"myKey"=>"This is my key",
"myValue"=>"10"
],
[
"myKey"=>"Oh!",
"myValue"=>"11"
]
];
With array_column you can specify your key column name:
if(($key = array_search("Oh!", array_column($exampleArray, 'myKey'))) !== false) {
unset($exampleArray[$key]);
}
And this will remove the indicated object.
Use this for your internal object storage instead: http://us2.php.net/manual/en/class.splobjectstorage.php
function obj_array_clean ($array, $objId)
{
$new = array() ;
foreach($array as $value)
{
$new[$value->{$objId}] = $value;
}
$array = array_values($new);
return $array;
}
$ext2 = obj_array_clean($ext, 'OnjId');
It will remove the duplicate object "OnjId" from array objects $array.
If you want to remove one or more objects from array of objects (using spl_object_hash to determine if objects are the same) you can use this method:
$this->arrObservers = Arr::diffObjects($this->arrObservers, [$o]);
from this library.
Reading the Observer pattern part of the GoF book? Here's a solution that will eliminate the need to do expensive searching to find the index of the object that you want to remove.
public function addObserver(string $aspect, string $viewIndex, Observer $view)
{
$this->observers[$aspect][$viewIndex] = $view;
}
public function removeObserver(string $aspect, string $viewIndex)
{
if (!isset($this->observers[$aspect])) {
throw new OutOfBoundsException("No such aspect ({$aspect}) of this Model exists: " . __CLASS__);
}
if (!isset($this->observers[$aspect][$viewIndex])) {
throw new OutOfBoundsException("No such View for ({$viewIndex}) was added to the aspect ({$aspect}) of this Model:" . __CLASS__);
}
unset($this->observers[$aspect][$viewIndex]);
}
You can loose the "aspect" dimension if you are not using that way of keeping track of which Views are updated by specific Models.
public function addObserver(string $viewIndex, Observer $view)
{
$this->observers[$viewIndex] = $view;
}
public function removeObserver(string $viewIndex)
{
if (!isset($this->observers[$viewIndex])) {
throw new OutOfBoundsException("No such View for ({$viewIndex}) was added to this Model:" . __CLASS__);
}
unset($this->observers[$viewIndex]);
}
Summary
Build in a way to find the element before assigning the object to the array. Otherwise, you will have to discover the index of the object element first.
If you have a large number of object elements (or, even more than a handful), then you may need to resort to finding the index of the object first. The PHP function array_search() is one way to start with a value, and get the index/key in return.
https://www.php.net/manual/en/function.array-search.php
Do be sure to use the strict argument when you call the function.
If the third parameter strict is set to true then the array_search()
function will search for identical elements in the haystack. This
means it will also perform a strict type comparison of the needle in
the haystack, and objects must be the same instance.
Try this, will solve your problem.
class Data{
private $arrObservers;
public add(Observer $o) {
array_push($this->arrObservers,$o);
}
public remove($Observer $o) {
unset($this->arrObservers[$o]);
}
}
I believe this is the best way
$index = array_search($o, $this->arrObservers, true);
unset($this->arrObservers[$index]);
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"