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

Before running rector, you need to comment out a line in the rector config class otherwise the @var docs will be copied into the constructor signature which is extremely ugly.
Comment out this line:
$this->propertyPromotionDocBlockMerger->mergePropertyAndParamDocBlocks($property, $param, $paramTagValueNode);
1. Run the following rector:

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;

return static function (RectorConfig $rectorConfig): void {
  $rectorConfig->paths([
    __DIR__ . '/app/composer',
    __DIR__ . '/app/core',
  ]);
  $rectorConfig->ruleWithConfiguration(ClassPropertyAssignToConstructorPromotionRector::class, [
    ClassPropertyAssignToConstructorPromotionRector::INLINE_PUBLIC => TRUE,
  ]);
  $rectorConfig->parallel(processTimeout: 300, maxNumberOfProcess: 4);
};

2. Rector will put the opening brace of the constructor onto a new line. To fix this run phpcbf with just the relevant sniff:
./bin/phpcbf --standard=app/core/phpcs.xml.dist --sniffs=Drupal.Functions.MultiLineFunctionDeclaration app/[core|composer]

Do not try to rebase the MR, simply run the steps again.

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

CommentFileSizeAuthor
#11 BBvaVdZr.png216.39 KBshadcn

Issue fork drupal-3278431

Command icon 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

longwave created an issue. See original summary.

mondrake’s picture

Interesting. #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.

wim leers’s picture

🤩 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!

chi’s picture

@param \Drupal\Core\Entity\EntityTypeManagerInterface $entityTypeManager
* The entity type manager.

Just 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.

longwave’s picture

@Chi see remaining tasks:

Decide if we need to document constructor parameters any more or if that is also unnecessary boilerplate.

longwave’s picture

