From dabd1b1aae3e014da624fa231ab461dc492e528f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Fri, 13 Oct 2023 14:20:48 +0200 Subject: [PATCH 1/5] Fixed doctrine metadata property/foreign key relationship methods --- .../Gateway/DoctrineSchemaMetadata.php | 50 ++++++++++--- .../DoctrineSchemaMetadataInterface.php | 4 +- src/lib/Gateway/ExpressionVisitor.php | 5 +- .../Gateway/DoctrineSchemaMetadataTest.php | 73 +++++++++++++++++++ 4 files changed, 118 insertions(+), 14 deletions(-) create mode 100644 tests/bundle/Gateway/DoctrineSchemaMetadataTest.php diff --git a/src/contracts/Gateway/DoctrineSchemaMetadata.php b/src/contracts/Gateway/DoctrineSchemaMetadata.php index 17feefe..5adeac5 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadata.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadata.php @@ -48,7 +48,12 @@ class DoctrineSchemaMetadata implements DoctrineSchemaMetadataInterface /** * @var array */ - private array $relationshipColumns = []; + private array $propertyToRelationship = []; + + /** + * @var array + */ + private array $columnToRelationship = []; /** @var class-string|null */ private ?string $className; @@ -364,33 +369,56 @@ public function isInheritanceTypeJoined(): bool public function addRelationship(DoctrineRelationshipInterface $relationship): void { $foreignProperty = $relationship->getForeignProperty(); - - if (isset($this->relationshipColumns[$foreignProperty])) { - throw new \LogicException(sprintf( + if (isset($this->propertyToRelationship[$foreignProperty])) { + throw new LogicException(sprintf( '"%s" is already added as foreign property.', $foreignProperty, )); } - $this->relationshipColumns[$foreignProperty] = $relationship; + $this->propertyToRelationship[$foreignProperty] = $relationship; + + $foreignColumn = $relationship->getForeignKeyColumn(); + if (isset($this->columnToRelationship[$foreignColumn])) { + throw new LogicException(sprintf( + '"%s" is already added as foreign column.', + $foreignColumn, + )); + } + + $this->columnToRelationship[$foreignColumn] = $relationship; } public function getRelationships(): array { - return $this->relationshipColumns; + return $this->propertyToRelationship; } - public function getRelationshipByForeignKeyColumn(string $foreignProperty): DoctrineRelationshipInterface + public function getRelationshipByForeignProperty(string $foreignProperty): DoctrineRelationshipInterface { - if (!isset($this->relationshipColumns[$foreignProperty])) { + if (!isset($this->propertyToRelationship[$foreignProperty])) { throw new InvalidArgumentException(sprintf( - '"%s" does not exist as a relationship for "%s" class metadata. Available relationship: "%s"', + '"%s" does not exist as a relationship for "%s" class metadata. Available relationship property: "%s"', $foreignProperty, $this->className, - implode('", "', array_keys($this->relationshipColumns)), + implode('", "', array_keys($this->propertyToRelationship)), + )); + } + + return $this->propertyToRelationship[$foreignProperty]; + } + + public function getRelationshipByForeignColumn(string $foreignColumn): DoctrineRelationshipInterface + { + if (!isset($this->columnToRelationship[$foreignColumn])) { + throw new InvalidArgumentException(sprintf( + '"%s" does not exist as a relationship for "%s" class metadata. Available relationship columns: "%s"', + $foreignColumn, + $this->className, + implode('", "', array_keys($this->columnToRelationship)), )); } - return $this->relationshipColumns[$foreignProperty]; + return $this->columnToRelationship[$foreignColumn]; } } diff --git a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php index d602f07..3702753 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php @@ -120,5 +120,7 @@ public function addRelationship(DoctrineRelationshipInterface $relationship): vo */ public function getRelationships(): array; - public function getRelationshipByForeignKeyColumn(string $foreignProperty): DoctrineRelationshipInterface; + public function getRelationshipByForeignProperty(string $foreignProperty): DoctrineRelationshipInterface; + + public function getRelationshipByForeignColumn(string $foreignColumn): DoctrineRelationshipInterface; } diff --git a/src/lib/Gateway/ExpressionVisitor.php b/src/lib/Gateway/ExpressionVisitor.php index 9dbd942..7d1318e 100644 --- a/src/lib/Gateway/ExpressionVisitor.php +++ b/src/lib/Gateway/ExpressionVisitor.php @@ -19,6 +19,7 @@ use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationship; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataInterface; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataRegistryInterface; +use InvalidArgumentException; use RuntimeException; /** @@ -79,10 +80,10 @@ public function walkComparison(Comparison $comparison) $foreignClassProperty, ] = explode('.', $column, 2); - $relationship = $this->schemaMetadata->getRelationshipByForeignKeyColumn($foreignProperty); + $relationship = $this->schemaMetadata->getRelationshipByForeignProperty($foreignProperty); $relationshipMetadata = $this->registry->getMetadata($relationship->getRelationshipClass()); if (!$relationshipMetadata->hasColumn($foreignClassProperty)) { - throw new \InvalidArgumentException(sprintf( + throw new InvalidArgumentException(sprintf( '"%s" does not exist as available column on "%s" class schema metadata. Available columns: "%s".', $foreignClassProperty, $relationshipMetadata->getClassName(), diff --git a/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php b/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php new file mode 100644 index 0000000..0899d74 --- /dev/null +++ b/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php @@ -0,0 +1,73 @@ +metadata = new DoctrineSchemaMetadata( + $this->createMock(\Doctrine\DBAL\Connection::class), + 'stdClass', + 'std_class_table', + [ + 'id' => 'integer', + 'column_string' => 'string', + ], + ['id'], + ); + } + + public function testGetRelationshipByForeignColumnWithoutRelationships(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('"foo" does not exist as a relationship for "stdClass" class metadata. Available relationship columns: ""'); + $this->metadata->getRelationshipByForeignColumn('foo'); + } + + public function testGetRelationshipByForeignPropertyWithoutRelationships(): void + { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('"foo" does not exist as a relationship for "stdClass" class metadata. Available relationship property: ""'); + $this->metadata->getRelationshipByForeignProperty('foo'); + } + + public function testGetRelationshipByForeignColumn(): void + { + $relationship = $this->getFooRelationship(); + $this->metadata->addRelationship($relationship); + + $result = $this->metadata->getRelationshipByForeignColumn('foo_column'); + self::assertSame($relationship, $result); + } + + public function testGetRelationshipByForeignProperty(): void + { + $relationship = $this->getFooRelationship(); + $this->metadata->addRelationship($relationship); + + $result = $this->metadata->getRelationshipByForeignProperty('foo_property'); + self::assertSame($relationship, $result); + } + + private function getFooRelationship(): DoctrineRelationshipInterface + { + $relationship = $this->createMock(DoctrineRelationshipInterface::class); + $relationship->method('getForeignKeyColumn')->willReturn('foo_column'); + $relationship->method('getForeignProperty')->willReturn('foo_property'); + + return $relationship; + } +} From 77906fc862d940e0197eff65e16d91975b1c0e1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Tue, 17 Oct 2023 12:10:13 +0200 Subject: [PATCH 2/5] Added MappingException and RuntimeMappingException interfaces --- src/contracts/Exception/MappingException.php | 2 +- .../Exception/MappingExceptionInterface.php | 9 + .../Exception/RuntimeMappingException.php | 9 + .../RuntimeMappingExceptionInterface.php | 10 + .../Gateway/DoctrineSchemaMetadata.php | 28 +-- .../DoctrineSchemaMetadataInterface.php | 32 +++ src/lib/Gateway/ExpressionVisitor.php | 20 +- .../bundle/Gateway/ExpressionVisitorTest.php | 197 +++++++++++++++++- 8 files changed, 281 insertions(+), 26 deletions(-) create mode 100644 src/contracts/Exception/MappingExceptionInterface.php create mode 100644 src/contracts/Exception/RuntimeMappingException.php create mode 100644 src/contracts/Exception/RuntimeMappingExceptionInterface.php diff --git a/src/contracts/Exception/MappingException.php b/src/contracts/Exception/MappingException.php index e587314..061890c 100644 --- a/src/contracts/Exception/MappingException.php +++ b/src/contracts/Exception/MappingException.php @@ -10,7 +10,7 @@ use Exception; -final class MappingException extends Exception +final class MappingException extends Exception implements MappingExceptionInterface { public static function singleIdNotAllowedOnCompositePrimaryKey(): self { diff --git a/src/contracts/Exception/MappingExceptionInterface.php b/src/contracts/Exception/MappingExceptionInterface.php new file mode 100644 index 0000000..a17adcb --- /dev/null +++ b/src/contracts/Exception/MappingExceptionInterface.php @@ -0,0 +1,9 @@ +columnTypeCache[$column]; } - /** - * @throws \Doctrine\DBAL\Exception - */ private function resolveColumnType(string $column): Type { if (isset($this->columnToTypesMap[$column])) { @@ -172,7 +167,7 @@ private function resolveColumnType(string $column): Type ); } - throw new InvalidArgumentException(sprintf( + throw new RuntimeMappingException(sprintf( 'Column "%s" does not exist in "%s" table. Available columns: "%s"', $column, $this->getTableName(), @@ -211,9 +206,6 @@ public function isInheritedColumn(string $column): bool return $this->getInheritanceMetadataWithColumn($column) !== null; } - /** - * @throws \Ibexa\Contracts\CorePersistence\Exception\MappingException - */ public function getIdentifierColumn(): string { if (count($this->identifierColumns) > 1) { @@ -293,7 +285,7 @@ public function hasTranslationSchemaMetadata(): bool public function getTranslationSchemaMetadata(): TranslationDoctrineSchemaMetadataInterface { if (!isset($this->translationMetadata)) { - throw new LogicException(sprintf( + throw new RuntimeMappingException(sprintf( '%s does not contain translation metadata. Ensure that %1$s::%s has been called.', DoctrineSchemaMetadata::class, 'setTranslationSchemaMetadata', @@ -321,7 +313,7 @@ public function isTranslatedColumn(string $column): bool public function getSubclassByDiscriminator(string $discriminator): DoctrineSchemaMetadataInterface { if (empty($this->discriminatorMap)) { - throw new RuntimeException(sprintf( + throw new RuntimeMappingException(sprintf( '"%s" is not registered as a subclass for table "%s". There are no registered subclasses', $discriminator, $this->getTableName(), @@ -329,7 +321,7 @@ public function getSubclassByDiscriminator(string $discriminator): DoctrineSchem } if (!isset($this->discriminatorMap[$discriminator])) { - throw new RuntimeException(sprintf( + throw new RuntimeMappingException(sprintf( '"%s" is not registered as a subclass for table "%s". Available discriminators: "%s"', $discriminator, $this->getTableName(), @@ -352,7 +344,7 @@ public function addSubclass(string $discriminator, DoctrineSchemaMetadataInterfa { $this->inheritanceType = self::INHERITANCE_TYPE_JOINED; if (isset($this->discriminatorMap[$discriminator])) { - throw new LogicException(sprintf( + throw new MappingException(sprintf( '"%s" is already added as a discriminator for a subtype.', $discriminator, )); @@ -370,7 +362,7 @@ public function addRelationship(DoctrineRelationshipInterface $relationship): vo { $foreignProperty = $relationship->getForeignProperty(); if (isset($this->propertyToRelationship[$foreignProperty])) { - throw new LogicException(sprintf( + throw new MappingException(sprintf( '"%s" is already added as foreign property.', $foreignProperty, )); @@ -380,7 +372,7 @@ public function addRelationship(DoctrineRelationshipInterface $relationship): vo $foreignColumn = $relationship->getForeignKeyColumn(); if (isset($this->columnToRelationship[$foreignColumn])) { - throw new LogicException(sprintf( + throw new MappingException(sprintf( '"%s" is already added as foreign column.', $foreignColumn, )); @@ -397,7 +389,7 @@ public function getRelationships(): array public function getRelationshipByForeignProperty(string $foreignProperty): DoctrineRelationshipInterface { if (!isset($this->propertyToRelationship[$foreignProperty])) { - throw new InvalidArgumentException(sprintf( + throw new RuntimeMappingException(sprintf( '"%s" does not exist as a relationship for "%s" class metadata. Available relationship property: "%s"', $foreignProperty, $this->className, @@ -411,7 +403,7 @@ public function getRelationshipByForeignProperty(string $foreignProperty): Doctr public function getRelationshipByForeignColumn(string $foreignColumn): DoctrineRelationshipInterface { if (!isset($this->columnToRelationship[$foreignColumn])) { - throw new InvalidArgumentException(sprintf( + throw new RuntimeMappingException(sprintf( '"%s" does not exist as a relationship for "%s" class metadata. Available relationship columns: "%s"', $foreignColumn, $this->className, diff --git a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php index 3702753..932653e 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php @@ -34,6 +34,9 @@ public function getClassName(): ?string; */ public function getTableName(): string; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface + */ public function getColumnType(string $column): Type; /** @@ -43,6 +46,9 @@ public function getColumns(): array; public function hasColumn(string $column): bool; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface + */ public function getColumn(string $column): string; public function getInheritanceMetadataWithColumn(string $column): ?self; @@ -70,6 +76,8 @@ public function convertToPHPValues(array $data): array; * @param array $data * * @return array + * + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface */ public function convertToDatabaseValues(array $data): array; @@ -77,6 +85,8 @@ public function convertToDatabaseValues(array $data): array; * @param array $data * * @return array + * + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface */ public function getBindingTypesForData(array $data): array; @@ -87,9 +97,13 @@ public function getIdentifierColumn(): string; /** * @throws \Doctrine\DBAL\Exception + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface */ public function getBindingTypeForColumn(string $columnName): int; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\MappingExceptionInterface + */ public function setParentMetadata(self $parentMetadata): void; public function getParentMetadata(): ?self; @@ -101,18 +115,30 @@ public function getSubclassByDiscriminator(string $discriminator): self; */ public function getSubclasses(): array; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\MappingExceptionInterface + */ public function addSubclass(string $discriminator, self $doctrineSchemaMetadata): void; public function isInheritanceTypeJoined(): bool; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\MappingExceptionInterface + */ public function setTranslationSchemaMetadata(TranslationDoctrineSchemaMetadataInterface $translationMetadata): void; public function hasTranslationSchemaMetadata(): bool; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface + */ public function getTranslationSchemaMetadata(): TranslationDoctrineSchemaMetadataInterface; public function isTranslatedColumn(string $column): bool; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\MappingExceptionInterface + */ public function addRelationship(DoctrineRelationshipInterface $relationship): void; /** @@ -120,7 +146,13 @@ public function addRelationship(DoctrineRelationshipInterface $relationship): vo */ public function getRelationships(): array; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface + */ public function getRelationshipByForeignProperty(string $foreignProperty): DoctrineRelationshipInterface; + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface + */ public function getRelationshipByForeignColumn(string $foreignColumn): DoctrineRelationshipInterface; } diff --git a/src/lib/Gateway/ExpressionVisitor.php b/src/lib/Gateway/ExpressionVisitor.php index 7d1318e..2bf78fd 100644 --- a/src/lib/Gateway/ExpressionVisitor.php +++ b/src/lib/Gateway/ExpressionVisitor.php @@ -15,6 +15,7 @@ use Doctrine\DBAL\Connection; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\DBAL\Query\QueryBuilder; +use Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingException; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineOneToManyRelationship; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationship; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataInterface; @@ -70,6 +71,9 @@ public function clearParameters(): void $this->parameters = []; } + /** + * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface + */ public function walkComparison(Comparison $comparison) { $column = $comparison->getField(); @@ -83,7 +87,7 @@ public function walkComparison(Comparison $comparison) $relationship = $this->schemaMetadata->getRelationshipByForeignProperty($foreignProperty); $relationshipMetadata = $this->registry->getMetadata($relationship->getRelationshipClass()); if (!$relationshipMetadata->hasColumn($foreignClassProperty)) { - throw new InvalidArgumentException(sprintf( + throw new RuntimeMappingException(sprintf( '"%s" does not exist as available column on "%s" class schema metadata. Available columns: "%s".', $foreignClassProperty, $relationshipMetadata->getClassName(), @@ -107,6 +111,12 @@ public function walkComparison(Comparison $comparison) $foreignClassProperty, $comparison->getValue(), ); + default: + throw new RuntimeMappingException(sprintf( + 'Unhandled relationship metadata. Expected one of "%s". Received "%s".', + implode('", "', [DoctrineRelationship::class, DoctrineOneToManyRelationship::class]), + $relationshipType, + )); } } @@ -114,6 +124,14 @@ public function walkComparison(Comparison $comparison) return $this->handleTranslation($comparison); } + if (!$this->schemaMetadata->hasColumn($column)) { + throw new RuntimeMappingException(sprintf( + '%s table metadata does not contain %s column.', + $this->schemaMetadata->getTableName(), + $column, + )); + } + $parameterName = $column . '_' . count($this->parameters); $placeholder = ':' . $parameterName; $value = $this->walkValue($comparison->getValue()); diff --git a/tests/bundle/Gateway/ExpressionVisitorTest.php b/tests/bundle/Gateway/ExpressionVisitorTest.php index 3bc13bb..ec2efc2 100644 --- a/tests/bundle/Gateway/ExpressionVisitorTest.php +++ b/tests/bundle/Gateway/ExpressionVisitorTest.php @@ -14,6 +14,11 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Query\Expression\ExpressionBuilder; use Doctrine\DBAL\Query\QueryBuilder; +use Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface; +use Ibexa\Contracts\CorePersistence\Gateway\DoctrineOneToManyRelationship; +use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationship; +use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface; +use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataInterface; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataRegistryInterface; use Ibexa\CorePersistence\Gateway\ExpressionVisitor; use Ibexa\CorePersistence\Gateway\Parameter; @@ -23,22 +28,41 @@ final class ExpressionVisitorTest extends TestCase { private ExpressionVisitor $expressionVisitor; + /** @var \Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataRegistryInterface&\PHPUnit\Framework\MockObject\MockObject */ + private DoctrineSchemaMetadataRegistryInterface $registry; + + /** @var \Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataInterface&\PHPUnit\Framework\MockObject\MockObject */ + private DoctrineSchemaMetadataInterface $schemaMetadata; + + private QueryBuilder $queryBuilder; + + /** @var \Doctrine\DBAL\Connection&\PHPUnit\Framework\MockObject\MockObject */ + private Connection $connection; + protected function setUp(): void { - $connection = $this->createMock(Connection::class); + $this->connection = $this->createMock(Connection::class); - $connection->method('getExpressionBuilder') - ->willReturn(new ExpressionBuilder($connection)); + $this->connection->method('getExpressionBuilder') + ->willReturn(new ExpressionBuilder($this->connection)); $platform = $this->getMockBuilder(AbstractPlatform::class) ->getMockForAbstractClass(); - $connection->method('getDatabasePlatform') + $this->connection->method('getDatabasePlatform') ->willReturn($platform); + $this->schemaMetadata = $this->createMock(DoctrineSchemaMetadataInterface::class); + + $this->registry = $this->createMock(DoctrineSchemaMetadataRegistryInterface::class); + $this->registry->method('getMetadataForTable') + ->with('table_name') + ->willReturn($this->schemaMetadata); + + $this->queryBuilder = new QueryBuilder($this->connection); $this->expressionVisitor = new ExpressionVisitor( - new QueryBuilder($connection), - $this->createMock(DoctrineSchemaMetadataRegistryInterface::class), + $this->queryBuilder, + $this->registry, 'table_name', 'table_alias', ); @@ -46,6 +70,8 @@ protected function setUp(): void public function testWalkComparison(): void { + $this->configureFieldInMetadata($this->schemaMetadata, ['field']); + $result = $this->expressionVisitor->dispatch(new Comparison('field', '=', 'value')); self::assertSame('table_alias.field = :field_0', $result); @@ -60,6 +86,8 @@ public function testWalkComparison(): void public function testLogicalNot(): void { + $this->configureFieldInMetadata($this->schemaMetadata, ['field']); + $result = $this->expressionVisitor->dispatch( new CompositeExpression('NOT', [new Comparison('field', '=', 'value')]), ); @@ -76,6 +104,8 @@ public function testLogicalNot(): void public function testLogicalAnd(): void { + $this->configureFieldInMetadata($this->schemaMetadata, ['field', 'field_2']); + $result = $this->expressionVisitor->dispatch( new CompositeExpression('AND', [ new Comparison('field', '=', 'value'), @@ -101,4 +131,159 @@ public function testLogicalAnd(): void ), ], $this->expressionVisitor->getParameters()); } + + public function testFieldNotFound(): void + { + $this->expectException(RuntimeMappingExceptionInterface::class); + $this->expressionVisitor->dispatch(new Comparison('field', '=', 'value')); + } + + public function testFieldFromMissingRelationship(): void + { + /** @var \Exception $exception */ + $exception = $this->createMock(RuntimeMappingExceptionInterface::class); + $this->schemaMetadata + ->expects(self::once()) + ->method('getRelationshipByForeignProperty') + ->with('relationship_1') + ->willThrowException($exception); + + $this->expectExceptionObject($exception); + $this->expressionVisitor->dispatch(new Comparison('relationship_1.field', '=', 'value')); + } + + public function testFieldUnknownRelationshipType(): void + { + /** @var class-string $relationshipClass pretend it's a class-string */ + $relationshipClass = 'relationship_class'; + + $doctrineRelationship = $this->createMock(DoctrineRelationshipInterface::class); + $doctrineRelationship->method('getRelationshipClass')->willReturn($relationshipClass); + $this->schemaMetadata + ->expects(self::once()) + ->method('getRelationshipByForeignProperty') + ->with('relationship_1') + ->willReturn($doctrineRelationship); + + $relationshipMetadata = $this->createRelationshipSchemaMetadata(); + + $this->registry + ->expects(self::once()) + ->method('getMetadata') + ->with(self::identicalTo($relationshipClass)) + ->willReturn($relationshipMetadata); + + $this->expectException(RuntimeMappingExceptionInterface::class); + $this->expectExceptionMessage( + 'Unhandled relationship metadata. Expected one of ' + . '"Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationship", ' + . '"Ibexa\Contracts\CorePersistence\Gateway\DoctrineOneToManyRelationship". ' + . 'Received "' . get_class($doctrineRelationship) . '"' + ); + $this->expressionVisitor->dispatch(new Comparison('relationship_1.field', '=', 'value')); + } + + public function testFieldFromSubSelectRelationship(): void + { + $this->connection + ->expects(self::once()) + ->method('createQueryBuilder') + ->willReturnCallback(fn () => new QueryBuilder($this->connection)); + + /** @var class-string $relationshipClass pretend it's a class-string */ + $relationshipClass = 'relationship_class'; + $doctrineRelationship = new DoctrineRelationship( + $relationshipClass, + 'relationship_1', + 'relationship_id', + 'id', + ); + + $this->schemaMetadata + ->expects(self::once()) + ->method('getRelationshipByForeignProperty') + ->with('relationship_1') + ->willReturn($doctrineRelationship); + + $relationshipMetadata = $this->createRelationshipSchemaMetadata(); + + $this->registry + ->expects(self::once()) + ->method('getMetadata') + ->with($relationshipClass) + ->willReturn($relationshipMetadata); + + $result = $this->expressionVisitor->dispatch(new Comparison('relationship_1.field', '=', 'value')); + self::assertSame( + 'table_alias.relationship_id IN (SELECT relationship_table_name.id FROM relationship_table_name ' + . 'WHERE relationship_table_name.field IN (:field_0))', + $result, + ); + } + + public function testFieldFromInheritedRelationship(): void + { + /** @var class-string $relationshipClass pretend it's a class-string */ + $relationshipClass = 'relationship_class'; + $doctrineRelationship = new DoctrineOneToManyRelationship( + $relationshipClass, + 'relationship_1', + 'relationship_id', + ); + + $this->schemaMetadata + ->expects(self::once()) + ->method('getRelationshipByForeignProperty') + ->with('relationship_1') + ->willReturn($doctrineRelationship); + + $relationshipMetadata = $this->createRelationshipSchemaMetadata(); + + $this->registry + ->expects(self::once()) + ->method('getMetadata') + ->with($relationshipClass) + ->willReturn($relationshipMetadata); + + $result = $this->expressionVisitor->dispatch(new Comparison('relationship_1.field', '=', 'value')); + self::assertSame( + 'relationship_table_name.field = :field_0', + $result, + ); + } + + private function createRelationshipSchemaMetadata(): DoctrineSchemaMetadataInterface + { + $relationshipMetadata = $this->createMock(DoctrineSchemaMetadataInterface::class); + $relationshipMetadata + ->method('hasColumn') + ->with('field') + ->willReturn(true); + + $relationshipMetadata + ->method('getTableName') + ->willReturn('relationship_table_name'); + + $relationshipMetadata + ->method('getIdentifierColumn') + ->willReturn('id'); + + return $relationshipMetadata; + } + + /** + * @param \Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadataInterface&\PHPUnit\Framework\MockObject\MockObject $metadata + * @param array $fields + */ + private function configureFieldInMetadata(DoctrineSchemaMetadataInterface $metadata, array $fields): void + { + $fields = array_map('preg_quote', $fields); + $regexp = sprintf('~^(%s)$~', implode('|', $fields)); + + $metadata + ->expects(self::atLeastOnce()) + ->method('hasColumn') + ->with(self::matchesRegularExpression($regexp)) + ->willReturn(true); + } } From f14d86a2af0a9712e05ed54737fbda821be8a3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Tue, 17 Oct 2023 12:16:49 +0200 Subject: [PATCH 3/5] Fixed method name --- src/contracts/Gateway/DoctrineSchemaMetadata.php | 2 +- src/contracts/Gateway/DoctrineSchemaMetadataInterface.php | 2 +- tests/bundle/Gateway/DoctrineSchemaMetadataTest.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/contracts/Gateway/DoctrineSchemaMetadata.php b/src/contracts/Gateway/DoctrineSchemaMetadata.php index 4b00b8e..68aea1d 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadata.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadata.php @@ -400,7 +400,7 @@ public function getRelationshipByForeignProperty(string $foreignProperty): Doctr return $this->propertyToRelationship[$foreignProperty]; } - public function getRelationshipByForeignColumn(string $foreignColumn): DoctrineRelationshipInterface + public function getRelationshipByForeignKeyColumn(string $foreignColumn): DoctrineRelationshipInterface { if (!isset($this->columnToRelationship[$foreignColumn])) { throw new RuntimeMappingException(sprintf( diff --git a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php index 932653e..18e4b1e 100644 --- a/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php +++ b/src/contracts/Gateway/DoctrineSchemaMetadataInterface.php @@ -154,5 +154,5 @@ public function getRelationshipByForeignProperty(string $foreignProperty): Doctr /** * @throws \Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface */ - public function getRelationshipByForeignColumn(string $foreignColumn): DoctrineRelationshipInterface; + public function getRelationshipByForeignKeyColumn(string $foreignColumn): DoctrineRelationshipInterface; } diff --git a/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php b/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php index 0899d74..880efd5 100644 --- a/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php +++ b/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php @@ -34,7 +34,7 @@ public function testGetRelationshipByForeignColumnWithoutRelationships(): void { $this->expectException(\InvalidArgumentException::class); $this->expectExceptionMessage('"foo" does not exist as a relationship for "stdClass" class metadata. Available relationship columns: ""'); - $this->metadata->getRelationshipByForeignColumn('foo'); + $this->metadata->getRelationshipByForeignKeyColumn('foo'); } public function testGetRelationshipByForeignPropertyWithoutRelationships(): void @@ -49,7 +49,7 @@ public function testGetRelationshipByForeignColumn(): void $relationship = $this->getFooRelationship(); $this->metadata->addRelationship($relationship); - $result = $this->metadata->getRelationshipByForeignColumn('foo_column'); + $result = $this->metadata->getRelationshipByForeignKeyColumn('foo_column'); self::assertSame($relationship, $result); } From 95810c7cb8a7fb79d0dfb58b84e26e864f70d082 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pawe=C5=82=20Niedzielski?= Date: Tue, 17 Oct 2023 12:17:58 +0200 Subject: [PATCH 4/5] Fixed code style --- src/contracts/Exception/MappingExceptionInterface.php | 6 ++++++ src/contracts/Exception/RuntimeMappingException.php | 6 ++++++ .../Exception/RuntimeMappingExceptionInterface.php | 7 ++++++- src/lib/Gateway/ExpressionVisitor.php | 1 - 4 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/contracts/Exception/MappingExceptionInterface.php b/src/contracts/Exception/MappingExceptionInterface.php index a17adcb..b8cdf1f 100644 --- a/src/contracts/Exception/MappingExceptionInterface.php +++ b/src/contracts/Exception/MappingExceptionInterface.php @@ -1,5 +1,11 @@ Date: Tue, 17 Oct 2023 12:37:44 +0200 Subject: [PATCH 5/5] Fixed tests --- tests/bundle/Gateway/DoctrineSchemaMetadataTest.php | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php b/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php index 880efd5..7bc2ea2 100644 --- a/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php +++ b/tests/bundle/Gateway/DoctrineSchemaMetadataTest.php @@ -8,6 +8,7 @@ namespace Ibexa\Tests\Bundle\CorePersistence\Gateway; +use Ibexa\Contracts\CorePersistence\Exception\RuntimeMappingExceptionInterface; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineRelationshipInterface; use Ibexa\Contracts\CorePersistence\Gateway\DoctrineSchemaMetadata; use PHPUnit\Framework\TestCase; @@ -32,14 +33,14 @@ protected function setUp(): void public function testGetRelationshipByForeignColumnWithoutRelationships(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(RuntimeMappingExceptionInterface::class); $this->expectExceptionMessage('"foo" does not exist as a relationship for "stdClass" class metadata. Available relationship columns: ""'); $this->metadata->getRelationshipByForeignKeyColumn('foo'); } public function testGetRelationshipByForeignPropertyWithoutRelationships(): void { - $this->expectException(\InvalidArgumentException::class); + $this->expectException(RuntimeMappingExceptionInterface::class); $this->expectExceptionMessage('"foo" does not exist as a relationship for "stdClass" class metadata. Available relationship property: ""'); $this->metadata->getRelationshipByForeignProperty('foo'); }