Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ release-by-release.

## [Unreleased]

### Added

- **`AddSymfonyConstraintValidatorTypeDeclarationsRector`** — adds the Symfony 8 / Drupal 12 type declarations (`mixed $value` and `: void` on `validate()`, `: void` on `initialize()`) to `Symfony\Component\Validator\ConstraintValidatorInterface` implementers. Backward compatible on all supported Drupal versions, so no version gate. [#3600790]

## [1.0.0] — 2026-07-02

### Changed
Expand Down
9 changes: 9 additions & 0 deletions config/drupal-11/drupal-11.0-deprecations.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use DrupalRector\Drupal11\Rector\Deprecation\MigrateSqlGetMigrationPluginManagerRector;
use DrupalRector\Drupal11\Rector\Deprecation\RemoveStateCacheSettingRector;
use DrupalRector\Drupal11\Rector\Deprecation\StripMigrationDependenciesExpandArgRector;
use DrupalRector\Drupal12\Rector\Deprecation\AddSymfonyConstraintValidatorTypeDeclarationsRector;
use DrupalRector\Rector\PHPUnit\PhpUnitTestAnnotationToAttributeRector;
use DrupalRector\Rector\PHPUnit\ValueObject\PhpUnitTestAnnotationToAttributeConfiguration;
use DrupalRector\Rector\ValueObject\DrupalIntroducedVersionConfiguration;
Expand Down Expand Up @@ -59,4 +60,12 @@
new PhpUnitTestAnnotationToAttributeConfiguration('11.0.0', '12.0.0', 'depends', 'PHPUnit\Framework\Attributes\Depends'),
new PhpUnitTestAnnotationToAttributeConfiguration('11.0.0', '12.0.0', 'testWith', 'PHPUnit\Framework\Attributes\TestWith'),
]);

// Forward-compat Symfony 8 / Drupal 12 signature change (NOT an 11.0
// deprecation). Registered here so it fires on drupal/core ^11.0 (every D11
// install) — the change is backward compatible, so applying it while still
// on D11 is safe and prepares the module for D12. Also registered in the
// drupal-12.0 set for installs already on ^12.0.
// https://git.drupalcode.org/project/redirect/-/merge_requests/200
$rectorConfig->rule(AddSymfonyConstraintValidatorTypeDeclarationsRector::class);
};
16 changes: 16 additions & 0 deletions config/drupal-12/drupal-12-all-deprecations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

declare(strict_types=1);

use DrupalRector\Set\Drupal12SetList;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
$rectorConfig->sets([
Drupal12SetList::DRUPAL_120,
]);

$rectorConfig->bootstrapFiles([
__DIR__.'/../drupal-phpunit-bootstrap-file.php',
]);
};
15 changes: 15 additions & 0 deletions config/drupal-12/drupal-12.0-deprecations.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

declare(strict_types=1);

use DrupalRector\Drupal12\Rector\Deprecation\AddSymfonyConstraintValidatorTypeDeclarationsRector;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
// Symfony 8 (Drupal 12) added native `: void` return types to
// ConstraintValidatorInterface::validate()/initialize() and `mixed $value`
// to validate() (the latter since Symfony 7). Add them to implementers.
// Backward compatible on all supported Drupal versions, so no version gate.
// https://git.drupalcode.org/project/redirect/-/merge_requests/200
$rectorConfig->rule(AddSymfonyConstraintValidatorTypeDeclarationsRector::class);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?php

declare(strict_types=1);

namespace DrupalRector\Drupal12\Rector\Deprecation;

use PhpParser\Node;
use PhpParser\Node\Identifier;
use PhpParser\Node\Stmt\Class_;
use PhpParser\Node\Stmt\ClassMethod;
use PHPStan\Type\ObjectType;
use Rector\Rector\AbstractRector;
use Symplify\RuleDocGenerator\ValueObject\CodeSample\CodeSample;
use Symplify\RuleDocGenerator\ValueObject\RuleDefinition;

/**
* Adds the Symfony 8 / Drupal 12 type declarations to
* ConstraintValidatorInterface implementers.
*
* Symfony 7.0 (Drupal 11.0) added `mixed $value` to
* ConstraintValidatorInterface::validate(); Symfony 8.0 (Drupal 12.0) added
* native `: void` return types to validate() and initialize(). This rector adds
* those to any implementer (`extends ConstraintValidator`,
* `implements ConstraintValidatorInterface`, or the legacy
* `extends Constraint implements ConstraintValidatorInterface`).
*
* The change is backward compatible on every supported Drupal version: a child
* may declare types its interface does not (Drupal core itself ships
* `validate(mixed $value, Constraint $constraint): void` while running on a
* Symfony 7.4 interface without the native `: void`), so no BC wrapping or
* version gate is needed.
*
* @see https://git.drupalcode.org/project/redirect/-/merge_requests/200 (issue #3602388)
* @see https://github.com/symfony/symfony/blob/8.0/src/Symfony/Component/Validator/ConstraintValidatorInterface.php
*/
final class AddSymfonyConstraintValidatorTypeDeclarationsRector extends AbstractRector
{
private const CONSTRAINT_VALIDATOR_INTERFACE = 'Symfony\Component\Validator\ConstraintValidatorInterface';

public function getRuleDefinition(): RuleDefinition
{
return new RuleDefinition(
'Add Symfony ConstraintValidatorInterface type declarations (mixed $value / : void) to validator classes.',
[
new CodeSample(
<<<'CODE_BEFORE'
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class MyValidator extends ConstraintValidator {
public function validate($value, Constraint $constraint) {
}
}
CODE_BEFORE,
<<<'CODE_AFTER'
use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class MyValidator extends ConstraintValidator {
public function validate(mixed $value, Constraint $constraint): void {
}
}
CODE_AFTER
),
]
);
}

/** @return array<class-string<Node>> */
public function getNodeTypes(): array
{
return [Class_::class];
}

/**
* @param Class_ $node
*/
public function refactor(Node $node): ?Node
{
if (!$this->isObjectType($node, new ObjectType(self::CONSTRAINT_VALIDATOR_INTERFACE))) {
return null;
}

$changed = false;

$validate = $node->getMethod('validate');
if ($validate instanceof ClassMethod) {
if ($this->addMixedFirstParam($validate)) {
$changed = true;
}
if ($this->addVoidReturnType($validate)) {
$changed = true;
}
}

$initialize = $node->getMethod('initialize');
if ($initialize instanceof ClassMethod) {
if ($this->addVoidReturnType($initialize)) {
$changed = true;
}
}

return $changed ? $node : null;
}

private function addVoidReturnType(ClassMethod $method): bool
{
if ($method->returnType !== null) {
return false;
}

$method->returnType = new Identifier('void');

return true;
}

private function addMixedFirstParam(ClassMethod $method): bool
{
if (!isset($method->params[0]) || $method->params[0]->type !== null) {
return false;
}

$method->params[0]->type = new Identifier('mixed');

return true;
}
}
11 changes: 11 additions & 0 deletions src/Set/Drupal12SetList.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

