From d71d29a9ebc502ee38d0aadf19bf60d6217b7163 Mon Sep 17 00:00:00 2001 From: lenamonj Date: Tue, 1 Sep 2026 19:44:29 -0400 Subject: [PATCH] Make the ordering assertions reject operands PHP can only compare by coercion greaterThan, greaterThanEq, lessThan, lessThanEq and range handed their operands straight to the relational operators, so lessThan(new stdClass(), 10) and lessThan(null, 10) passed: an object compares as 1, and null or bool against a number is compared as bool. Ask first whether the pair can be ordered (two numerics, two strings, two bools, two arrays or two objects) and report anything else as the failed assertion it is. Co-Authored-By: Claude Fable 5 --- src/Assert.php | 28 +++++++++++++++++++++++----- tests/AssertTest.php | 13 +++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/Assert.php b/src/Assert.php index 31c861b9..91679bea 100644 --- a/src/Assert.php +++ b/src/Assert.php @@ -1114,7 +1114,7 @@ public static function notSame(mixed $value, mixed $expect, string|callable $mes */ public static function greaterThan(mixed $value, mixed $limit, string|callable $message = ''): mixed { - if ($value <= $limit) { + if (!static::comparable($value, $limit) || $value <= $limit) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'Expected a value greater than %2$s. Got: %s', @@ -1135,7 +1135,7 @@ public static function greaterThan(mixed $value, mixed $limit, string|callable $ */ public static function greaterThanEq(mixed $value, mixed $limit, string|callable $message = ''): mixed { - if ($value < $limit) { + if (!static::comparable($value, $limit) || $value < $limit) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'Expected a value greater than or equal to %2$s. Got: %s', @@ -1156,7 +1156,7 @@ public static function greaterThanEq(mixed $value, mixed $limit, string|callable */ public static function lessThan(mixed $value, mixed $limit, string|callable $message = ''): mixed { - if ($value >= $limit) { + if (!static::comparable($value, $limit) || $value >= $limit) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'Expected a value less than %2$s. Got: %s', @@ -1177,7 +1177,7 @@ public static function lessThan(mixed $value, mixed $limit, string|callable $mes */ public static function lessThanEq(mixed $value, mixed $limit, string|callable $message = ''): mixed { - if ($value > $limit) { + if (!static::comparable($value, $limit) || $value > $limit) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'Expected a value less than or equal to %2$s. Got: %s', @@ -1200,7 +1200,7 @@ public static function lessThanEq(mixed $value, mixed $limit, string|callable $m */ public static function range(mixed $value, mixed $min, mixed $max, string|callable $message = ''): mixed { - if ($value < $min || $value > $max) { + if (!static::comparable($value, $min) || !static::comparable($value, $max) || $value < $min || $value > $max) { $message = self::resolveMessage($message); static::reportInvalidArgument(\sprintf( $message ?: 'Expected a value between %2$s and %3$s. Got: %s', @@ -2614,6 +2614,24 @@ protected static function resolveMessage(string|callable $message): string return \is_callable($message) ? $message() : $message; } + /** + * Whether two operands can be ordered against one another without coercion. + * + * PHP's relational operators answer for every pair: an object compares as 1, an array + * outranks any scalar, null and bool are compared as bool. The ordering assertions ask + * this first, so such a pair is reported as a failed assertion instead of a passed one. + * + * @psalm-pure + */ + protected static function comparable(mixed $value, mixed $other): bool + { + return (\is_numeric($value) && \is_numeric($other)) + || (\is_string($value) && \is_string($other)) + || (\is_bool($value) && \is_bool($other)) + || (\is_array($value) && \is_array($other)) + || (\is_object($value) && \is_object($other)); + } + private function __construct() { } diff --git a/tests/AssertTest.php b/tests/AssertTest.php index f0ec064d..6ab02459 100644 --- a/tests/AssertTest.php +++ b/tests/AssertTest.php @@ -295,6 +295,19 @@ public static function getTests(): array ['range', [2, 1, 2], true], ['range', [0, 1, 2], false], ['range', [3, 1, 2], false], + ['greaterThan', [new stdClass(), 10], false], + ['greaterThan', [[1, 2], 10], false], + ['greaterThan', ['abc', 10], false], + ['greaterThanEq', [null, 10], false], + ['lessThan', [new stdClass(), 10], false], + ['lessThan', [false, 10], false], + ['range', [new stdClass(), 1, 10], false], + ['range', [5, 1, 'x'], false], + ['lessThan', ['5', 10], true], + ['lessThan', ['abc', 'abd'], true], + ['lessThan', [[1], [1, 2]], true], + ['greaterThan', [true, false], true], + ['lessThan', [new DateTimeImmutable('2020-01-01'), new DateTimeImmutable('2021-01-01')], true], ['oneOf', [1, [1, 2, 3]], true], ['oneOf', [1, ['1', '2', '3']], false], ['notOneOf', [1, [1, 2, 3]], false],