Skip to main content
← blog / drupal
Jul 14, 2026  ·  #drupal  ·  3 min read

phpstan-drupal 2.1.0: stricter defaults

phpstan-drupal 2.1.0 is out. The theme of this release: rules and behaviors that proved themselves as opt-ins are now the defaults. If you run `composer update` and see new errors, that is the release working as intended — everything below includes the configuration to opt back out.

Nine rules are now enabled by default

These rules shipped as opt-ins over the 2.0 cycle. They have had time to bake, and they catch real bugs, so they no longer require configuration:

  • testClassSuffixNameRule
  • dependencySerializationTraitPropertyRule
  • accessResultConditionRule
  • cacheableDependencyRule
  • hookFormAlterRule
  • loggerFromFactoryPropertyAssignmentRule
  • entityStorageDirectInjectionRule
  • symfonyYamlParseRule
  • entityOperationsCacheabilityRule

Disable any of them individually if they don't fit your project:

parameters:
   drupal:
       rules:
           cacheableDependencyRule: false

One rename to watch for: hookRules is now hookFormAlterRule. If your phpstan.neon references the old key, PHPStan will reject the configuration until you rename it.

ContainerInterface::has() returns bool now

Previously, $container->has('some.service') resolved to always-true for any service the service map knew about. PHPStan then flagged the guard as redundant:

if ($this->container->has('some.service')) {
   // "Comparison will always evaluate to true" — so you delete the guard.
   $this->container->get('some.service')->doThing();
}

That guard is not redundant. Modules get uninstalled. Site builds differ. The static analyzer knowing about a service does not guarantee the runtime container has it. Deleting the check trades a static-analysis warning for a production crash.

This behavior shipped in bleedingEdge.neon first and has now graduated: has() returns bool for known services by default. If you want the old inference back:

parameters:
   drupal:
       bleedingEdge:
           containerHasAlwaysTrue: true

A three-year-old one-line fix: LoadIncludes

PR #587 from donquixote sat open since 2023. The LoadIncludes rule — which verifies $module_handler->loadInclude() points at a file that exists had its type check inverted:

// Before: bails on concrete ModuleHandler, fires on plain object/mixed.
if (!$type->isSuperTypeOf($moduleHandlerInterfaceType)->yes()) {
   return [];
}

// After: fires only when the receiver is definitely a ModuleHandlerInterface.
if (!$moduleHandlerInterfaceType->isSuperTypeOf($type)->yes()) {
   return [];
}

The practical effect: code that type-hints the concrete ModuleHandler class was silently skipped for years. It gets checked now, so you may see new loadIncludes.* errors in code that never changed. Those are real findings, not regressions.

Opting out of class resolver narrowing

ClassResolverInterface::getInstanceFromDefinition(Foo::class) narrows to Foo. Usually right, but the class resolver checks the container first, and a service definition can substitute an entirely different class. With the narrowed type, this defensive assertion gets flagged as always-true:

$foo = $this->classResolver->getInstanceFromDefinition(Foo::class);
assert($foo instanceof Foo);

The narrowing stays on by default because it is correct for the overwhelming majority of code. If you want your assertions to keep meaning something:

parameters:
   drupal:
       classResolverReturnType: false

Supporting 2.0.x

2.1.0 introduces new errors by default. So 2.0 is not abandoned.

If you hit a bug on 2.0.x, open an issue and note the version. When you are ready to move to 2.1, the release notes walk through every behavior change with its opt-out.

Thanks

donquixote for the LoadIncludes fix, Niklan for the detailed report that shaped classResolverReturnType, and Fame Helsinki for sponsoring the project. If phpstan-drupal saves your team time, consider sponsoring.
 

Thanks for reading

I write about Drupal, PHP, and the quiet infrastructure behind large sites — caching, config, and testing. New posts land every week or two.