diff --git a/.gitattributes b/.gitattributes index edb8a01..3dd9bf4 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,6 +1,7 @@ /.gitattributes export-ignore /.github/ export-ignore /.gitignore export-ignore +/phpstan.neon.dist export-ignore /phpunit.xml.dist export-ignore /phpunit.xml.legacy export-ignore /tests export-ignore diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6f9cfb4..587bd0c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,3 +31,26 @@ jobs: if: ${{ matrix.php >= 7.3 }} - run: vendor/bin/phpunit --coverage-text -c phpunit.xml.legacy if: ${{ matrix.php < 7.3 }} + + PHPStan: + name: PHPStan (PHP ${{ matrix.php }}) + runs-on: ubuntu-22.04 + strategy: + matrix: + php: + - 8.3 + - 8.2 + - 8.1 + - 8.0 + - 7.4 + - 7.3 + - 7.2 + - 7.1 + steps: + - uses: actions/checkout@v4 + - uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + coverage: none + - run: composer install + - run: vendor/bin/phpstan diff --git a/README.md b/README.md index cf6a8ea..152277b 100644 --- a/README.md +++ b/README.md @@ -142,7 +142,7 @@ Similarly, an expired cache item (once the time-to-live is expired) is considered a cache miss. ```php -$cache->getMultiple(['name', 'age'])->then(function (array $values) { +$cache->getMultiple(['name', 'age'])->then(function (array $values): void { $name = $values['name'] ?? 'User'; $age = $values['age'] ?? 'n/a'; @@ -369,6 +369,12 @@ To run the test suite, go to the project root and run: vendor/bin/phpunit ``` +On top of this, we use PHPStan on max level to ensure type safety across the project: + +```bash +vendor/bin/phpstan +``` + ## License MIT, see [LICENSE file](LICENSE). diff --git a/composer.json b/composer.json index 754be64..1cebfcf 100644 --- a/composer.json +++ b/composer.json @@ -30,6 +30,7 @@ "react/promise": "^3.0" }, "require-dev": { + "phpstan/phpstan": "1.11.1 || 1.4.10", "phpunit/phpunit": "^9.6 || ^7.5" }, "autoload": { diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000..895c841 --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,6 @@ +parameters: + level: max + + paths: + - src/ + - tests/ diff --git a/src/ArrayCache.php b/src/ArrayCache.php index 786cdfc..d52bee9 100644 --- a/src/ArrayCache.php +++ b/src/ArrayCache.php @@ -8,9 +8,16 @@ class ArrayCache implements CacheInterface { + /** @var ?int */ private $limit; + + /** @var array */ private $data = []; + + /** @var array */ private $expires = []; + + /** @var bool */ private $supportsHighResolution; /** @@ -124,6 +131,7 @@ public function getMultiple(array $keys, $default = null): PromiseInterface $values[$key] = $this->get($key, $default); } + /** @var PromiseInterface> */ return all($values); } diff --git a/src/CacheInterface.php b/src/CacheInterface.php index 4a486fb..278108a 100644 --- a/src/CacheInterface.php +++ b/src/CacheInterface.php @@ -106,7 +106,7 @@ public function delete(string $key): PromiseInterface; * considered a cache miss. * * ```php - * $cache->getMultiple(['name', 'age'])->then(function (array $values) { + * $cache->getMultiple(['name', 'age'])->then(function (array $values): void { * $name = $values['name'] ?? 'User'; * $age = $values['age'] ?? 'n/a'; * @@ -120,7 +120,7 @@ public function delete(string $key): PromiseInterface; * * @param string[] $keys A list of keys that can obtained in a single operation. * @param mixed $default Default value to return for keys that do not exist. - * @return PromiseInterface Returns a promise which resolves to an `array` of cached values + * @return PromiseInterface> Returns a promise which resolves to an `array` of cached values */ public function getMultiple(array $keys, $default = null): PromiseInterface; @@ -144,8 +144,8 @@ public function getMultiple(array $keys, $default = null): PromiseInterface; * This example eventually sets the list of values - the key `foo` to 1 value * and the key `bar` to 2. If some of the keys already exist, they are overridden. * - * @param array $values A list of key => value pairs for a multiple-set operation. - * @param ?float $ttl Optional. The TTL value of this item. + * @param array $values A list of key => value pairs for a multiple-set operation. + * @param ?float $ttl Optional. The TTL value of this item. * @return PromiseInterface Returns a promise which resolves to `true` on success or `false` on error */ public function setMultiple(array $values, ?float $ttl = null): PromiseInterface; diff --git a/tests/ArrayCacheTest.php b/tests/ArrayCacheTest.php index be86783..e46c726 100644 --- a/tests/ArrayCacheTest.php +++ b/tests/ArrayCacheTest.php @@ -14,93 +14,50 @@ class ArrayCacheTest extends TestCase /** * @before */ - public function setUpArrayCache() + public function setUpArrayCache(): void { $this->cache = new ArrayCache(); } /** @test */ - public function getShouldResolvePromiseWithNullForNonExistentKey() + public function getShouldResolvePromiseWithNullForNonExistentKey(): void { - $success = $this->createCallableMock(); - $success - ->expects($this->once()) - ->method('__invoke') - ->with(null); - - $this->cache - ->get('foo') - ->then( - $success, - $this->expectCallableNever() - ); + $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); } /** @test */ - public function setShouldSetKey() + public function setShouldSetKey(): void { - $setPromise = $this->cache - ->set('foo', 'bar'); - - $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo(true)); + $this->cache->set('foo', 'bar')->then($this->expectCallableOnceWith(true)); - $setPromise->then($mock); - - $success = $this->createCallableMock(); - $success - ->expects($this->once()) - ->method('__invoke') - ->with('bar'); - - $this->cache - ->get('foo') - ->then($success); + $this->cache->get('foo')->then($this->expectCallableOnceWith('bar')); } /** @test */ - public function deleteShouldDeleteKey() + public function deleteShouldDeleteKey(): void { - $this->cache - ->set('foo', 'bar'); - - $deletePromise = $this->cache - ->delete('foo'); - - $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($this->identicalTo(true)); + $this->cache->set('foo', 'bar'); - $deletePromise->then($mock); + $this->cache->delete('foo')->then($this->expectCallableOnceWith(true)); - $this->cache - ->get('foo') - ->then( - $this->expectCallableOnce(), - $this->expectCallableNever() - ); + $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); } - public function testGetWillResolveWithNullForCacheMiss() + public function testGetWillResolveWithNullForCacheMiss(): void { $this->cache = new ArrayCache(); $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); } - public function testGetWillResolveWithDefaultValueForCacheMiss() + public function testGetWillResolveWithDefaultValueForCacheMiss(): void { $this->cache = new ArrayCache(); $this->cache->get('foo', 'bar')->then($this->expectCallableOnceWith('bar')); } - public function testGetWillResolveWithExplicitNullValueForCacheHit() + public function testGetWillResolveWithExplicitNullValueForCacheHit(): void { $this->cache = new ArrayCache(); @@ -108,7 +65,7 @@ public function testGetWillResolveWithExplicitNullValueForCacheHit() $this->cache->get('foo', 'bar')->then($this->expectCallableOnceWith(null)); } - public function testLimitSizeToZeroDoesNotStoreAnyData() + public function testLimitSizeToZeroDoesNotStoreAnyData(): void { $this->cache = new ArrayCache(0); @@ -117,7 +74,7 @@ public function testLimitSizeToZeroDoesNotStoreAnyData() $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); } - public function testLimitSizeToOneWillOnlyReturnLastWrite() + public function testLimitSizeToOneWillOnlyReturnLastWrite(): void { $this->cache = new ArrayCache(1); @@ -128,7 +85,7 @@ public function testLimitSizeToOneWillOnlyReturnLastWrite() $this->cache->get('bar')->then($this->expectCallableOnceWith('2')); } - public function testOverwriteWithLimitedSizeWillUpdateLRUInfo() + public function testOverwriteWithLimitedSizeWillUpdateLRUInfo(): void { $this->cache = new ArrayCache(2); @@ -142,7 +99,7 @@ public function testOverwriteWithLimitedSizeWillUpdateLRUInfo() $this->cache->get('baz')->then($this->expectCallableOnceWith('4')); } - public function testGetWithLimitedSizeWillUpdateLRUInfo() + public function testGetWithLimitedSizeWillUpdateLRUInfo(): void { $this->cache = new ArrayCache(2); @@ -156,7 +113,7 @@ public function testGetWithLimitedSizeWillUpdateLRUInfo() $this->cache->get('baz')->then($this->expectCallableOnceWith('3')); } - public function testGetWillResolveWithValueIfItemIsNotExpired() + public function testGetWillResolveWithValueIfItemIsNotExpired(): void { $this->cache = new ArrayCache(); @@ -165,7 +122,7 @@ public function testGetWillResolveWithValueIfItemIsNotExpired() $this->cache->get('foo')->then($this->expectCallableOnceWith('1')); } - public function testGetWillResolveWithDefaultIfItemIsExpired() + public function testGetWillResolveWithDefaultIfItemIsExpired(): void { $this->cache = new ArrayCache(); @@ -174,7 +131,7 @@ public function testGetWillResolveWithDefaultIfItemIsExpired() $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); } - public function testSetWillOverwritOldestItemIfNoEntryIsExpired() + public function testSetWillOverwritOldestItemIfNoEntryIsExpired(): void { $this->cache = new ArrayCache(2); @@ -185,7 +142,7 @@ public function testSetWillOverwritOldestItemIfNoEntryIsExpired() $this->cache->get('foo')->then($this->expectCallableOnceWith(null)); } - public function testSetWillOverwriteExpiredItemIfAnyEntryIsExpired() + public function testSetWillOverwriteExpiredItemIfAnyEntryIsExpired(): void { $this->cache = new ArrayCache(2); @@ -197,7 +154,7 @@ public function testSetWillOverwriteExpiredItemIfAnyEntryIsExpired() $this->cache->get('bar')->then($this->expectCallableOnceWith(null)); } - public function testGetMultiple() + public function testGetMultiple(): void { $this->cache = new ArrayCache(); $this->cache->set('foo', '1'); @@ -207,7 +164,7 @@ public function testGetMultiple() ->then($this->expectCallableOnceWith(['foo' => '1', 'bar' => 'baz'])); } - public function testSetMultiple() + public function testSetMultiple(): void { $this->cache = new ArrayCache(); $this->cache->setMultiple(['foo' => '1', 'bar' => '2'], 10); @@ -217,7 +174,7 @@ public function testSetMultiple() ->then($this->expectCallableOnceWith(['foo' => '1', 'bar' => '2'])); } - public function testDeleteMultiple() + public function testDeleteMultiple(): void { $this->cache = new ArrayCache(); $this->cache->setMultiple(['foo' => 1, 'bar' => 2, 'baz' => 3]); @@ -239,7 +196,7 @@ public function testDeleteMultiple() ->then($this->expectCallableOnceWith(false)); } - public function testClearShouldClearCache() + public function testClearShouldClearCache(): void { $this->cache = new ArrayCache(); $this->cache->setMultiple(['foo' => 1, 'bar' => 2, 'baz' => 3]); @@ -259,7 +216,7 @@ public function testClearShouldClearCache() ->then($this->expectCallableOnceWith(false)); } - public function hasShouldResolvePromiseForExistingKey() + public function hasShouldResolvePromiseForExistingKey(): void { $this->cache = new ArrayCache(); $this->cache->set('foo', 'bar'); @@ -269,7 +226,7 @@ public function hasShouldResolvePromiseForExistingKey() ->then($this->expectCallableOnceWith(true)); } - public function hasShouldResolvePromiseForNonExistentKey() + public function hasShouldResolvePromiseForNonExistentKey(): void { $this->cache = new ArrayCache(); $this->cache->set('foo', 'bar'); @@ -279,7 +236,7 @@ public function hasShouldResolvePromiseForNonExistentKey() ->then($this->expectCallableOnceWith(false)); } - public function testHasWillResolveIfItemIsNotExpired() + public function testHasWillResolveIfItemIsNotExpired(): void { $this->cache = new ArrayCache(); $this->cache->set('foo', '1', 10); @@ -289,7 +246,7 @@ public function testHasWillResolveIfItemIsNotExpired() ->then($this->expectCallableOnceWith(true)); } - public function testHasWillResolveIfItemIsExpired() + public function testHasWillResolveIfItemIsExpired(): void { $this->cache = new ArrayCache(); $this->cache->set('foo', '1', 0); @@ -299,7 +256,7 @@ public function testHasWillResolveIfItemIsExpired() ->then($this->expectCallableOnceWith(false)); } - public function testHasWillResolveForExplicitNullValue() + public function testHasWillResolveForExplicitNullValue(): void { $this->cache = new ArrayCache(); $this->cache->set('foo', null); @@ -309,7 +266,7 @@ public function testHasWillResolveForExplicitNullValue() ->then($this->expectCallableOnceWith(true)); } - public function testHasWithLimitedSizeWillUpdateLRUInfo() + public function testHasWithLimitedSizeWillUpdateLRUInfo(): void { $this->cache = new ArrayCache(2); diff --git a/tests/TestCase.php b/tests/TestCase.php index 45aa5c2..9088454 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,52 +2,40 @@ namespace React\Tests\Cache; +use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as BaseTestCase; class TestCase extends BaseTestCase { - protected function expectCallableExactly($amount) + protected function expectCallableOnce(): callable { $mock = $this->createCallableMock(); - $mock - ->expects($this->exactly($amount)) - ->method('__invoke'); + $mock->expects($this->once())->method('__invoke'); + assert(is_callable($mock)); return $mock; } - protected function expectCallableOnce() + /** @param mixed $argument */ + protected function expectCallableOnceWith($argument): callable { $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke'); + $mock->expects($this->once())->method('__invoke')->with($argument); + assert(is_callable($mock)); return $mock; } - protected function expectCallableOnceWith($param) + protected function expectCallableNever(): callable { $mock = $this->createCallableMock(); - $mock - ->expects($this->once()) - ->method('__invoke') - ->with($param); + $mock->expects($this->never())->method('__invoke'); + assert(is_callable($mock)); return $mock; } - protected function expectCallableNever() - { - $mock = $this->createCallableMock(); - $mock - ->expects($this->never()) - ->method('__invoke'); - - return $mock; - } - - protected function createCallableMock() + protected function createCallableMock(): MockObject { $builder = $this->getMockBuilder(\stdClass::class); if (method_exists($builder, 'addMethods')) {