Problem/Motivation
#3311365: Use PHP attributes for route discovery added Attribute based discovery.
In TFA 1.x we have classes that extend Core classes where signatures may from time to time change, in TFA we thus have to selectively choose which class is loaded based on the version of parent class. We do so in a Route Alter Subscriber.
In 11.4.x it appears that the new processor attempts to load all classes (including 'malformed' classes) and does not properly catch errors that may occur.
Similar occurred for missing traits #3502913: Add a fallback classloader that can handle missing traits for attribute discovery
PHP Fatal error: Declaration of Drupal\tfa\Controller\TfaUserControllerDeprecated::resetPassLogin($uid, $timestamp, $hash) must be compatible with Drupal\user\Controller\UserController::resetPassLogin($uid, $timestamp, $hash, Symfony\Component\HttpFoundation\Request $request) in /var/www/html/web/modules/contrib/tfa/src/Controller/TfaUserControllerDeprecated.php on line 15
10 4.0281 14888440 Drupal\Core\Routing\AttributeRouteDiscovery->collectRoutes( ) .../StaticRouteDiscoveryBase.php:93
11 4.0281 14888440 Drupal\Core\Routing\AttributeRouteDiscovery->createRouteCollection( $className = 'Drupal\\tfa\\Controller\\TfaUserControllerDeprecated' ) .../AttributeRouteDiscovery.php:52
12 10.2775 14899488 class_exists( $class = 'Drupal\\tfa\\Controller\\TfaUserControllerDeprecated' ) .../AttributeRouteDiscovery.php:71
13 10.2776 14899600 Composer\Autoload\ClassLoader->loadClass( $class = 'Drupal\\tfa\\Controller\\TfaUserControllerDeprecated' ) .../AttributeRouteDiscovery.php:71
14 10.2777 14899712 {closure:/var/www/html/vendor/composer/ClassLoader.php:575-577}( $file = '/var/www/html/web/modules/custom/tfa/src/Controller/TfaUserControllerDeprecated.php' ) .../ClassLoader.php:427Steps to reproduce
Install TFA 8.x-1.12 on Core 11.x4.x, visit core/rebuild.php or drush cr
Proposed resolution
Catch errors during attribute scanning and continue on operations.
Remaining tasks
User interface changes
Introduced terminology
API changes
Data model changes
Release notes snippet
Issue fork drupal-3592887
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:
- 3592887-11.x
changes, plain diff MR !15958
- 3592887-attribute-route-invalid-classes
changes, plain diff MR !15945
Comments
Comment #2
longwaveWhat's the actual exception/error message here?
Comment #3
longwaveAlso I think #3584793: Use PHP attributes for form route discovery might contain a fix for this, can you check if applying the patch from there solves this please?
Comment #4
cmlaraSorry failed to copy the error message:
PHP Fatal error: Declaration of Drupal\tfa\Controller\TfaUserControllerDeprecated::resetPassLogin($uid, $timestamp, $hash) must be compatible with Drupal\user\Controller\UserController::resetPassLogin($uid, $timestamp, $hash, Symfony\Component\HttpFoundation\Request $request) in /var/www/html/web/modules/contrib/tfa/src/Controller/TfaUserControllerDeprecated.php on line 15Which is expected if one tries to use the deprecated class, two exist and the one chosen is based upon signature:
https://git.drupalcode.org/project/tfa/-/blob/ffccb9eb012695cdef8d0a236e...
Pre 11.4.x core would not attempt to load the incompatible class into memory.
Comment #5
cmlara#3584793: Use PHP attributes for form route discovery just changes the lines around
class_exists()still calls the auto loader(and apparently its not caught by\Error)Comment #6
godotislateDiscussed with @longwave and @catch on Slack: solution for route discovery for now will likely be to do a regex match for the FQCN of the
Routeattribute in the class file contents before checkingclass_exists()and doing reflection. Not sure how extensible this will be for, say, plugins, or hooks, etc., since those have multiple attributes targeted, but we can look into that. (And there could eventually be subclasses of the attributes outside core.)We have #3586328: [11.x] Handle attribute discovery fatal errors on missing traits for routing and other subsystems looking at centralizing how to handle exceptions and fatal errors in auto discovery, at least for PHP < 8.5. But since it's not just missing traits we have to worry about, we maybe have to widen scope there or create a meta.
Comment #7
longwaveComment #8
godotislateExplored using nikic/php-parser to parse attributes way back in #3490322: Use nickic/php-parser for attribute-based plugin discovery to avoid errors. Don't know how much would be relevant now, but linking as related as a possibility to explore using that or some other kind of static parsing.
Comment #9
godotislateAnother option is to make attribute route discovery opt in per module for now. Either with a property in .info.yml or service parameters like
{module}.allow_attribute_routes.Comment #10
longwaveI think we could likely just use the PHP tokenizer directly instead of
nikic/php-parser, but the most performant solution is juststr_contains()to look for the attribute or its class name; this won't cover all edge cases but it's probably good enough. I have this locally but right now the GitLab fork creation doesn't seem to be working...Comment #12
longwaveComment #13
catchI think the str_contains() is a good option, we should try to centralise all of this somewhere which would then help if we end up moving to use the tokenizer too. Not sure if there's already an issue.
Comment #14
cmlaraQuick test of MR against TFA's code base appears to confirm issue resolved.
Visual review: I forsee several false negatives on the detection, the most likely to be a comment similar to
We do not use the [#Route attribute due to foo bar. Possible other false negatives if another class is named Route and its attribute is utilized.Tokenzier of some sort sounds a lot more stable long term. I imagine it has higher performance costs however my understanding is almost all of these areas have been cached container so that they are infrequent.
Ideally none of those false negatives cause issues however worst at least acknowledging that the performant method is also the riskier method and core is doing so knowing these limitations exist.
There is a much larger discussion around how much core wants to designed around "everything works even when bad data is present" vs "throw errors early and often" both have a place in design.
Ultimately for me, as long as the changes don't break code that worked in the previous minor I could really care less which path is chosen as Majors are permitted to significantly break releases.
Subnote: I did a quick test of the earlier linked version and \Throwable did (on PHP8.4) catch the class load failure (though looking through the linked issues sounds like that may not be true on 8.3). That goes back to the above question of how much work with bad data present, or error early.
Comment #17
godotislateCommitted 5f815c3 and pushed to main. Thanks!
Merge conflicts cherry-picking to 11.x, so this will need a backport MR.
Comment #19
longwaveComment #20
godotislateBackport diff looks essentially the same as main.
Comment #23
longwaveCommitted and pushed b0d5f3a7aab to 11.x and 14166896510 to 11.4.x. Thanks!