<?php namespace Yoast\PHPUnitPolyfills\Polyfills; use ReflectionClass; use ReflectionException; use ReflectionNamedType; use ReflectionObject; use ReflectionType; use TypeError; use Yoast\PHPUnitPolyfills\Exceptions\InvalidComparisonMethodException; /** * Polyfill the Assert::assertObjectEquals() methods. * * Introduced in PHPUnit 9.4.0. * * The polyfill implementation closely matches the PHPUnit native implementation with the exception * of the return type check and the names of the thrown exceptions. * * @link https://github.com/sebastianbergmann/phpunit/issues/4467 * @link https://github.com/sebastianbergmann/phpunit/issues/4707 * @link https://github.com/sebastianbergmann/phpunit/commit/1dba8c3a4b2dd04a3ff1869f75daaeb6757a14ee * @link https://github.com/sebastianbergmann/phpunit/commit/6099c5eefccfda860c889f575d58b5fe6cc10c83 */ trait AssertObjectEquals { /** * Asserts that two objects are considered equal based on a custom object comparison * using a comparator method in the target object. * * The custom comparator method is expected to have the following method * signature: `equals(self $other): bool` (or similar with a different method name). * * Basically, the assertion checks the following: * - A method with name $method must exist on the $actual object. * - The method must accept exactly one argument and this argument must be required. * - This parameter must have a classname-based declared type. * - The $expected object must be compatible with this declared type. * - The method must have a declared bool return type. (JRF: not verified in this implementation) * - `$actual->$method($expected)` returns boolean true. * * @param object $expected Expected value. * @param object $actual The value to test. * @param string $method The name of the comparator method within the object. * @param string $message Optional failure message to display. * * @return void * * @throws TypeError When any of the passed arguments do not meet the required type. * @throws InvalidComparisonMethodException When the comparator method does not comply with the requirements. */ final public static function assertObjectEquals( $expected, $actual, $method = 'equals', $message = '' ) { /* * Parameter input validation. * In PHPUnit this is done via PHP native type declarations. Emulating this for the polyfill. */ if ( \is_object( $expected ) === false ) { throw new TypeError( \sprintf( 'Argument 1 passed to assertObjectEquals() must be an object, %s given', \gettype( $expected ) ) ); } if ( \is_object( $actual ) === false ) { throw new TypeError( \sprintf( 'Argument 2 passed to assertObjectEquals() must be an object, %s given', \gettype( $actual ) ) ); } if ( \is_scalar( $method ) === false ) { throw new TypeError( \sprintf( 'Argument 3 passed to assertObjectEquals() must be of the type string, %s given', \gettype( $method ) ) ); } else { $method = (string) $method; } /* * Comparator method validation. */ $reflObject = new ReflectionObject( $actual ); if ( $reflObject->hasMethod( $method ) === false ) { throw new InvalidComparisonMethodException( \sprintf( 'Comparison method %s::%s() does not exist.', \get_class( $actual ), $method ) ); } $reflMethod = $reflObject->getMethod( $method ); /* * As the next step, PHPUnit natively would validate the return type, * but as return type declarations is a PHP 7.0+ feature, the polyfill * skips this check in favour of checking the type of the actual * returned value. * * Also see the upstream discussion about this: * {@link https://github.com/sebastianbergmann/phpunit/issues/4707} */ /* * Comparator method parameter requirements validation. */ if ( $reflMethod->getNumberOfParameters() !== 1 || $reflMethod->getNumberOfRequiredParameters() !== 1 ) { throw new InvalidComparisonMethodException( \sprintf( 'Comparison method %s::%s() does not declare exactly one parameter.', \get_class( $actual ), $method ) ); } $noDeclaredTypeError = \sprintf( 'Parameter of comparison method %s::%s() does not have a declared type.', \get_class( $actual ), $method ); $notAcceptableTypeError = \sprintf( '%s is not an accepted argument type for comparison method %s::%s().', \get_class( $expected ), \get_class( $actual ), $method ); $reflParameter = $reflMethod->getParameters()[0]; if ( \method_exists( $reflParameter, 'hasType' ) ) { // PHP >= 7.0. $hasType = $reflParameter->hasType(); if ( $hasType === false ) { throw new InvalidComparisonMethodException( $noDeclaredTypeError ); } $type = $reflParameter->getType(); if ( \class_exists( 'ReflectionNamedType' ) ) { // PHP >= 7.1. if ( ( $type instanceof ReflectionNamedType ) === false ) { throw new InvalidComparisonMethodException( $noDeclaredTypeError ); } $typeName = $type->getName(); } else { /* * PHP 7.0. * Checking for `ReflectionType` will not throw an error on union types, * but then again union types are not supported on PHP 7.0. */ if ( ( $type instanceof ReflectionType ) === false ) { throw new InvalidComparisonMethodException( $noDeclaredTypeError ); } $typeName = (string) $type; } } else { // PHP < 7.0. try { /* * Using `ReflectionParameter::getClass()` will trigger an autoload of the class, * but that's okay as for a valid class type that would be triggered on the * function call to the $method (at the end of this assertion) anyway. */ $hasType = $reflParameter->getClass(); } catch ( ReflectionException $e ) { // Class with a type declaration for a non-existent class. throw new InvalidComparisonMethodException( $notAcceptableTypeError ); } if ( ( $hasType instanceof ReflectionClass ) === false ) { // Array or callable type. throw new InvalidComparisonMethodException( $noDeclaredTypeError ); } $typeName = $hasType->name; } /* * Validate that the $expected object complies with the declared parameter type. */ if ( $typeName === 'self' ) { $typeName = \get_class( $actual ); } if ( ( $expected instanceof $typeName ) === false ) { throw new InvalidComparisonMethodException( $notAcceptableTypeError ); } /* * Execute the comparator method. */ $result = $actual->{$method}( $expected ); if ( \is_bool( $result ) === false ) { throw new InvalidComparisonMethodException( \sprintf( 'Comparison method %s::%s() does not return a boolean value.', \get_class( $actual ), $method ) ); } $msg = \sprintf( 'Failed asserting that two objects are equal. The objects are not equal according to %s::%s()', \get_class( $actual ), $method ); if ( $message !== '' ) { $msg = $message . \PHP_EOL . $msg; } static::assertTrue( $result, $msg ); } }