Skip to content

Service map is empty when running under Rector — bootstrap never executes #995

Description

@bbrala

phpstan-drupal builds the service map in DrupalAutoloader::register(), which runs as a bootstrapFiles entry (drupal-autoloader.php). PHPStan executes those through CommandHelper::executeBootstrapFile(). Any tool that builds the PHPStan container directly never calls CommandHelper, so the bootstrap is skipped and the map stays empty.

Rector is the case. Its PHPStanServicesFactory calls ContainerFactory::create() and stops there — no CommandHelper. So under rector process, \Drupal::service('renderer') resolves to object instead of Drupal\Core\Render\Renderer. The same goes for everything else that leans on the map. Under phpstan analyse it's fine: I see 2075 services mapped. Under Rector the bootstrap never fires.

This matters because Rector uses phpstan-drupal for the type info that drives type-aware rules. With an empty map, every \Drupal::service('...')->someDeprecatedMethod() is invisible to those rules and silently skipped — no rewrite where phpstan analyse would have had the type.

The obvious Rector-side workaround doesn't work:

$rectorConfig->bootstrapFiles([__DIR__ . '/vendor/mglaman/phpstan-drupal/drupal-autoloader.php']);
[ERROR] The autoloader did not receive the container.

Rector requires bootstrap files with no $container in scope, and drupal-autoloader.php needs it. Larastan's bootstrap is container-free, so the same trick works for Larastan — see rectorphp/rector#8006.

The fix is to not depend on the bootstrap running. Populate the map during container build instead. A guarded, eagerly-instantiated service does it without touching the existing scan:

final class RectorServiceMapInitializer implements DynamicStaticMethodReturnTypeExtension
{
    public function __construct(Container $container, ServiceMap $serviceMap)
    {
        if ($serviceMap->getServices() === []) {
            (new DrupalAutoloader())->register($container);
        }
    }

    // getClass(): 'Drupal'; isStaticMethodSupported(): false; getTypeFromStaticMethodCall(): null
}

Register it with tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension] so PHPStan instantiates it when the broker builds. The empty-map guard keeps a normal phpstan analyse run a no-op, since the bootstrap already filled the map.

I've run this against Drupal 11 with Rector 2.4 across a full install (webform, photoswipe). \Drupal::service('renderer') resolves to Renderer and \Drupal::service('entity_type.manager') to EntityTypeManager, Rector processes both modules without errors, and standalone phpstan analyse behaves exactly as before.

I'm not attached to this shape. Lazy population inside ServiceMap would be cleaner if you'd prefer it. Happy to open a PR either way.


Claude was used for research and development of this issue and fix.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions