 [ ← 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 &#96;composer update&#96; 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:

```bash
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:

```bash
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:

```bash
parameters:
   drupal:
       bleedingEdge:
           containerHasAlwaysTrue: true
```

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

[PR #587](https://github.com/mglaman/phpstan-drupal/pull/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:

```bash
// 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:

```bash
$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:

```bash
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](https://github.com/mglaman/phpstan-drupal/issues) and note the version. When you are ready to move to 2.1, the [release notes](https://github.com/mglaman/phpstan-drupal/releases/tag/2.1.0) 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](https://www.fame.fi/) for sponsoring the project. If phpstan-drupal saves your team time, [consider sponsoring](https://github.com/sponsors/mglaman).

 

  ![](/themes/custom/mglaman/images/headshot.webp)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.

 [Subscribe via RSS →](/rss.xml) [About me →](/about-me) 

 



 

  [← Older

Dynamic type expressions in Drupal config schema

 ](/blog/dynamic-type-expressions-drupal-config-schema) [Newer →

Making the phpstan-drupal playground work for agents

 ](/blog/making-phpstan-drupal-playground-work-agents)