Skip to content

Allow class FQCN strings as authorization resources (draft, refs #135)#332

Draft
dereuromark wants to merge 2 commits into
3.xfrom
feature-fqcn-resource
Draft

Allow class FQCN strings as authorization resources (draft, refs #135)#332
dereuromark wants to merge 2 commits into
3.xfrom
feature-fqcn-resource

Conversation

@dereuromark
Copy link
Copy Markdown
Member

Draft — proposing an API shape, not asking for merge yet. Closes #135 if accepted.

Why

The recurring use case from #135 is:

if ($this->Authorization->can('add', \App\Model\Entity\Article::class)) {
    echo $this->Html->link('Add new', ['action' => 'add']);
}

i.e. checking permission to create a resource at a moment when no instance exists yet (menu rendering, button visibility, list-view "New" links, before newEmptyEntity()).

Today this requires creating a throwaway entity just to satisfy the resolver, or mapping ServerRequest::class to a RequestPolicy and using RequestAuthorizationMiddleware. Both feel like workarounds.

The 2020 thread on #135 (markstory: "the only limitation … has been a lack of imagination and use cases"; LordSimal in 2023: "this will be possible in the next major version") concluded the feature is wanted but had no champion. Picking it back up.

What

OrmResolver

getPolicy() learns to accept a class FQCN string. If the string contains one of the conventional namespace markers (\Model\Entity\ or \Model\Table\), the namespace and name segments are extracted and routed through the existing findPolicy() lookup — so the resolution is identical to what would happen for an instance of that class:

$user->can('add', \App\Model\Entity\Article::class);     // App\Policy\ArticlePolicy
$user->can('access', \App\Model\Table\ArticlesTable::class); // App\Policy\ArticlesTablePolicy

A string that is not a class (class_exists() === false) is left to fall through to the existing MissingPolicyException path. A class that does not match either marker also throws MissingPolicyException — same behavior as a non-resolvable instance.

MapResolver

getPolicy() learns to accept a class FQCN string. If the string resolves to an existing class, it is used directly as the map key:

$resolver->map(Article::class, ArticlePolicy::class);
$user->can('add', Article::class);  // returns ArticlePolicy

A registered FQCN with no policy mapping raises MissingPolicyException (parity with the object case). A non-class string still raises InvalidArgumentException, just with an updated message that names the offending string instead of just its type.

The pre-existing testGetPolicyPrimitive assertion message was updated accordingly — that is the only BC-visible change in the test suite.

Discussion points for maintainers

  1. Should OrmResolver accept class_exists($resource) === false? Current draft requires the class to exist. An alternative is to accept any string that contains the marker and let findPolicy() either return a policy or throw. Stricter is friendlier IMO but I can flip it.
  2. MapResolver — should we widen the type hint on getPolicy()? The interface signature is mixed, so technically nothing changes; the docblock and message strings now describe the broader contract.
  3. Should we also allow registering policies by FQCN that doesn't yet exist? Right now MapResolver::map() enforces class_exists($resourceClass) so this is moot, but if anyone uses it with classes only autoloaded under certain conditions it could be a footgun.
  4. ResolverCollection already chains resolvers, so a user with a partial map + ORM resolver gets the natural fallback for free. No changes there.
  5. Anything you want me to split (only OrmResolver, only MapResolver, etc.) — happy to.

Verification

  • composer test — 146 tests, 296 assertions (was 140 / 289). 6 new tests covering both resolvers, the missing-policy paths, and the unrelated-class case.
  • composer stan — clean.
  • composer cs-check — clean.

…d MapResolver

Enables the long-requested pattern from issue #135:

    $user->can('add', Article::class);

which is the natural shape for menu/button visibility checks and any
authorization gate that runs before an entity instance is on hand.

OrmResolver: a string matching one of the standard entity/table namespace
markers (\Model\Entity\ or \Model\Table\) is decomposed into namespace +
name and routed through the existing findPolicy() conventions.

MapResolver: a string that resolves to an existing class is treated as the
map key. Non-class strings still raise InvalidArgumentException; valid
class strings without a registered policy raise MissingPolicyException, in
line with the object case.
@dereuromark dereuromark added this to the 3.x milestone May 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Authorization Identity can() method need to allow second parms to optional.

1 participant