As 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:

  public function __construct(protected ?CacheBackendInterface $cache) {

I propose always using:

  public function __construct(
    protected ?CacheBackendInterface $cache,
  ) {

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:

  public function __construct(
    #[Autowire(service: 'cache.data')]
    protected ?CacheBackendInterface $cache,
  ) {
wim leers’s picture

RE: 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 :)

larowlan’s picture

Status: Postponed » Active

10.1 is open

Re #6 and multiline syntax, I agree, its much more readable

wim leers’s picture

Priority: Normal » Major
Issue tags: +DX (Developer Experience)

AFAICT #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

shadcn’s picture

StatusFileSize
new216.39 KB

Thanks for the bump on this @Wim Leers.

Agree with #4 re. the constructor docblock, this feels redundant:

  • Most of the comments end up as "The X service" or "The X manager" anyway.
  • Context can inferred from the type hint and/or is one-click away in an IDE

screenshot

mondrake’s picture

Actually, 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.

catch’s picture

I 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.

longwave’s picture

One 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.

chi’s picture

I'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.

chi’s picture

mstrelan’s picture

What 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?

longwave’s picture

I 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.

catch’s picture

Named 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.

mstrelan’s picture

There is an existing rector rule ClassPropertyAssignToConstructorPromotionRector but I'm not sure it handles the mismatched snake/camel case.

longwave’s picture

I ran it locally as a quick experiment, and it just works:

diff --git a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
index 48e75acb58..9243cf171b 100644
--- a/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
+++ b/core/lib/Drupal/Core/Access/CsrfAccessCheck.php
@@ -16,21 +16,14 @@
  */
 class CsrfAccessCheck implements RoutingAccessInterface {
 
-  /**
-   * The CSRF token generator.
-   *
-   * @var \Drupal\Core\Access\CsrfTokenGenerator
-   */
-  protected $csrfToken;
-
   /**
    * Constructs a CsrfAccessCheck object.
    *
-   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrf_token
+   * @param \Drupal\Core\Access\CsrfTokenGenerator $csrfToken
    *   The CSRF token generator.
    */
-  public function __construct(CsrfTokenGenerator $csrf_token) {
-    $this->csrfToken = $csrf_token;
+  public function __construct(protected CsrfTokenGenerator $csrfToken)
+  {
   }
mstrelan’s picture

Nice! Sorry I missed #14 where you had already identified that.

pasqualle’s picture

Version: 10.0.x-dev » 10.1.x-dev

Is the issue postponed on a coding standard? If yes, which coding standard issue?

catch’s picture

@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.

pasqualle’s picture

mondrake’s picture

xjm’s picture

chi’s picture

Re #6
Here is a well explained rationale for this.
https://stitcher.io/blog/why-curly-brackets-go-on-new-lines

quietone’s picture

#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.

catch’s picture

@quietone #2209735: [policy] Remove DI verbosity by removing unnecessary doc lines looks like the right issue for removing constructor param phpdoc.

catch’s picture

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.

This 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.

kim.pepper’s picture

Does anyone have the rector config and cli command handy to paste into this issue?

mstrelan’s picture

@kim.pepper I was able to get this working with the following config file and commands.

rector.php

<?php

declare(strict_types=1);

use Rector\Config\RectorConfig;
use Rector\Php80\Rector\Class_\ClassPropertyAssignToConstructorPromotionRector;

return static function (RectorConfig $rectorConfig): void {
    $rectorConfig->paths([
        __DIR__ . '/composer',
        __DIR__ . '/core',
    ]);
    $rectorConfig->rule(ClassPropertyAssignToConstructorPromotionRector::class);
    $rectorConfig->parallel(seconds: 300, maxNumberOfProcess: 4);
composer require --dev rector/rector
./vendor/bin/rector process

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.

mstrelan’s picture

It 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?

longwave’s picture

You need this config to make it change untyped protected or public properties:

  $rectorConfig->ruleWithConfiguration(ClassPropertyAssignToConstructorPromotionRector::class, [
    ClassPropertyAssignToConstructorPromotionRector::INLINE_PUBLIC => TRUE,
  ]);

Version: 10.1.x-dev » 11.x-dev

Drupal core is moving towards using a “main” branch. As an interim step, a new 11.x branch has been opened, as Drupal.org infrastructure cannot currently fully support a branch named main. New developments and disruptive changes should now be targeted for the 11.x branch, which currently accepts only minor-version allowed changes. For more information, see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

mcdruid’s picture

A 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.

catch’s picture

Status: Active » Needs work

There's an MR here now and this is unblocked.

markdorison made their first commit to this issue’s fork.

markdorison’s picture

Status: Needs work » Needs review

This was a bit gnarly to rebase so forgive me if I mangled something.

longwave’s picture

Status: Needs review » Needs work

Custom 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.

catch’s picture

Because 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?

wim leers’s picture

😳🫠

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:

  1. adopt constructor property promotion, but keep constructor argument order!
  2. adopt named arguments for calling that constructor for all its own calling code

And only then in a subsequent major version:

  1. deprecate constructor arguments

… 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:

<?php

class Something {
    public function __construct(protected string $a, public int $b) {
        var_dump(debug_backtrace(limit: 1));
        var_dump(func_get_args());
    }
}

$s_ordered = new Something('yo', 1337);
var_dump($s_ordered);

print "---------------\n";

$s_named = new Something(b: 1337, a: 'yo');
var_dump($s_named);

— output at https://3v4l.org/17UTb

AFAICT this would only be possible to do through PHPStan 😞

longwave’s picture

Re #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: Improve plugin autowiring performance by moving reflection to discovery time and then we don't even need create() methods any more in many cases.

wim leers’s picture

VERY good point!

Should we postpone this issue on that one and other auto-wiring issues? 🤔

mstrelan’s picture

How 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.

mstrelan’s picture

In fact I think 45 was only talking about replacing a param with another one, but often the param may be removed entirely.

catch’s picture

@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.

longwave’s picture

4 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).

catch’s picture

When 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

kim.pepper’s picture

We do the union types deprecation thing already. See https://git.drupalcode.org/project/drupal/-/blob/11.x/core/lib/Drupal/Co...

mstrelan’s picture

I 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

mstrelan’s picture

Another 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.

catch’s picture

#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 :/

mstrelan’s picture

With 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.

mstrelan’s picture

catch’s picture

Title: Use PHP 8 constructor property promotion » Use PHP 8 constructor property promotion for existing code

All the blockers are done here.

Clarifying the title that this is now about converting existing code over.

catch’s picture

Issue summary: View changes

I've updated the issue summary now that constructor docblocks are optional. That should hopefully reflect where we want to get to.

andypost’s picture

@catch in #49 you're about readonly to apply for the properties, is it separate issue?

catch’s picture

@andypost if it's easy I think we can bundle in readonly here, will save changing things multiple times.

longwave’s picture

Issue tags: +beta target

The best time to make these sorts of changes is in the window between beta and RC when we are just polishing and fixing stability issues, perhaps we can do this in the upcoming 10.3.x beta window given we are already applying this pattern in both new and changed constructors - it would be good to be consistent and apply it everywhere.

solideogloria’s picture

Could we create/have a coding standards rule that could be optionally added to encourage using the new constructors?

donquixote’s picture

@catch (#13)

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.

To me this seems to me like an unnecessary scope creep and distraction.
The issue has already filled up with comments that are not related to constructor property promotion.

Imo it is good practice to do incremental changes in separate issues, which reduce the scope of discussion and decisions in each issue.

Regarding the existing MR:
I prefer very much to have these constructor signatures broken to multiple lines:

  public function __construct(
    protected RootPackageInterface $rootPackage,
    protected string $eventName,
  ) {

This keeps it more readable, and also makes future MRs easier to review that want to add `readonly`, or `#[Autowire(..)]` attribute, or that want to add or remove parameters.

Then for parameter attributes we can either keep them on the same line or on a separate line.

  public function __construct(
    protected RootPackageInterface $rootPackage,
    #[Autowire(..)]
    protected string $eventName,
  ) {

or

  public function __construct(
    protected RootPackageInterface $rootPackage,
    #[Autowire(..)] protected string $eventName,
  ) {
donquixote’s picture

I also created a coding standards issue regarding the line breaks.
#3458314: Convention or recommendation for line breaks in constructor parameter signature with promoted properties

(and I don't think the line breaks are out of scope, we actually need to make this choice now)

This said, we already do have enough examples in core where we do add line breaks, so we don't need to wait for a decision in the coding standards issue.

catch’s picture

I don't think we need to block this on adding more verbiage to the coding standard, we already did https://www.drupal.org/node/3396762 which covers things enough for me - i.e. in this issue we can use multiple lines.

We might need a coding standards issues for multi-line method signatures with parameter attributes but we hardly have any of those so it shouldn't be blocking.

acbramley made their first commit to this issue’s fork.

acbramley’s picture

Issue summary: View changes
Status: Needs work » Needs review

I was able to get this a bit closer to what we want, have documented in the IS.

I can't find a rector rule to break constructor params onto newlines though.

New MR is up against 11.x

acbramley’s picture

Status: Needs review » Needs work

Some things are a bit borked :(

catch’s picture

I can't find a rector rule to break constructor params onto newlines though.

Maybe phpcbf has one? Would make it a two step process but might be worth it considering the actual commit would hopefully be one-hit.

donquixote’s picture

Good old (repeated) regex trick...

Detect and mark any constructor with at least one promoted parameter:

  public function __construct(// BREAK HERE
    $a, $b, protected $c, $d) {

Repeatedly apply a regex replace to move the marker step by step.

  public function __construct(
    $a,// BREAK HERE
    $b, protected $c, $d) {
  public function __construct(
    $a,
    $b,// BREAK HERE
    protected $c, $d) {
  public function __construct(
    $a,
    $b,
    protected $c,// BREAK HERE
    $d) {

Handle the last parameter and the {}.

  public function __construct(
    $a,
    $b,
    protected $c,
    $d,
  ) {

I hope I did not overshoot the target.
If anybody finds a phpcbf or rector that does the same, go ahead and compare that last commit :)

acbramley’s picture

Would make it a two step process but might be worth it considering the actual commit would hopefully be one-hit.

Yeah it already is anyway since rector is putting the opening brace onto newlines (see IS)

@donquixote you're a wizard! Can you share the script?

donquixote’s picture

@donquixote you're a wizard! Can you share the script?

I should have done this as a script, but instead just used phpstorm find/replace.
I am trying to find it in the history, be careful, I could be picking the wrong line.

First step:
Find: "^(  (?:public|protected|private) function __construct\()((?:(?!public)(?!protected)(?!private)[^,\)\n]*\$\w+(?: = [^,\)\n]*)?,(?: |\n    ))*(?:public|protected|private) [^,\)\n]*\$\w+(?: = [^,\)\n]*)?)"
Replace: "$1// BREAK HERE SQUIRREL\n    $2"

Second step (repeated):
Find: "// BREAK HERE SQUIRREL(\n    [^,\)]*,) "
Replace: "$1// BREAK HERE SQUIRREL\n    "

Last step for empty body:
Find: "// BREAK HERE SQUIRREL(\n    [^,\)]*)\) \{\n  \}"
Replace: "$1,\n  \) \{\}"

Last step for non-empty body:
Find: "// BREAK HERE SQUIRREL(\n    [^,\)]*)\) \{"
Replace: "$1,\n  \) \{"

Later you can use "git diff --ignore-all-space --word-diff -U0" to quickly review the changes.

donquixote’s picture

I created a branch and MR with a reduced set of conversions, using PhpStorm:
- Create inspection profile with only "Property can be promoted", all others disabled.
- Run inspection, apply fixes.

This only converts parameters where the property has the same name and type as the parameter.
So, no automatic rename.

Version: 11.x-dev » main

Drupal core is now using the main branch as the primary development branch. New developments and disruptive changes should now be targeted to the main branch.

Read more in the announcement.

longwave’s picture

Title: Use PHP 8 constructor property promotion for existing code » [May 2026] Use PHP 8 constructor property promotion for existing code

Bumping this in the hope someone will revive it and we can commit it in the upcoming beta window. I think we might want to land it in both 11.4 and 12.0 to make backports easier? But to start with let's concentrate on just getting this automated in main.

andypost’s picture

+1 to backport to 11.4