Allow class FQCN strings as authorization resources (draft, refs #135)#332
Allow class FQCN strings as authorization resources (draft, refs #135)#332dereuromark wants to merge 7 commits into
Conversation
…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.
| 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))); |
There was a problem hiding this comment.
Do we really want to be doing string searches? Can't we use the interfaces instead? What do we gain from substring matching?
There was a problem hiding this comment.
Two different jobs, conflated:
- Discrimination — "is this an entity or a table?" New code decides via strpos($class, '\Model\Entity') vs '\Model\Table'.
- Extraction — pull namespace + name segments to feed findPolicy().
The # 1:
Should probably use interfaces ideally
For # 2:
getEntityPolicy()/getRepositoryPolicy() already substring-search \Model\Entity\ to derive namespace+name for findPolicy() — even for instances. That's OrmResolver's existing convention. So interface approach removes substring from the decision, not from the lookup. A non-conventional namespace still fails at findPolicy() either way.
Co-authored-by: Mark Story <mark@mark-story.com>
Co-authored-by: Mark Story <mark@mark-story.com>
…ring Use is_subclass_of() against EntityInterface/RepositoryInterface to decide whether a class-name resource is an entity or a table, instead of substring matching the \Model\Entity\ / \Model\Table\ markers. The string path now folds into the existing getEntityPolicy()/getRepositoryPolicy() methods, so string and instance resolution share one body and getPolicyByClassName() is removed. This rejects classes that merely live under the entity/table namespace but do not implement the interface (covered by the new FakeEntity regression test), and keeps the namespace extraction that findPolicy() already relied on. Note for release: getEntityPolicy()/getRepositoryPolicy() change their protected signature from the object instance to a class-name string. Any subclass overriding these protected methods must update its signature. Minor BC break, release-notes worthy.
|
Applied the interface-based discrimination as suggested: the class-string path now uses Added a The namespace extraction that Minor BC to flag for release notes: |
Closes #135 if accepted.
Why
The recurring use case from #135 is:
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::classto aRequestPolicyand usingRequestAuthorizationMiddleware. 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
OrmResolvergetPolicy()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 existingfindPolicy()lookup — so the resolution is identical to what would happen for an instance of that class:A string that is not a class (
class_exists() === false) is left to fall through to the existingMissingPolicyExceptionpath. A class that does not match either marker also throwsMissingPolicyException— same behavior as a non-resolvable instance.MapResolvergetPolicy()learns to accept a class FQCN string. If the string resolves to an existing class, it is used directly as the map key:A registered FQCN with no policy mapping raises
MissingPolicyException(parity with the object case). A non-class string still raisesInvalidArgumentException, just with an updated message that names the offending string instead of just its type.The pre-existing
testGetPolicyPrimitiveassertion message was updated accordingly — that is the only BC-visible change in the test suite.Discussion points for all maintainers
OrmResolveracceptclass_exists($resource) === false? Current draft requires the class to exist. An alternative is to accept any string that contains the marker and letfindPolicy()either return a policy or throw. Stricter is friendlier IMO but I can flip it.MapResolver— should we widen the type hint ongetPolicy()? The interface signature ismixed, so technically nothing changes; the docblock and message strings now describe the broader contract.MapResolver::map()enforcesclass_exists($resourceClass)so this is moot, but if anyone uses it with classes only autoloaded under certain conditions it could be a footgun.ResolverCollectionalready chains resolvers, so a user with a partial map + ORM resolver gets the natural fallback for free. No changes there.