diff --git a/src/Exception/PropertyNotAccessibleException.php b/src/Exception/PropertyNotAccessibleException.php index 9a62f7c..8080bc6 100644 --- a/src/Exception/PropertyNotAccessibleException.php +++ b/src/Exception/PropertyNotAccessibleException.php @@ -11,9 +11,11 @@ final class PropertyNotAccessibleException extends RuntimeException public function __construct( public readonly string $class, public readonly string $property, + public readonly ?\Throwable $previous = null, ) { parent::__construct( message: \sprintf(self::MESSAGE, $class, $property), + previous: $previous, ); } } diff --git a/src/RouteProvider/PropertyAccess/PurgatoryPropertyAccessor.php b/src/RouteProvider/PropertyAccess/PurgatoryPropertyAccessor.php index e663ab6..39704d9 100644 --- a/src/RouteProvider/PropertyAccess/PurgatoryPropertyAccessor.php +++ b/src/RouteProvider/PropertyAccess/PurgatoryPropertyAccessor.php @@ -4,6 +4,7 @@ namespace Sofascore\PurgatoryBundle\RouteProvider\PropertyAccess; +use Sofascore\PurgatoryBundle\Exception\PropertyNotAccessibleException; use Sofascore\PurgatoryBundle\Exception\ValueNotIterableException; use Symfony\Component\PropertyAccess\Exception\AccessException; use Symfony\Component\PropertyAccess\Exception\UnexpectedTypeException; @@ -25,17 +26,36 @@ public function __construct( /** * @param object|array $objectOrArray * @param string|PropertyPathInterface $propertyPath + * + * @throws PropertyNotAccessibleException + * @throws ValueNotIterableException */ public function getValue($objectOrArray, $propertyPath): mixed { if (!str_contains((string) $propertyPath, self::DELIMITER)) { - return $this->propertyAccessor->getValue($objectOrArray, $propertyPath); + try { + return $this->propertyAccessor->getValue($objectOrArray, $propertyPath); + } catch (\InvalidArgumentException|AccessException|UnexpectedTypeException $exception) { + throw new PropertyNotAccessibleException( + \is_array($objectOrArray) ? 'array' : $objectOrArray::class, + (string) $propertyPath, + $exception, + ); + } } /** @var array{0: string, 1: string} $propertyPathParts */ $propertyPathParts = explode(separator: self::DELIMITER, string: (string) $propertyPath, limit: 2); - $collection = $this->propertyAccessor->getValue($objectOrArray, $propertyPathParts[0]); + try { + $collection = $this->propertyAccessor->getValue($objectOrArray, $propertyPathParts[0]); + } catch (\InvalidArgumentException|AccessException|UnexpectedTypeException $exception) { + throw new PropertyNotAccessibleException( + \is_array($objectOrArray) ? 'array' : $objectOrArray::class, + $propertyPathParts[0], + $exception, + ); + } if (!is_iterable($collection)) { throw new ValueNotIterableException($collection, $propertyPathParts[0]); @@ -60,6 +80,8 @@ public function getValue($objectOrArray, $propertyPath): mixed /** * @param object|array $objectOrArray * @param string|PropertyPathInterface $propertyPath + * + * @param-out object|array $objectOrArray */ public function setValue(&$objectOrArray, $propertyPath, mixed $value): void { @@ -89,7 +111,7 @@ public function isReadable($objectOrArray, $propertyPath): bool $this->getValue($objectOrArray, $propertyPath); return true; - } catch (AccessException|UnexpectedTypeException) { + } catch (PropertyNotAccessibleException) { return false; } } diff --git a/tests/RouteProvider/PropertyAccess/Fixtures/Foo.php b/tests/RouteProvider/PropertyAccess/Fixtures/Foo.php index ad52af1..4ae4722 100644 --- a/tests/RouteProvider/PropertyAccess/Fixtures/Foo.php +++ b/tests/RouteProvider/PropertyAccess/Fixtures/Foo.php @@ -11,6 +11,7 @@ class Foo public function __construct( public readonly int $id, public readonly Collection $children, + private readonly ?string $privateProperty = 'value', ) { } diff --git a/tests/RouteProvider/PropertyAccess/PurgatoryPropertyAccessorTest.php b/tests/RouteProvider/PropertyAccess/PurgatoryPropertyAccessorTest.php index 690dc18..ee7e29f 100644 --- a/tests/RouteProvider/PropertyAccess/PurgatoryPropertyAccessorTest.php +++ b/tests/RouteProvider/PropertyAccess/PurgatoryPropertyAccessorTest.php @@ -8,9 +8,11 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Sofascore\PurgatoryBundle\Exception\PropertyNotAccessibleException; use Sofascore\PurgatoryBundle\Exception\ValueNotIterableException; use Sofascore\PurgatoryBundle\RouteProvider\PropertyAccess\PurgatoryPropertyAccessor; use Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo; +use Symfony\Component\PropertyAccess\Exception\NoSuchPropertyException; use Symfony\Component\PropertyAccess\PropertyAccess; #[CoversClass(PurgatoryPropertyAccessor::class)] @@ -130,6 +132,60 @@ public static function traversableProvider(): iterable ]; } + public function testReadPathPropertyNotExist(): void + { + self::assertFalse($this->purgatoryPropertyAccessor->isReadable( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([]), + ), + propertyPath: 'nonExistentProperty', + )); + } + + public function testReadPathPropertyNotAccessible(): void + { + self::assertFalse($this->purgatoryPropertyAccessor->isReadable( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([]), + ), + propertyPath: 'privateProperty', + )); + } + + public function testReadTraversableChildPropertyNotExist(): void + { + self::assertFalse($this->purgatoryPropertyAccessor->isReadable( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([ + new Foo( + id: 1, + children: new ArrayCollection([]), + ), + ]), + ), + propertyPath: 'children[*].nonExistentProperty', + )); + } + + public function testReadTraversableChildPropertyNotAccessible(): void + { + self::assertFalse($this->purgatoryPropertyAccessor->isReadable( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([ + new Foo( + id: 1, + children: new ArrayCollection([]), + ), + ]), + ), + propertyPath: 'children[*].privateProperty', + )); + } + public function testNotTraversable(): void { $this->expectException(ValueNotIterableException::class); @@ -143,4 +199,138 @@ public function testNotTraversable(): void propertyPath: 'id[*].id', ); } + + public function testPropertyNotAccessible(): void + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionObject(new PropertyNotAccessibleException( + class: Foo::class, + property: 'privateProperty', + previous: new NoSuchPropertyException( + message: 'Can\'t get a way to read the property "privateProperty" in class "Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo".', + ), + )); + + $this->purgatoryPropertyAccessor->getValue( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([]), + ), + propertyPath: 'privateProperty', + ); + } + + public function testTraversablePropertyNotAccessible(): void + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionObject(new PropertyNotAccessibleException( + class: Foo::class, + property: 'privateProperty', + previous: new NoSuchPropertyException( + message: 'Can\'t get a way to read the property "privateProperty" in class "Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo".', + ), + )); + + $this->purgatoryPropertyAccessor->getValue( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([]), + ), + propertyPath: 'privateProperty[*].values', + ); + } + + public function testTraversableChildPropertyNotAccessible(): void + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionObject(new PropertyNotAccessibleException( + class: Foo::class, + property: 'privateProperty', + previous: new NoSuchPropertyException( + message: 'Can\'t get a way to read the property "privateProperty" in class "Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo".', + ), + )); + + $this->purgatoryPropertyAccessor->getValue( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection( + [ + new Foo( + id: 1, + children: new ArrayCollection([]), + ), + ], + ), + ), + propertyPath: 'children[*].privateProperty', + ); + } + + public function testPropertyNotExist(): void + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionObject(new PropertyNotAccessibleException( + class: Foo::class, + property: 'nonExistentProperty', + previous: new NoSuchPropertyException( + message: 'Can\'t get a way to read the property "nonExistentProperty" in class "Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo".', + ), + )); + + $this->purgatoryPropertyAccessor->getValue( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([]), + ), + propertyPath: 'nonExistentProperty', + ); + } + + public function testTraversablePropertyNotExist(): void + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionObject(new PropertyNotAccessibleException( + class: Foo::class, + property: 'nonExistentProperty', + previous: new NoSuchPropertyException( + message: 'Can\'t get a way to read the property "nonExistentProperty" in class "Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo".', + ), + )); + + $this->purgatoryPropertyAccessor->getValue( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection([]), + ), + propertyPath: 'nonExistentProperty[*].values', + ); + } + + public function testTraversableChildPropertyNotExist(): void + { + $this->expectException(PropertyNotAccessibleException::class); + $this->expectExceptionObject(new PropertyNotAccessibleException( + class: Foo::class, + property: 'nonExistentProperty', + previous: new NoSuchPropertyException( + message: 'Can\'t get a way to read the property "nonExistentProperty" in class "Sofascore\PurgatoryBundle\Tests\RouteProvider\PropertyAccess\Fixtures\Foo".', + ), + )); + + $this->purgatoryPropertyAccessor->getValue( + objectOrArray: new Foo( + id: 1, + children: new ArrayCollection( + [ + new Foo( + id: 1, + children: new ArrayCollection([]), + ), + ], + ), + ), + propertyPath: 'children[*].nonExistentProperty', + ); + } }