Skip to content

Do not fatal when a shorthand service class cannot be loaded - #1023

Open
mglaman wants to merge 2 commits into
mainfrom
fix/unloadable-service-class
Open

Do not fatal when a shorthand service class cannot be loaded#1023
mglaman wants to merge 2 commits into
mainfrom
fix/unloadable-service-class

Conversation

@mglaman

@mglaman mglaman commented Aug 4, 2026

Copy link
Copy Markdown
Owner

Problem

Service map generation calls class_exists() on shorthand service IDs (added in #817). class_exists() autoloads the class, and PHP resolves parent classes at declaration time. A service class extending a class from an unavailable module throws an Error that kills the bootstrap before PHPStan analyzes a single file:

Error thrown in modules/canvas_headless/src/RenderConverter/JsComponentCanvasRenderConverter.php
on line 15 while loading bootstrap file vendor/mglaman/phpstan-drupal/drupal-autoloader.php:
Class "Drupal\custom_elements\RenderConverter\CanvasRenderConverter" not found

Canvas hit this: canvas_headless registers a decorator for a custom_elements service with decoration_on_invalid: ignore, and drupal/custom_elements sits in require-dev. Symfony ignores the decorator at runtime. phpstan-drupal fataled anyway, because the crash happens during class load, not container compilation. See https://git.drupalcode.org/project/canvas/-/merge_requests/1460, which works around it with an explicit class: key.

Fix

Catch Throwable around both shorthand class_exists() calls (DrupalAutoloader, ServiceMap) and skip the service when its class cannot load. ServiceMap::setDrupalServices() also skips decorator registration when the decorating service was dropped — previously an undefined array key.

Tests

  • New fixture module service_map_broken with a shorthand service whose class extends a class from a missing module. Every bootstrap-dependent test now exercises the failure path; without the fix, DrupalContainerDynamicReturnTypeTest fatals with the exact error above.
  • ServiceMapFactoryTest::testShorthandServiceWithUnloadableClass covers ServiceMap directly: the broken service is skipped and the decorated service survives with zero decorators.

🤖 Generated with Claude Code

mglaman and others added 2 commits August 4, 2026 09:56
class_exists() on a shorthand service ID autoloads the class. PHP
resolves parent classes at declaration time, so a service class
extending a class from an unavailable module throws an Error that
killed the bootstrap before analysis started. Catch the Throwable and
skip the service instead. Also skip decorator registration when the
decorating service was dropped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Symfony removes the decorating service from the container when the
decorated service does not exist and decoration_on_invalid is set to
ignore. Drop it from the service map to match the runtime container.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mglaman

mglaman commented Aug 4, 2026

Copy link
Copy Markdown
Owner Author

Pushed a second commit that respects decoration_on_invalid: ignore: when the decorated service does not exist and the decorator opts into ignore, the decorating service is dropped from the service map. This matches Symfony's compile-time behavior — the service does not exist in the runtime container either. null and the default exception keep the current behavior.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant