diff --git a/README.md b/README.md
index d75fb0204c..1768ef1e47 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,7 @@ Here is a list of all components that are primarily developed and maintained by
* [sebastian/recursion-context](https://github.com/sebastianbergmann/recursion-context)
* [sebastian/type](https://github.com/sebastianbergmann/type)
* [sebastian/version](https://github.com/sebastianbergmann/version)
+* [sebastian/version-requirement](https://github.com/sebastianbergmann/version-requirement)
A very special thanks to everyone who has contributed to the [PHPUnit Manual](https://github.com/sebastianbergmann/phpunit-documentation-english).
diff --git a/build.xml b/build.xml
index d8b84f68c5..8fc7888927 100644
--- a/build.xml
+++ b/build.xml
@@ -303,6 +303,13 @@
+
+
+
+
+
+
+
diff --git a/composer.json b/composer.json
index 58ba7e0d06..68ce9012e5 100644
--- a/composer.json
+++ b/composer.json
@@ -27,6 +27,7 @@
"security": "https://github.com/sebastianbergmann/phpunit/security/policy"
},
"prefer-stable": true,
+ "minimum-stability": "dev",
"require": {
"php": ">=8.4.1",
"ext-dom": "*",
@@ -55,6 +56,7 @@
"sebastian/recursion-context": "^8.0.0",
"sebastian/type": "^7.0.1",
"sebastian/version": "^7.0.0",
+ "sebastian/version-requirement": "^1.0",
"staabm/side-effects-detector": "^1.0.5"
},
"config": {
diff --git a/composer.lock b/composer.lock
index 9e7067479c..3ed67ce81d 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "fdd1cf0b879485ea837501dd977847ec",
+ "content-hash": "ce29f3be7009692e093af4b0ebb7bc68",
"packages": [
{
"name": "myclabs/deep-copy",
@@ -1732,6 +1732,77 @@
],
"time": "2026-02-06T04:52:52+00:00"
},
+ {
+ "name": "sebastian/version-requirement",
+ "version": "dev-main",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/sebastianbergmann/version-requirement.git",
+ "reference": "7662b2811918858a5421fbbaebdfa87f68f39190"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/sebastianbergmann/version-requirement/zipball/7662b2811918858a5421fbbaebdfa87f68f39190",
+ "reference": "7662b2811918858a5421fbbaebdfa87f68f39190",
+ "shasum": ""
+ },
+ "require": {
+ "phar-io/version": "^3.2.1",
+ "php": ">=8.4"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^13.2.1"
+ },
+ "default-branch": true,
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.0-dev"
+ }
+ },
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de",
+ "role": "lead"
+ }
+ ],
+ "description": "Library for working with version requirements",
+ "homepage": "https://github.com/sebastianbergmann/version-requirement",
+ "support": {
+ "issues": "https://github.com/sebastianbergmann/version-requirement/issues",
+ "security": "https://github.com/sebastianbergmann/version-requirement/security/policy",
+ "source": "https://github.com/sebastianbergmann/version-requirement/tree/main"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/sebastianbergmann",
+ "type": "github"
+ },
+ {
+ "url": "https://liberapay.com/sebastianbergmann",
+ "type": "liberapay"
+ },
+ {
+ "url": "https://thanks.dev/u/gh/sebastianbergmann",
+ "type": "thanks_dev"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/sebastian/version-requirement",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2026-06-20T08:35:21+00:00"
+ },
{
"name": "staabm/side-effects-detector",
"version": "1.0.5",
@@ -1837,7 +1908,7 @@
],
"packages-dev": [],
"aliases": [],
- "minimum-stability": "stable",
+ "minimum-stability": "dev",
"stability-flags": {},
"prefer-stable": true,
"prefer-lowest": false,
diff --git a/src/Metadata/Api/Requirements.php b/src/Metadata/Api/Requirements.php
index a1198206ef..5f15be4919 100644
--- a/src/Metadata/Api/Requirements.php
+++ b/src/Metadata/Api/Requirements.php
@@ -42,6 +42,7 @@
use PHPUnit\Metadata\Version\Requirement;
use PHPUnit\Runner\Version;
use PHPUnit\TextUI\Configuration\Registry as ConfigurationRegistry;
+use SebastianBergmann\VersionRequirement\Requirement as RequirementImplementation;
/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
@@ -68,7 +69,7 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam
$this->warnAboutIncompleteVersion($metadata->versionRequirement(), $className, $methodName);
- if (!$versionRequirement->isSatisfiedBy(PHP_VERSION)) {
+ if (!$this->isSatisfiedBy($versionRequirement, PHP_VERSION)) {
$notSatisfied[] = sprintf(
'PHP %s is required.',
$versionRequirement->asString(),
@@ -91,7 +92,7 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam
if (!extension_loaded($metadata->extension()) ||
($metadata->hasVersionRequirement() &&
- !$metadata->versionRequirement()->isSatisfiedBy($extensionVersion))) {
+ !$this->isSatisfiedBy($metadata->versionRequirement(), $extensionVersion))) {
$notSatisfied[] = sprintf(
'PHP extension %s%s is required.',
$metadata->extension(),
@@ -107,7 +108,7 @@ public function requirementsNotSatisfiedFor(string $className, string $methodNam
$this->warnAboutIncompleteVersion($metadata->versionRequirement(), $className, $methodName);
- if (!$versionRequirement->isSatisfiedBy(Version::id())) {
+ if (!$this->isSatisfiedBy($versionRequirement, Version::id())) {
$notSatisfied[] = sprintf(
'PHPUnit %s is required.',
$versionRequirement->asString(),
@@ -269,6 +270,17 @@ public function requiresXdebug(string $className, string $methodName): bool
return false;
}
+ private function isSatisfiedBy(Requirement $versionRequirement, string $version): bool
+ {
+ $requirement = $versionRequirement->asString();
+
+ if ($versionRequirement instanceof InvalidVersionRequirement || $requirement === '') {
+ return false;
+ }
+
+ return RequirementImplementation::from($requirement)->isSatisfiedBy($version);
+ }
+
/**
* @param class-string $className
* @param non-empty-string $methodName
diff --git a/src/Metadata/Version/ComparisonRequirement.php b/src/Metadata/Version/ComparisonRequirement.php
index b87222fd0d..936080b840 100644
--- a/src/Metadata/Version/ComparisonRequirement.php
+++ b/src/Metadata/Version/ComparisonRequirement.php
@@ -9,7 +9,6 @@
*/
namespace PHPUnit\Metadata\Version;
-use function version_compare;
use PHPUnit\Util\VersionComparisonOperator;
/**
@@ -28,11 +27,6 @@ public function __construct(string $version, VersionComparisonOperator $operator
$this->operator = $operator;
}
- public function isSatisfiedBy(string $version): bool
- {
- return version_compare($version, $this->version, $this->operator->asString());
- }
-
public function asString(): string
{
return $this->operator->asString() . ' ' . $this->version;
diff --git a/src/Metadata/Version/ConstraintRequirement.php b/src/Metadata/Version/ConstraintRequirement.php
index 752fe7b6d1..0ba6f50b97 100644
--- a/src/Metadata/Version/ConstraintRequirement.php
+++ b/src/Metadata/Version/ConstraintRequirement.php
@@ -9,11 +9,6 @@
*/
namespace PHPUnit\Metadata\Version;
-use function assert;
-use function preg_replace;
-use PharIo\Version\Version;
-use PharIo\Version\VersionConstraint;
-
/**
* @immutable
*
@@ -21,35 +16,15 @@
*/
final readonly class ConstraintRequirement extends Requirement
{
- private VersionConstraint $constraint;
+ private string $constraint;
- public function __construct(VersionConstraint $constraint)
+ public function __construct(string $constraint)
{
$this->constraint = $constraint;
}
- public function isSatisfiedBy(string $version): bool
- {
- return $this->constraint->complies(
- new Version($this->sanitize($version)),
- );
- }
-
public function asString(): string
{
- return $this->constraint->asString();
- }
-
- private function sanitize(string $version): string
- {
- $sanitized = preg_replace(
- '/^(\d+\.\d+(?:.\d+)?).*$/',
- '$1',
- $version,
- );
-
- assert($sanitized !== null);
-
- return $sanitized;
+ return $this->constraint;
}
}
diff --git a/src/Metadata/Version/InvalidVersionRequirement.php b/src/Metadata/Version/InvalidVersionRequirement.php
index 9f3e370068..deb04fcb54 100644
--- a/src/Metadata/Version/InvalidVersionRequirement.php
+++ b/src/Metadata/Version/InvalidVersionRequirement.php
@@ -29,11 +29,6 @@ public function __construct(string $message)
$this->message = $message;
}
- public function isSatisfiedBy(string $version): bool
- {
- return false;
- }
-
/**
* @return non-empty-string
*/
diff --git a/src/Metadata/Version/Requirement.php b/src/Metadata/Version/Requirement.php
index 01f98f7310..321b8b5a18 100644
--- a/src/Metadata/Version/Requirement.php
+++ b/src/Metadata/Version/Requirement.php
@@ -9,12 +9,14 @@
*/
namespace PHPUnit\Metadata\Version;
-use function preg_match;
-use PharIo\Version\UnsupportedVersionConstraintException;
-use PharIo\Version\VersionConstraintParser;
+use function assert;
+use function explode;
use PHPUnit\Metadata\InvalidVersionRequirementException;
-use PHPUnit\Util\InvalidVersionOperatorException;
use PHPUnit\Util\VersionComparisonOperator;
+use SebastianBergmann\VersionRequirement\ComparisonRequirement as ComparisonRequirementImplementation;
+use SebastianBergmann\VersionRequirement\ConstraintRequirement as ConstraintRequirementImplementation;
+use SebastianBergmann\VersionRequirement\Exception;
+use SebastianBergmann\VersionRequirement\Requirement as RequirementImplementation;
/**
* @immutable
@@ -23,35 +25,32 @@
*/
abstract readonly class Requirement
{
- private const string VERSION_COMPARISON = "/(?P!=|<|<=|<>|=|==|>|>=)?\s*(?P[\d\.-]+(dev|(RC|alpha|beta)[\d\.])?)[ \t]*\r?$/m";
-
/**
- * @throws InvalidVersionOperatorException
+ * @param non-empty-string $versionRequirement
+ *
* @throws InvalidVersionRequirementException
*/
public static function from(string $versionRequirement): self
{
try {
- return new ConstraintRequirement(
- (new VersionConstraintParser)->parse(
- $versionRequirement,
+ $requirement = RequirementImplementation::from($versionRequirement);
+ } catch (Exception) {
+ throw new InvalidVersionRequirementException;
+ }
+
+ if ($requirement instanceof ComparisonRequirementImplementation) {
+ return new ComparisonRequirement(
+ $requirement->version(),
+ new VersionComparisonOperator(
+ explode(' ', $requirement->asString(), 2)[0],
),
);
- } catch (UnsupportedVersionConstraintException) {
- if (preg_match(self::VERSION_COMPARISON, $versionRequirement, $matches) > 0) {
- return new ComparisonRequirement(
- $matches['version'],
- new VersionComparisonOperator(
- $matches['operator'] !== '' ? $matches['operator'] : '>=',
- ),
- );
- }
}
- throw new InvalidVersionRequirementException;
- }
+ assert($requirement instanceof ConstraintRequirementImplementation);
- abstract public function isSatisfiedBy(string $version): bool;
+ return new ConstraintRequirement($requirement->asString());
+ }
abstract public function asString(): string;
}
diff --git a/src/Util/VersionComparisonOperator.php b/src/Util/VersionComparisonOperator.php
index 9dcba3c32a..0b0d7befcb 100644
--- a/src/Util/VersionComparisonOperator.php
+++ b/src/Util/VersionComparisonOperator.php
@@ -9,7 +9,8 @@
*/
namespace PHPUnit\Util;
-use function in_array;
+use SebastianBergmann\VersionRequirement\InvalidVersionOperatorException as InvalidVersionOperatorExceptionImplementation;
+use SebastianBergmann\VersionRequirement\VersionComparisonOperator as VersionComparisonOperatorImplementation;
/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
@@ -24,15 +25,15 @@
private string $operator;
/**
- * @param '!='|'<'|'<='|'<>'|'='|'=='|'>'|'>='|'eq'|'ge'|'gt'|'le'|'lt'|'ne' $operator
- *
* @throws InvalidVersionOperatorException
*/
public function __construct(string $operator)
{
- $this->ensureOperatorIsValid($operator);
-
- $this->operator = $operator;
+ try {
+ $this->operator = new VersionComparisonOperatorImplementation($operator)->asString();
+ } catch (InvalidVersionOperatorExceptionImplementation) {
+ throw new InvalidVersionOperatorException($operator);
+ }
}
/**
@@ -42,16 +43,4 @@ public function asString(): string
{
return $this->operator;
}
-
- /**
- * @param '!='|'<'|'<='|'<>'|'='|'=='|'>'|'>='|'eq'|'ge'|'gt'|'le'|'lt'|'ne' $operator
- *
- * @throws InvalidVersionOperatorException
- */
- private function ensureOperatorIsValid(string $operator): void
- {
- if (!in_array($operator, ['<', 'lt', '<=', 'le', '>', 'gt', '>=', 'ge', '==', '=', 'eq', '!=', '<>', 'ne'], true)) {
- throw new InvalidVersionOperatorException($operator);
- }
- }
}
diff --git a/tests/unit/Metadata/MetadataCollectionTest.php b/tests/unit/Metadata/MetadataCollectionTest.php
index 591b811d8b..0df5fdaa91 100644
--- a/tests/unit/Metadata/MetadataCollectionTest.php
+++ b/tests/unit/Metadata/MetadataCollectionTest.php
@@ -14,8 +14,7 @@
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
use PHPUnit\Framework\TestCase;
-use PHPUnit\Metadata\Version\ComparisonRequirement;
-use PHPUnit\Util\VersionComparisonOperator;
+use PHPUnit\Metadata\Version\Requirement;
use stdClass;
#[CoversClass(MetadataCollection::class)]
@@ -640,16 +639,10 @@ private function collectionWithOneOfEach(): MetadataCollection
Metadata::requiresOperatingSystemOnClass(''),
Metadata::requiresPhpExtensionOnClass('', null),
Metadata::requiresPhpOnClass(
- new ComparisonRequirement(
- '8.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 8.0.0'),
),
Metadata::requiresPhpunitOnClass(
- new ComparisonRequirement(
- '10.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 10.0.0'),
),
Metadata::requiresPhpunitExtensionOnClass(stdClass::class),
Metadata::requiresEnvironmentVariableOnClass('foo', 'bar'),
diff --git a/tests/unit/Metadata/MetadataTest.php b/tests/unit/Metadata/MetadataTest.php
index 5b0f662074..28e6878b2e 100644
--- a/tests/unit/Metadata/MetadataTest.php
+++ b/tests/unit/Metadata/MetadataTest.php
@@ -14,9 +14,8 @@
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\TestCase;
-use PHPUnit\Metadata\Version\ComparisonRequirement;
+use PHPUnit\Metadata\Version\Requirement;
use PHPUnit\TestFixture\Metadata\Attribute\ExampleTrait;
-use PHPUnit\Util\VersionComparisonOperator;
#[CoversClass(Metadata::class)]
#[CoversClassesThatExtendClass(Metadata::class)]
@@ -3662,10 +3661,7 @@ public function testCanBeRequiresOperatingSystemFamilyOnMethod(): void
public function testCanBeRequiresPhpOnClass(): void
{
$metadata = Metadata::requiresPhpOnClass(
- new ComparisonRequirement(
- '8.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 8.0.0'),
);
$this->assertFalse($metadata->isAfter());
@@ -3736,10 +3732,7 @@ public function testCanBeRequiresPhpOnClass(): void
public function testCanBeRequiresPhpOnMethod(): void
{
$metadata = Metadata::requiresPhpOnMethod(
- new ComparisonRequirement(
- '8.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 8.0.0'),
);
$this->assertFalse($metadata->isAfter());
@@ -3884,10 +3877,7 @@ public function testCanBeRequiresPhpExtensionWithVersionOnClass(): void
{
$metadata = Metadata::requiresPhpExtensionOnClass(
'test',
- new ComparisonRequirement(
- '1.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 1.0.0'),
);
$this->assertFalse($metadata->isAfter());
@@ -4034,10 +4024,7 @@ public function testCanBeRequiresPhpExtensionWithVersionOnMethod(): void
{
$metadata = Metadata::requiresPhpExtensionOnMethod(
'test',
- new ComparisonRequirement(
- '1.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 1.0.0'),
);
$this->assertFalse($metadata->isAfter());
@@ -4110,10 +4097,7 @@ public function testCanBeRequiresPhpExtensionWithVersionOnMethod(): void
public function testCanBeRequiresPhpunitOnClass(): void
{
$metadata = Metadata::requiresPhpunitOnClass(
- new ComparisonRequirement(
- '10.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 10.0.0'),
);
$this->assertFalse($metadata->isAfter());
@@ -4253,10 +4237,7 @@ public function testCanBeRequiresPhpunitExtensionOnClass(): void
public function testCanBeRequiresPhpunitOnMethod(): void
{
$metadata = Metadata::requiresPhpunitOnMethod(
- new ComparisonRequirement(
- '10.0.0',
- new VersionComparisonOperator('>='),
- ),
+ Requirement::from('>= 10.0.0'),
);
$this->assertFalse($metadata->isAfter());
diff --git a/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php b/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php
index 975054679a..8b1742b19b 100644
--- a/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php
+++ b/tests/unit/Metadata/Version/InvalidVersionRequirementTest.php
@@ -25,11 +25,4 @@ public function testCanBeRepresentedAsString(): void
$this->assertSame('message', $requirement->asString());
}
-
- public function testIsNeverSatisfied(): void
- {
- $requirement = new InvalidVersionRequirement('message');
-
- $this->assertFalse($requirement->isSatisfiedBy('1.0.0'));
- }
}
diff --git a/tests/unit/Metadata/Version/RequirementTest.php b/tests/unit/Metadata/Version/RequirementTest.php
index f94a05bd45..6eb4c378e2 100644
--- a/tests/unit/Metadata/Version/RequirementTest.php
+++ b/tests/unit/Metadata/Version/RequirementTest.php
@@ -9,9 +9,7 @@
*/
namespace PHPUnit\Metadata;
-use PharIo\Version\VersionConstraintParser;
use PHPUnit\Framework\Attributes\CoversClass;
-use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Group;
use PHPUnit\Framework\Attributes\Small;
use PHPUnit\Framework\Attributes\UsesClass;
@@ -29,32 +27,6 @@
#[Group('metadata')]
final class RequirementTest extends TestCase
{
- /**
- * @return non-empty-list
- */
- public static function constraintProvider(): array
- {
- return [
- [
- true,
- '1.0.0',
- new ConstraintRequirement(
- (new VersionConstraintParser)->parse('1.0.0'),
- ),
- ],
- ];
- }
-
- /**
- * @return non-empty-list
- */
- public static function comparisonProvider(): array
- {
- return [
- [true, '1.0.0', new ComparisonRequirement('1.0.0', new VersionComparisonOperator('='))],
- ];
- }
-
public function testCanBeCreatedFromStringWithVersionConstraint(): void
{
$requirement = Requirement::from('^1.0');
@@ -63,24 +35,13 @@ public function testCanBeCreatedFromStringWithVersionConstraint(): void
$this->assertSame('^1.0', $requirement->asString());
}
- #[DataProvider('constraintProvider')]
- public function testVersionRequirementCanBeCheckedUsingVersionConstraint(bool $expected, string $version, ConstraintRequirement $requirement): void
- {
- $this->assertSame($expected, $requirement->isSatisfiedBy($version));
- }
-
public function testCanBeCreatedFromStringWithSimpleComparison(): void
{
- $requirement = Requirement::from('>= 1.0');
+ $requirement = Requirement::from('>= 1.0.0');
$this->assertInstanceOf(ComparisonRequirement::class, $requirement);
- $this->assertSame('>= 1.0', $requirement->asString());
- }
-
- #[DataProvider('comparisonProvider')]
- public function testVersionRequirementCanBeCheckedUsingSimpleComparison(bool $expected, string $version, ComparisonRequirement $requirement): void
- {
- $this->assertSame($expected, $requirement->isSatisfiedBy($version));
+ $this->assertSame('>= 1.0.0', $requirement->asString());
+ $this->assertSame('1.0.0', $requirement->version());
}
public function testCannotBeCreatedFromInvalidString(): void