Compare two arrays of objects by specific object key value - php

This is probably really simple, but I cannot seem to get it. I have two arrays of objects $a and $b. In $a I have objects with key email and in $b I have objects with user_email (this cannot be changed as it comes from an API). I want as output a third array $c that has all the objects where email == user_email. I've tried using array_udiff like this:
$c = array_udiff($a, $b,
function ($obj_a, $obj_b) {
return $obj_a->email - $obj_b->user_email;
}
);
For some reason, $obj_b is not always an object from array $b as I would have thought. Is there any clean solution for this? Thank you.

You are probably looking for array_uintersect. Also, you should compare your strings with strcmp or even better with strcasecmp. Remember that the order in which PHP will pass array elements to the callback is not always the same as the order of arrays.
$a = [(object)['email' => 'a'], (object)['email' => 'b'], (object)['email' => 'c']];
$b = [(object)['user_email' => 'c'], (object)['user_email' => 'a'], (object)['user_email' => 'd']];
$comparer = function($obj_a, $obj_b) {
$email_a = property_exists($obj_a, 'email')
? $obj_a->email
: $obj_a->user_email;
$email_b = property_exists($obj_b, 'email')
? $obj_b->email
: $obj_b->user_email;
return strcasecmp($email_a, $email_b);
};
// only objects with email property
$c = array_uintersect($a, $b, $comparer);
// both objects with email and user_email property
$d = array_merge(
array_uintersect($a, $b, $comparer),
array_uintersect($b, $a, $comparer)
);
Testing with property_exists can be changed to testing with instanceof if the arguments are concrete classes.

Related

Is there a clean way to use undefined variables as optional parameters in PHP?

Is there any nice way to use (potentially) undefined Variables (like from external input) as optional Function parameters?
<?php
$a = 1;
function foo($a, $b=2){
//do stuff
echo $a, $b;
}
foo($a, $b); //notice $b is undefined, optional value does not get used.
//output: 1
//this is even worse as other erros are also suppressed
#foo($a, $b); //output: 1
//this also does not work since $b is now explicitly declared as "null" and therefore the default value does not get used
$b ??= null;
foo($a,$b); //output: 1
//very,very ugly hack, but working:
$r = new ReflectionFunction('foo');
$b = $r->getParameters()[1]->getDefaultValue(); //still would have to check if $b is already set
foo($a,$b); //output: 12
the only semi-useful method I can think of so far is to not defining the default value as parameter but inside the actual function and using "null" as intermediary like this:
<?php
function bar ($c, $d=null){
$d ??= 4;
echo $c,$d;
}
$c = 3
$d ??= null;
bar($c,$d); //output: 34
But using this I still have to check the parameter twice: Once if it is set before calling the function and once if it is null inside the function.
Is there any other nice solution?
Ideally you wouldn't pass $b in this scenario. I don't remember ever running into a situation where I didn't know if a variable existed and passed it to a function anyway:
foo($a);
But to do it you would need to determine how to call the function:
isset($b) ? foo($a, $b) : foo($a);
This is kind of hackish, but if you needed a reference anyway it will be created:
function foo($a, &$b){
$b = $b ?? 4;
var_dump($b);
}
$a = 1;
foo($a, $b);
I would do something like this if this was actually a requirement.
Just testing with sum of the supplied values just for showing an example.
<?php
$x = 1;
//Would generate notices but no error about $y and t
//Therefore I'm using # to suppress these
#$sum = foo($x,$y,4,3,t);
echo 'Sum = ' . $sum;
function foo(... $arr) {
return array_sum($arr);
}
Would output...
Sum = 8
...based on the array given (unknown nr of arguments with ... $arr)
array (size=5)
0 => int 1
1 => null
2 => int 4
3 => int 3
4 => string 't' (length=1)
array_sum() only sums up 1,4 and 3 here = 8.
Even if above actually works I would not recommend it, because then whatever data can be sent to your function foo() without you having any control over it. When it comes to user input of any kind you should always validate as much as you can in your code before using the actual data from the user.

Name of natural comparison function in php?

I want to do the intersection between two multidimensional arrays in php 5. E.g:
$a = array(array(), array(1), array(2));
$b = array(array(), array(0), array(2));
I naturally try:
array_intersect($a, $b)
But it gives me Notice because of array to string conversion. As a workaround, I want to precise the comparison function using uintersect:
array_uintersect($a, $b, function($x, $y){return $x == $y ? 0 : ( $x < $y ? -1 : 1 );})
And it works. But this is kind of ugly. My question is simple, is there a built-in natural comparison function which works on array and which I could use as callback function like in:
array_uintersect($a, $b, 'natcmp')
Thank you!

Changing a value in a copy of an array changes the value in the original arra

