1

I want to compare an array of complex objects with an array of ids, with the expected result being an array of any object that did not have its id listed.

This sounds like a perfect use case for array_udiff, but I couldn't get it work without some confusing hassle. To illustrate my problem with that function, here a boiled down example:

class Foo {
  public $id;
  public function __construct($id) {
    $this->id = $id;
  }
}

$foos = [new Foo(1), new Foo(2), new Foo(3), new Foo(4)];
$fooIds = [1, 2, 3, 4];

$diff = array_udiff($foos, $fooIds, function ($f, $i){ return $f->id - $i; });
print_r($diff);  // expected result: an empty array

// actual result:
// Array
// (
//     [1] => Foo Object
//         (
//             [id] => 2
//         )
// )
// Object of class Foo could not be converted to int :11

It sounds to me like array_udiff tries to do some type coercion between the elements of the arrays. I found no mention of this in the docs, and one question on SO seems to ask something similar, but doesn't have any answers. What I'd like to know:

  • Why does array_udiff behave this way? If we can supply an arbitrary callback function, this kind of coercion seems completely unnecessary, and in my case even very unhelpful.
  • Is there a good way to work around this, or should I use a different of function given my general problem?
Arne
  • 17,706
  • 5
  • 83
  • 99
  • It's not trying to coerce anything. But for some reason it's calling the function with elements of the same array, not different arrays. – Barmar May 14 '19 at 07:04
  • Have a read of the example on the manual page - https://www.php.net/manual/en/function.array-udiff.php#80149 – Nigel Ren May 14 '19 at 07:05
  • @NigelRen Thanks for the link, I guess I didn't understand it when I scanned over it. – Arne May 14 '19 at 07:11

3 Answers3

2

Though a bit ugly, the seemingly simplest way to get the required result is to type-check before comparing.

<?php
declare(strict_types=1);

error_reporting(-1);
ini_set('display_errors', 'On');

class Foo
{
    public $id;

    public function __construct($id) {
        $this->id = $id;
    }
}

$foos = [new Foo(1), new Foo(2), new Foo(3), new Foo(4)];
$fooIds = [1, 2, 3, 4];

$diff = array_udiff($foos, $fooIds, function ($a, $b) {
    echo gettype($a), ' <=> ', gettype($b), "\n";

    if ($a instanceof Foo && $b instanceof Foo) {
        return $a->id <=> $b->id;
    }

    if ($a instanceof Foo) {
        return $a->id <=> $b;
    }

    if ($b instanceof Foo) {
        return $a <=> $b->id;
    }

    return $a <=> $b;
});

print_r($diff);  // expected result: an empty array

demo: https://3v4l.org/1uVYf

Yoshi
  • 54,081
  • 14
  • 89
  • 103
  • Yeah, that does it, the output is quite useful. Do you have a clue why it works this way? – Arne May 14 '19 at 07:18
  • @Arne My guess would be that it's internally using a [sort algorithm](https://stackoverflow.com/questions/3165984/what-sort-algorithm-does-php-use) for which both operands can appear in multiple positions. – Yoshi May 14 '19 at 07:20
2

I think @apokryfos (from the answer you shared):

It makes sense that it would not have $a from the first array and $b from the second array. What you essentially have in the function is a comparator so if you sort both arrays based on the comparator (in O(nlogn) time) you can then get the diff in O(n) time in a sort-join manner. If it was just pairwise comparisons it would be O(n^2) so I suggest you treat your callback as the general comparator function which works for the combined first and second array

Any way I recommend that simple workaround using array_column and the star-ship operator:

$diff = array_udiff(array_column($foos, "id"), $fooIds, function ($f, $i){ return $f <=> $i; });

array_column works on object too from PHP 7

dWinder
  • 11,597
  • 3
  • 24
  • 39
  • That workaround won't work if he wants his result to contain `Foo` objects. It will contain just the numbers. – Barmar May 14 '19 at 07:07
  • Correct - but it may be what he looking for, and if not getting the objects is simple `array_filter` after that. I will leave it to OP to see if this can help him else will delete – dWinder May 14 '19 at 07:10
  • 1
    thanks for the answer, but I guess I do need the list of differences to be the objects. I don't see a need to delete though, a short and simple answer can still help a lot to others who may stumble over this question. – Arne May 14 '19 at 07:14
  • 1
    Also, now that the other answers have been edited, yours is the only one that contains an actual explanation of why `array_udiff` behaves the way it does, so it's helpful already. – Arne May 14 '19 at 07:27
2

Use an intermediate function that specializes in taking the comparison value of either Foo ($f->id) or non-Foo ($f):

$val = function($f) { 
    return $f instanceof Foo ? $f->id : $f;
};

$diff = array_udiff(
    $foos,
    $fooIds, 
    function ($f, $i) use ($val) { return $val($f) - $val($i); }
);

Or in a single comparison function:

function ($f, $i) { return ($f instanceof Foo ? $f->id : $f) - ($i instanceof Foo ? $i->id ? $i); }
Tom
  • 3,281
  • 26
  • 33