From 33c09fc57b07d8fb46a1ba893b94d9112ebdbb05 Mon Sep 17 00:00:00 2001 From: Miljan Ilic Date: Sat, 9 Mar 2024 23:44:29 +0100 Subject: [PATCH 01/11] Add PSR-6 Cache Abstraction Wrapper --- src/Cache/Cache.php | 25 +++++++++++++++++++ src/Cache/ItemPoolCompatibleCache.php | 35 +++++++++++++++++++++++++++ 2 files changed, 60 insertions(+) create mode 100644 src/Cache/Cache.php create mode 100644 src/Cache/ItemPoolCompatibleCache.php diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php new file mode 100644 index 0000000..2f5c619 --- /dev/null +++ b/src/Cache/Cache.php @@ -0,0 +1,25 @@ +cachePool = $cachePool; + } + + public function get(string $key, callable $callable, ?int $ttl = null): mixed + { + try { + $cacheItem = $this->cachePool->getItem($key); + } catch (InvalidArgumentException $e) { + throw new InvalidCacheKey($key); + } + + if (!$cacheItem->isHit()) { + $cacheItem->set($callable($cacheItem)); + $cacheItem->expiresAfter($ttl); + + $this->cachePool->save($cacheItem); + } + + return $cacheItem->get(); + } +} From 2c8861933fb05b6462a25ead52b08a4e11459521 Mon Sep 17 00:00:00 2001 From: Miljan Ilic Date: Sat, 9 Mar 2024 23:46:39 +0100 Subject: [PATCH 02/11] Move Exception to Cache Namespace --- src/Cache/Cache.php | 2 +- src/{Reader => Cache}/Exception/InvalidCacheKey.php | 2 +- src/Cache/ItemPoolCompatibleCache.php | 2 +- src/Reader/CachingObjectPropertiesReader.php | 2 +- tests/Reader/CachingObjectPropertiesReaderTest.php | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) rename src/{Reader => Cache}/Exception/InvalidCacheKey.php (89%) diff --git a/src/Cache/Cache.php b/src/Cache/Cache.php index 2f5c619..7cbdd2f 100644 --- a/src/Cache/Cache.php +++ b/src/Cache/Cache.php @@ -2,7 +2,7 @@ namespace IlicMiljan\SecureProps\Cache; -use IlicMiljan\SecureProps\Reader\Exception\InvalidCacheKey; +use IlicMiljan\SecureProps\Cache\Exception\InvalidCacheKey; use Psr\Cache\CacheItemInterface; interface Cache diff --git a/src/Reader/Exception/InvalidCacheKey.php b/src/Cache/Exception/InvalidCacheKey.php similarity index 89% rename from src/Reader/Exception/InvalidCacheKey.php rename to src/Cache/Exception/InvalidCacheKey.php index bc84d32..fd726dc 100644 --- a/src/Reader/Exception/InvalidCacheKey.php +++ b/src/Cache/Exception/InvalidCacheKey.php @@ -1,6 +1,6 @@ Date: Sat, 9 Mar 2024 23:57:50 +0100 Subject: [PATCH 03/11] Implement PSR-6 Compatible Cache Abstraction --- src/Reader/CachingObjectPropertiesReader.php | 82 +++---------------- .../Exception/InvalidCacheValueDataType.php | 31 ------- .../CachingObjectPropertiesReaderTest.php | 15 ---- 3 files changed, 12 insertions(+), 116 deletions(-) delete mode 100644 src/Reader/Exception/InvalidCacheValueDataType.php diff --git a/src/Reader/CachingObjectPropertiesReader.php b/src/Reader/CachingObjectPropertiesReader.php index a66ee69..f8fc7bf 100644 --- a/src/Reader/CachingObjectPropertiesReader.php +++ b/src/Reader/CachingObjectPropertiesReader.php @@ -2,53 +2,37 @@ namespace IlicMiljan\SecureProps\Reader; +use IlicMiljan\SecureProps\Cache\Cache; use IlicMiljan\SecureProps\Cache\Exception\InvalidCacheKey; -use IlicMiljan\SecureProps\Reader\Exception\InvalidCacheValueDataType; use IlicMiljan\SecureProps\Reader\Exception\ObjectPropertyNotFound; -use Psr\Cache\CacheItemInterface; -use Psr\Cache\CacheItemPoolInterface; -use Psr\Cache\InvalidArgumentException; use ReflectionException; -use ReflectionObject; use ReflectionProperty; class CachingObjectPropertiesReader implements ObjectPropertiesReader { public function __construct( private ObjectPropertiesReader $objectPropertiesReader, - private CacheItemPoolInterface $cache + private Cache $cache ) { } /** * @throws InvalidCacheKey - * @throws InvalidCacheValueDataType * @throws ObjectPropertyNotFound */ public function getPropertiesWithAttribute(object $object, string $attributeClass): array { - $cachedProperties = $this->getCacheItem($this->getCacheKey($object, $attributeClass)); + $propertyArray = $this->cache->get( + $this->getCacheKey($object, $attributeClass), + function($cacheItem) use ($object, $attributeClass) { + $propertiesWithAttribute = $this->objectPropertiesReader->getPropertiesWithAttribute($object, $attributeClass); - if ($cachedProperties->isHit()) { - $this->ensureCacheItemValueIsArray($cachedProperties); + $cacheItem->set($this->getCacheablePropertiesArray($propertiesWithAttribute)); - /** @var string[] $cachedPropertiesValue */ - $cachedPropertiesValue = $cachedProperties->get(); + return $propertiesWithAttribute; + }, 3600); - return $this->loadRuntimeReflectionProperties($object, $cachedPropertiesValue); - } - - $propertiesWithAttribute = $this->objectPropertiesReader->getPropertiesWithAttribute( - $object, - $attributeClass - ); - - $this->updateCache( - $cachedProperties, - $this->getCacheablePropertiesArray($propertiesWithAttribute) - ); - - return $propertiesWithAttribute; + return $this->loadRuntimeReflectionProperties($object, $propertyArray); } private function getCacheKey(object $object, string $attributeClass): string @@ -66,32 +50,15 @@ private function getCacheKey(object $object, string $attributeClass): string */ private function loadRuntimeReflectionProperties(object $object, array $propertyNames): array { - $reflection = new ReflectionObject($object); - try { - return array_map(function ($propertyName) use ($reflection) { - return $reflection->getProperty($propertyName); + return array_map(function ($propertyName) use ($object) { + return new ReflectionProperty($object, $propertyName); }, $propertyNames); } catch (ReflectionException $e) { throw new ObjectPropertyNotFound($object::class, $e); } } - /** - * @param string $cacheKey - * @return CacheItemInterface - * - * @throws InvalidCacheKey - */ - private function getCacheItem(string $cacheKey): CacheItemInterface - { - try { - return $this->cache->getItem($cacheKey); - } catch (InvalidArgumentException $e) { - throw new InvalidCacheKey($cacheKey, $e); - } - } - /** * @param ReflectionProperty[] $properties * @return string[] @@ -102,29 +69,4 @@ private function getCacheablePropertiesArray(array $properties): array return $property->getName(); }, $properties); } - - /** - * @param CacheItemInterface $cacheItem - * @return void - * - * @throws InvalidCacheValueDataType - */ - private function ensureCacheItemValueIsArray(CacheItemInterface $cacheItem): void - { - $cachedValue = $cacheItem->get(); - - if (is_array($cachedValue)) { - return; - } - - throw new InvalidCacheValueDataType(gettype($cachedValue), 'array'); - } - - private function updateCache(CacheItemInterface $cacheItem, mixed $data): void - { - $cacheItem->set($data); - $cacheItem->expiresAfter(3600); - - $this->cache->save($cacheItem); - } } diff --git a/src/Reader/Exception/InvalidCacheValueDataType.php b/src/Reader/Exception/InvalidCacheValueDataType.php deleted file mode 100644 index e42eaf9..0000000 --- a/src/Reader/Exception/InvalidCacheValueDataType.php +++ /dev/null @@ -1,31 +0,0 @@ -dataType; - } - - public function getExpectedDataType(): string - { - return $this->expectedDataType; - } -} diff --git a/tests/Reader/CachingObjectPropertiesReaderTest.php b/tests/Reader/CachingObjectPropertiesReaderTest.php index 9783c1e..d871df8 100644 --- a/tests/Reader/CachingObjectPropertiesReaderTest.php +++ b/tests/Reader/CachingObjectPropertiesReaderTest.php @@ -4,7 +4,6 @@ use IlicMiljan\SecureProps\Cache\Exception\InvalidCacheKey; use IlicMiljan\SecureProps\Reader\CachingObjectPropertiesReader; -use IlicMiljan\SecureProps\Reader\Exception\InvalidCacheValueDataType; use IlicMiljan\SecureProps\Reader\Exception\ObjectPropertyNotFound; use IlicMiljan\SecureProps\Reader\ObjectPropertiesReader; use IlicMiljan\SecureProps\Tests\Attribute\TestAttribute; @@ -83,20 +82,6 @@ public function testGetPropertiesWithAttributeThrowsInvalidCacheKey(): void }, TestAttribute::class); } - public function testGetPropertiesWithAttributeThrowsInvalidCacheValueDataType(): void - { - $this->expectException(InvalidCacheValueDataType::class); - - $this->cacheItem->method('isHit')->willReturn(true); - $this->cacheItem->method('get')->willReturn(null); - - $this->reader->getPropertiesWithAttribute( - new class () { - }, - TestAttribute::class - ); - } - public function testGetPropertiesWithAttributeThrowsObjectPropertyNotFound(): void { $this->expectException(ObjectPropertyNotFound::class); From f48b018121b929482831a0b954d5d0eaafe9f06f Mon Sep 17 00:00:00 2001 From: Miljan Ilic Date: Sun, 10 Mar 2024 00:17:21 +0100 Subject: [PATCH 04/11] Fix Invalid Data Caching --- src/Reader/CachingObjectPropertiesReader.php | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Reader/CachingObjectPropertiesReader.php b/src/Reader/CachingObjectPropertiesReader.php index f8fc7bf..52ae8b1 100644 --- a/src/Reader/CachingObjectPropertiesReader.php +++ b/src/Reader/CachingObjectPropertiesReader.php @@ -25,11 +25,12 @@ public function getPropertiesWithAttribute(object $object, string $attributeClas $propertyArray = $this->cache->get( $this->getCacheKey($object, $attributeClass), function($cacheItem) use ($object, $attributeClass) { - $propertiesWithAttribute = $this->objectPropertiesReader->getPropertiesWithAttribute($object, $attributeClass); + $propertiesWithAttribute = $this->objectPropertiesReader->getPropertiesWithAttribute( + $object, + $attributeClass + ); - $cacheItem->set($this->getCacheablePropertiesArray($propertiesWithAttribute)); - - return $propertiesWithAttribute; + return $this->getCacheablePropertiesArray($propertiesWithAttribute); }, 3600); return $this->loadRuntimeReflectionProperties($object, $propertyArray); From df9c637c893f9b9eb3d5a93b82ea5f0f0e5af35b Mon Sep 17 00:00:00 2001 From: Miljan Ilic Date: Sun, 10 Mar 2024 00:44:08 +0100 Subject: [PATCH 05/11] Fix Caching Object Properties Reader Test --- src/Reader/CachingObjectPropertiesReader.php | 6 +- .../CachingObjectPropertiesReaderTest.php | 86 +++++++++++-------- 2 files changed, 55 insertions(+), 37 deletions(-) diff --git a/src/Reader/CachingObjectPropertiesReader.php b/src/Reader/CachingObjectPropertiesReader.php index 52ae8b1..3d2b0b1 100644 --- a/src/Reader/CachingObjectPropertiesReader.php +++ b/src/Reader/CachingObjectPropertiesReader.php @@ -24,14 +24,16 @@ public function getPropertiesWithAttribute(object $object, string $attributeClas { $propertyArray = $this->cache->get( $this->getCacheKey($object, $attributeClass), - function($cacheItem) use ($object, $attributeClass) { + function ($cacheItem) use ($object, $attributeClass) { $propertiesWithAttribute = $this->objectPropertiesReader->getPropertiesWithAttribute( $object, $attributeClass ); return $this->getCacheablePropertiesArray($propertiesWithAttribute); - }, 3600); + }, + 3600 + ); return $this->loadRuntimeReflectionProperties($object, $propertyArray); } diff --git a/tests/Reader/CachingObjectPropertiesReaderTest.php b/tests/Reader/CachingObjectPropertiesReaderTest.php index d871df8..fd14c3d 100644 --- a/tests/Reader/CachingObjectPropertiesReaderTest.php +++ b/tests/Reader/CachingObjectPropertiesReaderTest.php @@ -1,18 +1,19 @@ objectPropertiesReader = $this->createMock(ObjectPropertiesReader::class); - $this->cacheItemPool = $this->createMock(CacheItemPoolInterface::class); + $this->cache = $this->createMock(Cache::class); $this->cacheItem = $this->createMock(CacheItemInterface::class); - $this->cacheItemPool->method('getItem')->willReturn($this->cacheItem); $this->reader = new CachingObjectPropertiesReader( $this->objectPropertiesReader, - $this->cacheItemPool + $this->cache ); } - public function testGetPropertiesWithAttributeReturnsCachedValueOnHit(): void + public function testGetPropertiesWithAttributeFromCache(): void { - $object = new class () { + $object = new class { #[TestAttribute] /** @phpstan-ignore-next-line */ - private string $propertyWithAttribute; + private string $propertyName; }; - $this->cacheItem->method('isHit')->willReturn(true); - $this->cacheItem->method('get')->willReturn(['propertyWithAttribute']); - - $this->objectPropertiesReader->expects($this->never())->method('getPropertiesWithAttribute'); + $this->cache->expects($this->once()) + ->method('get') + ->willReturn(['propertyName']); $result = $this->reader->getPropertiesWithAttribute($object, TestAttribute::class); - $this->assertNotEmpty($result); + + $this->assertCount(1, $result); + $this->assertInstanceOf(ReflectionProperty::class, $result[0]); } - public function testGetPropertiesWithAttributeFetchesFromDelegateOnCacheMiss(): void + /** + * @throws ReflectionException + */ + public function testGetPropertiesWithAttributeFromSource(): void { - $object = new class () { + $object = new class { + #[TestAttribute] + /** @phpstan-ignore-next-line */ + private string $propertyName; }; - $this->cacheItem->method('isHit')->willReturn(false); - $this->objectPropertiesReader->method('getPropertiesWithAttribute')->willReturn([]); + $properties = [new ReflectionProperty($object, 'propertyName')]; + + $this->cache->expects($this->once()) + ->method('get') + ->willReturnCallback(function ($key, $callback) { + return $callback($this->cacheItem); + }); + + $this->objectPropertiesReader->expects($this->once()) + ->method('getPropertiesWithAttribute') + ->willReturn($properties); $result = $this->reader->getPropertiesWithAttribute($object, TestAttribute::class); - $this->assertIsArray($result); + + $this->assertCount(1, $result); + $this->assertEquals('propertyName', $result[0]->getName()); } public function testGetPropertiesWithAttributeThrowsInvalidCacheKey(): void { $this->expectException(InvalidCacheKey::class); - $this->cacheItemPool->method('getItem')->willThrowException(new TestCacheException()); + $object = new class { + }; + + $this->cache->method('get')->willThrowException(new InvalidCacheKey('s')); - $this->reader->getPropertiesWithAttribute(new class () { - }, TestAttribute::class); + $this->reader->getPropertiesWithAttribute($object, TestAttribute::class); } public function testGetPropertiesWithAttributeThrowsObjectPropertyNotFound(): void { $this->expectException(ObjectPropertyNotFound::class); - $this->cacheItem->method('isHit')->willReturn(true); - $this->cacheItem->method('get')->willReturn(['nonExistentProperty']); + $object = new class { + }; - // Configure the mocked ObjectPropertiesReader to throw a ReflectionException - // This simulates the scenario where a property does not exist on the object - $this->objectPropertiesReader->method('getPropertiesWithAttribute') - ->willThrowException(new ReflectionException()); + $propertyNames = ['nonExistentProperty']; - $reader = new CachingObjectPropertiesReader($this->objectPropertiesReader, $this->cacheItemPool); - $reader->getPropertiesWithAttribute( - new class () { - }, - TestAttribute::class - ); + $this->cache->expects($this->once()) + ->method('get') + ->willReturn($propertyNames); + + $this->reader->getPropertiesWithAttribute($object, TestAttribute::class); } } From fc1f3004547290d6e976a5efdebfed288931eb8e Mon Sep 17 00:00:00 2001 From: Miljan Ilic Date: Sun, 10 Mar 2024 00:55:37 +0100 Subject: [PATCH 06/11] Add Cache Tests --- .idea/inspectionProfiles/Project_Default.xml | 5 + tests/Cache/ItemPoolCompatibleCacheTest.php | 118 +++++++++++++++++++ 2 files changed, 123 insertions(+) create mode 100644 tests/Cache/ItemPoolCompatibleCacheTest.php diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 61058a2..d6f9d6b 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -1,6 +1,11 @@