Object assignment is creating a reference in PHP - php

I have a weird behavior when assigning an existant object to an array.
In the example below, I have a class that contains one property. I create a first array with 3 instances (let's call them 2-4-6) and then a second array with 2 other instances using one object of the first array (instance 4). While modifying the values of the objects in second array to create 2 new instances (ie instances 3-5), the instance 4 is also modified. In consequence, at the first array query I get the right values (2-4-6) but after creating the second array I get the modified value (2-5-6). I would expect that the assignment operator copied the object, but instead it creates a reference to the instance. In the example I can get rid of this issue by explicitly calling clone, but in a larger scale, this isn't work (wrong code optimization?). Any clue (or good practice) of how to avoid this issue?
Thanks!
<?php
class TestBase
{
private int $m_test = 0;
public function SetTest(int $v)
{
$this->m_test = $v;
}
public function GetTest() : int
{
return $this->m_test;
}
}
function getNewList(TestBase $ref) : array
{
$newlist = [3 => new TestBase(), 5 => $ref];
$newlist[3]->SetTest(3);
$newlist[5]->SetTest(5);
return $newlist;
}
$listOfTest = [2 => new TestBase(), 4 => new TestBase(), 6 => new TestBase()];
$listOfTest[2]->SetTest(2);
$listOfTest[4]->SetTest(4);
$listOfTest[6]->SetTest(6);
foreach ($listOfTest as $test)
{
echo $test->GetTest().'<br>';
}
// 2
// 4
// 6
//$ref = clone $listOfTest[4];
$ref = $listOfTest[4];
$newList = getNewList($ref);
foreach ($listOfTest as $test)
{
echo $test->GetTest().'<br>';
}
// 2
// 5
// 6
?>

The problem is in the way the PHP uses arrays. Array assignment by reference
only works if both the argument and the lvalue are references to arrays. If an object is passed in, it will be copied and a reference to it will be set in both places. The subsequent reassignment of the lvalue will not have any effect on the original array.
Referencing an existing array and then copying it using clone can work around this. The example below shows how this can be done, but you could also use a factory or some other means to create a new array that has references to all of the existing elements (by calling get_object_vars() on each).
<?php
function getNewList(TestBase $ref) : array
{
$newlist = [3 => clone $ref, 5 => clone $ref];
$newlist[3]->SetTest(3);
$newlist[5]->SetTest(5);
return $newlist;
}
//$ref = clone $listOfTest[4];
$ref = &$listOfTest[4];
var_dump($ref); // TestBase Object (1) (2) { ["m_test"]=> int(5) }
//$newList = getNewList($ref);
foreach ($listOfTest as &$test)
{
echo "before: ".$test->GetTest().'<br>'; // 2, 4, 6 in output here!
var_dump($test); // TestBase Object (1) (2) { ["m_test"]=> int(4) }
echo "after: ".$test->GetTest().'<br>'; // 2, 4, 6 in output here!
var_dump($test); // TestBase Object (1) (2) { ["m_test"]=> int(4) }
}
?>

Related

Accumulate strings and array values into an array as a class property via method

I have a class with method add() that accepts strings and arrays. I need to have an array with all users, but I cannot seem to get it. All I get is multiple arrays with all users. How could I merge those arrays into one?
class Users {
function add($stringOrArray) {
$arr = array();
if(is_array($stringOrArray)) {
$arr = $stringOrArray;
} else if(is_string($stringOrArray)) {
$arr[] = $stringOrArray;
} else {
echo('errrrror');
}
print_r($arr);
}
When I use this test:
public function testOne() {
$users = new Users();
$users->add('Terrell Irving');
$users->add('Magdalen Sara Tanner');
$users->add('Chad Niles');
$users->add(['Mervin Spearing', 'Dean Willoughby', 'David Prescott']);
This is what I get, multiple arrays but I need one array.
Array
(
[0] => Terrell Irving
)
Array
(
[0] => Magdalen Sara Tanner
)
Array
(
[0] => Chad Niles
)
Array
(
[0] => Mervin Spearing
[1] => Dean Willoughby
[2] => David Prescott
)
You can cut a lot of unnecessary bloat from your method.
You can cast ALL incoming data to array type explicitly. This will convert a string into an array containing a single element. If the variable is already an array, nothing will change about the value.
Use the spread operator (...) to perform a variadic push into the class property.
Code: (Demo)
class Users
{
public $listOfUsers = [];
function add($stringOrArray): void
{
array_push($this->listOfUsers, ...(array)$stringOrArray);
}
}
$users = new Users;
$users->add('Terrell Irving');
$users->add(['Magdalen Sara Tanner', 'Chad Niles']);
$users->add(['Mervin Spearing']);
var_export($users->listOfUsers);
Output:
array (
0 => 'Terrell Irving',
1 => 'Magdalen Sara Tanner',
2 => 'Chad Niles',
3 => 'Mervin Spearing',
)
All you need is to store the added users in a class property, for example $listOfUsers.
If adding the array you use the array_merge() function otherwise just add new user at the end of indexed array.
<?php
class Users {
// here will be all the users stored
public $listOfUsers = array();
function add($stringOrArray) {
//$arr = array();
if(is_array($stringOrArray)) {
// merge two arrays - could create duplicate records
$this->listOfUsers = array_merge($this->listOfUsers, $stringOrArray);
} else if(is_string($stringOrArray)) {
// simply add new item into the array
$this->listOfUsers[] = $stringOrArray;
} else {
echo('errrrror');
}
print_r($this->listOfUsers);
}
}
In your example you are storing the data locally within the method add() and it is not kept for future usage. This behavior is corrected using the class property $listOfUsers that can be accesed using $this->listOfUsers within the class object and if needed outside of the class.

PHP arrays work differently for objects vs values [duplicate]

This question already has answers here:
How do I create a copy of an object in PHP?
(9 answers)
Closed 3 years ago.
Here's my code:
$words = array();
$word = "this";
$words[] = $word;
$word = "that";
$words[] = $word;
print_r($words);
class word{
private $text;
public function __construct($word){
$this->text=$word;
}
public function setWord($word){
$this->text=$word;
}
}
$class_words = array();
$word = new word("this");
$class_words[] = $word;
$word->setWord("that");
$class_words[] = $word;
print_r($class_words);
exit;
Here's the output:
Array
(
[0] => this
[1] => that
)
Array
(
[0] => word Object
(
[text:word:private] => that
)
[1] => word Object
(
[text:word:private] => that
)
)
I expected the second output to match the first in that the array should store 'this' and 'that'. It seems array_name[] = <item> makes a copy to the item when it's an array of values but not so when it's an array of objects. How do I make it copy the object to the array instead copying a reference to the object? Do I need to create a new object each time I need to add an object to the array?
If you want to copy the value of the object into the array you need to write a "getter" for the value e.g.
class word{
private $text;
public function __construct($word){
$this->text=$word;
}
public function setWord($word){
$this->text=$word;
}
public function getWord() {
return $this->text;
}
}
$class_words = array();
$word = new word("this");
$class_words[] = $word->getWord();
$word->setWord("that");
$class_words[] = $word->getWord();
print_r($class_words);
Output:
Array
(
[0] => this
[1] => that
)
Demo on 3v4l.org
$x = new X(); stores a reference to an object into $x. A subsequent $y = $x; copies the reference, not the object, so $x and $y both refer to the same object.
PHP has rather complex semantics regarding references.
Objects are always references; all your uses of $word refer to the same object and the same data structure. You need to do:
$class_words=[new word('this'),new word('that')];
Both elements of your array hold the same object. So whenever you make a change in that object, all references to that object will show that change-- they're all referring to the same object in its current state.
As mentioned previously, if you want different values, you need to either instantiate new objects, or get the property value via a getter() that returns a simple value (string, integer, boolean, etc), not the object itself.
As a side note, you can leverage the referential nature of objects to chain methods ($Obj->method1()->method2()->method3()) by having a method return a reference to the object, i.e., return $this;

PHP references not working as I expect them to

Basically how I understand references work is
$a = 5;
$b = &$a;
$a = 10;
echo $b; // 10;
However in this bit of code I'm getting unexpected (for me, which probably has an explanation) result
class Room {
private $users = array();
public function addUser(&$user){
$this->users[] = $user;
}
}
$users = array(
1 => 'Tom',
2 => 'Hank',
3 => 'Sam',
4 => 'John'
);
$room = new Room();
$room->addUser($users[1]);
$room->addUser($users[3]);
unset($users[3]);
echo "<pre>" . print_r($room, true) . "</pre>";
echo "<pre>" . print_r($users, true) . "</pre>";
I expect, after unsetting $users[3], the only user inside of $room to be Tom, but that is not the case, both Tom and Sam are present in the object. Why is unset not affecting the object's property?
EDIT:
Even if I take things a step further with the example and create a class User the effect is still the same
class Room {
private $users = array();
public function addUser(&$user){
$this->users[] = $user;
}
}
class User {
public $name;
function __construct($name){
$this->name = $name;
}
}
$users = array(
1 => new User('Tom'),
2 => new User('Hank'),
3 => new User('Sam'),
4 => new User('John')
);
$room = new Room();
$room->addUser($users[1]);
$room->addUser($users[3]);
unset($users[3]);
echo "<pre>" . print_r($room, true) . "</pre>";
echo "<pre>" . print_r($users, true) . "</pre>";
Unset operates on symbols, not reference targets.
That is why using unset on an undefined variable doesn't raise any kind of error.
$a = 10;
$b = &$a;
unset($b); // forget the name "$b" exists.
echo $a; // 10
If you want to unset it in both places, you have to assign null to one of the variables. This is a "hard unset", as opposed to a "soft unset" which is what you are currently doing.
Also you are not assigning a reference, you're assigning a copy.
$this->users[] = &$user;
Reference Counting Basics :
A PHP variable is stored in a container called a "zval". A zval
container contains, besides the variable's type and value, two
additional bits of information. The first is called "is_ref" and is a
boolean value indicating whether or not the variable is part of a
"reference set". (...) Since PHP allows user-land references, as
created by the & operator, a zval container also has an internal
reference counting mechanism to optimize memory usage. This second
piece of additional information, called "refcount", contains how many
variable names (also called symbols) point to this one zval container.
(...)
Variable containers get destroyed when the "refcount" reaches zero.
The "refcount" gets decreased by one when any symbol linked to the
variable container leaves the scope (e.g. when the function ends) or
when unset() is called on a symbol.
Example with arrays:
<?php
$a = array(
0 => 'aaa',
1 => 'bbb',
2 => 'ccc',
);
debug_zval_dump($a);
// ... string(3) "bbb" refcount(1) ...
$b = array();
$b[0] = &$a[0];
$b[1] = &$a[1];
$a[1] = 'ddd';
debug_zval_dump($a);
// ... &string(3) "bbb" refcount(2) ...
debug_zval_dump($b);
// ... &string(3) "bbb" refcount(2) ...
unset($a[1]);
debug_zval_dump($a);
/*
array(2) refcount(2){
[0]=>
&string(3) "aaa" refcount(2)
[1]=>
&string(3) "ddd" refcount(2)
}
*/
debug_zval_dump($b);
// ... string(3) "ddd" refcount(1) ...
var_dump($a);
/*
array (size=2)
0 => &string 'aaa' (length=3)
2 => string 'ccc' (length=3)
*/
var_dump($b);
/*
array (size=2)
0 => &string 'aaa' (length=3)
1 => string 'ddd' (length=3)
*/
I think there's a slight logical problem between your desired effect and the way you try to do it.
If I understand correctly, you want to assign users to a container, then unsetting one of those user in a way that it will also be unsetted in your container. This
unset($users[3]);
unsets the value of the fourth element of your users array.
if we did $user[3] = 'foo'; the value contained in the corresponding container's entry will be set to 'foo' as well, but the container's index key itself will not get unset, or affected by the reference, because it is not part of the referenced value
If you want to unset the user, either you keep track of which index key is assigned to which user in your container and then delete users with this index key, or you set the value of $users[3] to null (or whatever suits your needs) and skip the null values when dealing with your container
You can change a value of arrays, like this:
CODE:
private $users = array();
public function addUser(&$user){
$this->users[] = &$user;
}
}
$users = array(
1 => 'Tom',
2 => 'Hank',
3 => 'Sam',
4 => 'John'
);
$room = new Room();
$room->addUser($users[1]);
$room->addUser($users[3]);
$users[3] = "AAA123";
echo "<pre>" . print_r($room, true) . "</pre>";
echo "<pre>" . print_r($users, true) . "</pre>";
OUTPUT:
Room Object
(
[users:Room:private] => Array
(
[0] => Tom
[1] => AAA123
)
)
Array
(
[1] => Tom
[2] => Hank
[3] => AAA123
[4] => John
)
But delete it's not possible this way... I don't know how to explain, so just give example:
$a = 10;
$b = &$a;
unset($a);
echo $b; // 10
Then you deleting variable name, you not delete zval(container), until refcount reach 0... then "Garbage Collection" do all work and delete zval...
So method unset() remove variable name only in this case...
Be careful.
You are passing to addUser() a reference to the string 'Tom' allocated while building the array $users.
First, addUser() should read $this->users[] =& $user;, otherwise you will be copying the value into $this->users[] instead of sharing the reference.
Now, both $users and Room::$users share the same objects, however unset($users[3]) removes the element mapped by the index 3 from the array, it does not destroy the mapped object.

PHP RecursiveIteratorIterator overwrites array keys

Here is the function I wrote to flatten the multidimensional PHP array:
function flattenArray(array $array) {
if (! is_array($array)) {
throw new Exception ("Please specify an array.");
}
$resultArray = [];
$arrayObject = new RecursiveArrayIterator($array);
foreach(new RecursiveIteratorIterator($arrayObject) as $key => $value) {
$resultArray[$key] = $value;
}
return $resultArray;
}
And using it:
$arr = [
["sitepoint", "phpmaster"],
["buildmobile", "rubysource"],
["designfestival", "cloudspring"],
"not an array"
];
print_r(flattenArray($arr));
Result:
Array
(
[0] => designfestival
[1] => cloudspring
[3] => not an array
)
However, I was expecting:
0: sitepoint
1: phpmaster
2: buildmobile
3: rubysource
4: designfestival
5: cloudspring
6: not an array
But it is re-generating indexes as in:
0: sitepoint
1: phpmaster
0: buildmobile
1: rubysource
0: designfestival
1: cloudspring
3: not an array
So how do I modify function to get all elements of the array not just three:
Array
(
[0] => designfestival
[1] => cloudspring
[3] => not an array
)
Thanks for the help
if (!is_array($array)) is superfluous, since you have the array type hint in the function signature and PHP will enforce that.
You are overwriting the keys. Those elements all have the same keys in their respective subarray. Since it's not an associative array, you don't need to preserve the keys. Instead of
$resultArray[$key] = $value;
just do
$resultArray[] = $value;
I too hit this limitation with RecursiveIteratorIterator.
At first I had been using this concise, one-line array flattener wherever needed:
$outputs = iterator_to_array(new \RecursiveIteratorIterator(new \RecursiveArrayIterator([$inputs])), FALSE);
similar to your longer function above.
All was great: I was able to "normalize" my data structure into a 1D array, no matter if the incoming $inputs parameter came into my Symfony2 Controller as a single String/float value, 1D or 2+D multidimensional array. (I was writing a callback from AJAX that is to respond with JSON-formatted tables for an interactive Highcharts.com chart to be able to render, in my financial app.)
However, it refused to draw because in the final step, each data cell was in the form
0 => float 100.662
even though I had taken care that my $inputs creature only contained cells in the form:
'2002-04-30' => float 100.662
So basically the above array-flattening line had killed the keys (DateStamp).
Fed up with studying RecursiveIteratorIterator, I just broke down and came up with my own array_flatten that preserves keys, if any:
static public function array_flatten($inObj)
{
$outObj = []; $inObj=[$inObj];
array_walk_recursive($inObj, function ($incell, $inkey) use (&$outObj)
{
$outObj[$inkey] = $incell;
} );
return $outObj;
}
Note that you are responsible for ensuring that the keys in $inObj are globally unique (and either string or int type), otherwise, I don't know how my function behaves. Probably overwrites the value using the same key name?

Extended PHP ArrayObject Does Not Work Properly

I'm trying to extend the SPL ArrayObject but I've hit a little snag. Using an unmodified ArrayObject, this code works:
$a = new ArrayObject();
$a[1][2] = 'abc';
print_r($a);
yielding this output:
ArrayObject Object
(
[storage:ArrayObject:private] => Array
(
[1] => Array
(
[2] => abc
)
)
)
However if I extend ArrayObject and overload the offsetGet method
class ExtendedArray extends ArrayObject {
function offsetGet($i) {
return parent::offsetGet($i);
}
}
$a = new ExtendedArray();
$a[1][2] = 'abc';
print_r($a);
it then fails like this:
ExtendedArray Object
(
[storage:ArrayObject:private] => Array
(
)
)
What does it take to make my extended class work with multidimensional arrays?
For me, the snippet #1 is rather broken, not the #2. You're accessing an element that does not exists, and the code #2 gives you exactly what one would expect: a warning. The reason why #1 kinda "works" is a quirk, or two quirks of php. First, when you apply []= operator on null, this null is "magically" turned into an array - without single word of warning from interpreter.
$a = null;
$a[1] = 'foo'; // "works"
print_r($a);
Second, this (intentionally or not) does not apply to nulls returned from __get or offsetGet.
class foo {
function __get($s) { return null; }
}
$a = new foo;
$a->x[1] = 'foo'; // error
print_r($a);
the error message says "Indirect modification of overloaded property", and, whatever that means, it's a Good Thing - you're not allowed to modify the null value in any way.

Categories