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.
phpstan-drupal builds the service map in
DrupalAutoloader::register(), which runs as abootstrapFilesentry (drupal-autoloader.php). PHPStan executes those throughCommandHelper::executeBootstrapFile(). Any tool that builds the PHPStan container directly never callsCommandHelper, so the bootstrap is skipped and the map stays empty.Rector is the case. Its
PHPStanServicesFactorycallsContainerFactory::create()and stops there — noCommandHelper. So underrector process,\Drupal::service('renderer')resolves toobjectinstead ofDrupal\Core\Render\Renderer. The same goes for everything else that leans on the map. Underphpstan analyseit'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 wherephpstan analysewould have had the type.The obvious Rector-side workaround doesn't work:
Rector requires bootstrap files with no
$containerin scope, anddrupal-autoloader.phpneeds 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:
Register it with
tags: [phpstan.broker.dynamicStaticMethodReturnTypeExtension]so PHPStan instantiates it when the broker builds. The empty-map guard keeps a normalphpstan analyserun 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 toRendererand\Drupal::service('entity_type.manager')toEntityTypeManager, Rector processes both modules without errors, and standalonephpstan analysebehaves exactly as before.I'm not attached to this shape. Lazy population inside
ServiceMapwould 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.