diff --git a/.gitignore b/.gitignore
index b0f6ffa..f69ee25 100644
--- a/.gitignore
+++ b/.gitignore
@@ -7,3 +7,4 @@ phpstan.neon
infection.log
infection.html
.phpbench/
+.aider*
diff --git a/composer.json b/composer.json
index ce6fbd1..4d46762 100644
--- a/composer.json
+++ b/composer.json
@@ -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",
diff --git a/composer.lock b/composer.lock
index 9c7b621..9806354 100644
--- a/composer.lock
+++ b/composer.lock
@@ -4,20 +4,20 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
- "content-hash": "638c502b45f9b612af14255b8f5c7b76",
+ "content-hash": "5679695186cd93daaefd4e8f01bc87e6",
"packages": [
{
"name": "patchlevel/hydrator",
- "version": "1.18.0",
+ "version": "dev-add-methods-on-normalizers",
"source": {
"type": "git",
"url": "https://github.com/patchlevel/hydrator.git",
- "reference": "fb802134da1eb6294a4358b1f55dbe3621104d80"
+ "reference": "4b8f1007d56a47764057302c538241bc0b64b79f"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/patchlevel/hydrator/zipball/fb802134da1eb6294a4358b1f55dbe3621104d80",
- "reference": "fb802134da1eb6294a4358b1f55dbe3621104d80",
+ "url": "https://api.github.com/repos/patchlevel/hydrator/zipball/4b8f1007d56a47764057302c538241bc0b64b79f",
+ "reference": "4b8f1007d56a47764057302c538241bc0b64b79f",
"shasum": ""
},
"require": {
@@ -66,9 +66,9 @@
],
"support": {
"issues": "https://github.com/patchlevel/hydrator/issues",
- "source": "https://github.com/patchlevel/hydrator/tree/1.18.0"
+ "source": "https://github.com/patchlevel/hydrator/tree/add-methods-on-normalizers"
},
- "time": "2026-03-13T17:09:15+00:00"
+ "time": "2026-04-08T14:05:16+00:00"
},
{
"name": "psr/cache",
@@ -275,16 +275,16 @@
},
{
"name": "symfony/event-dispatcher",
- "version": "v8.0.4",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/event-dispatcher.git",
- "reference": "99301401da182b6cfaa4700dbe9987bb75474b47"
+ "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99301401da182b6cfaa4700dbe9987bb75474b47",
- "reference": "99301401da182b6cfaa4700dbe9987bb75474b47",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/f662acc6ab22a3d6d716dcb44c381c6002940df6",
+ "reference": "f662acc6ab22a3d6d716dcb44c381c6002940df6",
"shasum": ""
},
"require": {
@@ -336,7 +336,7 @@
"description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.4"
+ "source": "https://github.com/symfony/event-dispatcher/tree/v8.0.8"
},
"funding": [
{
@@ -356,7 +356,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-05T11:45:55+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/event-dispatcher-contracts",
@@ -436,16 +436,16 @@
},
{
"name": "symfony/type-info",
- "version": "v8.0.7",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/type-info.git",
- "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195"
+ "reference": "622d81551770029d44d16be68969712eb47892f1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/type-info/zipball/3c7de103dd6cb68be24e155838a64ef4a70ae195",
- "reference": "3c7de103dd6cb68be24e155838a64ef4a70ae195",
+ "url": "https://api.github.com/repos/symfony/type-info/zipball/622d81551770029d44d16be68969712eb47892f1",
+ "reference": "622d81551770029d44d16be68969712eb47892f1",
"shasum": ""
},
"require": {
@@ -494,7 +494,7 @@
"type"
],
"support": {
- "source": "https://github.com/symfony/type-info/tree/v8.0.7"
+ "source": "https://github.com/symfony/type-info/tree/v8.0.8"
},
"funding": [
{
@@ -514,7 +514,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-04T13:55:34+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
}
],
"packages-dev": [
@@ -1425,16 +1425,16 @@
},
{
"name": "justinrainbow/json-schema",
- "version": "v6.7.2",
+ "version": "6.8.0",
"source": {
"type": "git",
"url": "https://github.com/jsonrainbow/json-schema.git",
- "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0"
+ "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/6fea66c7204683af437864e7c4e7abf383d14bc0",
- "reference": "6fea66c7204683af437864e7c4e7abf383d14bc0",
+ "url": "https://api.github.com/repos/jsonrainbow/json-schema/zipball/89ac92bcfe5d0a8a4433c7b89d394553ae7250cc",
+ "reference": "89ac92bcfe5d0a8a4433c7b89d394553ae7250cc",
"shasum": ""
},
"require": {
@@ -1494,9 +1494,9 @@
],
"support": {
"issues": "https://github.com/jsonrainbow/json-schema/issues",
- "source": "https://github.com/jsonrainbow/json-schema/tree/v6.7.2"
+ "source": "https://github.com/jsonrainbow/json-schema/tree/6.8.0"
},
- "time": "2026-02-15T15:06:22+00:00"
+ "time": "2026-04-02T12:43:11+00:00"
},
{
"name": "marc-mabe/php-enum",
@@ -2335,11 +2335,11 @@
},
{
"name": "phpstan/phpstan",
- "version": "2.1.44",
+ "version": "2.1.46",
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/phpstan/phpstan/zipball/4a88c083c668b2c364a425c9b3171b2d9ea5d218",
- "reference": "4a88c083c668b2c364a425c9b3171b2d9ea5d218",
+ "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
+ "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25",
"shasum": ""
},
"require": {
@@ -2384,7 +2384,7 @@
"type": "github"
}
],
- "time": "2026-03-25T17:34:21+00:00"
+ "time": "2026-04-01T09:25:14+00:00"
},
{
"name": "phpstan/phpstan-phpunit",
@@ -4519,16 +4519,16 @@
},
{
"name": "symfony/console",
- "version": "v7.4.7",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
- "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d"
+ "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/console/zipball/e1e6770440fb9c9b0cf725f81d1361ad1835329d",
- "reference": "e1e6770440fb9c9b0cf725f81d1361ad1835329d",
+ "url": "https://api.github.com/repos/symfony/console/zipball/1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707",
+ "reference": "1e92e39c51f95b88e3d66fa2d9f06d1fb45dd707",
"shasum": ""
},
"require": {
@@ -4593,7 +4593,7 @@
"terminal"
],
"support": {
- "source": "https://github.com/symfony/console/tree/v7.4.7"
+ "source": "https://github.com/symfony/console/tree/v7.4.8"
},
"funding": [
{
@@ -4613,7 +4613,7 @@
"type": "tidelift"
}
],
- "time": "2026-03-06T14:06:20+00:00"
+ "time": "2026-03-30T13:54:39+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -4684,16 +4684,16 @@
},
{
"name": "symfony/filesystem",
- "version": "v7.4.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/filesystem.git",
- "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e"
+ "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/filesystem/zipball/3ebc794fa5315e59fd122561623c2e2e4280538e",
- "reference": "3ebc794fa5315e59fd122561623c2e2e4280538e",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/58b9790d12f9670b7f53a1c1738febd3108970a5",
+ "reference": "58b9790d12f9670b7f53a1c1738febd3108970a5",
"shasum": ""
},
"require": {
@@ -4730,7 +4730,7 @@
"description": "Provides basic utilities for the filesystem",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/filesystem/tree/v7.4.6"
+ "source": "https://github.com/symfony/filesystem/tree/v7.4.8"
},
"funding": [
{
@@ -4750,20 +4750,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-25T16:50:00+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/finder",
- "version": "v7.4.6",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/finder.git",
- "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf"
+ "reference": "e0be088d22278583a82da281886e8c3592fbf149"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/finder/zipball/8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
- "reference": "8655bf1076b7a3a346cb11413ffdabff50c7ffcf",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/e0be088d22278583a82da281886e8c3592fbf149",
+ "reference": "e0be088d22278583a82da281886e8c3592fbf149",
"shasum": ""
},
"require": {
@@ -4798,7 +4798,7 @@
"description": "Finds files and directories via an intuitive fluent interface",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/finder/tree/v7.4.6"
+ "source": "https://github.com/symfony/finder/tree/v7.4.8"
},
"funding": [
{
@@ -4818,20 +4818,20 @@
"type": "tidelift"
}
],
- "time": "2026-01-29T09:40:50+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/options-resolver",
- "version": "v8.0.0",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/options-resolver.git",
- "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7"
+ "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/options-resolver/zipball/d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
- "reference": "d2b592535ffa6600c265a3893a7f7fd2bad82dd7",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/b48bce0a70b914f6953dafbd10474df232ed4de8",
+ "reference": "b48bce0a70b914f6953dafbd10474df232ed4de8",
"shasum": ""
},
"require": {
@@ -4869,7 +4869,7 @@
"options"
],
"support": {
- "source": "https://github.com/symfony/options-resolver/tree/v8.0.0"
+ "source": "https://github.com/symfony/options-resolver/tree/v8.0.8"
},
"funding": [
{
@@ -4889,7 +4889,7 @@
"type": "tidelift"
}
],
- "time": "2025-11-12T15:55:31+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/polyfill-ctype",
@@ -5308,16 +5308,16 @@
},
{
"name": "symfony/process",
- "version": "v7.4.5",
+ "version": "v7.4.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/process.git",
- "reference": "608476f4604102976d687c483ac63a79ba18cc97"
+ "reference": "60f19cd3badc8de688421e21e4305eba50f8089a"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/process/zipball/608476f4604102976d687c483ac63a79ba18cc97",
- "reference": "608476f4604102976d687c483ac63a79ba18cc97",
+ "url": "https://api.github.com/repos/symfony/process/zipball/60f19cd3badc8de688421e21e4305eba50f8089a",
+ "reference": "60f19cd3badc8de688421e21e4305eba50f8089a",
"shasum": ""
},
"require": {
@@ -5349,7 +5349,7 @@
"description": "Executes commands in sub-processes",
"homepage": "https://symfony.com",
"support": {
- "source": "https://github.com/symfony/process/tree/v7.4.5"
+ "source": "https://github.com/symfony/process/tree/v7.4.8"
},
"funding": [
{
@@ -5369,7 +5369,7 @@
"type": "tidelift"
}
],
- "time": "2026-01-26T15:07:59+00:00"
+ "time": "2026-03-24T13:12:05+00:00"
},
{
"name": "symfony/service-contracts",
@@ -5460,16 +5460,16 @@
},
{
"name": "symfony/string",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
- "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4"
+ "reference": "ae9488f874d7603f9d2dfbf120203882b645d963"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/string/zipball/6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
- "reference": "6c9e1108041b5dce21a9a4984b531c4923aa9ec4",
+ "url": "https://api.github.com/repos/symfony/string/zipball/ae9488f874d7603f9d2dfbf120203882b645d963",
+ "reference": "ae9488f874d7603f9d2dfbf120203882b645d963",
"shasum": ""
},
"require": {
@@ -5526,7 +5526,7 @@
"utf8"
],
"support": {
- "source": "https://github.com/symfony/string/tree/v8.0.6"
+ "source": "https://github.com/symfony/string/tree/v8.0.8"
},
"funding": [
{
@@ -5546,20 +5546,20 @@
"type": "tidelift"
}
],
- "time": "2026-02-09T10:14:57+00:00"
+ "time": "2026-03-30T15:14:47+00:00"
},
{
"name": "symfony/var-dumper",
- "version": "v8.0.6",
+ "version": "v8.0.8",
"source": {
"type": "git",
"url": "https://github.com/symfony/var-dumper.git",
- "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209"
+ "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1"
},
"dist": {
"type": "zip",
- "url": "https://api.github.com/repos/symfony/var-dumper/zipball/2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209",
- "reference": "2e14f7e0bf5ff02c6e63bd31cb8e4855a13d6209",
+ "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1",
+ "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1",
"shasum": ""
},
"require": {
@@ -5613,7 +5613,7 @@
"dump"
],
"support": {
- "source": "https://github.com/symfony/var-dumper/tree/v8.0.6"
+ "source": "https://github.com/symfony/var-dumper/tree/v8.0.8"
},
"funding": [
{
@@ -5633,7 +5633,7 @@
"type": "tidelift"
}
],
- "time": "2026-02-15T10:53:29+00:00"
+ "time": "2026-03-31T07:15:36+00:00"
},
{
"name": "thecodingmachine/safe",
@@ -5936,9 +5936,17 @@
"time": "2024-03-07T20:33:40+00:00"
}
],
- "aliases": [],
+ "aliases": [
+ {
+ "package": "patchlevel/hydrator",
+ "version": "dev-add-methods-on-normalizers",
+ "alias": "1.23.0",
+ "alias_normalized": "1.23.0.0"
+ }
+ ],
"minimum-stability": "stable",
"stability-flags": {
+ "patchlevel/hydrator": 20,
"patchlevel/rango": 15
},
"prefer-stable": false,
diff --git a/phpunit.xml.dist b/phpunit.xml.dist
index 60acd9e..e944c91 100644
--- a/phpunit.xml.dist
+++ b/phpunit.xml.dist
@@ -6,8 +6,10 @@
cacheDirectory=".phpunit.cache">
- tests
- tests/IntegrationTest.php
+ tests/Integration
+
+
+ tests/Unit
diff --git a/src/Hydrator/ODMExtension.php b/src/Hydrator/ODMExtension.php
index bd030fc..5b4c391 100644
--- a/src/Hydrator/ODMExtension.php
+++ b/src/Hydrator/ODMExtension.php
@@ -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());
}
}
diff --git a/src/Hydrator/ODMMappingMetadataEnricher.php b/src/Hydrator/ODMMappingMetadataEnricher.php
deleted file mode 100644
index b83093b..0000000
--- a/src/Hydrator/ODMMappingMetadataEnricher.php
+++ /dev/null
@@ -1,44 +0,0 @@
-factory->metadata($classMetadata->className);
-
- $classMetadata->extras[DocumentMetadata::class] = $documentMetadata;
-
- $propertyMetadata = $classMetadata->properties[$documentMetadata->idProperty] ?? null;
-
- if ($propertyMetadata === null) {
- throw new RuntimeException();
- }
-
- if ($propertyMetadata->fieldName !== $propertyMetadata->propertyName) {
- throw new RuntimeException();
- }
-
- $propertyMetadata->fieldName = '_id';
- } catch (ClassIsNotAnDocument) {
- return;
- }
- }
-}
diff --git a/src/Hydrator/ODMMiddleware.php b/src/Hydrator/ODMMiddleware.php
new file mode 100644
index 0000000..50ddfca
--- /dev/null
+++ b/src/Hydrator/ODMMiddleware.php
@@ -0,0 +1,53 @@
+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;
+ }
+}
diff --git a/src/Metadata/AttributeDocumentMetadataFactory.php b/src/Metadata/AttributeDocumentMetadataFactory.php
index 4e02913..4845dcb 100644
--- a/src/Metadata/AttributeDocumentMetadataFactory.php
+++ b/src/Metadata/AttributeDocumentMetadataFactory.php
@@ -10,8 +10,18 @@
use Patchlevel\ODM\Index;
use ReflectionClass;
-final readonly class AttributeDocumentMetadataFactory implements DocumentMetadataFactory
+final class AttributeDocumentMetadataFactory implements DocumentMetadataFactory
{
+ /**
+ * @var array, DocumentMetadata