diff --git a/src/Framework/Assert.php b/src/Framework/Assert.php index 718687df74f..c7d85e5f928 100644 --- a/src/Framework/Assert.php +++ b/src/Framework/Assert.php @@ -51,6 +51,7 @@ use PHPUnit\Framework\Constraint\LogicalOr; use PHPUnit\Framework\Constraint\LogicalXor; use PHPUnit\Framework\Constraint\ObjectEquals; +use PHPUnit\Framework\Constraint\ObjectHasProperty; use PHPUnit\Framework\Constraint\RegularExpression; use PHPUnit\Framework\Constraint\SameSize; use PHPUnit\Framework\Constraint\StringContains; @@ -61,7 +62,6 @@ use PHPUnit\Framework\Constraint\TraversableContainsEqual; use PHPUnit\Framework\Constraint\TraversableContainsIdentical; use PHPUnit\Framework\Constraint\TraversableContainsOnly; -use PHPUnit\Util\Type; use PHPUnit\Util\Xml\Loader as XmlLoader; use PHPUnit\Util\Xml\XmlException; @@ -934,6 +934,36 @@ final public static function assertNan(mixed $actual, string $message = ''): voi static::assertThat($actual, static::isNan(), $message); } + /** + * Asserts that an object has a specified property. + * + * @throws ExpectationFailedException + */ + final public static function assertObjectHasProperty(string $propertyName, object $object, string $message = ''): void + { + static::assertThat( + $object, + new ObjectHasProperty($propertyName), + $message + ); + } + + /** + * Asserts that an object does not have a specified property. + * + * @throws ExpectationFailedException + */ + final public static function assertObjectNotHasProperty(string $propertyName, object $object, string $message = ''): void + { + static::assertThat( + $object, + new LogicalNot( + new ObjectHasProperty($propertyName) + ), + $message + ); + } + /** * Asserts that two variables have the same type and value. * Used on objects, it asserts that two variables reference diff --git a/src/Framework/Assert/Functions.php b/src/Framework/Assert/Functions.php index 03dd989f34b..5abdd2231e6 100644 --- a/src/Framework/Assert/Functions.php +++ b/src/Framework/Assert/Functions.php @@ -1126,6 +1126,38 @@ function assertNan(mixed $actual, string $message = ''): void } } +if (!function_exists('PHPUnit\Framework\assertObjectHasProperty')) { + /** + * Asserts that an object has a specified property. + * + * @throws ExpectationFailedException + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * + * @see Assert::assertObjectHasProperty() + */ + function assertObjectHasProperty(string $attributeName, object $objet, string $message = ''): void + { + Assert::assertObjectHasProperty(...func_get_args()); + } +} + +if (!function_exists('PHPUnit\Framework\assertObjectNotHasProperty')) { + /** + * Asserts that an object does not have a specified property. + * + * @throws ExpectationFailedException + * + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + * + * @see Assert::assertObjectNotHasProperty() + */ + function assertObjectNotHasProperty(string $attributeName, object $objet, string $message = ''): void + { + Assert::assertObjectNotHasProperty(...func_get_args()); + } +} + if (!function_exists('PHPUnit\Framework\assertSame')) { /** * Asserts that two variables have the same type and value. diff --git a/src/Framework/Constraint/Object/ObjectHasProperty.php b/src/Framework/Constraint/Object/ObjectHasProperty.php new file mode 100644 index 00000000000..6d09a8fcccc --- /dev/null +++ b/src/Framework/Constraint/Object/ObjectHasProperty.php @@ -0,0 +1,76 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework\Constraint; + +use function is_object; +use function sprintf; +use ReflectionException; +use ReflectionObject; + +/** + * @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit + */ +final class ObjectHasProperty extends Constraint +{ + private readonly string $propertyName; + + public function __construct(string $propertyName) + { + $this->propertyName = $propertyName; + } + + /** + * Returns a string representation of the constraint. + */ + public function toString(): string + { + return sprintf( + 'has property "%s"', + $this->propertyName + ); + } + + /** + * Evaluates the constraint for parameter $other. Returns true if the + * constraint is met, false otherwise. + * + * @param mixed $other value or object to evaluate + */ + protected function matches(mixed $other): bool + { + if (!is_object($other)) { + return false; + } + + try { + return (new ReflectionObject($other))->hasProperty($this->propertyName); + } catch (ReflectionException) { + return false; + } + } + + /** + * Returns the description of the failure. + * + * The beginning of failure messages is "Failed asserting that" in most + * cases. This method should return the second part of that sentence. + * + * @param mixed $other evaluated value or object + */ + protected function failureDescription(mixed $other): string + { + return sprintf( + '%sclass "%s" %s', + is_object($other) ? 'object of ' : '', + is_object($other) ? $other::class : $other, + $this->toString() + ); + } +} diff --git a/tests/unit/Framework/Constraint/ObjectHasPropertyTest.php b/tests/unit/Framework/Constraint/ObjectHasPropertyTest.php new file mode 100644 index 00000000000..955389bd25c --- /dev/null +++ b/tests/unit/Framework/Constraint/ObjectHasPropertyTest.php @@ -0,0 +1,70 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ +namespace PHPUnit\Framework\Constraint; + +use PHPUnit\Framework\ExpectationFailedException; +use PHPUnit\TestFixture\ClassWithNonPublicAttributes; +use stdClass; + +/** + * @small + */ +final class ObjectHasPropertyTest extends ConstraintTestCase +{ + public function testConstraintObjectHasAttribute(): void + { + $constraint = new ObjectHasProperty('privateAttribute'); + + $this->assertTrue($constraint->evaluate(new ClassWithNonPublicAttributes, '', true)); + $this->assertFalse($constraint->evaluate(new stdClass, '', true)); + $this->assertEquals('has property "privateAttribute"', $constraint->toString()); + $this->assertCount(1, $constraint); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage( + <<<'EOF' + Failed asserting that object of class "stdClass" has property "privateAttribute". + EOF + ); + $constraint->evaluate(new stdClass); + } + + public function testAssertObjectHasAttribute(): void + { + $this->assertObjectNotHasProperty('privateAttribute', new stdClass); + $this->assertObjectHasProperty('foo', new ClassWithNonPublicAttributes); + } + + public function testConstraintObjectHasAttribute2(): void + { + $constraint = new ObjectHasProperty('privateAttribute'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage( + <<evaluate(new stdClass, 'custom message'); + } + + public function testConstraintObjectHasAttributeWithNoObject(): void + { + $constraint = new ObjectHasProperty('privateAttribute'); + + $this->expectException(ExpectationFailedException::class); + $this->expectExceptionMessage( + <<evaluate('', 'custom message'); + } +}