Report constraint plugins that break on Drupal 12 because they have no named-arguments constructor

Open
#1,075 0 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

@goba is already working on this.

Since Nov 9, 2026.

  • #1077 by @goba — open

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
35/100
Issue type
Feature
Clarity
Clearly specified
Activity status
Stale
Tech stack
drupal, php

Research direction

Start by reading the existing PHPStan rule implementations and tests for Drupal constraint plugins, then inspect how they identify constraint classes and constructors. Add coverage for classes with public option properties and no constructor carrying HasNamedArguments, including the allowed inherited constructor case. Done when the rule reports the reproducing plugin and its tests pass; an open pull request is already linked to this issue.

Written by the indexing model from the issue text.

Description

Summary

Drupal 11.4 started moving constraint plugins to named arguments, because Symfony deprecated the options array. See https://www.drupal.org/node/3554746.

  • Drupal 11. For a plugin with options and no constructor with #[HasNamedArguments], ConstraintFactory::createInstance() passes the options as an array: new $plugin_class($configuration). Symfony 7.4 triggers at runtime: "Support for evaluating options in the base Constraint class is deprecated. Initialize properties in the constructor of ProbeConstraint instead."
  • Drupal 12. It uses Symfony 8.1, and ConstraintFactory::createInstance() spreads the options as named arguments: new $plugin_class(...$configuration). A plugin without its own constructor then fails with Error: Unknown named parameter $max.

PHPStan doesn't see either one, because the deprecation is triggered with data, not by a deprecated method.

Steps to reproduce

On Drupal 11.5.x-dev, with PHPStan 2.3.5, phpstan-drupal 2.2.2 and phpstan-deprecation-rules, at level 2:

<?php

declare(strict_types=1);

namespace Drupal\pst_probe\Plugin\Validation\Constraint;

use Drupal\Core\StringTranslation\TranslatableMarkup;
use Drupal\Core\Validation\Attribute\Constraint;
use Symfony\Component\Validator\Constraint as SymfonyConstraint;

#[Constraint(
  id: 'PstProbe',
  label: new TranslatableMarkup('Probe', [], ['context' => 'Validation']),
)]
final class ProbeConstraint extends SymfonyConstraint {

  public string $message = 'The value is not valid.';

  public int $max = 10;

}

And a use of it:

BaseFieldDefinition::create('string')->addConstraint('PstProbe', ['max' => 5]);
new ProbeConstraint(['message' => 'Too long.']);

Result: [OK] No errors.

The same class with Symfony 8.1, as Drupal 12 creates it:

$configuration = ['max' => 5];
new ProbeConstraint(...$configuration);
// Error: Unknown named parameter $max

How often this happens

In unpatched copies of 75 contributed modules and themes from the Drupal CMS stack, 52 constraint classes have public option properties and no constructor: canvas 34, ai_validations 7, scheduler_content_moderation_integration 3, ai 3, trash 2, crop 2 and eca 1. Each one breaks on Drupal 12 when a field definition, config schema or code passes options to it.

Proposed resolution

A rule should be added for classes that extend Symfony\Component\Validator\Constraint and are Drupal constraint plugins, with the Constraint attribute or annotation:

  • If the class declares public properties (its options), and its constructor is the one of the base Constraint class, report it. A constructor with the #[HasNamedArguments] attribute in the class or in a parent, like Symfony's Choice or Collection, is fine. For example: "Constraint plugin ProbeConstraint has options but no constructor with the HasNamedArguments attribute. Drupal 12 passes the options as named arguments, so this constraint fails when it is configured. Add a constructor with named arguments. See https://www.drupal.org/node/3554746."
  • Optionally, report new SomeConstraint([...]) with an array as the first argument, when the constructor of SomeConstraint has no #[HasNamedArguments].

In Drupal 11.5, 73 of core's 95 constraint plugins have a constructor with #[HasNamedArguments]. They are examples of the fix.

LLM disclosure

LLM was used to find and resolve this issue. With human review.

Dominant language
PHP
Stars
208
Forks
88
Avg merge
2d 17h
Merged PRs (30d)
15

Getting set up

This project ships no dev container, Dockerfile or contributing guide, so setting up is up to you: start from its README, and see our first-contribution guide for the general steps.

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from mglaman/phpstan-drupal

All issues in mglaman/phpstan-drupal

Similar issues

More PHP issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.