How to unset nested array with ArrayObject? - php

ideone
Sample Code:
<?php
$a = new ArrayObject();
$a['b'] = array('c'=>array('d'));
print_r($a);
unset($a['b']['c']);
print_r($a);
Output
ArrayObject Object
(
[b] => Array
(
[c] => Array
(
[0] => d
)
)
)
ArrayObject Object
(
[b] => Array
(
[c] => Array
(
[0] => d
)
)
)
You notice that $a['b']['c'] is still there, even after unsetting. I would expect $a to have just the one value left (b).
In my actual app, I get the following warning:
Indirect modification of overloaded element of MyClass has no effect
Where MyClass extends ArrayObject. I have a lot of code that depends on being able to unset nested elements like this, so how can I get this to work?

One way to do it
<?php
$a = new ArrayObject();
$a['b'] = array('c' => array('d'));
$d =& $a['b'];
unset($d['c']);
print_r($a['b']);
prints:
Array
(
)
Would have to think a bit longer for an explanation as to why the syntax you've originally used doesn't remove the element.
EDIT: Explanation of behavior
What's happening is the call to unset($a['b']['c']); is translated into:
$temp = $a->offsetGet('b');
unset($temp['c']);
since $temp is a copy of $a instead of a reference to it, PHP uses copy-on-write internally and creates a second array where $temp doesn't have ['b']['c'], but $a still does.
ANOTHER EDIT: Reusable Code
So, no matter which way you slice it, seems like trying to overload function offsetGet($index) to be function &offsetGet($index) leads to trouble; so here's the shortest helper method I came up w/ could add it as a static or instance method in a subclass of ArrayObject, whatever floats your boat:
function unsetNested(ArrayObject $oArrayObject, $sIndex, $sNestedIndex)
{
if(!$oArrayObject->offSetExists($sIndex))
return;
$aValue =& $oArrayObject[$sIndex];
if(!array_key_exists($sNestedIndex, $aValue))
return;
unset($aValue[$sNestedIndex]);
}
So the original code would become
$a = new ArrayObject();
$a['b'] = array('c' => array('d'));
// instead of unset($a['b']['c']);
unsetNested($a, 'b', 'c');
print_r($a['b']);
YET ANOTHER EDIT: OO Solution
OK - So I must have been scrambling this morning b/c I found an error in my code, and when revised, we can implement a solution, based on OO.
Just so you know I tried it, extension segfaults..:
/// XXX This does not work, posted for illustration only
class BadMoxuneArrayObject extends ArrayObject
{
public function &offsetGet($index)
{
$var =& $this[$index];
return $var;
}
}
Implementing a Decorator on the other hand works like a charm:
class MoxuneArrayObject implements IteratorAggregate, ArrayAccess, Serializable, Countable
{
private $_oArrayObject; // Decorated ArrayObject instance
public function __construct($mInput=null, $iFlags=0, $sIteratorClass='')
{
if($mInput === null)
$mInput = array();
if($sIteratorClass === '')
$this->_oArrayObject = new ArrayObject($mInput, $iFlags);
else
$this->_oArrayObject = new ArrayObject($mInput, $iFlags, $sIteratorClass);
}
// -----------------------------------------
// override offsetGet to return by reference
// -----------------------------------------
public function &offsetGet($index)
{
$var =& $this->_oArrayObject[$index];
return $var;
}
// ------------------------------------------------------------
// everything else is passed through to the wrapped ArrayObject
// ------------------------------------------------------------
public function append($value)
{
return $this->_oArrayObject->append($value);
}
public function asort()
{
return $this->_oArrayObject->asort();
}
public function count()
{
return $this->_oArrayObject->count();
}
public function exchangeArray($mInput)
{
return $this->_oArrayObject->exchangeArray($mInput);
}
public function getArrayCopy()
{
return $this->_oArrayObject->getArrayCopy();
}
public function getFlags()
{
return $this->_oArrayObject->getFlags();
}
public function getIterator()
{
return $this->_oArrayObject->getIterator();
}
public function getIteratorClass()
{
return $this->_oArrayObject->getIteratorClass();
}
public function ksort()
{
return $this->_oArrayObject->ksort();
}
public function natcassesort()
{
return $this->_oArrayObject->natcassesort();
}
public function offsetExists($index)
{
return $this->_oArrayObject->offsetExists($index);
}
public function offsetSet($index, $value)
{
return $this->_oArrayObject->offsetSet($index, $value);
}
public function offsetUnset($index)
{
return $this->_oArrayObject->offsetUnset($index);
}
public function serialize()
{
return $this->_oArrayObject->serialize();
}
public function setFlags($iFlags)
{
return $this->_oArrayObject->setFlags($iFlags);
}
public function setIteratorClass($iterator_class)
{
return $this->_oArrayObject->setIteratorClass($iterator_class);
}
public function uasort($cmp_function)
{
return $this->_oArrayObject->uasort($cmp_function);
}
public function uksort($cmp_function)
{
return $this->_oArrayObject->uksort($cmp_function);
}
public function unserialize($serialized)
{
return $this->_oArrayObject->unserialize($serialized);
}
}
Now this code works as desired:
$a = new MoxuneArrayObject();
$a['b'] = array('c' => array('d'));
unset($a['b']['c']);
var_dump($a);
Still have to modify some code though..; I don't see any way round that.

