How can I merge two objects and sum the values of matching properties? I am hoping for a built in function in PHP, otherwise I am seeking an easy way of doing it.
See code under, where I have $objectA and $objectB which I want to become $obj_merged.
$objectA = (object) [];
$objectA->a = 1;
$objectA->b = 1;
$objectA->d = 1;
$objectB = (object) [];
$objectB->a = 2;
$objectB->b = 2;
$objectB->d = 2;
$obj_merged = (object) [
'a' => 3,
'b' => 3,
'd' => 3
];
What you want to achieve is a sum of the properties. A merge would overwrite the values. There is no built-in PHP function to do this with objects.
But you can use a simple helper function where you could put in as many objects as you like to sum up the public properties.
function sumObjects(...$objects): object
{
$result = [];
foreach($objects as $object) {
foreach (get_object_vars($object) as $key => $value) {
isset($result[$key]) ? $result[$key] += $value : $result[$key] = $value;
}
}
return (object)$result;
}
$sumObject = sumObjects($objectA, $objectB);
stdClass Object
(
[a] => 3
[b] => 3
[d] => 3
)
There is no need to loop over both objects. Save the first object to the result object, then loop over the second object and add the related values between the current property and the result object. The null coalescing operator is used to add zero when there is no corresponding property in the result object.
Code: (Demo)
$result = $objectA;
foreach ($objectB as $prop => $value) {
$result->$prop = ($result->$prop ?? 0) + $value;
}
var_export($result);
If the properties between the two objects are guaranteed to be identical, then there is no need to coalesce with zero. Demo
$result = $objectA;
foreach ($objectB as $prop => $value) {
$result->$prop += $value;
}
Related
I have two objects like this.
$array1
stdClass Object (
[BellId] => 2
[BellCode] => BP001
[BellDescription] => SPI SPEED ABNORMAL,CHK BELT
[ControllerId] => 3
[CreatedBy] => 1
[CreatedOn] => 2016-08-19 15:09:25
[ModifiedBy] =>
[ModifiedOn] =>
)
$array2
stdClass Object (
[BellId] => 1
[BellCode] => BP002
[BellDescription] => MCB TRIPPED,CHK MTR SHORT,O/L.
[ControllerId] => 3
[CreatedBy] => 1
[CreatedOn] => 2016-08-19 15:09:25
[ModifiedBy] =>
[ModifiedOn] =>
)
I need to compare this object and get the difference in these two objects only.
I have checked the below links but no use.
Comparing two stdClass Objects
Comparing 2 objects PHP
My Sample code is as follows
function recursive_array_diff($a1, $a2) {
$r = array();
foreach ($a1 as $k => $v) {
if (array_key_exists($k, $a2)) {
if (is_array($v)) {
$rad = recursive_array_diff($v, $a2[$k]);
if (count($rad)) {
$r[$k] = $rad;
}
} else {
if ($v != $a2[$k]) {
$r[$k] = $v;
}
}
} else {
$r[$k] = $v;
}
}
return $r;
}
Can someone help me with the code.
Use array_diff_assoc(); e.g:
<?php
$foo = new stdClass();
$foo->BellId = 1;
$foo->BellDescription = 'foo';
$foo->CreatedBy = 1;
$bar = new stdClass();
$bar->BellId = 2;
$bar->BellDescription = 'bar';
$bar->CreatedBy = 1;
$diff = array_diff_assoc((array) $foo, (array) $bar);
print_r($diff);
array_diff_assoc performs a diff of arrays with additional index check. In your case this is required because you want to perform a key/value diff, not a diff on the values alone.
The above code yields:
Array
(
[BellId] => 1
[BellDescription] => foo
)
Note: you can transparently cast an instance of stdClass() to an array and vice versa:
$arr = ['id' => 1];
$obj = (object) $arr;
$arr = (array) $obj;
// etc.
Hope this helps :)
First convert both objects to arrays:
$arrayA = (array)$objectA;
$arrayB = (array)$objectB;
then just use array_diff to get the difference between arrays
$difference = array_diff($arrayA, $arrayB);
This will return an array containing keys from the first array ($arrayA) and their value, which gives an indication as to what fields are different between the two objects
foreach($difference as $key => $diff) {
echo $objectA->$key;
echo $objectB->$key;
// the above two values will be different
}
Note: array_diff can be used if you know the order of fields in both objects are the same, however it's probably best to use array_diff_assoc here, as this offers an additional index check.
With array_udiff_assoc you can compare the items as you like using a callback. Of course, you need to cast the objects to arrays:
$d = array_udiff_assoc((array)$array1, (array)$array2, function ($x, $y) {
if (! (is_scalar($x) && is_scalar($y))) {
trigger_error("skipping non-scalar members!", E_USER_WARNING);
// you might want to handle this in the app-specific way
}
if (is_numeric($x) && is_numeric($y))
return $x - $y;
return strcmp($x, $y);
});
var_dump($d);
where $x and $y are the items from the arrays being compared.
Sample output
array(3) {
["BellId"]=>
int(2)
["BellCode"]=>
string(5) "BP001"
["BellDescription"]=>
string(27) "SPI SPEED ABNORMAL,CHK BELT"
}
This is a very flexible way. You can put your own comparison logic into the callback. For instance, you might want to compare instances of classes:
static $date_fmt = 'YmdHis';
if ($x instanceof DateTime)
$x = $x->format($date_fmt);
if ($y instanceof DateTime)
$y = $y->format($date_fmt);
Let's pretend I have this:
$str = "/a/b/c/d/";
$arr = array_filter( explode("/", $str );
At this point $arr contains 4 elements. Is there a way I could create a path in an array with those 4 elements, such as:
$result = [
"a" => [
"b" => [
"c" => [
"d" => [
]
]
]
]
]
...without iterating over $arr?
I know that
$x["a"]["b"]["c"]["d"] = 1;
is perfectly valid and it will create a 4 levels array even if $x wasn't declared as an array, so what I'm asking should be possible.
I DO NOT recommend this as there are security implications when using eval(). However, because I stated in the comments that it couldn't be done without iteration, I felt compelled to post this as an answer (yes, I know implode() iterates internally).
$str = "/a/b/c/d/";
$arr = array_filter( explode("/", $str ));
$keys = '["'.implode('"]["', $arr).'"]';
eval('$x'.$keys.' = 1;');
print_r($x);
For a more practical way see How to write getter/setter to access multi-leveled array by dot separated key names?
I wrote a function once, that had this behaviour as a side effect. It doesn't iterate, but uses recursion.
See: https://github.com/feeela/php-utils/blob/master/function.getArrayValueReference.php
You may call like that:
<?php
$newArray = array();
$keys = 'a/b/c/d';
$referenceToD =& getArrayValueReference( $newArray, explode( '/', $keys ), true );
$referenceToD[0] = 'foo';
$referenceToD[1] = 'bar';
print_r( $newArray );
This modifies the array $newArray and creates all the levels. The functions return value is a reference to the last key ('d' in that example).
…which results in:
Array (
[a] => Array (
[b] => Array (
[c] => Array (
[d] => Array (
[0] => foo
[1] => bar
)
)
)
)
)
There is no way to use all the values of $arr without iterating over it.
I guess you don't want to write a foreach loop but use some PHP function that does the iteration for you.
A simple solution that iterates two times over the array (behind the scene)
This is a possible solution:
$x = array_reduce(
array_reverse($arr),
function ($carry, $item) {
return [$item => $carry];
},
1
);
It generates the same result as:
$x = [];
$x['a']['b']['c']['d'] = 1;
Unfortunately it iterates over $arr two times (array_reverse() and array_reduce()).
Another solution that generates a hierarchy of objects
Another approach that generates the required embedding using objects (stdClass) instead of arrays:
$out = new stdClass;
array_reduce(
$arr,
function ($carry, $item) {
$v = new stdClass;
$carry->{$item} = $v;
return $v;
},
$out
);
It works using a single iteration over $arr but it relies on the way the objects are handled in PHP to work (and this doesn't work with arrays).
PHP handles the objects in a way that makes them look like they are passed by reference. It's a common misconception that the objects are "passed by reference" in PHP but this is not true. A double indirection is responsible for this behaviour.
A recursive solution
function makeArray(array $arr, $initial)
{
if (! count($arr)) {
return $initial;
} else {
$key = array_shift($arr);
return [ $key => makeArray($arr, $initial) ];
}
}
$out = makeArray($arr, 1);
This solution iterates only once over the array and generates a hierarchy of arrays but recursivity is disastrous for large input arrays because it uses a lot of memory.
I have an array that is a object which I carry in session lifeFleetSelectedTrucksList
I also have objects of class fleetUnit
class fleetUnit {
public $idgps_unit = null;
public $serial = null;
}
class lifeFleetSelectedTrucksList {
public $arrayList = array();
}
$listOfTrucks = new lifeFleetSelectedTrucksList(); //this is the array that I carry in session
if (!isset($_SESSION['lifeFleetSelectedTrucksList'])) {
$_SESSION['lifeFleetSelectedTrucksList'] == null; //null the session and add new list to it.
} else {
$listOfTrucks = $_SESSION['lifeFleetSelectedTrucksList'];
}
I use this to remove element from array:
$listOfTrucks = removeElement($listOfTrucks, $serial);
And this is my function that removes the element and returns the array without the element:
function removeElement($listOfTrucks, $remove) {
for ($i = 0; $i < count($listOfTrucks->arrayList); $i++) {
$unit = new fleetUnit();
$unit = $listOfTrucks->arrayList[$i];
if ($unit->serial == $remove) {
unset($listOfTrucks->arrayList[$i]);
break;
} elseif ($unit->serial == '') {
unset($listOfTrucks->arrayList[$i]);
}
}
return $listOfTrucks;
}
Well, it works- element gets removed, but I have array that has bunch of null vaues instead. How do I return the array that contains no null elements? Seems that I am not suing something right.
I think what you mean is that the array keys are not continuous anymore. An array does not have "null values" in PHP, unless you set a value to null.
$array = array('foo', 'bar', 'baz');
// array(0 => 'foo', 1 => 'bar', 2 => 'baz');
unset($array[1]);
// array(0 => 'foo', 2 => 'baz');
Two approaches to this:
Loop over the array using foreach, not a "manual" for loop, then it won't matter what the keys are.
Reset the keys with array_values.
Also, removing trucks from the list should really be a method of $listOfTrucks, like $listOfTrucks->remove($remove). You're already using objects, use them to their full potential!
You can use array_filter
<?php
$entry = array(
0 => 'foo',
1 => false,
2 => -1,
3 => null,
4 => ''
);
print_r(array_filter($entry));
?>
output:
Array
(
[0] => foo
[2] => -1
)
Quick question: is there a way to provide a closure in PHP to some equivalent function to the array_unique function so that you can specify your own comparison closure to be used when comparing two items in the array? I have an array of class instances (which may contain duplicates) and want to tell PHP to use particular logic to determine uniqueness.
PHP provides this with sorting using the usort() method - just wondering if it is also available for uniqueness checks. Thanks!
there is array_filter that you can apply a callback to each element in an array and return true/false of whether to keep that value in the returning array. Here is a comment using array_filter to remove duplicates in an array.
I couldn't find exactly what you are looking for but i thought maybe it wouldn't be too tough to write your own function...
$a = new StdClass;
$b = new StdClass;
$c = new StdClass;
$d = new StdClass;
$a->a = 1;
$b->a = 1;
$c->c = 1;
$d->c = 1;
$objects = array( $a,$b,$c,$d );
function custom_array_unique( array $objects ) {
foreach( $objects as $k =>$object ) {
foreach( $objects as $k2 => $object2 ) {
if ( $k !== $k2 && $object == $object2 ) {
unset( $objects[$k] );
}
}
}
return $objects;
}
print_r( custom_array_unique($objects));
Array
(
[1] => stdClass Object
(
[a] => 1
)
[3] => stdClass Object
(
[c] => 1
)
)
The manual page for array_unique() doesn't provide any links to a callback version, and there isn't a function in the list of links on the left called array_uunique() (which is what such a function should be called if it follows the naming convention of the other array sorting functions - but then PHP isn't very reliable when it comes to function naming conventions).
You could add this functionality yourself using a double foreach loop:
$uniqueness_fails = false;
foreach ( $myarray as $keyA => $valueA ) {
foreach ( $myarray as $keyB => $valueB ) {
if ( $keyA != $keyB and my_equality_function($valueA, $valueB) ) {
$uniqueness_fails = true;
break 2;
}
}
}
I suspect I'm doing something stupid here, but I'm confused by what seems like a simple problem with SPL:
How do I modified the contents of an array (the values in this example), using a RecursiveArrayIterator / RecursiveIteratorIterator?
Using the follow test code, I can alter the value within the loop using getInnerIterator() and offsetSet(), and dump out the modified array while I'm within the loop.
But when I leave the loop and dump the array from the iterator, it's back to the original values. What's happening?
$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);
$cArray = new ArrayObject($aNestedArray);
$cRecursiveIter = new RecursiveIteratorIterator(new RecursiveArrayIterator($cArray), RecursiveIteratorIterator::LEAVES_ONLY);
// Zero any array elements under 200
while ($cRecursiveIter->valid())
{
if ($cRecursiveIter->current() < 200)
{
$cInnerIter = $cRecursiveIter->getInnerIterator();
// $cInnerIter is a RecursiveArrayIterator
$cInnerIter->offsetSet($cInnerIter->key(), 0);
}
// This returns the modified array as expected, with elements progressively being zeroed
print_r($cRecursiveIter->getArrayCopy());
$cRecursiveIter->next();
}
$aNestedArray = $cRecursiveIter->getArrayCopy();
// But this returns the original array. Eh??
print_r($aNestedArray);
It seems that values in plain arrays aren't modifiable because they can't be passed by reference to the constructor of ArrayIterator (RecursiveArrayIterator inherits its offset*() methods from this class, see SPL Reference). So all calls to offsetSet() work on a copy of the array.
I guess they chose to avoid call-by-reference because it doesn't make much sense in an object-oriented environment (i. e. when passing instances of ArrayObject which should be the default case).
Some more code to illustrate this:
$a = array();
// Values inside of ArrayObject instances will be changed correctly, values
// inside of plain arrays won't
$a[] = array(new ArrayObject(range(100, 200, 100)),
new ArrayObject(range(200, 100, -100)),
range(100, 200, 100));
$a[] = new ArrayObject(range(225, 75, -75));
// The array has to be
// - converted to an ArrayObject or
// - returned via $it->getArrayCopy()
// in order for this field to get handled properly
$a[] = 199;
// These values won't be modified in any case
$a[] = range(100, 200, 50);
// Comment this line for testing
$a = new ArrayObject($a);
$it = new RecursiveIteratorIterator(new RecursiveArrayIterator($a));
foreach ($it as $k => $v) {
// getDepth() returns the current iterator nesting level
echo $it->getDepth() . ': ' . $it->current();
if ($v < 200) {
echo "\ttrue";
// This line is equal to:
// $it->getSubIterator($it->getDepth())->offsetSet($k, 0);
$it->getInnerIterator()->offsetSet($k, 0);
}
echo ($it->current() == 0) ? "\tchanged" : '';
echo "\n";
}
// In this context, there's no real point in using getArrayCopy() as it only
// copies the topmost nesting level. It should be more obvious to work with $a
// itself
print_r($a);
//print_r($it->getArrayCopy());
You need to call getSubIterator at the current depth, use offsetSet at that depth, and do the same for all depths going back up the tree.
This is really useful for doing unlimited level array merge and replacements, on arrays or values within arrays. Unfortunately, array_walk_recursive will NOT work in this case as that function only visits leaf nodes.. so the 'replace_this_array' key in $array below will never be visited.
As an example, to replace all values within an array unknown levels deep, but only those that contain a certain key, you would do the following:
$array = [
'test' => 'value',
'level_one' => [
'level_two' => [
'level_three' => [
'replace_this_array' => [
'special_key' => 'replacement_value',
'key_one' => 'testing',
'key_two' => 'value',
'four' => 'another value'
]
],
'ordinary_key' => 'value'
]
]
];
$arrayIterator = new \RecursiveArrayIterator($array);
$completeIterator = new \RecursiveIteratorIterator($arrayIterator, \RecursiveIteratorIterator::SELF_FIRST);
foreach ($completeIterator as $key => $value) {
if (is_array($value) && array_key_exists('special_key', $value)) {
// Here we replace ALL keys with the same value from 'special_key'
$replaced = array_fill(0, count($value), $value['special_key']);
$value = array_combine(array_keys($value), $replaced);
// Add a new key?
$value['new_key'] = 'new value';
// Get the current depth and traverse back up the tree, saving the modifications
$currentDepth = $completeIterator->getDepth();
for ($subDepth = $currentDepth; $subDepth >= 0; $subDepth--) {
// Get the current level iterator
$subIterator = $completeIterator->getSubIterator($subDepth);
// If we are on the level we want to change, use the replacements ($value) other wise set the key to the parent iterators value
$subIterator->offsetSet($subIterator->key(), ($subDepth === $currentDepth ? $value : $completeIterator->getSubIterator(($subDepth+1))->getArrayCopy()));
}
}
}
return $completeIterator->getArrayCopy();
// return:
$array = [
'test' => 'value',
'level_one' => [
'level_two' => [
'level_three' => [
'replace_this_array' => [
'special_key' => 'replacement_value',
'key_one' => 'replacement_value',
'key_two' => 'replacement_value',
'four' => 'replacement_value',
'new_key' => 'new value'
]
],
'ordinary_key' => 'value'
]
]
];
Not using the Iterator classes (which seem to be copying data on the RecursiveArrayIterator::beginChildren() instead of passing by reference.)
You can use the following to achieve what you want
function drop_200(&$v) { if($v < 200) { $v = 0; } }
$aNestedArray = array();
$aNestedArray[101] = range(100, 1000, 100);
$aNestedArray[201] = range(300, 25, -25);
$aNestedArray[301] = range(500, 0, -50);
array_walk_recursive ($aNestedArray, 'drop_200');
print_r($aNestedArray);
or use create_function() instead of creating the drop_200 function, but your mileage may vary with the create_function and memory usage.
Looks like getInnerIterator creates a copy of the sub-iterator.
Maybe there is a different method? (stay tuned..)
Update: after hacking at it for a while, and pulling in 3 other engineers, it doesn't look like PHP gives you a way to alter the values of the subIterator.
You can always use the old stand by:
<?php
// Easy to read, if you don't mind references (and runs 3x slower in my tests)
foreach($aNestedArray as &$subArray) {
foreach($subArray as &$val) {
if ($val < 200) {
$val = 0;
}
}
}
?>
OR
<?php
// Harder to read, but avoids references and is faster.
$outherKeys = array_keys($aNestedArray);
foreach($outherKeys as $outerKey) {
$innerKeys = array_keys($aNestedArray[$outerKey]);
foreach($innerKeys as $innerKey) {
if ($aNestedArray[$outerKey][$innerKey] < 200) {
$aNestedArray[$outerKey][$innerKey] = 0;
}
}
}
?>
Convert the array to an object first and it works as expected..
$array = [
'one' => 'One',
'two' => 'Two',
'three' => [
'four' => 'Four',
'five' => [
'six' => 'Six',
'seven' => 'Seven'
]
]
];
// Convert to object (using whatever method you want)
$array = json_decode(json_encode($array));
$iterator = new RecursiveIteratorIterator(new RecursiveArrayIterator($array));
foreach($iterator as $key => $value) {
$iterator->getInnerIterator()->offsetSet($key, strtoupper($value));
}
var_dump($iterator->getArrayCopy());
I know this doesn't answer your question directly, but it's not a good practice to modify the object under iteration while iterating over it.
Could it come down to passing by reference vs passing by value?
For example try changing:
$cArray = new ArrayObject($aNestedArray);
to:
$cArray = new ArrayObject(&$aNestedArray);