diff --git a/README.md b/README.md index 19dc284..c690920 100644 --- a/README.md +++ b/README.md @@ -50,7 +50,8 @@ these PHP releases: * Romans `1.1.*`: PHP `^7.0` (Tiberius) * Romans `1.2.*`: PHP `>=7.4` (Caligula) * Romans `1.3.*`: PHP `>=7.4` (Claudius) -* Romans `1.4.*`: PHP `>=8.1` (Nero) +* Romans `1.4.*`: PHP `>=7.4` (Nero) +* Romans `1.5.*`: PHP `>=8.0` (Galba) ## Integrations @@ -154,6 +155,31 @@ $filter = new IntToRoman(); $result = $filter->filter(0); // N ``` +### Cache + +This package uses [PSR-6 Caching Interface](https://www.php-fig.org/psr/psr-6) +to improve execution, mainly over loops (like `while` or `foreach`) using cache +libraries. Any PSR-6 implementation can be used and we suggest +[Symfony Cache](https://packagist.org/packages/symfony/cache) package. + +```php +use Romans\Filter\IntToRoman; +use Romans\Filter\RomanToInt; +use Symfony\Component\Cache\Adapter\ArrayAdapter; + +$cache = new ArrayAdapter(); + +$filter = new RomanToInt(); +$filter->setCache($cache); +$result = $filter->filter('MCMXCIX'); // 1999 +$result = $filter->filter('MCMXCIX'); // 1999 (from cache) + +$filter = new IntToRoman(); +$filter->setCache($cache); +$result = $filter->filter(1999); // MCMXCIX +$result = $filter->filter(1999); // MCMXCIX (from cache) +``` + ## Development You can use Docker Compose to build an image and run a container to develop and diff --git a/composer.json b/composer.json index 5599328..00e23e8 100644 --- a/composer.json +++ b/composer.json @@ -25,10 +25,14 @@ "require": { "php": ">=7.4" }, + "suggest": { + "symfony/cache": "Cache results to improve performance (or any PSR-6 implementation)" + }, "require-dev": { "php-parallel-lint/php-parallel-lint": "1.3.*", "phpmd/phpmd": "2.10.*", "phpunit/phpunit": "9.5.*", + "psr/cache": "1.0.*", "sebastian/phpcpd": "6.0.*", "slevomat/coding-standard": "7.0.*", "squizlabs/php_codesniffer": "3.6.*" diff --git a/composer.lock b/composer.lock index d5303ab..899de77 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": "4695b3b819edf757d0f2971ad1f97228", + "content-hash": "b631e238123f90c8356c25d4b328aec4", "packages": [], "packages-dev": [ { @@ -1329,6 +1329,55 @@ ], "time": "2021-09-25T07:38:51+00:00" }, + { + "name": "psr/cache", + "version": "1.0.1", + "source": { + "type": "git", + "url": "https://github.com/php-fig/cache.git", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/cache/zipball/d11b50ad223250cf17b86e38383413f5a6764bf8", + "reference": "d11b50ad223250cf17b86e38383413f5a6764bf8", + "shasum": "" + }, + "require": { + "php": ">=5.3.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\Cache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "http://www.php-fig.org/" + } + ], + "description": "Common interface for caching libraries", + "keywords": [ + "cache", + "psr", + "psr-6" + ], + "support": { + "source": "https://github.com/php-fig/cache/tree/master" + }, + "time": "2016-08-06T20:24:11+00:00" + }, { "name": "psr/container", "version": "1.1.1", diff --git a/src/Cache/CacheAwareTrait.php b/src/Cache/CacheAwareTrait.php new file mode 100644 index 0000000..9236c6a --- /dev/null +++ b/src/Cache/CacheAwareTrait.php @@ -0,0 +1,47 @@ +cache !== null; + } + + /** + * Set Cache + * + * @param ?CacheInterface $cache Cache Object + * @param self Fluent Interface + */ + public function setCache(?CacheInterface $cache): self + { + $this->cache = $cache; + return $this; + } + + /** + * Get Cache + * + * @return ?CacheInterface Cache Object + */ + public function getCache(): ?CacheInterface + { + return $this->cache; + } +} diff --git a/src/Filter/IntToRoman.php b/src/Filter/IntToRoman.php index 5cda8b8..463dc40 100644 --- a/src/Filter/IntToRoman.php +++ b/src/Filter/IntToRoman.php @@ -4,6 +4,7 @@ namespace Romans\Filter; +use Romans\Cache\CacheAwareTrait; use Romans\Grammar\Grammar; use Romans\Grammar\GrammarAwareTrait; @@ -12,6 +13,7 @@ */ class IntToRoman { + use CacheAwareTrait; use GrammarAwareTrait; /** @@ -24,6 +26,20 @@ public function __construct(?Grammar $grammar = null) $this->setGrammar($grammar ?? new Grammar()); } + /** + * Helper to Cache a Result from Value + * + * @param int $value Integer + * @param string Roman Number Result + */ + private function cache(int $value, string $result): void + { + if ($this->hasCache()) { + $item = $this->getCache()->getItem($value)->set($result); + $this->getCache()->save($item); + } + } + /** * Filter Integer to Roman Number * @@ -36,29 +52,32 @@ public function filter(int $value): string throw new Exception(sprintf('Invalid integer: %d', $value), Exception::INVALID_INTEGER); } + if ($this->hasCache() && $this->getCache()->hasItem($value)) { + return $this->getCache()->getItem($value)->get(); + } + $tokens = $this->getGrammar()->getTokens(); $values = array_reverse($this->getGrammar()->getValuesWithModifiers(), true /* preserve keys */); $result = ''; if ($value === 0) { $dataset = $values[0]; + $result = array_reduce($dataset, fn($result, $token) => $result . $tokens[$token], $result); - foreach ($dataset as $token) { - $result = $result . $tokens[$token]; - } + $this->cache($value, $result); return $result; } foreach ($values as $current => $dataset) { while ($current > 0 && $value >= $current) { - $value = $value - $current; - foreach ($dataset as $token) { - $result = $result . $tokens[$token]; - } + $value = $value - $current; + $result = array_reduce($dataset, fn($result, $token) => $result . $tokens[$token], $result); } } + $this->cache($value, $result); + return $result; } } diff --git a/src/Filter/RomanToInt.php b/src/Filter/RomanToInt.php index 68519b7..fee72e9 100644 --- a/src/Filter/RomanToInt.php +++ b/src/Filter/RomanToInt.php @@ -4,6 +4,7 @@ namespace Romans\Filter; +use Romans\Cache\CacheAwareTrait; use Romans\Grammar\Grammar; use Romans\Lexer\Lexer; use Romans\Parser\Parser; @@ -13,6 +14,8 @@ */ class RomanToInt { + use CacheAwareTrait; + /** * Lexer */ @@ -89,6 +92,17 @@ public function getParser(): Parser */ public function filter(string $value): int { - return $this->getParser()->parse($this->getLexer()->tokenize($value)); + if ($this->hasCache() && $this->getCache()->hasItem($value)) { + return $this->getCache()->getItem($value)->get(); + } + + $result = $this->getParser()->parse($this->getLexer()->tokenize($value)); + + if ($this->hasCache()) { + $item = $this->getCache()->getItem($value)->set($result); + $this->getCache()->save($item); + } + + return $result; } } diff --git a/test/Cache/CacheAwareTraitTest.php b/test/Cache/CacheAwareTraitTest.php new file mode 100644 index 0000000..9822f37 --- /dev/null +++ b/test/Cache/CacheAwareTraitTest.php @@ -0,0 +1,35 @@ +createMock(CacheInterface::class); + $element = $this->getMockForTrait(CacheAwareTrait::class); + + $this->assertNull($element->getCache()); + $this->assertFalse($element->hasCache()); + + $this->assertSame($element, $element->setCache($cache)); + $this->assertSame($cache, $element->getCache()); + $this->assertTrue($element->hasCache()); + + $this->assertSame($element, $element->setCache(null)); + $this->assertNull($element->getCache()); + $this->assertFalse($element->hasCache()); + } +} diff --git a/test/Filter/IntToRomanTest.php b/test/Filter/IntToRomanTest.php index 116cf22..08f2b54 100644 --- a/test/Filter/IntToRomanTest.php +++ b/test/Filter/IntToRomanTest.php @@ -5,6 +5,8 @@ namespace RomansTest\Filter; use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemInterface as CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface as CacheInterface; use Romans\Filter\Exception as FilterException; use Romans\Filter\IntToRoman; use Romans\Grammar\Grammar; @@ -80,4 +82,60 @@ public function testFilterWithNegative(): void $this->filter->filter(-1); } + + /** + * Test Cache Found + */ + public function testCacheFound(): void + { + $item = $this->createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheInterface::class); + + $item->expects($this->once()) + ->method('get') + ->willReturn('I'); + + $cache->method('hasItem') + ->with($this->equalTo('1')) + ->willReturn(true); + + $cache->expects($this->once()) + ->method('getItem') + ->willReturn($item); + + $this->filter->setCache($cache); + + $this->assertSame('I', $this->filter->filter(1)); + } + + /** + * Test Cache not Found + */ + public function testCacheNotFound(): void + { + $item = $this->createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheInterface::class); + + $item->expects($this->once()) + ->method('set') + ->with($this->equalTo('I')) + ->willReturnSelf(); + + $cache->method('hasItem') + ->with($this->equalTo('1')) + ->willReturn(false); + + $cache->expects($this->once()) + ->method('getItem') + ->willReturn($item); + + $cache->expects($this->once()) + ->method('save') + ->with($this->equalTo($item)) + ->willReturn(true); + + $this->filter->setCache($cache); + + $this->assertSame('I', $this->filter->filter(1)); + } } diff --git a/test/Filter/RomanToIntTest.php b/test/Filter/RomanToIntTest.php index 2c77716..0c15b4e 100644 --- a/test/Filter/RomanToIntTest.php +++ b/test/Filter/RomanToIntTest.php @@ -5,6 +5,8 @@ namespace RomansTest\Filter; use PHPUnit\Framework\TestCase; +use Psr\Cache\CacheItemInterface as CacheItemInterface; +use Psr\Cache\CacheItemPoolInterface as CacheInterface; use Romans\Filter\RomanToInt; use Romans\Grammar\Grammar; use Romans\Lexer\Lexer; @@ -104,4 +106,60 @@ public function testFilterWithZero(): void { $this->assertSame(0, $this->filter->filter('N')); } + + /** + * Test Cache Found + */ + public function testCacheFound(): void + { + $item = $this->createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheInterface::class); + + $item->expects($this->once()) + ->method('get') + ->willReturn(1); + + $cache->method('hasItem') + ->with($this->equalTo('I')) + ->willReturn(true); + + $cache->expects($this->once()) + ->method('getItem') + ->willReturn($item); + + $this->filter->setCache($cache); + + $this->assertSame(1, $this->filter->filter('I')); + } + + /** + * Test Cache not Found + */ + public function testCacheNotFound(): void + { + $item = $this->createMock(CacheItemInterface::class); + $cache = $this->createMock(CacheInterface::class); + + $item->expects($this->once()) + ->method('set') + ->with($this->equalTo(1)) + ->willReturnSelf(); + + $cache->method('hasItem') + ->with($this->equalTo('I')) + ->willReturn(false); + + $cache->expects($this->once()) + ->method('getItem') + ->willReturn($item); + + $cache->expects($this->once()) + ->method('save') + ->with($this->equalTo($item)) + ->willReturn(true); + + $this->filter->setCache($cache); + + $this->assertSame(1, $this->filter->filter('I')); + } }