It seems to me that the "overloaded" bracket operator of ArrayObject is returning a copy of the nested array, and not a reference to the original. Thus, when you call $a['b'], you are getting a copy of the internal array that ArrayObject is using to store the data. Further resolving it to $a['b']['c'] is just giving you the element "c" inside a copy, so calling unset() on it is not unsetting the element "c" in the original.
ArrayObject implements the ArrayAccess interface, which is what actually allows the bracket operator to work on an object. The documentation for ArrayAccess::offsetGet indicates that, as of PHP 5.3.4, references to the original data in ArrayObject's internal array can be acquired using the =& operator, as quickshiftin indicated in his example.

You can use unset($a->b['c']); instead of unset($a['b']['c']); in case if there won't be a huge problem to do a such replacement for all same situations within your project

I seem to have a partial solution. unset seems to work if all the nested arrays are instances of ArrayObject. In order to ensure all the nested arrays are ArrayObjects as well, we can derive instead from this class:
class ArrayWrapper extends ArrayObject {
public function __construct($input=array(), $flags=ArrayObject::STD_PROP_LIST, $iterator_class='ArrayIterator') {
foreach($input as $key=>$value) {
if(is_array($value)) {
$input[$key] = new self($value, $flags, $iterator_class);
}
}
parent::__construct($input, $flags, $iterator_class);
}
public function offsetSet($offset, $value) {
parent::offsetSet($offset, is_array($value) ? new ArrayWrapper($value) : $value);
}
}
(updated for recursiveness; untested)
And then whenever you try to add a nested array, it will automatically get converted to an ArrayWrapper instead.
Unfortunately many of the other array functions, such as array_key_exists don't work on ArrayObjects.

Related

PHP object comparison and private properties

