|
| 1 | +<?php |
| 2 | + |
| 3 | +namespace Yoast\PHPUnitPolyfills\Polyfills; |
| 4 | + |
| 5 | +use ReflectionClass; |
| 6 | +use ReflectionException; |
| 7 | +use ReflectionNamedType; |
| 8 | +use ReflectionObject; |
| 9 | +use ReflectionType; |
| 10 | +use TypeError; |
| 11 | +use Yoast\PHPUnitPolyfills\Exceptions\InvalidComparisonMethodException; |
| 12 | + |
| 13 | +/** |
| 14 | + * Polyfill the Assert::assertObjectEquals() methods. |
| 15 | + * |
| 16 | + * Introduced in PHPUnit 9.4.0. |
| 17 | + * |
| 18 | + * The polyfill implementation closely matches the PHPUnit native implementation with the exception |
| 19 | + * of the return type check and the names of the thrown exceptions. |
| 20 | + * |
| 21 | + * @link https://github.com/sebastianbergmann/phpunit/issues/4467 |
| 22 | + * @link https://github.com/sebastianbergmann/phpunit/issues/4707 |
| 23 | + * @link https://github.com/sebastianbergmann/phpunit/commit/1dba8c3a4b2dd04a3ff1869f75daaeb6757a14ee |
| 24 | + * @link https://github.com/sebastianbergmann/phpunit/commit/6099c5eefccfda860c889f575d58b5fe6cc10c83 |
| 25 | + */ |
| 26 | +trait AssertObjectEquals { |
| 27 | + |
| 28 | + /** |
| 29 | + * Asserts that two objects are considered equal based on a custom object comparison |
| 30 | + * using a comparator method in the target object. |
| 31 | + * |
| 32 | + * The custom comparator method is expected to have the following method |
| 33 | + * signature: `equals(self $other): bool` (or similar with a different method name). |
| 34 | + * |
| 35 | + * Basically, the assertion checks the following: |
| 36 | + * - A method with name $method must exist on the $actual object. |
| 37 | + * - The method must accept exactly one argument and this argument must be required. |
| 38 | + * - This parameter must have a classname-based declared type. |
| 39 | + * - The $expected object must be compatible with this declared type. |
| 40 | + * - The method must have a declared bool return type. (JRF: not verified in this implementation) |
| 41 | + * - `$actual->$method($expected)` returns boolean true. |
| 42 | + * |
| 43 | + * @param object $expected Expected value. |
| 44 | + * @param object $actual The value to test. |
| 45 | + * @param string $method The name of the comparator method within the object. |
| 46 | + * @param string $message Optional failure message to display. |
| 47 | + * |
| 48 | + * @return void |
| 49 | + * |
| 50 | + * @throws TypeError When any of the passed arguments do not meet the required type. |
| 51 | + * @throws InvalidComparisonMethodException When the comparator method does not comply with the requirements. |
| 52 | + */ |
| 53 | + public static function assertObjectEquals( $expected, $actual, $method = 'equals', $message = '' ) { |
| 54 | + /* |
| 55 | + * Parameter input validation. |
| 56 | + * In PHPUnit this is done via PHP native type declarations. Emulating this for the polyfill. |
| 57 | + */ |
| 58 | + if ( \is_object( $expected ) === false ) { |
| 59 | + throw new TypeError( |
| 60 | + \sprintf( |
| 61 | + 'Argument 1 passed to assertObjectEquals() must be an object, %s given', |
| 62 | + \gettype( $expected ) |
| 63 | + ) |
| 64 | + ); |
| 65 | + } |
| 66 | + |
| 67 | + if ( \is_object( $actual ) === false ) { |
| 68 | + throw new TypeError( |
| 69 | + \sprintf( |
| 70 | + 'Argument 2 passed to assertObjectEquals() must be an object, %s given', |
| 71 | + \gettype( $actual ) |
| 72 | + ) |
| 73 | + ); |
| 74 | + } |
| 75 | + |
| 76 | + if ( \is_scalar( $method ) === false ) { |
| 77 | + throw new TypeError( |
| 78 | + \sprintf( |
| 79 | + 'Argument 3 passed to assertObjectEquals() must be of the type string, %s given', |
| 80 | + \gettype( $method ) |
| 81 | + ) |
| 82 | + ); |
| 83 | + } |
| 84 | + else { |
| 85 | + $method = (string) $method; |
| 86 | + } |
| 87 | + |
| 88 | + /* |
| 89 | + * Comparator method validation. |
| 90 | + */ |
| 91 | + $reflObject = new ReflectionObject( $actual ); |
| 92 | + |
| 93 | + if ( $reflObject->hasMethod( $method ) === false ) { |
| 94 | + throw new InvalidComparisonMethodException( |
| 95 | + \sprintf( |
| 96 | + 'Comparison method %s::%s() does not exist.', |
| 97 | + \get_class( $actual ), |
| 98 | + $method |
| 99 | + ) |
| 100 | + ); |
| 101 | + } |
| 102 | + |
| 103 | + $reflMethod = $reflObject->getMethod( $method ); |
| 104 | + |
| 105 | + /* |
| 106 | + * As the next step, PHPUnit natively would validate the return type, |
| 107 | + * but as return type declarations is a PHP 7.0+ feature, the polyfill |
| 108 | + * skips this check in favour of checking the type of the actual |
| 109 | + * returned value. |
| 110 | + * |
| 111 | + * Also see the upstream discussion about this: |
| 112 | + * {@link https://github.com/sebastianbergmann/phpunit/issues/4707} |
| 113 | + */ |
| 114 | + |
| 115 | + /* |
| 116 | + * Comparator method parameter requirements validation. |
| 117 | + */ |
| 118 | + if ( $reflMethod->getNumberOfParameters() !== 1 |
| 119 | + || $reflMethod->getNumberOfRequiredParameters() !== 1 |
| 120 | + ) { |
| 121 | + throw new InvalidComparisonMethodException( |
| 122 | + \sprintf( |
| 123 | + 'Comparison method %s::%s() does not declare exactly one parameter.', |
| 124 | + \get_class( $actual ), |
| 125 | + $method |
| 126 | + ) |
| 127 | + ); |
| 128 | + } |
| 129 | + |
| 130 | + $noDeclaredTypeError = \sprintf( |
| 131 | + 'Parameter of comparison method %s::%s() does not have a declared type.', |
| 132 | + \get_class( $actual ), |
| 133 | + $method |
| 134 | + ); |
| 135 | + |
| 136 | + $notAcceptableTypeError = \sprintf( |
| 137 | + '%s is not an accepted argument type for comparison method %s::%s().', |
| 138 | + \get_class( $expected ), |
| 139 | + \get_class( $actual ), |
| 140 | + $method |
| 141 | + ); |
| 142 | + |
| 143 | + $reflParameter = $reflMethod->getParameters()[0]; |
| 144 | + |
| 145 | + if ( \method_exists( $reflParameter, 'hasType' ) ) { |
| 146 | + // PHP >= 7.0. |
| 147 | + $hasType = $reflParameter->hasType(); |
| 148 | + if ( $hasType === false ) { |
| 149 | + throw new InvalidComparisonMethodException( $noDeclaredTypeError ); |
| 150 | + } |
| 151 | + |
| 152 | + $type = $reflParameter->getType(); |
| 153 | + if ( \class_exists( 'ReflectionNamedType' ) ) { |
| 154 | + // PHP >= 7.1. |
| 155 | + if ( ( $type instanceof ReflectionNamedType ) === false ) { |
| 156 | + throw new InvalidComparisonMethodException( $noDeclaredTypeError ); |
| 157 | + } |
| 158 | + |
| 159 | + $typeName = $type->getName(); |
| 160 | + } |
| 161 | + else { |
| 162 | + /* |
| 163 | + * PHP 7.0. |
| 164 | + * Checking for `ReflectionType` will not throw an error on union types, |
| 165 | + * but then again union types are not supported on PHP 7.0. |
| 166 | + */ |
| 167 | + if ( ( $type instanceof ReflectionType ) === false ) { |
| 168 | + throw new InvalidComparisonMethodException( $noDeclaredTypeError ); |
| 169 | + } |
| 170 | + |
| 171 | + $typeName = (string) $type; |
| 172 | + } |
| 173 | + } |
| 174 | + else { |
| 175 | + // PHP < 7.0. |
| 176 | + try { |
| 177 | + /* |
| 178 | + * Using `ReflectionParameter::getClass()` will trigger an autoload of the class, |
| 179 | + * but that's okay as for a valid class type that would be triggered on the |
| 180 | + * function call to the $method (at the end of this assertion) anyway. |
| 181 | + */ |
| 182 | + $hasType = $reflParameter->getClass(); |
| 183 | + } catch ( ReflectionException $e ) { |
| 184 | + // Class with a type declaration for a non-existent class. |
| 185 | + throw new InvalidComparisonMethodException( $notAcceptableTypeError ); |
| 186 | + } |
| 187 | + |
| 188 | + if ( ( $hasType instanceof ReflectionClass ) === false ) { |
| 189 | + // Array or callable type. |
| 190 | + throw new InvalidComparisonMethodException( $noDeclaredTypeError ); |
| 191 | + } |
| 192 | + |
| 193 | + $typeName = $hasType->name; |
| 194 | + } |
| 195 | + |
| 196 | + /* |
| 197 | + * Validate that the $expected object complies with the declared parameter type. |
| 198 | + */ |
| 199 | + if ( $typeName === 'self' ) { |
| 200 | + $typeName = \get_class( $actual ); |
| 201 | + } |
| 202 | + |
| 203 | + if ( ( $expected instanceof $typeName ) === false ) { |
| 204 | + throw new InvalidComparisonMethodException( $notAcceptableTypeError ); |
| 205 | + } |
| 206 | + |
| 207 | + /* |
| 208 | + * Execute the comparator method. |
| 209 | + */ |
| 210 | + $result = $actual->{$method}( $expected ); |
| 211 | + |
| 212 | + if ( \is_bool( $result ) === false ) { |
| 213 | + throw new InvalidComparisonMethodException( |
| 214 | + \sprintf( |
| 215 | + '%s::%s() does not return a boolean value.', |
| 216 | + \get_class( $actual ), |
| 217 | + $method |
| 218 | + ) |
| 219 | + ); |
| 220 | + } |
| 221 | + |
| 222 | + $msg = \sprintf( |
| 223 | + 'Failed asserting that two objects are equal. The objects are not equal according to %s::%s()', |
| 224 | + \get_class( $actual ), |
| 225 | + $method |
| 226 | + ); |
| 227 | + |
| 228 | + if ( $message !== '' ) { |
| 229 | + $msg = $message . \PHP_EOL . $msg; |
| 230 | + } |
| 231 | + |
| 232 | + static::assertTrue( $result, $msg ); |
| 233 | + } |
| 234 | +} |
0 commit comments