Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
PHP 8 allows us to significantly reduce boilerplate in OO classes with injected services.
PHP 7:
/**
* The entity type manager.
*
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
*/
protected $entityTypeManager;
/**
* The entity repository.
*
* @var \Drupal\Core\Entity\EntityRepositoryInterface
*/
protected $entityRepository;
/**
* The configuration factory.
*
* @var \Drupal\Core\Config\ConfigFactoryInterface
*/
protected $configFactory;
/**
* The typed config manager.
*
* @var \Drupal\Core\Config\TypedConfigManagerInterface
*/
protected $typedConfigManager;
/**
* The active configuration storage.
*
* @var \Drupal\Core\Config\StorageInterface
*/
protected $activeStorage;
/**
* The event dispatcher.
*
* @var \Symfony\Contracts\EventDispatcher\EventDispatcherInterface
*/
protected $eventDispatcher;
/**
* The extension path resolver.
*
* @var \Drupal\Core\Extension\ExtensionPathResolver
*/
protected $extensionPathResolver;
/**
* Creates ConfigManager objects.
*
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
* The entity type manager.
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
* The configuration factory.
* @param \Drupal\Core\Config\TypedConfigManagerInterface $typed_config_manager
* The typed config manager.
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
* The string translation service.
* @param \Drupal\Core\Config\StorageInterface $active_storage
* The active configuration storage.
* @param \Symfony\Contracts\EventDispatcher\EventDispatcherInterface $event_dispatcher
* The event dispatcher.
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
* The entity repository.
* @param \Drupal\Core\Extension\ExtensionPathResolver $extension_path_resolver
* The extension path resolver.
*/
public function __construct(EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typed_config_manager, TranslationInterface $string_translation, StorageInterface $active_storage, EventDispatcherInterface $event_dispatcher, EntityRepositoryInterface $entity_repository, ExtensionPathResolver $extension_path_resolver) {
$this->entityTypeManager = $entity_type_manager;
$this->configFactory = $config_factory;
$this->typedConfigManager = $typed_config_manager;
$this->stringTranslation = $string_translation;
$this->activeStorage = $active_storage;
$this->eventDispatcher = $event_dispatcher;
$this->entityRepository = $entity_repository;
$this->extensionPathResolver = $extension_path_resolver;
}
vs PHP 8:
public function __construct(
protected EntityTypeManagerInterface $entityTypeManager,
protected ConfigFactoryInterface $configFactory,
protected TypedConfigManagerInterface $typedConfigManager,
protected TranslationInterface $stringTranslation,
protected StorageInterface $activeStorage,
protected EventDispatcherInterface $eventDispatcher,
protected EntityRepositoryInterface $entityRepository,
protected ExtensionPathResolver $extensionPathResolver,
) {}
Steps to reproduce
Proposed resolution
Once 10.1.x is open, convert all constructors to use property promotion.
Remaining tasks
Write a rector rule to help us with this.
Decide if we need to document constructor parameters any more or if that is also unnecessary boilerplate.
Discover any edge cases and figure out what to do with them.
User interface changes
API changes
Data model changes
Release notes snippet
Comment | File | Size | Author |
---|---|---|---|
#11 | BBvaVdZr.png | 216.39 KB | shadcn |
Issue fork drupal-3278431
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 #2
mondrakeInteresting. #3217699: Convert select query extenders to backend-overrideable services was an early attempt to introduce this. It was even committed for some time. Too early, then.
Comment #3
Wim Leers🤩 Yes please!
In #3302755-53: On-the-fly JavaScript minification I wrongly thought this was a bug, I A) didn't know we were doing this, B) had never before seen the new syntax in real code!
So much less boilerplate!
Comment #4
Chi CreditAttribution: Chi commentedJust ouf of curiosity. Don't you think this annotation is absolutely redundant for parameters that have native PHP type hints?
Here is another conversation about this.
Comment #5
longwave@Chi see remaining tasks:
Comment #6
longwaveAs noted in #3302755-60: On-the-fly JavaScript minification I am in favour of always using explicit multiline syntax for property promoted constructors, even if they would fit on one line, as it makes the use of them clearer to see. For example instead of:
I propose always using:
I think this is even more useful if we are dropping the @param section of the docblock.
And then if we adopt Symfony autowiring attributes we can say:
Comment #7
Wim LeersRE: autowiring: I tried to adopt that a year ago in #3103682-20: CDN module 4.x: no new features + drop BC layers + drop now irrelevant headers + PHP >=8.1 + Drupal >=9.4, but that special comment syntax thing is new I think? Looking forward to trying it in any case :)
Comment #8
Wim LeersRelated: #3049525: Enable service autowiring by adding interface aliases to core service definitions.
Comment #9
larowlan10.1 is open
Re #6 and multiline syntax, I agree, its much more readable
Comment #10
Wim LeersAFAICT #1014086: Stampedes and cold cache performance issues with css/js aggregation was the first commit to land in Drupal 10 that used this 😊
Bumping priority and tagging to increase visibility; this will have a huge impact on DX. See the (justified!) complaint of just a few hours ago: https://twitter.com/shadcn/status/1576868763932106752
Comment #11
shadcn CreditAttribution: shadcn at Chapter Three commentedThanks for the bump on this @Wim Leers.
Agree with #4 re. the constructor docblock, this feels redundant:
Comment #12
mondrakeActually, https://git.drupalcode.org/project/drupal/-/commit/d0f7178 came first (8 months ago), but was eventually reverted.
One of the reasons of the revert was #3217699-46: Convert select query extenders to backend-overrideable services, and I do not have seen consensus anywhere yet.
Also, it's not in the coding standard AFAICT - which is Catch 22 I think, since the coding standards process seems to be idle at the moment.
All in all, I would welcome this very much, but cannot see clear guidance yet.
Comment #13
catchI also think we should drop type hinted parameter documentation from constructors, however because that's enforced by coder, we'll likely need a coding standards issue and then a patch to coder to remove it. @quietone has re-started the coding standards group so it might actually be possible to make changes again there - but it'd have to go through the steps...
As mentioned we've already started using property promotion for new code. So I think the main thing here is whether we want to drop parameter documentation at the same time as we apply constructor promotion to old code so we can make all the changes at once, if so we probably need to postpone this one on a coding standards issue to do that, if not we could go ahead with the initial change to property promotion here and do the param docs separately.
Comment #14
longwaveOne issue with #13 is what to do with e.g. plugins where we have a mix of "normal" arguments and injected services. But I guess we could just use @inheritdoc and delegate to the base class (e.g. PluginBase) which documents the normal arguments only?
Rector has a ClassPropertyAssignToConstructorPromotionRector rule which on first glance seems to mostly do what we need (including updating variable names in @param) and we could likely modify to handle our exact requirements once we've decided on them.
Comment #15
Chi CreditAttribution: Chi commentedI'd like to indicate that the issue with reduntant dockblocks is not specific to constructors with promoted properties.
Most constructors already have typehinted arguments. Promoting their properties makes no difference.
And besides constructors there are lots of ordinary methods, functions, class properties that already have native typehints.
Comment #16
Chi CreditAttribution: Chi commentedComment #17
mstrelan CreditAttribution: mstrelan commentedWhat would we do if a constructor argument uses snake case and the property uses camel case? Do we need to deprecate the old argument in case folks are using named arguments?
Also since this would be a very large MR/patch should this be split up by module/component?
Comment #18
longwaveI think we would have to assume we do not support downstream users using named arguments yet; as this is a relatively new language feature hopefully we can get away with this? The disruption of having to add new arguments with different names and deprecate all the old ones seems far worse than possibly disrupting a handful of people who are actually using named arguments.
Regarding scoping usually we prefer not to work module-by-module as it often ends up with inconsistencies, so any suggestions on scoping this appropriately would be welcome: https://www.drupal.org/core/scope
If the whole MR/patch can be made repeatable with an automated process (e.g. Rector) then it is possible that a single issue is viable, and we would have to commit that during the beta window to avoid disruption to other issues.
Comment #19
catchNamed arguments we don't need to support, I think we discussed that already in a different issue a while back and just decided if someone is relying on them, it's up to them to keep up.
We already use camel case for properties, so they wouldn't change, just the snake case constructor arguments, so that ought to be OK I would think.
Comment #20
mstrelan CreditAttribution: mstrelan commentedThere is an existing rector rule ClassPropertyAssignToConstructorPromotionRector but I'm not sure it handles the mismatched snake/camel case.
Comment #21
longwaveI ran it locally as a quick experiment, and it just works:
Comment #22
mstrelan CreditAttribution: mstrelan commentedNice! Sorry I missed #14 where you had already identified that.
Comment #23
PasqualleIs the issue postponed on a coding standard? If yes, which coding standard issue?
Comment #24
catch@pasqualle property promotion itself we can do without a coding standards issue, and we can already do this for new code.
The bit that needs coding standards changes is removing @param documentation on constructors. Not sure if we have an issue for that yet.
Comment #25
PasqualleBased on discussions in #2209735: [policy] Remove DI verbosity by removing unnecessary doc lines and #2140961: Allow constructor methods to omit docblocks it seems the constructor doc block should be fully removed.
Comment #26
mondrakeComment #27
xjmComment #28
Chi CreditAttribution: Chi commentedRe #6
Here is a well explained rationale for this.
https://stitcher.io/blog/why-curly-brackets-go-on-new-lines
Comment #29
quietone CreditAttribution: quietone at PreviousNext commented#24. The closest Coding Standards issue is #2209735: [policy] Remove DI verbosity by removing unnecessary doc lines. That looks close enough that it can be used for this. Although, I'd like another opinion on that.
I looked at Drupal api for the class mentioned early that already implements this is core, CssCollectionOptimizerLazy.php. It appears that the file is pretty much ignored and there is nothing of value there for the reader. Even the 'view source' link doesn't work for me. I presume an issues is needed in the API project. Based on that I think this needs to be Postponed until that is fixed.
Comment #30
catch@quietone #2209735: [policy] Remove DI verbosity by removing unnecessary doc lines looks like the right issue for removing constructor param phpdoc.
Comment #31
catchThis seems to be more generalised on api.drupal.org and not specific to constructor property promotion, I've opened #3342815: Classes missing from Drupal 10 since I couldn't find an existing issue.
Comment #32
kim.pepperDoes anyone have the rector config and cli command handy to paste into this issue?
Comment #33
mstrelan CreditAttribution: mstrelan commented@kim.pepper I was able to get this working with the following config file and commands.
rector.php
Note I had to remove mglaman/phpstan-drupal as it was throwing an error, and had to twiddle the parallel configuration so as not to destroy my laptop.
I assume we can configure code style changes like multi-line parameters.
Comment #35
mstrelan CreditAttribution: mstrelan commentedIt seems this only worked for native types, but in #21 @longwave had it working for CsrfTokenGenerator. Was there any additional configuration needed for that to work?
Comment #36
longwaveYou need this config to make it change untyped protected or public properties:
Comment #38
mcdruidA big +1 for type declarations on class properties is that it really helps avoid abuse in the context of unsafe deserialisation / gadget chains.
See for example #3379512: Harden Drupal against PHP Gadget Chain Drupal9/RCE1.
Comment #39
catchThere's an MR here now and this is unblocked.
Comment #41
markdorisonThis was a bit gnarly to rebase so forgive me if I mangled something.
Comment #42
longwaveCustom commands failed.
Instead of trying to rebase the existing branch, I think it might be simpler to have a rector script in a patch or branch, and then upload the output from that as a patch; others can confirm that the script output is the same and the bot will happily test the new patch each time HEAD moves on.
Comment #43
catchBecause we no longer have all the boilerplate, adding bc for a constructor has to add a lot of it back.
Probably needs its own dedicated issue, discovered in #3354211: Tidy up URL generation for asset aggregates.
For example if you want to remove a constructor argument from somewhere in the middle of the parameters, you would need to do something like this:
1. Define all of the protected properties from the argument being removed onwards.
2. Undo constructor property promotion from the argument being removed onwards.
3. Add the usual deprecation dance in the constructor (this bit is not new).
Then when removing the deprecation, you'd need to undo all of steps 1-3 and go back to constructor property promotion again.
This turns adding bc for a constructor change from a small amount of work to a large amount of work.
That means I think we might need to pair this with #3218030: [meta] Always use named arguments when creating a class instance. See also the discussion in #3183180: [policy, no patch] Document named arguments BC policy.
i.e. we'd make an exception for constructors, recommend calling them with named arguments both from ::create() functions and subclasses (and everywhere else), and then convert core to use named arguments in ::create() functions. And then stop doing the bc dance for constructors when arguments are re-ordered or removed because named arguments should handle it.
But this adds a yet further problem that we're going to be changing the argument names here to match the property names, so if modules started to do that before this issue lands, those modules would break when the argument names change. So really we can only recommend this once versions of core before this change lands are out of support, which would when 10.3 is released/10.1 goes EOL assuming this issue lands before 10.2. I guess we could have a transition period where we do the very verbose workaround, maybe only for classes that contrib is extending or something?
Comment #44
Wim Leers😳🫠
A technically enforceable solution is impossible, only guidance to do these things in the right sequence would help. Or in fact, it'd almost need to be not a sequence but an atomic change?
If every constructor in core (and contrib) would make these two changes in a single commit:
And only then in a subsequent major version:
… then we'd be in a good place, right? What if we did a PSA for encouraging ecosystem-wide adoption of this pattern?
Is it feasible to detect the use of named arguments and when they're not used, throw a deprecation notice? 🤔 Or would we only be able to do that using something like PHPStan?
My 2 hunches definitely did not work:
— output at https://3v4l.org/17UTb
AFAICT this would only be possible to do through PHPStan 😞
Comment #45
longwaveRe #43 can't we just use union types? https://3v4l.org/CLnhZ looks to work.
I think we should avoid named arguments if possible because I see them causing more problems than they solve - once we have started using named arguments it's more difficult to change or stop using them.
Also I am hoping that we land more autowiring changes and #3294266: Allow plugin service wiring via constructor parameter attributes and then we don't even need
create()
methods any more in many cases.Comment #46
Wim LeersVERY good point!
Should we postpone this issue on that one and other auto-wiring issues? 🤔
Comment #47
mstrelan CreditAttribution: mstrelan commentedHow would union types look when there are 4 params and you want to deprecate the second one? Wouldn't you need a union type for params 2, 3 and 4? And if 4 then becomes nullable, when the bc layer is removed there may be confusion as to whether param 4 should remain nullable.
Comment #48
mstrelan CreditAttribution: mstrelan commentedIn fact I think 45 was only talking about replacing a param with another one, but often the param may be removed entirely.
Comment #49
catch@longwave oh nice! I did think about union types, but I didn't think about overwriting the property in the constructor and that's not bad at all. We'd have to drop
readonly
as well as using union types, but that's fine (https://3v4l.org/X6PNN), can just add it back when we remove the bc layer.@mstrelan yes pretty much, but that's what it would look like now without property promotion - still have to add union types all the way along if you remove the second of five parameters.
@Wim no I don't think we need to postpone, the union types + overwriting makes this no worse than what we currently have to do for bc, which is more than good enough.
Comment #50
longwave4 parameters A, B, C, D and we want to deprecate B: https://3v4l.org/ONeE9
It seems pretty clear to me that by renaming the nullable argument to $unused that we don't need it after deprecation cleanups. Maybe there is a better name for the variable but given we are removing arguments we do have an additional one in the BC case that we have no good name for.
Not sure how this works with autowiring; if you specify a union type and each matches a service in your container, what happens? I guess either an error or there is some ordering rule, we will have to find this out (for the generic services case, at least; in the plugin case that is our custom code and we can do what we want, though we should probably follow Symfony).
Comment #51
catchWhen the last parameter is unused, we can omit it, and use use func_get_arg() - this is already used in some places in core so not really a change from now:
https://3v4l.org/WVIp1
Comment #52
kim.pepperWe do the union types deprecation thing already. See https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co...
Comment #53
mstrelan CreditAttribution: mstrelan commentedI wonder if we can provide a trait that will reassign all the property values after the deprecated param using Reflection. This is pretty ugly but as a proof of concept it seems to work:
https://3v4l.org/kWEm7
Edit: simplified a bit:
https://3v4l.org/MtHNJ
Comment #54
mstrelan CreditAttribution: mstrelan commentedAnother issue to consider with union types and constructor property promotion is that beyond the constructor the properties are still union types, so any other method can assign the wrong value. In most cases this is unlikely to be exploited, but it will likely cause phpstan to complain that calls to members on those properties may not exist as it can't be certain what type the properties are, so we would have to provide a typehint or assertion to coerce it to agree.
Comment #55
catch#53 is very clever and worth opening a new issue for.
#54 was about to say this is a pre-existing issue with how we do constructor deprecations, although I wonder if phpstan is able to use the @var on the property definition, in which case maybe it's not a pre-existing issue at all :/
Comment #56
mstrelan CreditAttribution: mstrelan commentedWith existing deprecations the property definitions are at least correctly typed, or at least can be correct, since the union is only required for the param.
Comment #57
mstrelan CreditAttribution: mstrelan commentedOpened #3382742: Provide a helper trait for removing deprecated constructor params for #53.
Comment #58
catchAll the blockers are done here.
Clarifying the title that this is now about converting existing code over.
Comment #59
catchI've updated the issue summary now that constructor docblocks are optional. That should hopefully reflect where we want to get to.
Comment #60
andypost@catch in #49 you're about
readonly
to apply for the properties, is it separate issue?Comment #61
catch@andypost if it's easy I think we can bundle in
readonly
here, will save changing things multiple times.