namespace DrupalRector\Set;

final class Drupal12SetList
{
public const DRUPAL_12 = __DIR__.'/../../config/drupal-12/drupal-12-all-deprecations.php';
public const DRUPAL_120 = __DIR__.'/../../config/drupal-12/drupal-12.0-deprecations.php';
}
3 changes: 2 additions & 1 deletion src/Set/DrupalSetProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ final class DrupalSetProvider implements SetProviderInterface
'11.2' => Drupal11SetList::DRUPAL_112,
'11.3' => Drupal11SetList::DRUPAL_113,
'11.4' => Drupal11SetList::DRUPAL_114,
'12.0' => Drupal12SetList::DRUPAL_120,
];

/**
Expand All @@ -74,7 +75,7 @@ final class DrupalSetProvider implements SetProviderInterface
*
* @var string[]
*/
private const MAJOR_FLOORS = ['10.0', '11.0'];
private const MAJOR_FLOORS = ['10.0', '11.0', '12.0'];

/**
* @return SetInterface[]
Expand Down
13 changes: 13 additions & 0 deletions stubs/Symfony/Component/Validator/Constraint.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\Validator;

if (class_exists(\Symfony\Component\Validator\Constraint::class)) {
return;
}

abstract class Constraint
{
}
22 changes: 22 additions & 0 deletions stubs/Symfony/Component/Validator/ConstraintValidator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\Validator;

use Symfony\Component\Validator\Context\ExecutionContextInterface;

if (class_exists(\Symfony\Component\Validator\ConstraintValidator::class)) {
return;
}

abstract class ConstraintValidator implements ConstraintValidatorInterface
{
public function initialize(ExecutionContextInterface $context): void
{
}

public function validate(mixed $value, Constraint $constraint)
{
}
}
18 changes: 18 additions & 0 deletions stubs/Symfony/Component/Validator/ConstraintValidatorInterface.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\Validator;

use Symfony\Component\Validator\Context\ExecutionContextInterface;

if (interface_exists(\Symfony\Component\Validator\ConstraintValidatorInterface::class)) {
return;
}

interface ConstraintValidatorInterface
{
public function initialize(ExecutionContextInterface $context);

public function validate(mixed $value, Constraint $constraint);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Symfony\Component\Validator\Context;

if (interface_exists(\Symfony\Component\Validator\Context\ExecutionContextInterface::class)) {
return;
}

interface ExecutionContextInterface
{
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<?php

declare(strict_types=1);

namespace DrupalRector\Tests\Drupal12\Rector\Deprecation\AddSymfonyConstraintValidatorTypeDeclarationsRector;

use DrupalRector\Tests\AbstractDrupalRectorTestCase;

class AddSymfonyConstraintValidatorTypeDeclarationsRectorTest extends AbstractDrupalRectorTestCase
{
#[\PHPUnit\Framework\Attributes\DataProvider('provideData')]
public function test(string $filePath): void
{
$this->doTestFile($filePath);
}

public static function provideData(): \Iterator
{
return self::yieldFilesFromDirectory(__DIR__.'/fixture');
}

public function provideConfigFilePath(): string
{
return __DIR__.'/config/configured_rule.php';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
<?php

declare(strict_types=1);

use DrupalRector\Drupal12\Rector\Deprecation\AddSymfonyConstraintValidatorTypeDeclarationsRector;
use DrupalRector\Tests\Rector\Deprecation\DeprecationBase;
use Rector\Config\RectorConfig;

return static function (RectorConfig $rectorConfig): void {
DeprecationBase::addClass(AddSymfonyConstraintValidatorTypeDeclarationsRector::class, $rectorConfig, false);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

namespace Drupal\mymodule\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class AlreadyTypedValidator extends ConstraintValidator {

public function validate(mixed $value, Constraint $constraint): void {
}

}
?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Drupal\mymodule\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;

class SomeConstraint extends Constraint {

public function validate($value, Constraint $constraint) {
}

}
?>
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
<?php

namespace Drupal\mymodule\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class UniqueHashValidator extends ConstraintValidator {

public function validate($redirect, Constraint $constraint) {
}

}
?>
-----
<?php

namespace Drupal\mymodule\Plugin\Validation\Constraint;

use Symfony\Component\Validator\Constraint;
use Symfony\Component\Validator\ConstraintValidator;

class UniqueHashValidator extends ConstraintValidator {

public function validate(mixed $redirect, Constraint $constraint): void {
}

}
?>
Loading
Loading