From 6fdc7d550cd4c1d5228da6c00584e6cfa540953b Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Tue, 15 Sep 2026 11:41:43 +0200 Subject: [PATCH 1/7] Avoid concatenated cache keys in CompilingMatcher --- src/CompilingMatcher.php | 25 ++++++++++++++----------- tests/CompilingMatcherTest.php | 30 ++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 11 deletions(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index 45bce70a..fc6fc17e 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -21,12 +21,12 @@ class CompilingMatcher { /** * @var array - * @phpstan-var array + * @phpstan-var array> */ private static $compiledCheckerCache = array(); /** * @var array - * @phpstan-var array + * @phpstan-var array>> */ private static $resultCache = array(); @@ -68,27 +68,30 @@ public static function clear() */ public static function match(ConstraintInterface $constraint, $operator, $version) { - $resultCacheKey = $operator.$constraint.';'.$version; + $constraintString = (string) $constraint; + // Resolve the operator and constraint cache bucket once, so each read or write below + // only needs to look up the version. Using a reference avoids repeating both outer + // array lookups and ensures new results are stored in the static cache, not a local copy. + $resultCache = &self::$resultCache[$operator][$constraintString]; - if (isset(self::$resultCache[$resultCacheKey])) { - return self::$resultCache[$resultCacheKey]; + if (isset($resultCache[$version])) { + return $resultCache[$version]; } if (self::$enabled === null) { self::$enabled = !\in_array('eval', explode(',', (string) ini_get('disable_functions')), true); } if (!self::$enabled) { - return self::$resultCache[$resultCacheKey] = $constraint->matches(new Constraint(self::$transOpInt[$operator], $version)); + return $resultCache[$version] = $constraint->matches(new Constraint(self::$transOpInt[$operator], $version)); } - $cacheKey = $operator.$constraint; - if (!isset(self::$compiledCheckerCache[$cacheKey])) { + if (!isset(self::$compiledCheckerCache[$operator][$constraintString])) { $code = $constraint->compile($operator); - self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};'); + self::$compiledCheckerCache[$operator][$constraintString] = $function = eval('return function($v, $b){return '.$code.';};'); } else { - $function = self::$compiledCheckerCache[$cacheKey]; + $function = self::$compiledCheckerCache[$operator][$constraintString]; } - return self::$resultCache[$resultCacheKey] = $function($version, strpos($version, 'dev-') === 0); + return $resultCache[$version] = $function($version, strpos($version, 'dev-') === 0); } } diff --git a/tests/CompilingMatcherTest.php b/tests/CompilingMatcherTest.php index dd932a11..df985c20 100644 --- a/tests/CompilingMatcherTest.php +++ b/tests/CompilingMatcherTest.php @@ -27,4 +27,34 @@ public function testCacheKey() $this->assertFalse(CompilingMatcher::match(new Constraint('>=', '2.11'), Constraint::OP_EQ, '1.0')); $this->assertTrue(CompilingMatcher::match(new Constraint('>=', '2.1'), Constraint::OP_EQ, '11.0')); } + + public function testCacheSeparatesOperatorsConstraintsAndVersions() + { + CompilingMatcher::clear(); + $constraint = new Constraint('>=', '2'); + + $this->assertFalse(CompilingMatcher::match($constraint, Constraint::OP_EQ, '1')); + $this->assertTrue(CompilingMatcher::match($constraint, Constraint::OP_GT, '1')); + $this->assertTrue(CompilingMatcher::match(new Constraint('<', '2'), Constraint::OP_EQ, '1')); + $this->assertTrue(CompilingMatcher::match($constraint, Constraint::OP_EQ, '3')); + $this->assertFalse(CompilingMatcher::match($constraint, Constraint::OP_EQ, '1')); + CompilingMatcher::clear(); + } + + public function testReusesCompiledCheckerAndClearsBothCaches() + { + CompilingMatcher::clear(); + $constraint = $this->getMockBuilder('Composer\\Semver\\Constraint\\Constraint') + ->setConstructorArgs(array('>=', '1')) + ->setMethods(array('compile')) + ->getMock(); + $constraint->expects($this->exactly(2))->method('compile')->willReturn('$v === "2"'); + + $this->assertFalse(CompilingMatcher::match($constraint, Constraint::OP_EQ, '1')); + $this->assertFalse(CompilingMatcher::match($constraint, Constraint::OP_EQ, '1')); + $this->assertTrue(CompilingMatcher::match($constraint, Constraint::OP_EQ, '2')); + CompilingMatcher::clear(); + $this->assertFalse(CompilingMatcher::match($constraint, Constraint::OP_EQ, '1')); + CompilingMatcher::clear(); + } } From def024df213fd6285cb883c90d85abf994212a8e Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Thu, 24 Sep 2026 14:01:57 +0200 Subject: [PATCH 2/7] Update src/CompilingMatcher.php Co-authored-by: Jordi Boggiano --- src/CompilingMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index fc6fc17e..430a1657 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -26,7 +26,7 @@ class CompilingMatcher private static $compiledCheckerCache = array(); /** * @var array - * @phpstan-var array>> + * @phpstan-var array>> */ private static $resultCache = array(); From ae0b2e525710f04084c45d52d8f006e49877a36c Mon Sep 17 00:00:00 2001 From: Yanick Witschi Date: Thu, 24 Sep 2026 14:02:04 +0200 Subject: [PATCH 3/7] Update src/CompilingMatcher.php Co-authored-by: Jordi Boggiano --- src/CompilingMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index 430a1657..4be4efcb 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -21,7 +21,7 @@ class CompilingMatcher { /** * @var array - * @phpstan-var array> + * @phpstan-var array> */ private static $compiledCheckerCache = array(); /** From 046c67bc43abec6f093a6cc258a7c924901b02f9 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 24 Sep 2026 15:04:20 +0200 Subject: [PATCH 4/7] Update PHPDoc annotations and optimize cache usage --- src/CompilingMatcher.php | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index 4be4efcb..41392b8a 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -21,12 +21,12 @@ class CompilingMatcher { /** * @var array - * @phpstan-var array> + * @phpstan-var array> */ private static $compiledCheckerCache = array(); /** * @var array - * @phpstan-var array>> + * @phpstan-var array>> */ private static $resultCache = array(); @@ -85,13 +85,12 @@ public static function match(ConstraintInterface $constraint, $operator, $versio return $resultCache[$version] = $constraint->matches(new Constraint(self::$transOpInt[$operator], $version)); } - if (!isset(self::$compiledCheckerCache[$operator][$constraintString])) { + $compiledCheckerCache = &self::$compiledCheckerCache[$operator][$constraintString]; + if (null === $compiledCheckerCache) { $code = $constraint->compile($operator); - self::$compiledCheckerCache[$operator][$constraintString] = $function = eval('return function($v, $b){return '.$code.';};'); - } else { - $function = self::$compiledCheckerCache[$operator][$constraintString]; + $compiledCheckerCache = eval('return function($v, $b){return '.$code.';};'); } - return $resultCache[$version] = $function($version, strpos($version, 'dev-') === 0); + return $resultCache[$version] = $compiledCheckerCache($version, strpos($version, 'dev-') === 0); } } From ed338473336ee357f4e8a0777c26d261c93841c8 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 24 Sep 2026 15:06:05 +0200 Subject: [PATCH 5/7] Change null check to isset for compiledCheckerCache --- src/CompilingMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index 41392b8a..3cf51670 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -86,7 +86,7 @@ public static function match(ConstraintInterface $constraint, $operator, $versio } $compiledCheckerCache = &self::$compiledCheckerCache[$operator][$constraintString]; - if (null === $compiledCheckerCache) { + if (!isset($compiledCheckerCache)) { $code = $constraint->compile($operator); $compiledCheckerCache = eval('return function($v, $b){return '.$code.';};'); } From 969ae4c844f4eb4a9a7e17e2ab5a3600459b744c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 24 Sep 2026 15:08:57 +0200 Subject: [PATCH 6/7] Update phpstan-var annotation to allow null values --- src/CompilingMatcher.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index 3cf51670..e7eea188 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -21,7 +21,7 @@ class CompilingMatcher { /** * @var array - * @phpstan-var array> + * @phpstan-var array|null> */ private static $compiledCheckerCache = array(); /** From 69c6b2e9f936e1c97865e5a148e7b68830ee828c Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 24 Sep 2026 15:39:16 +0200 Subject: [PATCH 7/7] Fix PHPStan type annotation for compiledCheckerCache --- src/CompilingMatcher.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index e7eea188..f69fbdf8 100644 --- a/src/CompilingMatcher.php +++ b/src/CompilingMatcher.php @@ -21,7 +21,7 @@ class CompilingMatcher { /** * @var array - * @phpstan-var array|null> + * @phpstan-var array> */ private static $compiledCheckerCache = array(); /** @@ -86,7 +86,7 @@ public static function match(ConstraintInterface $constraint, $operator, $versio } $compiledCheckerCache = &self::$compiledCheckerCache[$operator][$constraintString]; - if (!isset($compiledCheckerCache)) { + if (null === $compiledCheckerCache) { $code = $constraint->compile($operator); $compiledCheckerCache = eval('return function($v, $b){return '.$code.';};'); }