diff --git a/src/CompilingMatcher.php b/src/CompilingMatcher.php index 45bce70a..f69fbdf8 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,29 @@ 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])) { + $compiledCheckerCache = &self::$compiledCheckerCache[$operator][$constraintString]; + if (null === $compiledCheckerCache) { $code = $constraint->compile($operator); - self::$compiledCheckerCache[$cacheKey] = $function = eval('return function($v, $b){return '.$code.';};'); - } else { - $function = self::$compiledCheckerCache[$cacheKey]; + $compiledCheckerCache = eval('return function($v, $b){return '.$code.';};'); } - return self::$resultCache[$resultCacheKey] = $function($version, strpos($version, 'dev-') === 0); + return $resultCache[$version] = $compiledCheckerCache($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(); + } }