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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@ phpstan.neon
infection.log
infection.html
.phpbench/
.aider*
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
],
"require": {
"php": "~8.3.0 || ~8.4.0 || ~8.5.0" ,
"patchlevel/hydrator": "^1.18.0"
"patchlevel/hydrator": "dev-add-methods-on-normalizers as 1.23.0"
},
"require-dev": {
"ext-mongodb": "^2.1",
Expand Down
152 changes: 80 additions & 72 deletions composer.lock

Large diffs are not rendered by default.

6 changes: 4 additions & 2 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
cacheDirectory=".phpunit.cache">
<testsuites>
<testsuite name="integration">
<directory>tests</directory>
<exclude>tests/IntegrationTest.php</exclude>
<directory>tests/Integration</directory>
</testsuite>
<testsuite name="unit">
<directory>tests/Unit</directory>
</testsuite>
</testsuites>
<source>
Expand Down
9 changes: 1 addition & 8 deletions src/Hydrator/ODMExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,11 @@

use Patchlevel\Hydrator\Extension;
use Patchlevel\Hydrator\StackHydratorBuilder;
use Patchlevel\ODM\Metadata\AttributeDocumentMetadataFactory;
use Patchlevel\ODM\Metadata\DocumentMetadataFactory;

final class ODMExtension implements Extension
{
public function __construct(
private readonly DocumentMetadataFactory $factory = new AttributeDocumentMetadataFactory(),
) {
}

public function configure(StackHydratorBuilder $builder): void
{
$builder->addMetadataEnricher(new ODMMappingMetadataEnricher($this->factory));
$builder->addMiddleware(new ODMMiddleware());
}
}
44 changes: 0 additions & 44 deletions src/Hydrator/ODMMappingMetadataEnricher.php

This file was deleted.

53 changes: 53 additions & 0 deletions src/Hydrator/ODMMiddleware.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
<?php

declare(strict_types=1);

namespace Patchlevel\ODM\Hydrator;

use Patchlevel\Hydrator\Metadata\ClassMetadata;
use Patchlevel\Hydrator\Middleware\Middleware;
use Patchlevel\Hydrator\Middleware\Stack;
use Patchlevel\ODM\Metadata\DocumentMetadata;

class ODMMiddleware implements Middleware
{
private const ID_FIELD_NAME = '_id';

public function hydrate(ClassMetadata $metadata, array $data, array $context, Stack $stack): object
{
$documentMetadata = $context[DocumentMetadata::class] ?? null;

if (!$documentMetadata instanceof DocumentMetadata) {
return $stack->next()->hydrate($metadata, $data, $context, $stack);
}

unset($context[DocumentMetadata::class]);

$fieldName = $metadata->properties[$documentMetadata->idProperty]->fieldName;

$data[$fieldName] = $data[self::ID_FIELD_NAME];
unset($data[self::ID_FIELD_NAME]);

return $stack->next()->hydrate($metadata, $data, $context, $stack);
}

public function extract(ClassMetadata $metadata, object $object, array $context, Stack $stack): array
{
$documentMetadata = $context[DocumentMetadata::class] ?? null;

if (!$documentMetadata instanceof DocumentMetadata) {
return $stack->next()->extract($metadata, $object, $context, $stack);
}

unset($context[DocumentMetadata::class]);

$data = $stack->next()->extract($metadata, $object, $context, $stack);

$fieldName = $metadata->properties[$documentMetadata->idProperty]->fieldName;

$data[self::ID_FIELD_NAME] = $data[$fieldName];
unset($data[$fieldName]);

return $data;
}
}
36 changes: 34 additions & 2 deletions src/Metadata/AttributeDocumentMetadataFactory.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,18 @@
use Patchlevel\ODM\Index;
use ReflectionClass;

final readonly class AttributeDocumentMetadataFactory implements DocumentMetadataFactory
final class AttributeDocumentMetadataFactory implements DocumentMetadataFactory
{
/**
* @var array<class-string<object>, DocumentMetadata<object>>
*/
private array $metadataCache = [];

public function __construct(
private readonly FieldMappingResolver|null $fieldResolver = null,
) {
}

/**
* @param class-string<T> $className
*
Expand All @@ -21,6 +31,10 @@
*/
public function metadata(string $className): DocumentMetadata
{
if (isset($this->metadataCache[$className])) {
return $this->metadataCache[$className];

Check failure on line 35 in src/Metadata/AttributeDocumentMetadataFactory.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.4, ubuntu-latest)

Method Patchlevel\ODM\Metadata\AttributeDocumentMetadataFactory::metadata() should return Patchlevel\ODM\Metadata\DocumentMetadata<T of object> but returns Patchlevel\ODM\Metadata\DocumentMetadata<object>.
}

$reflection = new ReflectionClass($className);

$attributes = $reflection->getAttributes(Document::class);
Expand All @@ -34,6 +48,7 @@
$collection = $attribute->collection;
$database = $attribute->database;
$idProperty = null;
$fields = [];

foreach ($reflection->getProperties() as $reflectionProperty) {
$attributes = $reflectionProperty->getAttributes(Id::class);
Expand All @@ -49,16 +64,33 @@
$idProperty = $reflectionProperty->getName();
}

foreach ($reflection->getProperties() as $reflectionProperty) {
if ($idProperty === $reflectionProperty->getName()) {
$fields[$reflectionProperty->getName()] = new FieldMapping('_id');

continue;
}

$field = $this->fieldResolver?->resolve($reflectionProperty);

if (!$field) {
continue;
}

$fields[$reflectionProperty->getName()] = $field;
}

if ($idProperty === null) {
throw new NoIdPropertyFound($className);
}

return new DocumentMetadata(
return $this->metadataCache[$className] = new DocumentMetadata(
$className,
$database,
$collection,
$idProperty,
$this->indexes($reflection),
$fields,
);
}

Expand Down
84 changes: 82 additions & 2 deletions src/Metadata/DocumentMetadata.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,19 +6,99 @@

use Patchlevel\ODM\Index;

/** @template T of object */
/**
* @template T of object
*/
final readonly class DocumentMetadata
{
/**
* @param class-string<T> $className
* @param list<Index> $indexes
* @param list<Index> $indexes
* @param array<string, FieldMapping> $fields
*/
public function __construct(
public string $className,
public string|null $database,
public string $collection,
public string $idProperty,
public array $indexes = [],
public array $fields = [],
) {
}

public function propertyPathToFieldPath(string $propertyPath): string
{
$parts = explode('.', $propertyPath);
$fields = $this->fields;
$fieldParts = [];

foreach ($parts as $part) {
if (!isset($fields[$part])) {
$fieldParts[] = $part;
continue;
}

$field = $fields[$part];
$fieldParts[] = $field->fieldName;
$fields = $field->children;
}

return implode('.', $fieldParts);
}

/**
* @param array<string|int, mixed> $filter
*
* @return array<string|int, mixed>
*/
public function mapFilterToFieldPaths(array $filter): array
{
$result = [];

foreach ($filter as $key => $value) {
if (!is_string($key)) {
$result[$key] = is_array($value) ? $this->mapFilterToFieldPaths($value) : $value;
continue;
}

if (str_starts_with($key, '$')) {
if (is_array($value)) {
if (array_is_list($value)) {
$result[$key] = array_map(
static fn (mixed $item): mixed => is_array($item) ? $this->mapFilterToFieldPaths($item) : $item,

Check failure on line 68 in src/Metadata/DocumentMetadata.php

View workflow job for this annotation

GitHub Actions / Static Analysis by PHPStan (locked, 8.4, ubuntu-latest)

Cannot call method mapFilterToFieldPaths() on mixed.
$value
);
} else {
$result[$key] = $this->mapFilterToFieldPaths($value);
}
} else {
$result[$key] = $value;
}

continue;
}

$fieldPath = $this->propertyPathToFieldPath($key);

$result[$fieldPath] = is_array($value) ? $this->mapFilterToFieldPaths($value) : $value;
}

return $result;
}

/**
* @param array<string, 'asc' | 'desc'> $orderBy
*
* @return array<string, -1|1>
*/
public function mapSortingToFieldPaths(array $orderBy): array
{
$result = [];

foreach ($orderBy as $propertyPath => $direction) {
$result[$this->propertyPathToFieldPath($propertyPath)] = $direction === 'desc' ? -1 : 1;
}

return $result;
}
}
15 changes: 15 additions & 0 deletions src/Metadata/FieldMapping.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<?php

namespace Patchlevel\ODM\Metadata;

final readonly class FieldMapping
{
/**
* @param array<string, FieldMapping> $children
*/
public function __construct(
public string $fieldName,
public array $children = [],
) {
}
}
10 changes: 10 additions & 0 deletions src/Metadata/FieldMappingResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
<?php

namespace Patchlevel\ODM\Metadata;

use ReflectionProperty;

interface FieldMappingResolver
{
public function resolve(ReflectionProperty $reflectionProperty): FieldMapping|null;
}
Loading
Loading