I am wondering how PHP determines the equality of instances of a class with private properties:
class Example {
private $x;
public $y;
public __construct($x,$y) {
$this->x = $x; $this->y = $y;
}
}
and something like
$needle = new Example(1,2);
$haystack = [new Example(2,2), new Example(1,2)];
$index = array_search($needle, $haystack); // result is 1
The result is indeed 1, so the private member is compared. Is there a possibility to only match public properties?
I know I could overwrite the __toString method and cast all arrays and needles to string, but that leads to ugly code.
I am hoping to find a solution that is elegant enough to work with in_array, array_search, array_unique, etc.
A possible solution could be the PHP Reflection API. With that in mind you can read the public properties of a class and compare them to other public properties of another instance of the same class.
The following code is a simple comparison of public class properties. The base for the comparison is a simple value object.
declare(strict_types=1);
namespace Marcel\Test;
use ReflectionClass;
use ReflectionProperty;
class Example
{
private string $propertyA;
public string $propertyB;
public string $propertyC;
public function getPropertyA(): string
{
return $this->propertyA;
}
public function setPropertyA(string $propertyA): self
{
$this->propertyA = $propertyA;
return $this;
}
public function getPropertyB(): string
{
return $this->propertyB;
}
public function setPropertyB($propertyB): self
{
$this->propertyB = $propertyB;
return $this;
}
public function getPropertyC(): string
{
return $this->propertyC;
}
public function setPropertyC($propertyC): self
{
$this->propertyC = $propertyC;
return $this;
}
public function __compare(Example $b, $filter = ReflectionProperty::IS_PUBLIC): bool
{
$reflection = new ReflectionClass($b);
$properties = $reflection->getProperties($filter);
$same = true;
foreach ($properties as $property) {
if (!property_exists($this, $property->getName())) {
$same = false;
}
if ($this->{$property->getName()} !== $property->getValue($b)) {
$same = false;
}
}
return $same;
}
}
The __compare method of the Example class uses the PHP Reflection API. First we build a reflection instance of the class to which we want to compare to the current instance. Then we request all public properties of the class we want to compare to. If a public property does not exist in the instance or the value of the property is not the same as in the object we want to compare to, the method returns false, otherwise true.
Some examples.
$objectA = (new Example())
->setPropertyA('bla')
->setPropertyB('yadda')
->setPropertyC('bar');
$objectB = (new Example())
->setPropertyA('foo')
->setPropertyB('yadda')
->setPropertyC('bar');
$result = $objectA->__compare($objectB);
var_dump($result); // true
In this example the comparison results into true because the public properties PropertyB and PropertyC exist in both instances and have the same values. Keep in mind, that this comparison works only, if the second instance is the same class. One could spin this solution further and compare all possible objects based on their characteristics.
In Array Filter Example
It is a kind of rebuild of the in_array function based on the shown __compare method.
declare(strict_types=1);
namespace Marcel\Test;
class InArrayFilter
{
protected ArrayObject $data;
public function __construct(ArrayObject $data)
{
$this->data = $data;
}
public function contains(object $b)
{
foreach ($this->data as $object) {
if ($b->__compare($object)) {
return true;
}
}
return false;
}
}
This filter class acts like the in_array function. It takes a collection of objects and checks, if an object with the same public properties is in the collection.
Conclusion
If you want this solution to act like array_unique, array_search or ìn_array you have to code your own callback functions which execute the __compare method in the way you want to get the result.
It depends on the amount of data to be handled and the performance of the callback methods. The application could consume much more memory and therefore become slower.

What is the iterator_to_array function in PHP?

I'm having a hard time understanding PHP's iterator_to_array function.
I tried reading the manual but that didn't help.
What is it? How can I use it? What are the appropriate use cases?
In a nutshell, iterator_to_array() function takes an iterator of type Traversable and convert it to an associative/non-associative array, depending upon the argument provided. From the documentation,
array iterator_to_array ( Traversable $iterator [, bool $use_keys = true ] )
The function takes the following two arguments,
The first argument is of type Traversal, which is an interface. Both IteratorAggregate and Iterator class extends this interface. You can implement these two classes in your custom class, like this:
class myIterator implements IteratorAggregate {
private $array = array('key1'=>'value1', 'value2', 'value3', 'value4');
public function getIterator(){
return new ArrayIterator($this->array);
}
}
$obj = new myIterator;
$array = iterator_to_array($obj->getIterator(), true);
var_dump($array);
Or,
class myIterator implements Iterator {
private $key;
private $array = array('key1'=>'value1', 'value2', 'value3', 'value4');
public function __construct(){
$this->key = key($this->array);
}
public function rewind(){
reset($this->array);
$this->key = key($this->array);
}
public function current(){
return $this->array[$this->key];
}
public function key(){
return $this->key;
}
public function next(){
next($this->array);
$this->key = key($this->array);
}
public function valid(){
return isset($this->array[$this->key]);
}
}
$obj = new myIterator;
$array = iterator_to_array($obj, true);
var_dump($array);
The most important point to note here is that argument 1 passed to iterator_to_array() function must implement interface Traversable, so you cannot directly pass an array or object of any other type to this function. See the following example,
$array = array('key1'=>'value1', 'value2', 'value3', 'value4');
$array = iterator_to_array($array, true); // wrong
The second argument is a boolean value, to indicate whether to use the iterator element keys as index or not. See Example #1 here.
Iterators are useful because they allow you to use a custom defined order of data within a foreach loop. Let's take this (slightly pared down) example from the PHP manual:
class myIterator implements Iterator {
private $position = 0;
private $array = array(
"firstelement",
"secondelement",
"lastelement",
);
public function __construct() {
$this->position = 0;
}
function rewind() {
$this->position = 0;
}
function current() {
return $this->array[$this->position];
}
function key() {
return $this->position;
}
function next() {
++$this->position;
}
function valid() {
return isset($this->array[$this->position]);
}
}
$it = new myIterator;
To iterate over it, we can use a foreach loop just like an array:
foreach($it as $ele){
echo $ele;
}
Unlike an array, however, you cannot access a single element without iterating to it first. Attempting to do so will give you a fatal error:
/*
* Fatal error: Uncaught Error: Cannot use object of type myIterator as array
* in /home/hpierce/sandbox.php
*/
echo $it[1];
//Iterating to the second element.
$it->next();
// "secondelement"
echo $it->current();
To access a single element, you could use iterator_to_array() to cast the iterator as an array and then access the element without the extra step:
$array = iterator_to_array($it);
// "secondelement"
echo $array[1];
If you know ahead of time that you will need to access a single element within an iterator, you should consider using ArrayIterator instead.

