Problem/Motivation
Continuing the trend (1, 2, 3, 4) of adding more service locators in core. This issue resolves to convert user permission callbacks as defined in *.permissions.yml files to anonymous services, bundled together in a service locator, injected to a dedicated locator service, which is then used by PermissionHandler to build a list of available permissions.
The result, will be that devs can create permission providers in the same manner as existing, but with proper (tagged) services, not a bespoke definition in a YAML file. In turn, since these become real services, we'll be able to use Attributes and Autoconfig[er/u]ration.
The ultimate goal (out of scope) would be to simply create a file in a module, add an attribute to a method, without the need for a services.yml entry. However we are a couple of steps away from that. So here are the building blocks for permissions as tagged services.
Future
After this issue, we can look to implement a namespace + attribute so we dont need to manually define permission providers in either a services.yml/permissions.yml. It'll get discovered automatically, for example:
namespace Drupal\my_module\PermissionProvider;
class MyPermissionProvider {
#[AsPermissionProvider]
public function permissions(): array {}
}
#3422359: Directory based automatic service creation can be used to realize this feature.
Proposed resolution
- Allow permission providers to be defined as tagged services.
- Promote existing permission providers from permissions.yml files to services
- Implement a service locator for the tagged services.
- Utilise the service locator in
PermissionHandler
Remaining tasks
Review, Merge.
User interface changes
None.
API changes
- Existing permission providers are converted to anonymous services (unusable outside of the locator)
- Arguments for
user.permissionsmodified. - Deprecation message for extenders of
\Drupal\user\PermissionHandler - New service locator service, accessible by autowiring.
Data model changes
Nil.
Release notes snippet
Permission providers may now be implemented as tagged services instead of callbacks in module.permissions.yml files.
Change record: https://www.drupal.org/node/3421580
| Comment | File | Size | Author |
|---|---|---|---|
| #17 | 3421573-nr-bot.txt | 90 bytes | needs-review-queue-bot |
| #13 | 3421573-nr-bot.txt | 90 bytes | needs-review-queue-bot |
Issue fork drupal-3421573
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
dpiImplementation (MR) notes
The implementation here follows one I am familiar in with Messenger; classes are converted to anonymous tagged services, and grouped together with SendersLocator, which is then injected elsewhere.
The mapping between a method and a service is stored in a mapping array, and passed to the locator service so it knows which method to invoke on the service in locator. This mapping strategy is traditionally the way of storing in the locator which method is associated with a method tagged with a PHP attribute.
Services are created using the anonymous ID with hash + random strategy.
YAML discovery in
PermissionHandleris still used for statically defined permissions. permission_callbacks is now completely ignored.PermissionHandlerhas been switched to use autowiring. I think we can modify the services.yml definition at will, while the class constructor itself is able to handle deprecations (?)Extensive use of PHPStan array shapes to improve static analysis of arrays.
Each way of implementing a permission provider, has been added to the new user_permission_provider_test.module.
The existing way of working exists, including compatibility with
ControllerResolver/ContainerInjectionInterface.If there is an existing service with the same ID (via autowiring) as the class, it will be used instead of creating a new service.
The
PermissionHandlerunit test (PermissionHandlerTest) has a few YAML mocks removed, in favor of the new Locator with mock data. There is still a YAML test case there for necessary coverage. This unit test file has also been cleaned up a bit with redundant property assigns removed.The permission provider locator (
PermissionProvidersLocator) intentionally does not implement an interface, and the service is private. I don't see a need for it, and it could be added (and un-privatized) at a later date.Comment #4
dpiCreated a change record: https://www.drupal.org/project/drupal/issues/3421573
Comment #5
dpiUpdated MR with feedback.
Comment #6
kim.pepperThis issue might conflict #2339487: Static cache permissions
Comment #7
dpiHappy to deal with #2339487 whether it or this issue gets in before. Fortunately theres no dependency/blocker between these issues.
I've just added a review to that issues MR @ !6635.
Comment #8
dpi#3422359: Directory based automatic service creation will fulfill the future possibilities for automatic permission provider registration.
Comment #9
kristiaanvandeneyndeOkay so after skimming this:
All in all I really appreciate you working on this as I fully agree what we currently have isn't modern at all. Listing callbacks inside yaml files feels a bit outdated.
Comment #10
dpiThanks for the feedback, I'll tweak the locator a little and deprecate callbacks in YAML.
Comment #11
dpiI've refactored the permission locator slightly, per Kristiaan's feedback.
I've also deprecated permission_callbacks in YAML as suggested by Kristiaan. This change increases the LOC's significantly, but removes quite a lot of redundant lines in permission classes. Overall a win.
Changelog has been updated with deprecation wording, plus notes on deprecation scope — https://www.drupal.org/node/3421580
Tests back to green.
Comment #12
dpiComment #13
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #14
dpiFixed conflicts after #2926070: Deprecate ModuleHandlerInterface::getName().
Still needs review.
Comment #15
longwaveWhy do we need a service locator here? We always need all tagged services, we don't need to refer to them individually for any reason, we just loop over the whole set - the service collector pattern seems to fit better? We could just add each permissions provider directly to the PermissionHandler.
Also, we shouldn't need to specify the "provider" tag manually, that should somehow be automatically inferred.
I am also still weak -1 on having to specify the method, it feels like permissions services should just have a fixed interface; most modules will have 0 or 1 of these services, so I don't see why they all have to allow individual method names to be configured. And if we had a fixed interface we could autoconfigure it so the tag doesn't even need to be specified in services.yml.
Comment #16
andypostIs there a way to populate
providerat discovery level instead of duplicating the module name?Comment #17
needs-review-queue-bot commentedThe Needs Review Queue Bot tested this issue. It no longer applies to Drupal core. Therefore, this issue status is now "Needs work".
This does not mean that the patch necessarily needs to be re-rolled or the MR rebased. Read the Issue Summary, the issue tags and the latest discussion here to determine what needs to be done.
Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.
Comment #18
usmanjutt84Updated the "Change record" URL.
Comment #19
nicxvan commentedI unpublished the change record, pretty sure it's not ready.
Comment #20
usmanjutt84I agree. Thank you @nicxvan.
I appreciate the idea of generating dynamic permissions using services and tags, rather than relying on permission callbacks. The current implementation in the GraphQL 4 module utilizes the service name as a permission callback, which leads to errors on installin the module via drush because the service is not be registered at the time permissions are being generated. By adopting a service-based approach, this issue would be resolved in future releases, ensuring smoother functionality and enhanced reliability. See the issue service class does not exist on installing.
Comment #21
kristiaanvandeneyndeI just read @longwave's comment in #15 and I fully agree we don't need a locator here. A simple service collector (tagged services) would be better here. Repeating what he said:
Furthermore I agree with the following:
If a module for some reason needs multiple ways to provide permissions, it should simply provide multiple services; not multiple methods on a single service. Heavy +1 for a simple interface.
Comment #22
joachim commentedI think this is a change in the wrong direction. Declaring dynamic service provider classes in the same location as the fixed services is good DX.
This is a pattern we should be extending -- #2910814: Deprecate magic ServiceProvider file discovery -- not removing.
Comment #23
kristiaanvandeneynde@joachim Are you alluding to the IS (which might need updating)? My last comment basically steers the MR towards tagged services, which are declared in the services.yml file. And that seems to be exactly what you want to see more of, or am I misreading this?
Comment #24
joachim commentedArgh I mistyped, sorry. I meant to say:
Declaring dynamic permissions provider classes in the same location as the fixed permissions is good DX.
So in other words, I think a dynamic service provider class should be declared in services.yml, and a dynamic permissions provider class should be remain as being declared in permissions.yml.
If you're looking for services or permissions, those files are where you go to look.
Comment #25
kristiaanvandeneyndeNormally I would agree, but as the IS states we're specifically looking to turning these providers into services so that we can benefit from everything the container has to offer us. If we were to keep them inside the permissions.yml file, we are severely limited as to what we can do.
One could even argue the permissions.yml file is another "magic file in a magic place" like the MyModuleServiceProvider class. In my opinion, the more callable code we can put into the container, the less custom code we have to write for these types of discoveries. The current controller approach means we need to inject the container to fetch said controller's dependencies, an approach that is not encouraged by Symfony (see removal of ContainerAwareTrait).
The way I see it:
Upside of this approach is that you can now turn off a service you don't want permissions from or decorate it to alter the result. With the current approach that's simply not possible.
Comment #29
stborchertExperimented a bit using an attribute to define a permission provider (see TaxonomyPermissions.php).
It works, seems easy to use, ....
I didn't remove the option to use
permission_callbacksin the MR yet but it's easy to deprecate that here.Settings to "Needs review" to get some thoughts about MR 15644.
Comment #30
joachim commentedDoes the service need to be in a particular sub-namespace?
Comment #31
stborchertNo, see https://git.drupalcode.org/project/drupal/-/merge_requests/15644/diffs#f.... All classes using the attribute are tagged with
user.permission_providerwithout the need of a specific sub-namespace.Comment #32
tolstoydotcomPlease also consider my proposal to add sections and extended help to permissions files:
https://www.drupal.org/project/drupal/issues/3495351
E.g., there could be:
The nomenclature would need to be worked on.
Comment #33
joachim commentedI think that just as EventSubscribers go in a standard namespace, we should do the same here.
It's an opportunity to establish convention before we need to worry about BC.
Having things in standard locations improves DX.
Comment #34
longwaveI still think we should introduce an interface contract here, what is the use case for having a permission provider service with two tagged methods? Seems better to have a fixed method name (as we do elsewhere), we don't even need the attribute, we can use autoconfiguration to tag and collect PermissionProviderInterface (or whatever name we choose) services...
Comment #35
stborchert> what is the use case for having a permission provider service with two tagged methods
That's not what the MR does. The MR adds the attribute to the class.
Comment #36
longwaveI meant that TestPermissionProvider has two methods,
__invoke()andgetPermissions(), both of which can provide permissions, what's the use case here? Why do we need attributes at all?Comment #37
stborchertAh sorry.
Using the attribute without any parameter will call
__invoke(). By settings the parameter you are able to change that to a method.The test simply shows both ways.
Comment #39
longwaveImplemented #34 in MR!15845 - no attribute needed, just a new PermissionProviderInterface interface with a single method. Services that implement this method are collected into a service iterator which is injected into PermissionHandler.
Comment #40
godotislateI prefer @longwave's approach in MR 15845. The IS mentions attributes a future state about autodiscovery, but that would be a future state, and I think using autoconfiguration only for now makes sense.
Until we can do general autodiscovery via #3422359: Directory based automatic service creation, I think we should be judicious about adding autodiscovery for specific things, because there is always the problem of reflecting classes with missing optional dependencies.
Had a few comments on the MR.
Comment #41
nicxvan commentedIs there a consideration for when these are collected and compiled?
@tolstoydotcom, I think that's a great idea, but likely a follow up issue.
Comment #42
longwaveThe collection point hasn't changed, I think moving that here is out of scope.
Addressed MR feedback, back to NR.
Comment #43
joachim commentedCan we restrict these to a subfolder for consistency and DX.
Comment #44
longwaveDo you mean restricting the attribute version? My suggestion just uses services, we shouldn't restrict those.
Comment #45
joachim commentedI mean making permission providers go in a specific subfolder, for better DX.
We do that already with EventSubscriber don't we? And hooks are services and they go in a specific place.
Comment #46
longwaveIt's not strictly enforced, but I guess we could - but if we move the class do we also need to provide BC? Not sure there is a use case for calling permission providers separately, or extending them, but maybe someone's doing it somewhere?
Having said that given that each module is likely to have zero or one permission provider services, what's really wrong with
Drupal\module\ModulePermissions?Comment #47
godotislateAdded a suggestion to the MR that hopefully covers all the deprecation test failures.
Calling permission providers separately also seems unlikely to me, but there may be a use case of extending a permission provider if someone wanted to replace the provider with their own class that extends the core class. It occurs to me that with the class being changed to a service, a subclass could break anyway if it is expecting to implement ContainerInjectionInterface and
::create().Maybe the service should be a new class, and the old class deprecated as a whole?Comment #48
longwaveHow would someone override a permission provider before though? There is no mechanism to alter the YAML inside
PermissionHandler::buildPermissionsYaml(). In theory you could extend an existing provider and register it separately, but in practice would anyone actually have done that?Comment #49
godotislateYeah, you're right. There's no way to alter the definitions from YAML, and at a glance of existing providers, there's very little there for a module to extend in order to register its own provider.
I also think
Drupal\module\ModulePermissionsis fine as convention.Comment #50
joachim commented> I also think Drupal\module\ModulePermissions is fine as convention.
I'd be okay with that.
We should then revive #2910814: Deprecate magic ServiceProvider file discovery, as we could make that follow the same pattern be Drupal\module\ServiceProvider.