diff --git a/src/Policy/MapResolver.php b/src/Policy/MapResolver.php index 170ed71..24b7d40 100644 --- a/src/Policy/MapResolver.php +++ b/src/Policy/MapResolver.php @@ -89,20 +89,29 @@ public function map(string $resourceClass, callable|object|string $policy) /** * {@inheritDoc} * - * @throws \InvalidArgumentException When a resource is not an object. + * Accepts either an object instance or a class FQCN string registered in + * the map. Strings that are not valid class names continue to raise + * InvalidArgumentException. + * + * @throws \InvalidArgumentException When a resource is neither an object nor a class FQCN string. * @throws \Authorization\Policy\Exception\MissingPolicyException When a policy for a resource has not been defined. */ public function getPolicy($resource): mixed { - if (!is_object($resource)) { - $message = sprintf('Resource must be an object, `%s` given.', gettype($resource)); + if (is_object($resource)) { + $class = $resource::class; + } elseif (is_string($resource) && class_exists($resource)) { + $class = $resource; + } else { + $message = sprintf( + 'Resource must be an object or class FQCN string, `%s` given.', + is_string($resource) ? $resource : gettype($resource), + ); throw new InvalidArgumentException($message); } - $class = $resource::class; - if (!isset($this->map[$class])) { - throw new MissingPolicyException($resource); + throw new MissingPolicyException(is_object($resource) ? $resource : [$class]); } $policy = $this->map[$class]; diff --git a/src/Policy/OrmResolver.php b/src/Policy/OrmResolver.php index d52aedd..3e2f615 100644 --- a/src/Policy/OrmResolver.php +++ b/src/Policy/OrmResolver.php @@ -65,7 +65,11 @@ public function __construct( } /** - * Get a policy for an ORM Table, Entity or Query. + * Get a policy for an ORM Table, Entity, Query or class name string. + * + * Accepting an entity/table FQCN string as the resource allows checks + * like `$user->can('add', Article::class)` where no instance is on hand + * (e.g. menu rendering before a `newEmptyEntity()`). * * @param mixed $resource The resource. * @return mixed @@ -88,10 +92,39 @@ public function getPolicy(mixed $resource): mixed return $this->getRepositoryPolicy($repo); } + if (is_string($resource) && class_exists($resource)) { + return $this->getPolicyByClassName($resource); + } throw new MissingPolicyException([get_debug_type($resource)]); } + /** + * Locate a policy from a class name string by matching the standard + * entity/table namespace markers. + * + * @param string $class The fully qualified class name. + * @return mixed + * @throws \Authorization\Policy\Exception\MissingPolicyException When the + * string does not match an entity/table namespace pattern or no policy + * exists at the conventional location. + */ + protected function getPolicyByClassName(string $class): mixed + { + foreach (['\Model\Entity\\', '\Model\Table\\'] as $marker) { + $pos = strpos($class, $marker); + if ($pos === false) { + continue; + } + $namespace = str_replace('\\', '/', substr($class, 0, $pos)); + $name = str_replace('\\', '/', substr($class, $pos + strlen($marker))); + + return $this->findPolicy($class, $name, $namespace); + } + + throw new MissingPolicyException([$class]); + } + /** * Get a policy for an entity * diff --git a/tests/TestCase/Policy/MapResolverTest.php b/tests/TestCase/Policy/MapResolverTest.php index abbf2f5..39668b2 100644 --- a/tests/TestCase/Policy/MapResolverTest.php +++ b/tests/TestCase/Policy/MapResolverTest.php @@ -92,11 +92,30 @@ public function testGetPolicyPrimitive(): void $resolver = new MapResolver(); $this->expectException(InvalidArgumentException::class); - $this->expectExceptionMessage('Resource must be an object, `string` given.'); + $this->expectExceptionMessage('Resource must be an object or class FQCN string, `Foo` given.'); $resolver->getPolicy('Foo'); } + public function testGetPolicyClassNameAsResource(): void + { + $resolver = new MapResolver(); + $resolver->map(Article::class, ArticlePolicy::class); + + $result = $resolver->getPolicy(Article::class); + $this->assertInstanceOf(ArticlePolicy::class, $result); + } + + public function testGetPolicyUnregisteredClassString(): void + { + $resolver = new MapResolver(); + + $this->expectException(MissingPolicyException::class); + $this->expectExceptionMessage('Policy for `TestApp\Model\Entity\Article` has not been defined.'); + + $resolver->getPolicy(Article::class); + } + public function testGetPolicyMissing(): void { $resolver = new MapResolver(); diff --git a/tests/TestCase/Policy/OrmResolverTest.php b/tests/TestCase/Policy/OrmResolverTest.php index b8d19c7..617ca64 100644 --- a/tests/TestCase/Policy/OrmResolverTest.php +++ b/tests/TestCase/Policy/OrmResolverTest.php @@ -28,6 +28,7 @@ use OverridePlugin\Policy\TagPolicy as OverrideTagPolicy; use stdClass; use TestApp\Model\Entity\Article; +use TestApp\Model\Table\ArticlesTable; use TestApp\Policy\ArticlePolicy; use TestApp\Policy\ArticlesTablePolicy; use TestApp\Policy\TestPlugin\BookmarkPolicy; @@ -125,6 +126,36 @@ public function testGetPolicyUnknownTable(): void $resolver->getPolicy($articles); } + public function testGetPolicyFromEntityClassString(): void + { + $resolver = new OrmResolver('TestApp'); + $policy = $resolver->getPolicy(Article::class); + $this->assertInstanceOf(ArticlePolicy::class, $policy); + } + + public function testGetPolicyFromTableClassString(): void + { + $resolver = new OrmResolver('TestApp'); + $policy = $resolver->getPolicy(ArticlesTable::class); + $this->assertInstanceOf(ArticlesTablePolicy::class, $policy); + } + + public function testGetPolicyFromUnrelatedClassString(): void + { + $resolver = new OrmResolver('TestApp'); + + $this->expectException(MissingPolicyException::class); + $resolver->getPolicy(TestService::class); + } + + public function testGetPolicyFromNonClassString(): void + { + $resolver = new OrmResolver('TestApp'); + + $this->expectException(MissingPolicyException::class); + $resolver->getPolicy('NotAClassName'); + } + public function testGetPolicyViaDIC(): void { $container = new Container();