Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions src/Metadata/CacheKey.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Metadata;

use function hash;

/**
* PSR-6 and PSR-16 reserve the characters {}()/\@: and only guarantee keys up to 64 characters,
* so class names are hashed instead of being used as they are.
*
* @internal
*/
final class CacheKey
{
/** @param class-string $class */
public static function forClass(string $class): string
{
return 'hydrator_metadata_' . hash('xxh128', $class);
}
}
6 changes: 5 additions & 1 deletion src/Metadata/ClassMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,11 @@ public function __serialize(): array
/** @param serialized $data */
public function __unserialize(array $data): void
{
$this->reflection = new ReflectionClass($data['className']);
/** @var class-string<T> $className */
$className = $data['className'];

$this->reflection = new ReflectionClass($className);
$this->className = $className;
$this->normalizer = $data['normalizer'];
$this->properties = $data['properties'];
$this->lazy = $data['lazy'];
Expand Down
7 changes: 3 additions & 4 deletions src/Metadata/Psr16MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,15 @@ public function __construct(
*/
public function metadata(string $class): ClassMetadata
{
/** @var ?ClassMetadata<T> $metadata */
$metadata = $this->cache->get($class);
$metadata = $this->cache->get(CacheKey::forClass($class));

if ($metadata !== null) {
if ($metadata instanceof ClassMetadata && $metadata->className === $class) {
return $metadata;
}

$metadata = $this->metadataFactory->metadata($class);

$this->cache->set($class, $metadata);
$this->cache->set(CacheKey::forClass($class), $metadata);

return $metadata;
}
Expand Down
7 changes: 4 additions & 3 deletions src/Metadata/Psr6MetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,14 @@ public function __construct(
*/
public function metadata(string $class): ClassMetadata
{
$item = $this->cache->getItem($class);
$item = $this->cache->getItem(CacheKey::forClass($class));

if ($item->isHit()) {
/** @var ClassMetadata<T> $data */
$data = $item->get();

return $data;
if ($data instanceof ClassMetadata && $data->className === $class) {
return $data;
}
}

$metadata = $this->metadataFactory->metadata($class);
Expand Down
22 changes: 22 additions & 0 deletions tests/Unit/Metadata/CacheKeyTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Metadata;

use Patchlevel\Hydrator\Metadata\CacheKey;
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(CacheKey::class)]
final class CacheKeyTest extends TestCase
{
public function testForClass(): void
{
self::assertSame(
'hydrator_metadata_461ebb84f6b57aacd859c578c8ae216c',
CacheKey::forClass(ProfileCreated::class),
);
}
}
16 changes: 16 additions & 0 deletions tests/Unit/Metadata/ClassMetadataTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

namespace Patchlevel\Hydrator\Tests\Unit\Metadata;

use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\PropertyMetadata;
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use ReflectionClass;
use ReflectionProperty;
use Symfony\Component\TypeInfo\Type;

use function serialize;
use function unserialize;

#[CoversClass(ClassMetadata::class)]
final class ClassMetadataTest extends TestCase
{
Expand Down Expand Up @@ -41,4 +46,15 @@ public function testPropertiesHashmap(): void
self::assertSame($fooMetadata, $classMetadata->properties['foo']);
self::assertSame($barMetadata, $classMetadata->properties['bar']);
}

public function testSerialize(): void
{
$classMetadata = (new AttributeMetadataFactory())->metadata(ProfileCreated::class);

$unserialized = unserialize(serialize($classMetadata));

self::assertInstanceOf(ClassMetadata::class, $unserialized);
self::assertSame(ProfileCreated::class, $unserialized->className);
self::assertEquals($classMetadata, $unserialized);
}
}
38 changes: 38 additions & 0 deletions tests/Unit/Metadata/PropertyMetadataTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace Patchlevel\Hydrator\Tests\Unit\Metadata;

use Patchlevel\Hydrator\Metadata\PropertyMetadata;
use Patchlevel\Hydrator\Tests\Unit\Fixture\Email;
use Patchlevel\Hydrator\Tests\Unit\Fixture\EmailNormalizer;
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use ReflectionProperty;
use Symfony\Component\TypeInfo\Type;

use function serialize;
use function unserialize;

#[CoversClass(PropertyMetadata::class)]
final class PropertyMetadataTest extends TestCase
{
public function testSerialize(): void
{
$propertyMetadata = new PropertyMetadata(
new ReflectionProperty(ProfileCreated::class, 'email'),
Type::object(Email::class),
'email_field',
new EmailNormalizer(),
['foo' => 'bar'],
);

$unserialized = unserialize(serialize($propertyMetadata));

self::assertInstanceOf(PropertyMetadata::class, $unserialized);
self::assertSame('email', $unserialized->propertyName);
self::assertEquals($propertyMetadata, $unserialized);
}
}
49 changes: 46 additions & 3 deletions tests/Unit/Metadata/Psr16MetadataFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,19 @@

namespace Patchlevel\Hydrator\Tests\Unit\Metadata;

use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\Metadata\CacheKey;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use Patchlevel\Hydrator\Metadata\Psr16MetadataFactory;
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Psr\SimpleCache\CacheInterface;
use ReflectionClass;
use stdClass;
use Symfony\Component\Cache\Adapter\ArrayAdapter;
use Symfony\Component\Cache\Psr16Cache;

#[CoversClass(Psr16MetadataFactory::class)]
final class Psr16MetadataFactoryTest extends TestCase
Expand All @@ -23,7 +28,7 @@ public function testMetadataWithHit(): void
$cache = $this->createMock(CacheInterface::class);
$cache->expects(self::once())
->method('get')
->with(stdClass::class)
->with(CacheKey::forClass(stdClass::class))
->willReturn($classMetadata);

$innerFactory = $this->createMock(MetadataFactory::class);
Expand All @@ -43,11 +48,37 @@ public function testMetadataWithMiss(): void
$cache = $this->createMock(CacheInterface::class);
$cache->expects(self::once())
->method('get')
->with(stdClass::class)
->with(CacheKey::forClass(stdClass::class))
->willReturn(null);
$cache->expects(self::once())
->method('set')
->with(stdClass::class, $classMetadata);
->with(CacheKey::forClass(stdClass::class), $classMetadata);

$innerFactory = $this->createMock(MetadataFactory::class);
$innerFactory->expects(self::once())
->method('metadata')
->with(stdClass::class)
->willReturn($classMetadata);

$factory = new Psr16MetadataFactory($innerFactory, $cache);
$result = $factory->metadata(stdClass::class);

self::assertSame($classMetadata, $result);
}

public function testMetadataIgnoresEntryOfOtherClass(): void
{
$otherMetadata = new ClassMetadata(new ReflectionClass(ProfileCreated::class));
$classMetadata = new ClassMetadata(new ReflectionClass(stdClass::class));

$cache = $this->createMock(CacheInterface::class);
$cache->expects(self::once())
->method('get')
->with(CacheKey::forClass(stdClass::class))
->willReturn($otherMetadata);
$cache->expects(self::once())
->method('set')
->with(CacheKey::forClass(stdClass::class), $classMetadata);

$innerFactory = $this->createMock(MetadataFactory::class);
$innerFactory->expects(self::once())
Expand All @@ -60,4 +91,16 @@ public function testMetadataWithMiss(): void

self::assertSame($classMetadata, $result);
}

public function testMetadataWithSymfonyCache(): void
{
$cache = new Psr16Cache(new ArrayAdapter());
$factory = new Psr16MetadataFactory(new AttributeMetadataFactory(), $cache);

$metadata = $factory->metadata(ProfileCreated::class);

self::assertSame(ProfileCreated::class, $metadata->className);
self::assertTrue($cache->has(CacheKey::forClass(ProfileCreated::class)));
self::assertEquals($metadata, $factory->metadata(ProfileCreated::class));
}
}
55 changes: 54 additions & 1 deletion tests/Unit/Metadata/Psr6MetadataFactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,19 @@

namespace Patchlevel\Hydrator\Tests\Unit\Metadata;

use Patchlevel\Hydrator\Metadata\AttributeMetadataFactory;
use Patchlevel\Hydrator\Metadata\CacheKey;
use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Metadata\MetadataFactory;
use Patchlevel\Hydrator\Metadata\Psr6MetadataFactory;
use Patchlevel\Hydrator\Tests\Unit\Fixture\ProfileCreated;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;
use Psr\Cache\CacheItemInterface;
use Psr\Cache\CacheItemPoolInterface;
use ReflectionClass;
use stdClass;
use Symfony\Component\Cache\Adapter\ArrayAdapter;

#[CoversClass(Psr6MetadataFactory::class)]
final class Psr6MetadataFactoryTest extends TestCase
Expand All @@ -32,7 +36,7 @@ public function testMetadataWithHit(): void
$cache = $this->createMock(CacheItemPoolInterface::class);
$cache->expects(self::once())
->method('getItem')
->with(stdClass::class)
->with(CacheKey::forClass(stdClass::class))
->willReturn($item);

$innerFactory = $this->createMock(MetadataFactory::class);
Expand Down Expand Up @@ -60,7 +64,44 @@ public function testMetadataWithMiss(): void
$cache = $this->createMock(CacheItemPoolInterface::class);
$cache->expects(self::once())
->method('getItem')
->with(CacheKey::forClass(stdClass::class))
->willReturn($item);
$cache->expects(self::once())
->method('save')
->with($item);

$innerFactory = $this->createMock(MetadataFactory::class);
$innerFactory->expects(self::once())
->method('metadata')
->with(stdClass::class)
->willReturn($classMetadata);

$factory = new Psr6MetadataFactory($innerFactory, $cache);
$result = $factory->metadata(stdClass::class);

self::assertSame($classMetadata, $result);
}