Using object implementing ArrayAccess and Iterator as variadic parameter

I have a class that implements both ArrayAccess and Iterator.
I'm trying to figure out how to pass this object variadic parameter to a native function like array_merge:
array_merge(...$object);
To my disappointment I'm getting an error saying that $object is not an array.
array_merge(): Argument #1 is not an array
I've looked at these other interfaces but non of them seem obvious: IteratorAggregate, Serializable, Countable. Also ArrayObject turned out to be a dead end.
I do have a getter for to convert to array. But I was kind of hopping to discover my $object transform into an array just by implementing either ArrayAccess or Iterator, since it is about unfolding the array.
Is there another interface I can implement to make my class be more array-like?
This is a new language feature as documented in the migration guide from 5.5.x to 5.6.x in the manual (section Argument unpacking via ...), you must be on a pre-5.6.x runtime.
If you cannot upgrade your runtime, you must make use of your getter for converting it to an array (similar to ArrayObject's getArrayCopy):
call_user_func_array('array_merge', $arr->getArrayCopy());
Tests
The code below (based on PHP's documentation examples for ArrayAccess and Iterator) executed successfully on PHP 5.6.2, 5.6.17 and 7.0.1. It fails indeed on older versions (5.5.31 and older).
$arr = new MyArray();
$arr[0] = array(1, 2);
$arr[1] = array(3, 4);
// MyArray
print(get_class($arr));
// Array ( [0] => 1 [1] => 2 [2] => 3 [3] => 4 )
print_r(array_merge(...$arr));
Implementation of MyArray:
class MyArray implements ArrayAccess, Iterator
{
private $container = array();
private $position = 0;
public function getArrayCopy() {
return $this->container;
}
public function offsetSet($offset, $value) {
if (is_null($offset)) {
$this->container[] = $value;
} else {
$this->container[$offset] = $value;
}
}
public function offsetExists($offset) {
return isset($this->container[$offset]);
}
public function offsetUnset($offset) {
unset($this->container[$offset]);
}
public function offsetGet($offset) {
return isset($this->container[$offset]) ? $this->container[$offset] : null;
}
function rewind() {
$this->position = 0;
}
function current() {
return $this->container[$this->position];
}
function key() {
return $this->position;
}
function next() {
++$this->position;
}
function valid() {
return isset($this->container[$this->position]);
}
}

A faster way of doing objectToArray

Ive got this snippet of code below which works perfectly fine. I have been profiling it and the bit of code gets used alot of times, so I want to try figure out how to write it in a way that will perform better than the current way its written.
Is there a more efficient way to write this?
function objectToArray($d) {
if (is_object($d)) {
// Gets the properties of the given object
// with get_object_vars function
$d = get_object_vars($d);
}
if (is_array($d)) {
// Return array converted to object Using __FUNCTION__ (Magic constant) for recursive call
return array_map(__FUNCTION__, $d);
}
else {
// Return array
return $d;
}
}
You could implement a toArray() method to the class that needs to be converted:
e.g.
class foo
{
protected $property1;
protected $property2;
public function __toArray()
{
return array(
'property1' => $this->property1,
'property2' => $this->property2
);
}
}
Having access to the protected properties and having the whole conversion encapsulated in the class is in my opinion the best way.
Update
One thing to note is that the get_object_vars() function will only return the publically accessible properties - Probably not what you are after.
If the above is too manual of a task the accurate way from outside the class would be to use PHP (SPL) built in ReflectionClass:
$values = array();
$reflectionClass = new \ReflectionClass($object);
foreach($reflectionClass->getProperties() as $property) {
$values[$property->getName()] = $property->getValue($object);
}
var_dump($values);
depends what kind of object it is, many standard php objects have methods built in to convert them
for example MySQLi results can be converted like this
$resultArray = $result->fetch_array(MYSQLI_ASSOC);
if its a custom class object you might consider implementing a method in that class for that purpose as AlexP sugested
Ended up going with:
function objectToArray($d) {
$d = (object) $d;
return $d;
}
function arrayToObject($d) {
$d = (array) $d;
return $d;
}
As AlexP said you can implement a method __toArray(). Alternatively to ReflexionClass (which is complex and expensive), making use of object iteration properties, you can iterate $this as follow
class Foo
{
protected $var1;
protected $var2;
public function __toArray()
{
$result = array();
foreach ($this as $key => $value) {
$result[$key] = $value;
}
return $result;
}
}
This will also iterate object attributes not defined in the class: E.g.
$foo = new Foo;
$foo->var3 = 'asdf';
var_dump($foo->__toArray());)
See example http://3v4l.org/OnVkf
This is the fastest way I have found to convert object to array. Works with Capsule as well.
function objectToArray ($object) {
return json_decode(json_encode($object, JSON_FORCE_OBJECT), true);
}

Making a PHP object behave like an array?

I'd like to be able to write a PHP class that behaves like an array and uses normal array syntax for getting & setting.
For example (where Foo is a PHP class of my making):
$foo = new Foo();
$foo['fooKey'] = 'foo value';
echo $foo['fooKey'];
I know that PHP has the _get and _set magic methods but those don't let you use array notation to access items. Python handles it by overloading __getitem__ and __setitem__.
Is there a way to do this in PHP? If it makes a difference, I'm running PHP 5.2.
If you extend ArrayObject or implement ArrayAccess then you can do what you want.
ArrayObject
ArrayAccess
Nope, casting just results in a normal PHP array -- losing whatever functionality your ArrayObject-derived class had. Check this out:
class CaseInsensitiveArray extends ArrayObject {
public function __construct($input = array(), $flags = 0, $iterator_class = 'ArrayIterator') {
if (isset($input) && is_array($input)) {
$tmpargs = func_get_args();
$tmpargs[0] = array_change_key_case($tmpargs[0], CASE_LOWER);
return call_user_func_array(array('parent', __FUNCTION__), $tmp args);
}
return call_user_func_array(array('parent', __FUNCTION__), func_get_args());
}
public function offsetExists($index) {
if (is_string($index)) return parent::offsetExists(strtolower($index));
return parent::offsetExists($index);
}
public function offsetGet($index) {
if (is_string($index)) return parent::offsetGet(strtolower($index));
return parent::offsetGet($index);
}
public function offsetSet($index, $value) {
if (is_string($index)) return parent::offsetSet(strtolower($index, $value));
return parent::offsetSet($index, $value);
}
public function offsetUnset($index) {
if (is_string($index)) return parent::offsetUnset(strtolower($index));
return parent::offsetUnset($index);
}
}
$blah = new CaseInsensitiveArray(array(
'A'=>'hello',
'bcD'=>'goodbye',
'efg'=>'Aloha',
));
echo "is array: ".is_array($blah)."\n";
print_r($blah);
print_r(array_keys($blah));
echo $blah['a']."\n";
echo $blah['BCD']."\n";
echo $blah['eFg']."\n";
echo $blah['A']."\n";
As expected, the array_keys() call fails. In addition, is_array($blah) returns false. But if you change the constructor line to:
$blah = (array)new CaseInsensitiveArray(array(
then you just get a normal PHP array (is_array($blah) returns true, and array_keys($blah) works), but all of the functionality of the ArrayObject-derived subclass is lost (in this case, case-insensitive keys no longer work). Try running the above code both ways, and you'll see what I mean.
PHP should either provide a native array in which the keys are case-insensitive, or make ArrayObject be castable to array without losing whatever functionality the subclass implements, or just make all array functions accept ArrayObject instances.

Categories