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 src and 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 in InnerPluginSettingsTrait, InteractionPluginConfigTrait and OrchestraCmForm: the interface does not declare it, so the call depends on the plugin also being ConfigurableInterface.
  • FormInterface::getEntity() in DomainTenantHook and ConsumerTenantHook, same shape: the form has to be an EntityFormInterface for that to hold.
  • Two Views field plugins, InstanceReference and InstanceTraceLink, whose render() returns a render array where the signature promises MarkupInterface|string.
  • DelegationForm::validateForm() declares void against ContentEntityFormInterface::validateForm().
  • AdvanceQueueDrainer and OrchestraUiController read ->data off claimItem(), which core documents as object|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

Command icon 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

mably created an issue. See original summary.

mably’s picture

Status: Active » Needs review

  • mably committed d2490b10 on 1.x
    task: #3618612 Raise PHPStan to level 3 with an entity mapping, because...
mably’s picture

Status: Needs review » Fixed

Now that this issue is closed, review the contribution record.

As a contributor, attribute any organization that helped you, or if you volunteered your own time.

Maintainers, credit people who helped resolve this issue.

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.