The phpstan job runs at level 0. This project ships no phpstan configuration, so CI fetches the Drupal Association default asset, which sets level: 0, and _PHPSTAN_LEVEL is not set either. The job log confirms it: phpstan analyze . is invoked with no --level at all. At that level almost nothing is checked, so a wrong type reaches CI unnoticed.
Level 3 is reachable here, and the domain project shows how. Its 4.x branch runs a project phpstan.neon at level: 3 with two companions: an entity_mapping.neon that tells phpstan-drupal which concrete class each entity type id loads as, and a phpstan-baseline.neon holding the five leftovers. The mapping is what makes the level affordable, because without it every $storage->load() is an EntityInterface and every concrete method call on it is reported.
Measured on this module
Against Drupal 11.3.16, over src, modules and tests:
- level 3 with no entity mapping: 448 findings.
- level 3 with a mapping for orchestra's eleven entity types: 178 findings, 31 of them in
srcand 147 in tests.
So the mapping alone removes 270 of them, and what is left is small enough to fix rather than baseline.
The 31 in src
17 distinct messages across 23 files: 20 method.notFound, 6 return.type, 2 property.nonObject, and one each of property.notFound, parameterByRef.type and method.childReturnType. They look like genuine typing bugs rather than noise. A sample:
PluginFormInterface::getConfiguration()called on a plugin form inInnerPluginSettingsTrait,InteractionPluginConfigTraitandOrchestraCmForm: the interface does not declare it, so the call depends on the plugin also beingConfigurableInterface.FormInterface::getEntity()inDomainTenantHookandConsumerTenantHook, same shape: the form has to be anEntityFormInterfacefor that to hold.- Two Views field plugins,
InstanceReferenceandInstanceTraceLink, whoserender()returns a render array where the signature promisesMarkupInterface|string. DelegationForm::validateForm()declaresvoidagainstContentEntityFormInterface::validateForm().AdvanceQueueDrainerandOrchestraUiControllerread->dataoffclaimItem(), which core documents asobject|bool.
The 147 in tests
106 of them are one pattern, property.nonObject, from reading a property off a json_decode() result typed object|true. A single decode helper that asserts the shape would settle most of them at once.
Scope
This is not a Drupal 12 matter. The next-major phpstan job is green today, because the deprecation rules that decide forward compatibility run at every level; see #3618602: Declare Drupal 12 compatibility, so a dependent's next-major lane resolves instead of failing on orchestra's core pin, which declared Drupal 12 support and left this untouched on purpose so the compatibility change stayed reviewable.
Proposed: add phpstan.neon at level 3 with an entity_mapping.neon beside it, then fix the findings rather than baseline them. If any genuinely cannot be fixed, a baseline entry for those should be agreed here first rather than added quietly.
AI-Generated: Yes (Claude Code was used to help draft this issue summary and to take the measurements it quotes. I reviewed them; no code change is proposed yet.)
Issue fork orchestra-3618612
Show commands
Start within a Git clone of the project using the version control instructions.
Or, if you do not have SSH keys set up on git.drupalcode.org:
Comments
Comment #3
mably commentedComment #5
mably commented