When reading the RFC we find out that it contradicts itself:
Add a new operator (expr) <=> (expr), it returns 0 if both operands are equal, 1 if the left is greater, and -1 if the right is greater. It uses exactly the same comparison rules as used by our existing comparison operators: <, <=, ==, >= and >. (See the manual for details)
Note: See the ==
, this means the spaceship operator does a loosely comparison.
And later down in the examples:
// only values are compared
$a = (object) ["a" => "b"];
$b = (object) ["b" => "b"];
echo $a $b; // 0
The spaceship operator is just a combination of the operators <
, ==
and >
. And it gives respective return values depending on what it evaluates to:
operator(s): < = >
return value: -1 0 1
Now arrays and objects are a bit more complex types. To understand what the <=>
PHP spaceship operator does, we need to look and understand how <
, ==
and >
work for arrays and objects.
So let's look at the comparison operators <
, >
, ==
for each type. First we will look at <
and >
and then after that we also look at ==
.
Array comparison operators
Now as for arrays <
and >
are documented here:
┌───────────┬───────────┬──────────────────────────────────────────────────┐
│ type of │ type of │ │
│ Operand 1 │ Operand 2 │ Result │
├───────────┼───────────┼──────────────────────────────────────────────────┤
│ array │ array │ Array with fewer members is smaller, │
│ │ │ if key from operand 1 is not found in operand 2 │
│ │ │ then arrays are uncomparable, │
│ │ │ otherwise - compare value by value │
└───────────┴───────────┴──────────────────────────────────────────────────┘
This can also be written and represented by code:
Example #2 Transcription of standard array comparison
<?php
// Arrays are compared like this with standard comparison operators
function standard_array_compare($op1, $op2)
{
if (count($op1) < count($op2)) {
return -1; // $op1 < $op2
} elseif (count($op1) > count($op2)) {
return 1; // $op1 > $op2
}
foreach ($op1 as $key => $val) {
if (!array_key_exists($key, $op2)) {
return null; // uncomparable
} elseif ($val < $op2[$key]) {
return -1;
} elseif ($val > $op2[$key]) {
return 1;
}
}
return 0; // $op1 == $op2
}
?>
We can test this easily with some testing. Using methods like in math and always change only one thing, so we can make sure we are correct here:
/**
/*
/* Testing operators: < and >
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 9
// Failed: 0
// Passed: 9
{
//Test case 1.1
$a = [1];
$b = [1];
//Passed
var_dump("Same amount of elements, keys and values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.2
$a = [1];
$b = [1, 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.3
$a = [10];
$b = [1, 1];
//Passed
var_dump("NOT same amount of elements nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.4
$a = [1 => 1];
$b = [10 => 1];
//Passed
var_dump("Same amount of element and values, NOT same keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.5
$a = [10];
$b = [1];
//Passed
var_dump("Same amount of elements and keys, NOT same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.6
$a = [1 => 1, 2 => 1];
$b = [2 => 1, 1 => 1];
//Passed
var_dump("Same amount of elements and keys in different order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.7
$a = [1 => 1, 2 => 5];
$b = [2 => 5];
//Passed
var_dump("Same values, NOT same amount of elements nor keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.8
$a = [10 => 1];
$b = [1 => 10];
//Passed
var_dump("NOT same keys nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.9
$a = [1 => 1, 2 => 1];
$b = [2 => 10, 1 => 1];
//Passed
var_dump("Same amount of elements and values, NOT same keys nor order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
For the equality/identity operators ==
and ===
we find the documentation for arrays here:
┌───────────┬──────────┬──────────────────────────────────────────────────┐
│ Example │ Name │ Result │
├───────────┼──────────┼──────────────────────────────────────────────────┤
│ $a == $b │ Equality │ TRUE if $a and $b have the same key/value pairs. │
│ $a === $b │ Identity │ TRUE if $a and $b have the same key/value pairs │
│ │ │ in the same order and of the same types. │
└───────────┴──────────┴──────────────────────────────────────────────────┘
As before we can simply test this with some testing code:
/**
/*
/* Testing operators: == and ===
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 5
// Failed: 0
// Passed: 5
{
//Test case 2.1
$a = [1];
$b = [1];
//Passed
var_dump("Same amount of elements, values and keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.2
$a = [1];
$b = [10, 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.3
$a = [10];
$b = [1];
//Passed
var_dump("Same amount of elements, but not values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.4
$a = [1 => 1];
$b = [10 => 1];
//Passed
var_dump("Same amount of elements and values, but not keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.5
$a = [1 => 1, 2 => 2];
$b = [2 => 2, 1 => 1];
//Passed
var_dump("Same amount of elements, key and values, but different order: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
So we can see and confirm that the comparison operators for arrays work as expected and as documented!
Full Testing File
Object comparison operators
The documentation for <
and >
with objects is documented here:
┌───────────┬───────────┬──────────────────────────────────────────────────┐
│ type of │ type of │ │
│ Operand 1 │ Operand 2 │ Result │
├───────────┼───────────┼──────────────────────────────────────────────────┤
│ object │ object │ Built-in classes can define its own comparison, │
│ │ │ different classes are uncomparable, │
│ │ │ same class compare properties same as arrays │
└───────────┴───────────┴──────────────────────────────────────────────────┘
As before we can also test this:
/**
/*
/* Testing operators: < and >
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 10
// Failed: 0
// Passed: 10
{
//Test case 1.1
$a = (object)["a" => 1];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements, keys and values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.2
$a = (object)["a" => 1];
$b = (object)["a" => 1, "b" => 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.3
$a = (object)["a" => 10];
$b = (object)["a" => 1, "b" => 1];
//Passed
var_dump("NOT same amount of elements nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.4
$a = (object)["a" => 1];
$b = (object)["b" => 1];
//Passed
var_dump("Same amount of element and values, NOT same keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.5
$a = (object)["a" => 10];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements and keys, NOT same values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.6
$a = (object)["a" => 1, "b" => 1];
$b = (object)["b" => 1, "a" => 1];
//Passed
var_dump("Same amount of elements and keys in different order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.7
$a = (object)["a" => 1, "b" => 5];
$b = (object)["b" => 5];
//Passed
var_dump("Same values, NOT same amount of elements nor keys: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.8
$a = (object)["c" => 1];
$b = (object)["a" => 10];
//Passed
var_dump("NOT same keys nor values: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.9
$a = (object)["a" => 1, "b" => 1];
$b = (object)["b" => 10, "a" => 1];
//Passed
var_dump("Same amount of elements and values, NOT same keys nor order: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
//Test case 1.10
class A {public $a = 1;}
$a = new A;
class B {public $a = 1;}
$b = new B;
//Passed
var_dump("Same amount of elements and values and keys, but different not built-in class: " . "'<' -> " . bool2str($a < $b) . " '>' -> " . bool2str($a > $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
The documentation for ==
and ===
with objects has its own page here:
When using the comparison operator (==), object variables are compared in a simple manner, namely: Two object instances are equal if they have the same attributes and values, and are instances of the same class.
When using the identity operator (===), object variables are identical if and only if they refer to the same instance of the same class.
And again this can be tested:
/**
/*
/* Testing operators: == and ===
/*
*/
//Test case
//Variations: amount, values and keys (order)
//Test count: 7
// Failed: 0
// Passed: 7
{
//Test case 2.1
$a = (object)["a" => 1];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements, values and keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.2
$a = (object)["a" => 1];
$b = (object)["a" => 10, "b" => 1];
//Passed
var_dump("NOT same amount of elements, but same values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.3
$a = (object)["a" => 10];
$b = (object)["a" => 1];
//Passed
var_dump("Same amount of elements, but not values: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.4
$a = (object)["a" => 1];
$b = (object)["b" => 1];
//Passed
var_dump("Same amount of elements and values, but not keys: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.5
$a = (object)["a" => 1, "b" => 2];
$b = (object)["b" => 2, "a" => 1];
//Passed
var_dump("Same amount of elements, key and values, but different order: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.6
class C {public $a = 1;}
$a = new A;
class D {public $a = 1;}
$b = new B;
//Passed
var_dump("Same amount of elements and values and keys, but different not built-in class: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
//Test case 2.7
$a = (object)["a" => 1];
$b = $a;
//Passed
var_dump("Same exact instance: " . "'==' -> " . bool2str($a == $b) . " '===' -> " . bool2str($a === $b));
}
echo PHP_EOL . PHP_EOL . PHP_EOL; //Test case separator
/**
/*
/* Test case end
/*
*/
//NULL, TRUE, FALSE 2 str func
function bool2str($v){if($v === NULL)return "NULL";elseif($v === FALSE)return "FALSE";elseif($v === TRUE)return "TRUE";else return "UNEXPECTED: '$v'";}
So we see, that the comparison operators with objects behave exactly like expected and documented! Even with loose comparison the attributes and values are being considered.
Full Testing File
Conclusion
As this bug has been reported here, the bug report is probably based on the comment in the RFC, which says:
// only values are compared
But besides that this is the only example in the RFC with a comment, the RFC clearly states that it uses the same comparison rules as <
, ==
and >
.
This means that the code example provided would be uncomparable, because it doesn't have the same attributes/keys.
As for equality it would need same attributes/keys and values so it can't be equal, and for less- or greater- than it is uncomparable as shown in the code example above how the comparison works:
if (!array_key_exists($key, $op2)) {
return null; // uncomparable
}
We also see this if we try each comparison operator alone:
$a = (object)["b" => "b"];
$b = (object)["a" => "b"];
var_dump($a > $b); //FALSE
var_dump($a < $b); //FALSE
var_dump($a == $b); //FALSE
All return false, since it's uncomparable.
And just for the case StdClass
would have its own comparison, we can test it with our own class:
class A {
public $a = "";
public $b = "";
}
$a = new A;
$a->a = "b";
unset($a->b);
$b = new A;
$b->b = "b";
unset($b->a);
var_dump($a);
var_dump($b);
var_dump($a <=> $b); //1
Also same output: 1.
So I would say since it is uncomparable it shouldn't return 0
, 1
nor -1
. It should probably return FALSE
or NULL
or something like this.
Right now I would say this behaviour isn't documented correctly.