diff --git a/Makefile b/Makefile index 673af92..4b83860 100644 --- a/Makefile +++ b/Makefile @@ -26,7 +26,7 @@ phpunit: vendor phpunit-unit phpunit-integration # .PHONY: phpunit-integration phpunit-integration: vendor ## run phpunit integration tests - MONGODB_URI="mongodb://localhost:27017" POSTGRES_URI="pgsql:host=localhost;port=5432;dbname=eventstore;user=postgres;password=postgres" vendor/bin/phpunit --testsuite=integrationn --no-coverage + MONGODB_URI="mongodb://localhost:27017" POSTGRES_URI="pgsql:host=localhost;port=5432;dbname=eventstore;user=postgres;password=postgres" vendor/bin/phpunit --testsuite=integration --no-coverage .PHONY: phpunit-integration-postgres phpunit-integration-postgres: vendor ## run phpunit integration tests on postgres diff --git a/composer.json b/composer.json index ff1812c..ee0e846 100644 --- a/composer.json +++ b/composer.json @@ -34,7 +34,8 @@ "phpbench/phpbench": "^1.6.1", "phpstan/phpstan": "^2.1.46", "phpstan/phpstan-phpunit": "^2.0.16", - "phpunit/phpunit": "^11.5.55" + "phpunit/phpunit": "^11.5.55", + "symfony/var-dumper": "^v7.4.8 || ^v8.0.0" }, "config": { "preferred-install": { diff --git a/composer.lock b/composer.lock index 2a56a69..6540086 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "5f6fd67afd1c794ec76dc5f3051a1d21", + "content-hash": "62bba0fff901fc3b8200ff3cabe759a4", "packages": [ { "name": "patchlevel/hydrator", @@ -1241,16 +1241,16 @@ }, { "name": "infection/infection", - "version": "0.32.6", + "version": "0.32.7", "source": { "type": "git", "url": "https://github.com/infection/infection.git", - "reference": "4ed769947eaf2ecf42203027301bad2bedf037e5" + "reference": "066ee69f1e8e6dec8965a79d5ba020656c590b5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/infection/infection/zipball/4ed769947eaf2ecf42203027301bad2bedf037e5", - "reference": "4ed769947eaf2ecf42203027301bad2bedf037e5", + "url": "https://api.github.com/repos/infection/infection/zipball/066ee69f1e8e6dec8965a79d5ba020656c590b5e", + "reference": "066ee69f1e8e6dec8965a79d5ba020656c590b5e", "shasum": "" }, "require": { @@ -1269,7 +1269,7 @@ "justinrainbow/json-schema": "^6.0", "nikic/php-parser": "^5.6.2", "ondram/ci-detector": "^4.1.0", - "php": "^8.2", + "php": "^8.3", "psr/log": "^2.0 || ^3.0", "sanmai/di-container": "^0.1.12", "sanmai/duoclock": "^0.1.0", @@ -1289,9 +1289,11 @@ "dg/bypass-finals": "<1.4.1" }, "require-dev": { + "carthage-software/mago": "^1.20", "ext-simplexml": "*", "fidry/makefile": "^1.0", "fig/log-test": "^1.2", + "phpat/phpat": "^0.12.4", "phpbench/phpbench": "^1.4", "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "^2.1", @@ -1300,7 +1302,7 @@ "phpstan/phpstan-webmozart-assert": "^2.0", "phpunit/phpunit": "^11.5.27", "rector/rector": "^2.2.4", - "shipmonk/dead-code-detector": "^0.14.0", + "shipmonk/dead-code-detector": "^0.15", "shipmonk/name-collision-detector": "^2.1", "sidz/phpstan-rules": "^0.5.1", "symfony/yaml": "^6.4 || ^7.0 || ^8.0", @@ -1361,7 +1363,7 @@ ], "support": { "issues": "https://github.com/infection/infection/issues", - "source": "https://github.com/infection/infection/tree/0.32.6" + "source": "https://github.com/infection/infection/tree/0.32.7" }, "funding": [ { @@ -1373,7 +1375,7 @@ "type": "open_collective" } ], - "time": "2026-02-26T14:34:26+00:00" + "time": "2026-04-23T21:11:41+00:00" }, { "name": "infection/mutator", @@ -2340,11 +2342,11 @@ }, { "name": "phpstan/phpstan", - "version": "2.1.46", + "version": "2.1.51", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", - "reference": "a193923fc2d6325ef4e741cf3af8c3e8f54dbf25", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/dc3b523c45e714c70de2ac5113b958223b55dc59", + "reference": "dc3b523c45e714c70de2ac5113b958223b55dc59", "shasum": "" }, "require": { @@ -2389,7 +2391,7 @@ "type": "github" } ], - "time": "2026-04-01T09:25:14+00:00" + "time": "2026-04-21T18:22:01+00:00" }, { "name": "phpstan/phpstan-phpunit", @@ -4890,7 +4892,7 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.34.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -4949,7 +4951,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.34.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.37.0" }, "funding": [ { @@ -4973,16 +4975,16 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.34.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df" + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/ad1b7b9092976d6c948b8a187cec9faaea9ec1df", - "reference": "ad1b7b9092976d6c948b8a187cec9faaea9ec1df", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/4864388bfbd3001ce88e234fab652acd91fdc57e", + "reference": "4864388bfbd3001ce88e234fab652acd91fdc57e", "shasum": "" }, "require": { @@ -5031,7 +5033,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.34.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.37.0" }, "funding": [ { @@ -5051,11 +5053,11 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:19:22+00:00" + "time": "2026-04-26T13:13:48+00:00" }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.34.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -5116,7 +5118,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.34.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.37.0" }, "funding": [ { @@ -5140,7 +5142,7 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.34.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", @@ -5201,7 +5203,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.34.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.37.0" }, "funding": [ { @@ -5225,16 +5227,16 @@ }, { "name": "symfony/polyfill-php85", - "version": "v1.34.0", + "version": "v1.37.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php85.git", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e" + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/2c408a6bb0313e6001a83628dc5506100474254e", - "reference": "2c408a6bb0313e6001a83628dc5506100474254e", + "url": "https://api.github.com/repos/symfony/polyfill-php85/zipball/fcfa4973a9917cef23f2e38774da74a2b7d115ee", + "reference": "fcfa4973a9917cef23f2e38774da74a2b7d115ee", "shasum": "" }, "require": { @@ -5281,7 +5283,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php85/tree/v1.34.0" + "source": "https://github.com/symfony/polyfill-php85/tree/v1.37.0" }, "funding": [ { @@ -5301,7 +5303,7 @@ "type": "tidelift" } ], - "time": "2026-04-10T16:50:15+00:00" + "time": "2026-04-26T13:10:57+00:00" }, { "name": "symfony/process", @@ -5545,6 +5547,93 @@ ], "time": "2026-03-30T15:14:47+00:00" }, + { + "name": "symfony/var-dumper", + "version": "v8.0.8", + "source": { + "type": "git", + "url": "https://github.com/symfony/var-dumper.git", + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/var-dumper/zipball/cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", + "reference": "cfb7badd53bf4177f6e9416cfbbccc13c0e773a1", + "shasum": "" + }, + "require": { + "php": ">=8.4", + "symfony/polyfill-mbstring": "^1.0" + }, + "conflict": { + "symfony/console": "<7.4", + "symfony/error-handler": "<7.4" + }, + "require-dev": { + "symfony/console": "^7.4|^8.0", + "symfony/http-kernel": "^7.4|^8.0", + "symfony/process": "^7.4|^8.0", + "symfony/uid": "^7.4|^8.0", + "twig/twig": "^3.12" + }, + "bin": [ + "Resources/bin/var-dump-server" + ], + "type": "library", + "autoload": { + "files": [ + "Resources/functions/dump.php" + ], + "psr-4": { + "Symfony\\Component\\VarDumper\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nicolas Grekas", + "email": "p@tchwork.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Provides mechanisms for walking through any arbitrary PHP variable", + "homepage": "https://symfony.com", + "keywords": [ + "debug", + "dump" + ], + "support": { + "source": "https://github.com/symfony/var-dumper/tree/v8.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://github.com/nicolas-grekas", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2026-03-31T07:15:36+00:00" + }, { "name": "thecodingmachine/safe", "version": "v3.4.0", diff --git a/src/Metadata/DocumentMetadata.php b/src/Metadata/DocumentMetadata.php index 2ce3e7b..6634bcc 100644 --- a/src/Metadata/DocumentMetadata.php +++ b/src/Metadata/DocumentMetadata.php @@ -7,6 +7,7 @@ use Patchlevel\ODM\Index; use function array_is_list; +use function array_keys; use function array_map; use function explode; use function implode; @@ -93,16 +94,22 @@ private function propertyPathToFieldPathWithChildren(string $propertyPath, array { $parts = explode('.', $propertyPath); $fieldParts = []; + $mappedPropertyParts = []; foreach ($parts as $part) { if (!isset($fields[$part])) { - $fieldParts[] = $part; - $fields = []; - continue; + throw new UnknownPropertyPath( + $this->className, + $propertyPath, + $part, + implode('.', $mappedPropertyParts), + array_keys($fields), + ); } $field = $fields[$part]; $fieldParts[] = $field->fieldName; + $mappedPropertyParts[] = $part; $fields = $field->children; } diff --git a/src/Metadata/UnknownPropertyPath.php b/src/Metadata/UnknownPropertyPath.php new file mode 100644 index 0000000..92dfc68 --- /dev/null +++ b/src/Metadata/UnknownPropertyPath.php @@ -0,0 +1,37 @@ + $knownProperties + */ + public function __construct( + string $className, + string $propertyPath, + string $unknownSegment, + string $mappedPrefix, + array $knownProperties, + ) { + $context = $mappedPrefix !== '' ? $mappedPrefix : ''; + $known = $knownProperties !== [] ? implode(', ', $knownProperties) : ''; + + parent::__construct(sprintf( + 'Unknown property path "%s" for class %s: segment "%s" is not mapped under "%s". Known segments: %s.', + $propertyPath, + $className, + $unknownSegment, + $context, + $known, + )); + } +} diff --git a/tests/Integration/MongoDBRepositoryTest.php b/tests/Integration/MongoDBRepositoryTest.php index 6e7315e..2a80781 100644 --- a/tests/Integration/MongoDBRepositoryTest.php +++ b/tests/Integration/MongoDBRepositoryTest.php @@ -5,18 +5,14 @@ namespace Patchlevel\ODM\Tests\Integration; use MongoDB\Client; -use Patchlevel\Hydrator\HydratorWithContext; -use Patchlevel\ODM\Metadata\DocumentMetadataFactory; use Patchlevel\ODM\Repository\MongoDBRepositoryManager; use function getenv; final class MongoDBRepositoryTest extends RepositoryTestCase { - public function createRepositoryManager( - HydratorWithContext $hydrator, - DocumentMetadataFactory $documentMetadataFactory, - ): MongoDBRepositoryManager { + public function createRepositoryManager(): MongoDBRepositoryManager + { $uri = getenv('MONGODB_URI'); if (!$uri) { @@ -25,11 +21,6 @@ public function createRepositoryManager( $client = new Client($uri); - return new MongoDBRepositoryManager( - $client, - $documentMetadataFactory, - $hydrator, - 'patchlevel', - ); + return MongoDBRepositoryManager::create($client); } } diff --git a/tests/Integration/RangoRepositoryTest.php b/tests/Integration/RangoRepositoryTest.php index fd143cc..03ac02b 100644 --- a/tests/Integration/RangoRepositoryTest.php +++ b/tests/Integration/RangoRepositoryTest.php @@ -4,8 +4,6 @@ namespace Patchlevel\ODM\Tests\Integration; -use Patchlevel\Hydrator\HydratorWithContext; -use Patchlevel\ODM\Metadata\DocumentMetadataFactory; use Patchlevel\ODM\Repository\RangoRepositoryManager; use Patchlevel\Rango\Client; @@ -13,10 +11,8 @@ final class RangoRepositoryTest extends RepositoryTestCase { - public function createRepositoryManager( - HydratorWithContext $hydrator, - DocumentMetadataFactory $documentMetadataFactory, - ): RangoRepositoryManager { + public function createRepositoryManager(): RangoRepositoryManager + { $uri = getenv('POSTGRES_URI'); if (!$uri) { @@ -25,11 +21,6 @@ public function createRepositoryManager( $client = new Client($uri); - return new RangoRepositoryManager( - $client, - $documentMetadataFactory, - $hydrator, - 'patchlevel', - ); + return RangoRepositoryManager::create($client); } } diff --git a/tests/Integration/RepositoryTestCase.php b/tests/Integration/RepositoryTestCase.php index ed365af..a9f0b6e 100644 --- a/tests/Integration/RepositoryTestCase.php +++ b/tests/Integration/RepositoryTestCase.php @@ -4,12 +4,6 @@ namespace Patchlevel\ODM\Tests\Integration; -use Patchlevel\Hydrator\CoreExtension; -use Patchlevel\Hydrator\HydratorWithContext; -use Patchlevel\Hydrator\StackHydratorBuilder; -use Patchlevel\ODM\Hydrator\ODMExtension; -use Patchlevel\ODM\Metadata\AttributeDocumentMetadataFactory; -use Patchlevel\ODM\Metadata\DocumentMetadataFactory; use Patchlevel\ODM\Repository\InsertionFailed; use Patchlevel\ODM\Repository\MongoDBRepositoryManager; use Patchlevel\ODM\Repository\RangoRepositoryManager; @@ -27,21 +21,11 @@ abstract class RepositoryTestCase extends TestCase { protected MongoDBRepositoryManager|RangoRepositoryManager $repositoryManager; - abstract public function createRepositoryManager( - HydratorWithContext $hydrator, - DocumentMetadataFactory $documentMetadataFactory, - ): MongoDBRepositoryManager|RangoRepositoryManager; + abstract public function createRepositoryManager(): MongoDBRepositoryManager|RangoRepositoryManager; public function setUp(): void { - $documentMetadataFactory = new AttributeDocumentMetadataFactory(); - - $hydrator = (new StackHydratorBuilder()) - ->useExtension(new CoreExtension()) - ->useExtension(new ODMExtension()) - ->build(); - - $this->repositoryManager = $this->createRepositoryManager($hydrator, $documentMetadataFactory); + $this->repositoryManager = $this->createRepositoryManager(); $this->repositoryManager->get(Profile::class)->database()->drop(); } diff --git a/tests/Unit/Metadata/DocumentMetadataTest.php b/tests/Unit/Metadata/DocumentMetadataTest.php index 4f9d24b..06efaff 100644 --- a/tests/Unit/Metadata/DocumentMetadataTest.php +++ b/tests/Unit/Metadata/DocumentMetadataTest.php @@ -6,6 +6,7 @@ use Patchlevel\ODM\Metadata\DocumentMetadata; use Patchlevel\ODM\Metadata\FieldMapping; +use Patchlevel\ODM\Metadata\UnknownPropertyPath; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use stdClass; @@ -13,7 +14,7 @@ #[CoversClass(DocumentMetadata::class)] final class DocumentMetadataTest extends TestCase { - public function testPropertyPathToFieldPathWithoutMapping(): void + public function testPropertyPathToFieldPathWithoutMappingThrowsException(): void { $metadata = new DocumentMetadata( className: stdClass::class, @@ -22,10 +23,13 @@ className: stdClass::class, idProperty: 'id', ); - self::assertSame('name', $metadata->propertyPathToFieldPath('name')); + $this->expectException(UnknownPropertyPath::class); + $this->expectExceptionMessage('segment "name" is not mapped under ""'); + + $metadata->propertyPathToFieldPath('name'); } - public function testPropertyPathToFieldPathWithoutMappingNested(): void + public function testPropertyPathToFieldPathWithoutMappingNestedThrowsException(): void { $metadata = new DocumentMetadata( className: stdClass::class, @@ -34,7 +38,10 @@ className: stdClass::class, idProperty: 'id', ); - self::assertSame('a.b.c', $metadata->propertyPathToFieldPath('a.b.c')); + $this->expectException(UnknownPropertyPath::class); + $this->expectExceptionMessage('segment "a" is not mapped under ""'); + + $metadata->propertyPathToFieldPath('a.b.c'); } public function testPropertyPathToFieldPathWithMapping(): void @@ -69,7 +76,7 @@ className: stdClass::class, self::assertSame('_address._street', $metadata->propertyPathToFieldPath('address.street')); } - public function testPropertyPathToFieldPathWithPartialNestedMapping(): void + public function testPropertyPathToFieldPathWithPartialNestedMappingThrowsException(): void { $metadata = new DocumentMetadata( className: stdClass::class, @@ -81,7 +88,10 @@ className: stdClass::class, ], ); - self::assertSame('_address.street', $metadata->propertyPathToFieldPath('address.street')); + $this->expectException(UnknownPropertyPath::class); + $this->expectExceptionMessage('segment "street" is not mapped under "address"'); + + $metadata->propertyPathToFieldPath('address.street'); } public function testPropertyPathToFieldPathWithDeeplyNestedMapping(): void @@ -103,7 +113,7 @@ className: stdClass::class, self::assertSame('_a._b._c', $metadata->propertyPathToFieldPath('a.b.c')); } - public function testPropertyPathToFieldPathUnmappedFieldPassedThrough(): void + public function testPropertyPathToFieldPathUnmappedFieldThrowsException(): void { $metadata = new DocumentMetadata( className: stdClass::class, @@ -113,10 +123,13 @@ className: stdClass::class, fields: [], ); - self::assertSame('unknown', $metadata->propertyPathToFieldPath('unknown')); + $this->expectException(UnknownPropertyPath::class); + $this->expectExceptionMessage('segment "unknown" is not mapped under ""'); + + $metadata->propertyPathToFieldPath('unknown'); } - public function testMapFilterWithoutMapping(): void + public function testMapFilterWithoutMappingThrowsException(): void { $metadata = new DocumentMetadata( className: stdClass::class, @@ -125,7 +138,10 @@ className: stdClass::class, idProperty: 'id', ); - self::assertSame(['name' => 'foo'], $metadata->mapFilterToFieldPaths(['name' => 'foo'])); + $this->expectException(UnknownPropertyPath::class); + $this->expectExceptionMessage('segment "name" is not mapped under ""'); + + $metadata->mapFilterToFieldPaths(['name' => 'foo']); } public function testMapFilterWithMapping(): void @@ -326,7 +342,7 @@ className: stdClass::class, ); } - public function testMapSortingWithoutMapping(): void + public function testMapSortingWithoutMappingThrowsException(): void { $metadata = new DocumentMetadata( className: stdClass::class, @@ -335,7 +351,10 @@ className: stdClass::class, idProperty: 'id', ); - self::assertSame(['name' => 1], $metadata->mapSortingToFieldPaths(['name' => 'asc'])); + $this->expectException(UnknownPropertyPath::class); + $this->expectExceptionMessage('segment "name" is not mapped under ""'); + + $metadata->mapSortingToFieldPaths(['name' => 'asc']); } public function testMapSortingAscWithMapping(): void