I have some issues with variables in php that i don't understand.
This is a simplified code example of the issue.
//Create an initial array with an sub-array
$a = array();
$a['test'] = array(1,2);
//Create an reference to $a['test'] in $b
//Changing $b[0] should now change the value in $a['test'][0]
$b = &$a['test'];
$b[0] = 3;
//Create an copy of $a into $c
$c = $a;
//Change one value in $c, which is an copy of $a.
//This should NOT change the original value of $a as it is a copy.
$c['test'][1] = 5;
print_r($a);
print_r($b);
print_r($c);
This is the output:
Array
(
[test] => Array
(
[0] => 3
[1] => 5
)
)
Array
(
[0] => 3
[1] => 5
)
Array
(
[test] => Array
(
[0] => 3
[1] => 5
)
)
The script creates an array with an sub-array and puts two values in it.
A reference to the sub-array is then put into b and one of the values in a is changed in this way.
I then make a copy of a into c.
I then change one value of c.
As c is a copy of a i would expect that the change on c did not affect a. But the output tells a different tale.
Can anyone explain why changing a value in the variable $c affect the value in $a when $c is just a copy of $a? Why is there a 5 in the values of $a?
You're assigning $b to $a by reference (that's what the & prefix does). Any changes to $b will effectively modify $a. Just force a declaration assignment:
$b = $a['test'];
$c does not modify $a. Here's the order of what's going on, and why the arrays are identical:
$a['test'] is assigned an array of 1,2.
$b is assigned as a reference to $a['test'], and modifies its values
$c is then assigned to $a, which has now been modified by $b.
I think i found the answer to my own question... On this page: http://www.php.net/manual/en/language.references.whatdo.php
I can't really understand why it does what it does. I do understand that i should probably avoid mixing references and arrays in the future.
Im refering to this section:
Note, however, that references inside arrays are potentially
dangerous. Doing a normal (not by reference) assignment with a
reference on the right side does not turn the left side into a
reference, but references inside arrays are preserved in these normal
assignments. This also applies to function calls where the array is
passed by value. Example:
<?php
/* Assignment of scalar variables */
$a = 1;
$b =& $a;
$c = $b;
$c = 7; //$c is not a reference; no change to $a or $b
/* Assignment of array variables */
$arr = array(1);
$a =& $arr[0]; //$a and $arr[0] are in the same reference set
$arr2 = $arr; //not an assignment-by-reference!
$arr2[0]++;
/* $a == 2, $arr == array(2) */
/* The contents of $arr are changed even though it's not a reference! */
?>
In other words, the reference behavior of arrays is defined in an
element-by-element basis; the reference behavior of individual
elements is dissociated from the reference status of the array
container.
You are passing the reference $a to $b by using $b = &$a['test']; Hence
change
$b = &$a['test'];
to
$b = $a['test'];

How to cast variable to array

I have a variable $v that can be either single string or array of stringsand I have a code:
$a = array();
if (is_array($v)) {
$a = $v;
} else {
$a[] = $v;
}
How it can be done in more elegant way? (in other words, how to cast a variable to array)
You can cast a variable to an array by using:
$var = (array)$arr;
$a = (array) $v;
is the answer.
I would write your could snippet like this (short and you read it and know exactly what is happening):
$a = is_array($v) ? $v : array($v);
Alternatively you could use settype:
settype($a, "array");
For expliciting the variable type. It's exactly the same as what happens with a typecast behind the scenes. (More useful for group-wise typecasting e.g. in loops.)
As others have said, casting a scalar value to an array will produce a singleton array (i.e. an array with the scalar as its only element). However, as still others have pointed out, take care to only do this if you know the value is going to be a scalar and not a class instance.
From the PHP docs:
For any of the types integer, float, string, boolean and resource,
converting a value to an array results in an array with a single
element with index zero and the value of the scalar which was
converted. In other words, (array)$scalarValue is exactly the same as
array($scalarValue).
If an object is converted to an array, the result is an array whose
elements are the object's properties. The keys are the member variable
names, with a few notable exceptions: integer properties are
unaccessible; private variables have the class name prepended to the
variable name; protected variables have a '*' prepended to the
variable name. These prepended values have null bytes on either side.
If $v is a scalar (Boolean, String, Number) you can use:
a) $v = (array)$v;
If $v is an object, you have to use:
b) $v = is_array($v) ? $v : array($v);
Method (b) works in every case (with scalars too).
If you are looking to convert an object to a single count array you can use the follow code:
$list = array([0] => $obj);
The other provided answers won't work when trying to convert an object, it will simply convert the fields of that object into an associative array (unless that is what you are trying to do).
$var = (array)$arr;
Actually if you want to cast to an array and not have to worry about what you put into it, the answer is
$var = (is_object($var)) ? array($var) : (array) $var;
You can test this with the following code
function toArray($var) {
return (is_object($var)) ? array($var) : (array) $var;
}
$object = new stdClass;
$resource = fopen('php://stdout', 'w');
$closure = function () {};
$tests = array(
array(toArray(true), array(true), 'boolean true'),
array(toArray(false), array(false), 'boolean false'),
array(toArray(null), array(), 'null'),
array(toArray(1), array(1), 'positive integer'),
array(toArray(0), array(0), 'zero integer'),
array(toArray(-1), array(-1), 'negative integer'),
array(toArray(1.5), array(1.5), 'positive float'),
array(toArray(0.0), array(0.0), 'zero float'),
array(toArray(-1.5), array(-1.5), 'negative float'),
array(toArray(''), array(''), 'empty string'),
array(toArray('foo'), array('foo'), 'string'),
array(toArray(array()), array(), 'array'),
array(toArray($object), array($object), 'object'),
array(toArray($resource), array($resource), 'resource'),
array(toArray($closure), array($closure), 'closure'),
);
foreach ($tests as $test) {
ob_start();
var_dump($test[0]);
$a = ob_get_clean();
ob_start();
var_dump($test[1]);
$b = ob_get_clean();
assert($a === $b, "{$test[2]} is not the same");
}

