Problem/Motivation

Coming from #3190585-14: [META] Run PHPStan on Drupal core but as 10.0 core should be compatible with PHP 8.2 this now required

After #3299327: Replace deprecated static::method() calls for PHP 8.2 and https://wiki.php.net/rfc/deprecate_partially_supported_callables it would be great to normalize usage of callables to first-class as D10 requires PHP 8.1

If compatibility with PHP < 8.1 is not desired, use of the first-class callable syntax self::method(...) is also possible.

Replace usages of [static::class, '::methodName'] to static::methodName(...) which is more readable and performant since PHP 8.1

Steps to reproduce

- https://wiki.php.net/rfc/deprecate_partially_supported_callables
- https://wiki.php.net/rfc/partially-supported-callables-expand-deprecatio...

- https://php.watch/versions/8.2/partially-supported-callable-deprecation
- https://php.watch/versions/8.1/first-class-callable-syntax

Proposed resolution

- adjust places where this is used - uasort, array_map(), array_filter,call_user_func_array(),
- fix usage and use rector
- review/commit

Remaining tasks

User interface changes

API changes

Data model changes

Release notes snippet

Issue fork drupal-3259716

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

mallezie created an issue. See original summary.

beatrizrodrigues’s picture

Assigned: Unassigned » beatrizrodrigues

I will work at this.

beatrizrodrigues’s picture

Assigned: beatrizrodrigues » Unassigned
Status: Active » Needs review

Hope it fits.

mondrake’s picture

We need also to adjust PHPStan baseline, this would have failed if #3259355: Always do a full phpstan analysis on DrupalCI were in

mallezie’s picture

Status: Needs review » Needs work
mallezie’s picture

Status: Needs work » Needs review

