Composer Based Custom Set
This page is for package maintainers who want to ship a single set that adapts to whatever version of the package a project has installed. If you only want to use composer-based sets, see Composer-Based Sets.
For a real-world example, have a look at the Laravel one: rector-laravel composer-based.php.
Make a rule bound to a package version
Let's say a rule should only run on PHPUnit 11 and above, because createStub() did not exist before. Implement ComposerPackageConstraintInterface:
use PhpParser\Node;
use Rector\Rector\AbstractRector;
use Rector\VersionBonding\Contract\ComposerPackageConstraintInterface;
use Rector\VersionBonding\ValueObject\ComposerPackageConstraint;
final class CreateStubOverCreateMockArgRector extends AbstractRector implements ComposerPackageConstraintInterface
{
public function provideComposerPackageConstraint(): ComposerPackageConstraint
{
return new ComposerPackageConstraint('phpunit/phpunit', '>=11.0');
}
// getNodeTypes(), refactor() as usual
}
The constraint is a plain composer version constraint, so anything composer/semver understands works - >=11.0, >=10.0 <13.0, ^7.4.
That's it. The rule can now be registered in any set. On a project with PHPUnit 9 it is filtered out before the first file is parsed, and vendor/bin/rector composer-based reports it as not active.
Test it against a version you don't have installed
Your own test suite runs on a single PHPUnit version, but the rule must be tested against the version it targets. Override provideComposerJsonFilePath() in the test case and point it to a standalone composer.json. The versions are then read from its require and require-dev sections, instead of from the installed packages:
use Rector\Testing\PHPUnit\AbstractRectorTestCase;
final class CreateStubOverCreateMockArgRectorTest extends AbstractRectorTestCase
{
protected function provideComposerJsonFilePath(): ?string
{
return __DIR__ . '/composer.json';
}
// ...
}
{
"require-dev": {
"phpunit/phpunit": "^11.0"
}
}
Bind a rule configuration to a package version
Sometimes the rule itself is version-agnostic, but its configuration is not. A method rename only makes sense once the new method exists, and an annotation-to-attribute conversion only makes sense while that attribute exists.
Use ruleWithConfigurationComposerVersionBound() in your set file - the same rule class, registered once per version range:
use Rector\Config\RectorConfig;
use Rector\Renaming\Rector\MethodCall\RenameMethodRector;
use Rector\Renaming\ValueObject\MethodCallRename;
return static function (RectorConfig $rectorConfig): void {
// MockBuilder::onlyMethods() was added in PHPUnit 8.3
$rectorConfig->ruleWithConfigurationComposerVersionBound(RenameMethodRector::class, [
new MethodCallRename('PHPUnit\Framework\MockObject\MockBuilder', 'setMethods', 'onlyMethods'),
], 'phpunit/phpunit', '>=8.3');
// expectExceptionMessageIsOrContains() was added in PHPUnit 13.2
$rectorConfig->ruleWithConfigurationComposerVersionBound(RenameMethodRector::class, [
new MethodCallRename(
'PHPUnit\Framework\TestCase',
'expectExceptionMessage',
'expectExceptionMessageIsOrContains'
),
], 'phpunit/phpunit', '>=13.2');
};
The arguments are the rule class, its configuration, the package name and the version constraint.
If the installed version does not satisfy the constraint, the configuration is not registered at all - but it is still reported by vendor/bin/rector composer-based as inactive, so nothing silently disappears.
Upper bounds work as well, for configuration that must stop being applied:
use Rector\Php80\Rector\Class_\AnnotationToAttributeRector;
use Rector\Php80\ValueObject\AnnotationToAttribute;
// the RunClassInSeparateProcess attribute was added in PHPUnit 10.0 and removed in PHPUnit 13.0
$rectorConfig->ruleWithConfigurationComposerVersionBound(AnnotationToAttributeRector::class, [
new AnnotationToAttribute(
'runClassInSeparateProcess',
'PHPUnit\Framework\Attributes\RunClassInSeparateProcess'
),
], 'phpunit/phpunit', '>=10.0 <13.0');
That way a single set file handles every version of the package, and your rector.php stays a one-liner - even 3 major versions later.