array intersect for object array php

I want to know how to array_intersect for object array.
You can use array_uintersect in conjunction with spl_object_hash, see an example:
array_uintersect($a, $b, function($a, $b) {
return strcmp(spl_object_hash($a), spl_object_hash($b));
});
where '$a' and '$b' are arrays of some objects that you want to intersect.
nice toString function is already implemented and is called serialize ;) so
array_map(
'unserialize',
array_intersect(
array_map(
'serialize',
$obj1
),
array_map(
'serialize',
$obj2
)
)
);
will do the work, example mentioned higher don't work 'cause array_intersect work's only with strings as someone mentioned too
array_intersect() returns an array containing all the values of array1 that are present in all the arguments.
Then what mean present in this context (exacly this function), i found on php.net my answer:
Two elements are considered equal if
and only if (string) $elem1 ===
(string) $elem2. In words: when the
string representation is the
same.
Then you can't use it on array of objects if your objects not implements unique conversion to string.
Had a similar problem a few days ago, while these are answers are on the right path; I used them to work out the following:
From Artefacto's answer return $obj1 == $obj2 didn't really work, so I wrote a simple comparative function (basically gets the md5 of the serialised object and compares that):
function object_compare($obj1, $obj2){
$md5 = function($obj){
return md5(serialize($obj));
};
return strcmp($md5($obj1), $md5($obj2));
}
Then it’s jut a matter of calling array_uintersect with our comparative function to get the intersection:
# $array1 / $array2 are the array of objects we want to compare
return array_uintersect($array1, $array2, 'object_compare');
In my case, I had an unknown / dynamic array of objects, so I took it a step further so I don't have to declare array_uintersect($array1, $array2, ...) specifically - but just be able to pass in an array of arrays (of objects):
# $multiarray_of_objects is our array of arrays
$multiarray_of_objects[] = 'object_compare';
return call_user_func_array('array_uintersect', $multiarray_of_objects);
Just gotta remember to pass in the reference to our callback / comparative function as the last string in the array. Works like a charm!
The correct way to check whether two objects are equal is to use ==. Therefore:
array_uintersect($arr1, $arr2, function ($a1, $a2) { return $a1 == $a2; });
Just for completeness: Implement __toString() method in your object returning a unique value. For database entities this might be as easy as returning the fully qualified class name postfixed with the ID of the record. But it can also be arbitrarily complex by doing some hashing or even worse things.
In my opinion, it's the class's duty to serialize itself or create something unique to compare its objects by. Using anything outside of a class to serialize an object might result in strange behaviour (including comparing objects of different classes, which must never result in equality).
I use array_udiff to implement array_intersect for an object array.
function diff($a, $b) {
if($a === $b) {
return 0;
} else {
return 1;}
}
$array_1 = array('a', 'b', 'c');
$array_2 = array('c', 'd','e');
$array = array_udiff($array_1, array_udiff($array_1, $array_2, 'diff'),'diff');
var_dump($array);
return array(1) { [2]=> string(1) "c" }
You can have your own diff function for any scheme.
The correct solution would be:
array_uintersect($arr1, $arr2, function ($a1, $a2) { return $a1 != $a2; });
Note the != in the callback function as opposed to the answer from #Artefacto. Based on the documentation of array_uintersect, the callback function has to return 0 (false) if array items are equal.

Categories