I added the regenerated baseline.
Reverted some unrelated changes (although at first sight they don't seem wrong, they were not flagged / in scope here).
I've also added changes for big pipe test files which had the same issue in the baseline.

lucienchalom’s picture

Status: Needs review » Needs work

I do believe something went wrong with the MR, there are 92 files changed, 1064 changes in code, including some related to static::class but also... coding standards in css files? scaffold deprecation? and documentation of the migration plugin. And many more.
i couldn't review, moving back to needs work, i'm sorry.

mallezie’s picture

Status: Needs work » Needs review

I've rebased the MR.
This only shows needed changes here.

longwave’s picture

Status: Needs review » Needs work

Lots of test fails.

mallezie’s picture

Status: Needs work » Needs review

Test fails are unrelated here. Seems LayoutBuilder JS tests are flaky today.

See also #3267124: Temporarily skip failing tests

daffie’s picture

Status: Needs review » Needs work
Issue tags: +Needs reroll

The MR needs to be rebased.

mallezie’s picture

Status: Needs work » Needs review
Issue tags: -Needs reroll
daffie’s picture

Status: Needs review » Needs work

When I look to phpstan-baseline.neon, I find 2 other instances:

  1. message: "#^\\#lazy_builder value 'string' at key '0' is invalid\\.$#"
    count: 1
    path: modules/filter/src/FilterProcessResult.php
    

    for:

    '#lazy_builder' => [$callback, $args],
    

    I think we need to do explode('::', $callback) first.

  2. message: "#^\\#access_callback value ''Drupal\\\\\\\\Tests\\\\\\\\Core…'\\|'Drupal\\\\\\\\Tests\\\\\\\\Core…'\\|'Drupal\\\\\\\\Tests\\\\\\\\Core…'\\|'Drupal\\\\\\\\Tests\\\\\\\\Core…'' at key '0' is invalid\\.$#"
    count: 1
    path: tests/Drupal/Tests/Core/Render/RendererTest.php
    

    for:

    '#access_callback' => 'Drupal\Tests\Core\Render\TestAccessClass::' . $method,
    

All the other changes look good.

andregp’s picture

@daffie, I'll check this.

andregp’s picture

Status: Needs work » Needs review

Updated the phpstan-baseline.neon file.

andregp’s picture

Status: Needs review » Needs work

I'll try to fix this merge

andregp’s picture

Status: Needs work » Needs review

Merged master

longwave’s picture

Status: Needs review » Needs work

PHPStan is failing.

andregp’s picture

@longwave, sorry, I'll check this

andregp’s picture

If I try to change the line in RenderTest.php to

    $build = [
      '#access_callback' => ['Drupal\Tests\Core\Render\TestAccessClass', $method],
    ];

phpstan returs the following error:

Running PHPStan on changed files.
 ------ ------------------------------------------------------------------------------------
  Line   tests/Drupal/Tests/Core/Render/RendererTest.php                                                                                                               
 ------ ------------------------------------------------------------------------------------
  619    #access_callback callback array{'Drupal\\Tests\\Core…', 'accessFalse'|'accessResultAllowed'|'accessResultForbidd…'|'accessTrue'} at key '0' is not callable.  
 ------ ------------------------------------------------------------------------------------ 

So it might be better to keep as it is.

The errors on phpstan at #17 were due to a bad merge, I reverted the merge and did it again.

andregp’s picture

Status: Needs work » Needs review

Updating status.

lucienchalom’s picture

Status: Needs review » Reviewed & tested by the community

with the MR, the phpstan-baseline.neon is okay, I couldn't find any more errors for "'non\\-empty\\-string' at key '0' is invalid", and all the calls for methods that created them were fixed besides the one cited in #21.

i found another 9 uses of this concatenation, i will list them here, but I consider them out of the scope of the issue because the focus was phpstan. considering this, moving to RTBC.

core/modules/comment/src/Entity/Comment.php (292)
core/modules/content_translation/src/ContentTranslationHandler.php (179)
core/modules/image/tests/modules/image_module_test/src/Plugin/Field/FieldWidget/DummyAjaxWidget.php (34)
core/modules/media/src/Entity/Media.php (492)
core/modules/media/src/Plugin/Filter/MediaEmbed.php (250)
core/modules/user/src/EntityOwnerTrait.php (41)
core/tests/Drupal/KernelTests/Core/Field/Entity/BaseFieldOverrideTest.php (77)
core/tests/Drupal/Tests/Core/Entity/BaseFieldDefinitionTest.php (356)
core/tests/Drupal/Tests/Core/Entity/FieldDefinitionTest.php (273)

longwave’s picture

Status: Reviewed & tested by the community » Needs review

Is it really right to change all these cases, given the code apparently worked before and we are just doing this to please PHPStan?

This feels like it could be raised as a bug in mglaman/phpstan-drupal instead, as that has special code to deal with #lazy_builder and friends: https://github.com/mglaman/phpstan-drupal/blob/main/src/Rules/Drupal/Ren... - if this is valid Drupal code I think it should be fixed over there or in PHPStan instead?

lucienchalom’s picture

sorry, the list I send is a suggestion of other possible concatenations.

they were not changed!

I am sorry that I made this confusing.
this list is not included in the MR.

longwave’s picture

Status: Needs review » Needs work

Asked @mglaman in Slack about changing PHPStan but agree that it's probably more trouble than it's worth trying to get it to understand static::class . '::someMethod' properly, so I retract my comment in #24, but I think one of the changes is incorrect - see MR comment.

longwave’s picture

Having said that, I then looked into PHPStan internals and think I can fix it, so I opened https://github.com/mglaman/phpstan-drupal/pull/436 which should allow it to parse static::class . '::someMethod' correctly.

andregp’s picture

Status: Needs work » Needs review
andypost’s picture

andypost’s picture

Issue summary: View changes

Updated IS steps with links to PHP 8.2 changes so it may need more work (I did not check current MR)

- https://wiki.php.net/rfc/deprecate_partially_supported_callables
- https://php.watch/versions/8.2/partially-supported-callable-deprecation

andypost’s picture

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

spokje’s picture

Status: Needs review » Needs work

There's still one left: \Drupal\filter\FilterProcessResult::createPlaceholder()

    // Add the placeholder attachment.
    $this->addAttachments([
      'placeholders' => [
        $placeholder_markup => [
          '#lazy_builder' => [$callback, $args],
        ],
      ],
    ]);

Suppression in the PHPStan baseline:

		-
			message: "#^\\#lazy_builder value 'non\\-empty\\-string' at key '0' is invalid\\.$#"
			count: 1
			path: modules/node/src/NodeViewBuilder.php

A lot of people (including me) when to a lot of different approaches to get it fixed (see the various commits on the MR), but we seem to all haven't found the correct way to do it.

mglaman’s picture

https://github.com/mglaman/phpstan-drupal/pull/436 is about to be merged, which makes phpstan-drupal more intelligent for resolving static::class . '::callback'.

As for that last example, seems like something phpstan-drupal needs a test case for. We're not resolving the value of $callback

mglaman’s picture

https://github.com/mglaman/phpstan-drupal/releases/tag/1.1.22 was released. This removes the warning about not being able to parse static::class concatenation with method names.

spokje’s picture

Tagging for IS update:

Coming from #3190585-14: [META] Run PHPStan on Drupal core but as 10.0 core should be compatible with PHP 8.2 this now required

(emphasis mine)

vs

It's just a PHPStan thing where static:class is handled a specify way for generics

vs

https://github.com/mglaman/phpstan-drupal/releases/tag/1.1.22 was released. This removes the warning about not being able to parse static::class concatenation with method names.

So either this _was_ a PHPStan(-drupal) thing, that isn't a PHPStan(-drupal) thing any more, making this issue obsolete, or we need this anyway to become PHP 8.2 compatible.

Or something completely different?

longwave’s picture

Status: Needs work » Postponed

@Spokje This was a phpstan-drupal thing, where it did not understand static::class . '::methodName' was equivalent to [static::class, 'methodName'] for callables. It now does understand that those are equivalent and so postponing this until we upgrade to mglaman/phpstan-drupal 1.1.22, which should solve the false positives in the baseline that relate to this issue.

spokje’s picture

Thanks @longwave,

So if I understand this correctly: As soon as we require mglaman/phpstan-drupal 1.1.22 this false positives will disappear, making this issue obsolete and there's no need for doing this anyway to become (more) PHP 8.2 compatible?

spokje’s picture

FWIW: I locally did a rebuild of the PHPStan baseline with mglaman/phpstan-drupal 1.1.22 and all the failures we're fixing in the MR here are indeed gone.

So this issue will be obsolete when Core requires that version or higher.

mglaman’s picture

The next release of phpstan-drupal will include a stub for FilterProcessResult to define $callable as callable-string, fixing the one reported error here. See https://github.com/mglaman/phpstan-drupal/pull/453

andypost’s picture

andypost’s picture

Filed bug for PHP 8.2 #3299327-3: Replace deprecated static::method() calls for PHP 8.2

https://wiki.php.net/rfc/deprecate_partially_supported_callables
btw the recommended variant of Callable is

If compatibility with PHP < 8.1 is not desired, use of the first-class callable syntax self::method(...) is also possible.

andypost’s picture

There's even Rector rule https://github.com/rectorphp/rector-src/pull/2544 and follow-up so rectorphp >=0.13.8 is required

spokje’s picture

Status: Postponed » Closed (outdated)

As far as I can tell, this issue is now outdated, since all the (false) positives in PHPStan are gone since #3299606: Update mglaman/phpstan-drupal to 1.1.25 to unblock testing on PHP 8.2 landed.

andypost’s picture

Status: Closed (outdated) » Postponed
Related issues: +#3299327: Replace deprecated static::method() calls for PHP 8.2

@Spokje please check #42 - I'm sure we must normalize callables as 10+ requires PHP 8.1

I did not update IS because not clear how to apply rector rule to convert'em all

Anyway it should be postponed on #3299327: Replace deprecated static::method() calls for PHP 8.2

spokje’s picture

I looked at #37 when I closed this one. I'll leave it as it is now, because I'm lost at this point.

longwave’s picture

static::class . "::method" is the format used here, and this is fine; this is suggested in the RFC as a replacement for static::method. I don't think we need to do anything here, even for future PHP versions - only #3299327: Replace deprecated static::method() calls for PHP 8.2.

andypost’s picture

@longwave the suggested method is to use first-class callable since 8.1 as it's more optimized (require no conversion from array to callable so improve performance)

andypost’s picture

Title: Replace usages of static::class . '::methodName' to [static::class, 'methodName'] » Replace usages of static::class . '::methodName' to first-class callable syntax static::method(...)
Issue summary: View changes
Issue tags: -Needs issue summary update +PHP 8.1

re-title to make it clear and updated IS

andypost’s picture

Status: Postponed » Needs work

Parent issue commited and there's related follow-up #3309750: Fix callable syntax for PHP 8.2 in Views.php

andypost’s picture

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

last blocker for PHP 8.2 commited, so this can be commited as improvement only to 10.1

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new3.68 KB

Here's a start patch

andypost’s picture

StatusFileSize
new8.92 KB
new12.61 KB

And here conversion of "static::" and "self::" I found (surely needs rector)

Meantime discovered bug, so callables help #3318581: FIx \Drupal\Core\Config\ConfigImporter::doSyncStep() to accept callable

andypost’s picture

StatusFileSize
new12.67 KB

patch #54 for 10.1.x (some offset)

Status: Needs review » Needs work

The last submitted patch, 55: 3259716-55.patch, failed testing. View results

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new581 bytes
new12.7 KB

Fix failed test

andypost’s picture

StatusFileSize
new515 bytes
new12.74 KB

Use callable in module handler instead of call_user_func_array

nod_’s picture

Should this form be changed also?

 $class = static::class;

      '#element_validate' => [
        [$class, 'validateDatetime'],
      ]

into something like


    // Note that since this information is cached, the #date_timezone property
    // is not set here, as this needs to vary potentially by-user.
    return [
      '#input' => TRUE,
      '#element_validate' => [
        static::validateDatetime(...),
      ]

Not sure I get all the implications. In any case the patch from #58 looks good

mondrake’s picture

Issue tags: -PHPStan, -PHPStan-0

This doesn't look like it's related to PHPStan anymore.

andypost’s picture

@nod_ it's great question but closures are not serializable so it should break render cache, I see a lot of usage in render element classes so they should remain serializable for form-api purposes.

$ php -r '$f=strlen(...);echo serialize($f);'
PHP Fatal error:  Uncaught Exception: Serialization of 'Closure' is not allowed in Command line code:1
Stack trace:
#0 Command line code(1): serialize()
#1 {main}
  thrown in Command line code on line 1
nod_’s picture

Status: Needs review » Needs work

Thanks a lot for the explanations :)

  • Maybe \Drupal\user\Entity\Role::postLoad could be update from uasort($entities, [static::class, 'sort']); uasort($entities, static::sort(...));?
  • \Drupal\migrate_drupal_ui\Batch\MigrateUpgradeImportBatch::run might have some, it's for batch related things so it might not be possible because of a serialization thing
  • in \Drupal\Core\Mail\MailFormatHelper::htmlToText the call to $string = preg_replace_callback($pattern, [static::class, 'htmlToMailUrls'], $string); could be updated?
  • in \Drupal\views\Plugin\views\filter\Date::hasValidGroupedValue the call to $actual = count(array_filter($group['value'], [static::class, 'arrayFilterZero'])); could be updated?

All the other static::class i could see were used in render arrays or in tests so per #61 they should be left alone.

Saw a couple of array_map([static::class, __FUNCTION__], $merged) in json API module, or 'callback' => [static::class, 'doesNotExist'] in tests but I'm not sure those are relevant for this issue.

kim.pepper’s picture

Status: Needs work » Needs review
StatusFileSize
new12.62 KB

Here's patch #58 for Drupal 9.5.x

needs-review-queue-bot’s picture

Status: Needs review » Needs work
StatusFileSize
new144 bytes

The Needs Review Queue Bot tested this issue. It either no longer applies to Drupal core, or fails the Drupal core commit checks. Therefore, this issue status is now "Needs work".

Apart from a re-roll or rebase, this issue may need more work to address feedback in the issue or MR comments. To progress an issue, incorporate this feedback as part of the process of updating the issue. This helps other contributors to know what is outstanding.

Consult the Drupal Contributor Guide to find step-by-step guides for working with issues.

andypost’s picture

Status: Needs work » Needs review
StatusFileSize
new3.19 KB
new12.74 KB
andypost’s picture

StatusFileSize
new1.23 KB
new13.97 KB

And 2 more places found

andypost’s picture

Issue summary: View changes
StatusFileSize
new8.33 KB
new21.99 KB

And a bit more for git grep "'strtolower'" and git grep "'array_merge'"

ahmadhalah’s picture

StatusFileSize
new12.57 KB

Tested manually it works on Drupal core 9.5.6

smustgrave’s picture

Status: Needs review » Needs work
Issue tags: +Needs Review Queue Initiative

@ahmadhalah patch #67 applied cleanly to D10 and patch #68 seems to be missing bulk of changes.

Applied patch #67

Searched for [static::class,
There are 82 matches still.

Could it be added to the issue summary specifically what is being replaced and what isn't?

ameymudras’s picture

Status: Needs work » Needs review
StatusFileSize
new39.31 KB
new15.12 KB

Replacing a few more instances of static::class

smustgrave’s picture

Status: Needs review » Needs work

I don’t know if replacing more is the answer. Should be documented in IS what is being replaced though

ahmadhalah’s picture

@smustgrave patch #68 works with Drupal 9.6.5. Did you test it on that version?

anchal_gupta’s picture

StatusFileSize
new39.31 KB
new814 bytes

Fixed CCF

smustgrave’s picture

Hiding files up till #67

@andypost could you update what is being replaced in the IS please.

andypost’s picture

new candidates from #3351004: Deprecations for PHP 8.1 get_class() and get_called_class() without argument

@smustgrave it looks like it needs split as growing, basically all places that are not supposed to be serializable can benefit from that

dpi’s picture

Looks like we're addressing array-callable, not just string callable. And \Closure::fromCallable, others?

Issue summary needs update, title could lose the mention of string.

dpi’s picture

kim.pepper’s picture

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.

donquixote’s picture

We should be careful with this.

Replace usages of [static::class, '::methodName'] to static::methodName(...) which is more readable and performant since PHP 8.1

Readable, yes.
Also, closures are _verified_. Once we have a closure object, we know it can be called. The 'Call to undefined function' error happens when the closure is created, not when it is called - which makes for nicer error detection.

But:
More performant - yes, but it depends.
Also it is not serializable - but this is only relevant for caching between requests.

I created this script to compare different ways of calling different types of callbacks:
https://gist.github.com/donquixote/85efcca90056111e967dd14cb1f9de9c
It can be a good idea to copy the result into a spreadsheet app and sort by different columns.
The results could be different depending on php configuration etc. Also I found some unexpected side effects from changing the total number of repetitions and the order of comparison candidates.

Conclusions:

  • For any callable value, If we want to call it only one time, it is faster to call it directly than to first convert it to a closure object.
  • If we have a closure, the cost of calling is almost the same no matter if it is a function or a class (static) method. (if they have the same parameter signature).
  • Calling a closure is generally faster than calling a callable-string or callable-array, if we don't include the time for converting it to a closure.
  • Time can be lost for the extra step of converting to closure. So it is only worthwhile if we call it repeatedly, or if the alternative would cost us the same.

E.g. the way we currently do it in ModuleHandler actually makes things slower, not faster, because we do not reuse the $hookInvoker.

  public function invokeAllWith(string $hook, callable $callback): void {
    foreach (array_keys($this->getImplementationInfo($hook)) as $module) {
      $hookInvoker = \Closure::fromCallable($module . '_' . $hook);
      $callback($hookInvoker, $module);
    }
  }

It would be faster to pass the callable-string directly:

  public function invokeAllWith(string $hook, callable $callback): void {
    foreach (array_keys($this->getImplementationInfo($hook)) as $module) {
      $callback($module . '_' . $hook, $module);
    }
  }

The performance argument does not need to stop us, there can be sufficient other reasons to use first-class callables.
However, if we want to use performance as an argument _for_ first-class callables, we need to look more closely.

donquixote’s picture

-    $map = array_map('strtolower', $connection->schema()->getFieldTypeMap());
+    $map = array_map(strtolower(...), $connection->schema()->getFieldTypeMap());

We can do this, but from what I found it will be slower, not faster. Imo in a case like this, a literal string is totally fine, and looks even simpler than the first-class callable. It is even less ambiguous because it is automatically in the root namespace.
Also, the IDE (or a static analysis tool) sees that the parameter is callable, so it can know that the literal string refers to a function.

-    $element['#element_validate'][] = [static::class, 'validateElement'];
+    $element['#element_validate'][] = static::validateElement(...);

Should the form and render array stuff not be serializable?
Maybe in some cases it doesn't need to be.
I think a raw original form is cached, but a processed form is not. So maybe it's fine.
This being said, the first-class callables do look nicer than the callable arrays. So for these I totally see the point, whenever it works.

-        '#element_validate' => [[static::class, 'elementValidateFilter']],
+        '#element_validate' => static::elementValidateFilter(...),

This is missing brackets.

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.