public function testMetadataIgnoresEntryOfOtherClass(): void
{
$otherMetadata = new ClassMetadata(new ReflectionClass(ProfileCreated::class));
$classMetadata = new ClassMetadata(new ReflectionClass(stdClass::class));

$item = $this->createMock(CacheItemInterface::class);
$item->expects(self::once())
->method('isHit')
->willReturn(true);
$item->expects(self::once())
->method('get')
->willReturn($otherMetadata);
$item->expects(self::once())
->method('set')
->with($classMetadata);

$cache = $this->createMock(CacheItemPoolInterface::class);
$cache->expects(self::once())
->method('getItem')
->with(CacheKey::forClass(stdClass::class))
->willReturn($item);
$cache->expects(self::once())
->method('save')
Expand All @@ -77,4 +118,16 @@ public function testMetadataWithMiss(): void

self::assertSame($classMetadata, $result);
}

public function testMetadataWithSymfonyCache(): void
{
$cache = new ArrayAdapter();
$factory = new Psr6MetadataFactory(new AttributeMetadataFactory(), $cache);

$metadata = $factory->metadata(ProfileCreated::class);

self::assertSame(ProfileCreated::class, $metadata->className);
self::assertTrue($cache->hasItem(CacheKey::forClass(ProfileCreated::class)));
self::assertEquals($metadata, $factory->metadata(ProfileCreated::class));
}
}
Loading