diff --git a/composer.json b/composer.json index 1326f5c5..3c14bf30 100644 --- a/composer.json +++ b/composer.json @@ -11,9 +11,9 @@ "require": { "php": "^8.0", "doctrine/collections": "^1.8 || ^2.0", - "doctrine/dbal": "^3.4", + "doctrine/dbal": "^3.6", "doctrine/event-manager": "^1.2 || ^2.0", - "doctrine/orm": "^2.14", + "doctrine/orm": "^2.14 || ^3.0", "doctrine/persistence": "^3.0", "psr/clock": "^1.0", "symfony/config": "^5.4 || ^6.2 || ^7.0", @@ -26,7 +26,7 @@ "doctrine/doctrine-bundle": "^2.7", "doctrine/doctrine-fixtures-bundle": "^3.4", "friendsofphp/php-cs-fixer": "^3.4", - "gedmo/doctrine-extensions": "^3.7", + "gedmo/doctrine-extensions": "^3.15", "matthiasnoback/symfony-dependency-injection-test": "^4.2.1 || ^5.0", "phpstan/extension-installer": "^1.1", "phpstan/phpstan": "^1.0", diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 5733a34c..ee3489dc 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,5 +1,5 @@ - + diff --git a/src/AuditConfiguration.php b/src/AuditConfiguration.php index 971082a4..b66f677c 100644 --- a/src/AuditConfiguration.php +++ b/src/AuditConfiguration.php @@ -14,7 +14,7 @@ namespace SimpleThings\EntityAudit; use Doctrine\DBAL\Types\Types; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use SimpleThings\EntityAudit\Metadata\MetadataFactory; class AuditConfiguration @@ -71,7 +71,7 @@ public static function forEntities(array $classes) } /** - * @param ClassMetadataInfo $metadata + * @param ClassMetadata $metadata * * @return string * @@ -79,7 +79,7 @@ public static function forEntities(array $classes) * * @psalm-suppress MoreSpecificReturnType,LessSpecificReturnStatement https://github.com/vimeo/psalm/issues/10910 */ - public function getTableName(ClassMetadataInfo $metadata) + public function getTableName(ClassMetadata $metadata) { /** @var literal-string $tableName */ $tableName = $metadata->getTableName(); diff --git a/src/AuditReader.php b/src/AuditReader.php index 97466b84..f5e919e4 100644 --- a/src/AuditReader.php +++ b/src/AuditReader.php @@ -19,9 +19,9 @@ use Doctrine\DBAL\Platforms\AbstractPlatform; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; +use Doctrine\ORM\Exception\ORMException; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\QuoteStrategy; -use Doctrine\ORM\ORMException; use Doctrine\ORM\PersistentCollection; use Doctrine\ORM\Persisters\Entity\EntityPersister; use SimpleThings\EntityAudit\Collection\AuditedCollection; @@ -31,10 +31,12 @@ use SimpleThings\EntityAudit\Exception\NotAuditedException; use SimpleThings\EntityAudit\Metadata\MetadataFactory; use SimpleThings\EntityAudit\Utils\ArrayDiff; +use SimpleThings\EntityAudit\Utils\ORMCompatibilityTrait; use SimpleThings\EntityAudit\Utils\SQLResultCasing; class AuditReader { + use ORMCompatibilityTrait; use SQLResultCasing; private AbstractPlatform $platform; @@ -213,7 +215,7 @@ public function find($className, $id, $revision, array $options = []) $idKeys = array_keys($id); $columnName = $idKeys[0]; } elseif (isset($classMetadata->fieldMappings[$idField])) { - $columnName = $classMetadata->fieldMappings[$idField]['columnName']; + $columnName = self::getMappingColumnNameValue($classMetadata->fieldMappings[$idField]); } elseif (isset($classMetadata->associationMappings[$idField]['joinColumns'])) { $columnName = $classMetadata->associationMappings[$idField]['joinColumns'][0]['name']; } else { @@ -237,7 +239,7 @@ public function find($className, $id, $revision, array $options = []) ? 're' // root entity : 'e'; - $type = Type::getType($classMetadata->fieldMappings[$field]['type']); + $type = Type::getType(self::getMappingValue($classMetadata->fieldMappings[$field], 'type')); $columnList[] = sprintf( '%s AS %s', $type->convertToPHPValueSQL( @@ -250,18 +252,15 @@ public function find($className, $id, $revision, array $options = []) } foreach ($classMetadata->associationMappings as $assoc) { - if ( - ($assoc['type'] & ClassMetadata::TO_ONE) === 0 - || false === $assoc['isOwningSide'] - || !isset($assoc['joinColumnFieldNames']) - ) { + if (!self::isToOneOwningSide($assoc)) { continue; } - foreach ($assoc['joinColumnFieldNames'] as $sourceCol) { + /** @var string $sourceCol */ + foreach (self::getMappingValue($assoc, 'joinColumnFieldNames') as $sourceCol) { $tableAlias = $classMetadata->isInheritanceTypeJoined() - && $classMetadata->isInheritedAssociation($assoc['fieldName']) - && !$classMetadata->isIdentifier($assoc['fieldName']) + && $classMetadata->isInheritedAssociation(self::getMappingFieldNameValue($assoc)) + && !$classMetadata->isIdentifier(self::getMappingFieldNameValue($assoc)) ? 're' // root entity : 'e'; $columnList[] = $tableAlias.'.'.$sourceCol; @@ -286,7 +285,7 @@ public function find($className, $id, $revision, array $options = []) !$classMetadata->isInheritanceTypeNone() && null !== $classMetadata->discriminatorColumn ) { - $columnList[] = $classMetadata->discriminatorColumn['name']; + $columnList[] = self::getMappingNameValue($classMetadata->discriminatorColumn); if ($classMetadata->isInheritanceTypeSingleTable() && null !== $classMetadata->discriminatorValue) { // Support for single table inheritance sub-classes @@ -298,7 +297,7 @@ public function find($className, $id, $revision, array $options = []) $whereSQL .= sprintf( ' AND %s IN (%s)', - $classMetadata->discriminatorColumn['name'], + self::getMappingNameValue($classMetadata->discriminatorColumn), implode(', ', $queriedDiscrValues) ); } @@ -410,7 +409,7 @@ public function findEntitiesChangedAtRevision($revision) $columnMap = []; foreach ($classMetadata->fieldNames as $columnName => $field) { - $type = Type::getType($classMetadata->fieldMappings[$field]['type']); + $type = Type::getType(self::getMappingValue($classMetadata->fieldMappings[$field], 'type')); $tableAlias = $classMetadata->isInheritanceTypeJoined() && $classMetadata->isInheritedField($field) && !$classMetadata->isIdentifier($field) @@ -424,12 +423,8 @@ public function findEntitiesChangedAtRevision($revision) } foreach ($classMetadata->associationMappings as $assoc) { - if ( - ($assoc['type'] & ClassMetadata::TO_ONE) > 0 - && true === $assoc['isOwningSide'] - && isset($assoc['targetToSourceKeyColumns']) - ) { - foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) { + if (self::isToOneOwningSide($assoc)) { + foreach (self::getTargetToSourceKeyColumns($assoc) as $sourceCol) { $columnList .= ', '.$sourceCol; $columnMap[$sourceCol] = $this->getSQLResultCasing($this->platform, $sourceCol); } @@ -441,15 +436,15 @@ public function findEntitiesChangedAtRevision($revision) $classMetadata->isInheritanceTypeSingleTable() && null !== $classMetadata->discriminatorColumn ) { - $columnList .= ', e.'.$classMetadata->discriminatorColumn['name']; - $whereSQL .= ' AND e.'.$classMetadata->discriminatorColumn['fieldName'].' = ?'; + $columnList .= ', e.'.self::getMappingNameValue($classMetadata->discriminatorColumn); + $whereSQL .= ' AND e.'.self::getMappingFieldNameValue($classMetadata->discriminatorColumn).' = ?'; $params[] = $classMetadata->discriminatorValue; } elseif ( $classMetadata->isInheritanceTypeJoined() && $classMetadata->rootEntityName !== $classMetadata->name && null !== $classMetadata->discriminatorColumn ) { - $columnList .= ', re.'.$classMetadata->discriminatorColumn['name']; + $columnList .= ', re.'.self::getMappingNameValue($classMetadata->discriminatorColumn); $rootClass = $this->em->getClassMetadata($classMetadata->rootEntityName); $rootTableName = $this->config->getTableName($rootClass); @@ -543,7 +538,7 @@ public function findRevisions($className, $id) if ('' !== $whereSQL) { $whereSQL .= ' AND '; } - $whereSQL .= 'e.'.$classMetadata->fieldMappings[$idField]['columnName'].' = ?'; + $whereSQL .= 'e.'.self::getMappingColumnNameValue($classMetadata->fieldMappings[$idField]).' = ?'; } elseif (isset($classMetadata->associationMappings[$idField]['joinColumns'])) { if ('' !== $whereSQL) { $whereSQL .= ' AND '; @@ -605,12 +600,12 @@ public function getCurrentRevision($className, $id) if ('' !== $whereSQL) { $whereSQL .= ' AND '; } - $whereSQL .= 'e.'.$classMetadata->fieldMappings[$idField]['columnName'].' = ?'; + $whereSQL .= 'e.'.self::getMappingColumnNameValue($classMetadata->fieldMappings[$idField]).' = ?'; } elseif (isset($classMetadata->associationMappings[$idField]['joinColumns'])) { if ('' !== $whereSQL) { $whereSQL .= ' AND '; } - $whereSQL .= 'e.'.$classMetadata->associationMappings[$idField]['joinColumns'][0]['name'].' = ?'; + $whereSQL .= 'e.'.self::getMappingNameValue($classMetadata->associationMappings[$idField]['joinColumns'][0]).' = ?'; } } @@ -720,11 +715,9 @@ public function getEntityHistory($className, $id) $whereId = []; foreach ($classMetadata->identifier as $idField) { if (isset($classMetadata->fieldMappings[$idField])) { - /** @phpstan-var literal-string $columnName */ - $columnName = $classMetadata->fieldMappings[$idField]['columnName']; + $columnName = self::getMappingColumnNameValue($classMetadata->fieldMappings[$idField]); } elseif (isset($classMetadata->associationMappings[$idField]['joinColumns'])) { - /** @phpstan-var literal-string $columnName */ - $columnName = $classMetadata->associationMappings[$idField]['joinColumns'][0]['name']; + $columnName = self::getMappingNameValue($classMetadata->associationMappings[$idField]['joinColumns'][0]); } else { continue; } @@ -737,7 +730,7 @@ public function getEntityHistory($className, $id) $columnMap = []; foreach ($classMetadata->fieldNames as $columnName => $field) { - $type = Type::getType($classMetadata->fieldMappings[$field]['type']); + $type = Type::getType(self::getMappingValue($classMetadata->fieldMappings[$field], 'type')); /** @phpstan-var literal-string $sqlExpr */ $sqlExpr = $type->convertToPHPValueSQL( $this->quoteStrategy->getColumnName($field, $classMetadata, $this->platform), @@ -750,16 +743,11 @@ public function getEntityHistory($className, $id) } foreach ($classMetadata->associationMappings as $assoc) { - if ( - ($assoc['type'] & ClassMetadata::TO_ONE) === 0 - || false === $assoc['isOwningSide'] - || !isset($assoc['targetToSourceKeyColumns']) - ) { + if (!self::isToOneOwningSide($assoc)) { continue; } - /** @phpstan-var literal-string $sourceCol */ - foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) { + foreach (self::getTargetToSourceKeyColumns($assoc) as $sourceCol) { $columnList[] = $sourceCol; $columnMap[$sourceCol] = $this->getSQLResultCasing($this->platform, $sourceCol); } @@ -845,10 +833,7 @@ private function createEntity($className, array $columnMap, array $data, $revisi !$classMetadata->isInheritanceTypeNone() && null !== $classMetadata->discriminatorColumn ) { - if (!isset($data[$classMetadata->discriminatorColumn['name']])) { - throw new \RuntimeException('Expecting discriminator value in data set.'); - } - $discriminator = $data[$classMetadata->discriminatorColumn['name']]; + $discriminator = $data[self::getMappingNameValue($classMetadata->discriminatorColumn)]; if (!isset($classMetadata->discriminatorMap[$discriminator])) { throw new \RuntimeException("No mapping found for [{$discriminator}]."); } @@ -884,7 +869,7 @@ private function createEntity($className, array $columnMap, array $data, $revisi foreach ($data as $field => $value) { if (isset($classMetadata->fieldMappings[$field])) { - $type = Type::getType($classMetadata->fieldMappings[$field]['type']); + $type = Type::getType(self::getMappingValue($classMetadata->fieldMappings[$field], 'type')); $value = $type->convertToPHPValue($value, $this->platform); $reflField = $classMetadata->reflFields[$field]; @@ -895,12 +880,12 @@ private function createEntity($className, array $columnMap, array $data, $revisi foreach ($classMetadata->associationMappings as $field => $assoc) { /** @phpstan-var class-string $targetEntity */ - $targetEntity = $assoc['targetEntity']; + $targetEntity = self::getMappingTargetEntityValue($assoc); $targetClass = $this->em->getClassMetadata($targetEntity); $mappedBy = $assoc['mappedBy'] ?? null; - if (0 !== ($assoc['type'] & ClassMetadata::TO_ONE)) { + if (self::isToOne($assoc)) { if ($this->metadataFactory->isAudited($targetEntity)) { if ($this->loadAuditedEntities) { // Primary Key. Used for audit tables queries. @@ -908,8 +893,8 @@ private function createEntity($className, array $columnMap, array $data, $revisi // Primary Field. Used when fallback to Doctrine finder. $pf = []; - if (true === $assoc['isOwningSide'] && isset($assoc['targetToSourceKeyColumns'])) { - foreach ($assoc['targetToSourceKeyColumns'] as $foreign => $local) { + if (self::isToOneOwningSide($assoc)) { + foreach (self::getTargetToSourceKeyColumns($assoc) as $foreign => $local) { $key = $data[$columnMap[$local]]; if (null === $key) { continue; @@ -922,15 +907,15 @@ private function createEntity($className, array $columnMap, array $data, $revisi $otherEntityAssoc = $this->em->getClassMetadata($targetEntity) ->associationMappings[$mappedBy]; - if (isset($otherEntityAssoc['targetToSourceKeyColumns'])) { - foreach ($otherEntityAssoc['targetToSourceKeyColumns'] as $local => $foreign) { + if (self::isToOneOwningSide($otherEntityAssoc)) { + foreach (self::getTargetToSourceKeyColumns($otherEntityAssoc) as $local => $foreign) { $key = $data[$classMetadata->getFieldName($local)]; if (null === $key) { continue; } $pk[$foreign] = $key; - $pf[$otherEntityAssoc['fieldName']] = $key; + $pf[self::getMappingFieldNameValue($otherEntityAssoc)] = $key; } } } @@ -957,13 +942,13 @@ private function createEntity($className, array $columnMap, array $data, $revisi } } else { if ($this->loadNativeEntities) { - if (true === $assoc['isOwningSide'] && isset($assoc['targetToSourceKeyColumns'])) { + if (self::isToOneOwningSide($assoc)) { $associatedId = []; - foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { + foreach (self::getTargetToSourceKeyColumns($assoc) as $targetColumn => $srcColumn) { $joinColumnValue = $data[$columnMap[$srcColumn]] ?? null; if (null !== $joinColumnValue) { $targetField = $targetClass->fieldNames[$targetColumn]; - $joinColumnType = Type::getType($targetClass->fieldMappings[$targetField]['type']); + $joinColumnType = Type::getType(self::getMappingValue($targetClass->fieldMappings[$targetField], 'type')); $joinColumnValue = $joinColumnType->convertToPHPValue( $joinColumnValue, $this->platform @@ -1030,15 +1015,11 @@ private function createEntity($className, array $columnMap, array $data, $revisi $reflField = $classMetadata->reflFields[$assoc['fieldName']]; \assert(null !== $reflField); $reflField->setValue($entity, $collection); - } elseif (0 !== ($assoc['type'] & ClassMetadata::MANY_TO_MANY)) { - if ($assoc['isOwningSide'] && isset( - $assoc['relationToSourceKeyColumns'], - $assoc['relationToTargetKeyColumns'], - $assoc['joinTable']['name'] - )) { + } elseif (self::isManyToMany($assoc)) { + if (self::isManyToManyOwningSideMapping($assoc)) { $whereId = [$this->config->getRevisionFieldName().' = ?']; $values = [$revision]; - foreach ($assoc['relationToSourceKeyColumns'] as $sourceKeyJoinColumn => $sourceKeyColumn) { + foreach (self::getRelationToSourceKeyColumns($assoc) as $sourceKeyJoinColumn => $sourceKeyColumn) { $whereId[] = "{$sourceKeyJoinColumn} = ?"; $reflField = $classMetadata->reflFields['id']; @@ -1053,10 +1034,10 @@ private function createEntity($className, array $columnMap, array $data, $revisi $this->config->getRevisionTypeFieldName(), ]; $tableName = $this->config->getTablePrefix() - .$assoc['joinTable']['name'] + .self::getMappingJoinTableNameValue($assoc) .$this->config->getTableSuffix(); - foreach ($assoc['relationToTargetKeyColumns'] as $targetKeyJoinColumn => $targetKeyColumn) { + foreach (self::getRelationToTargetKeyColumns($assoc) as $targetKeyJoinColumn => $targetKeyColumn) { $columnList[] = $targetKeyJoinColumn; } @@ -1077,8 +1058,7 @@ private function createEntity($className, array $columnMap, array $data, $revisi foreach ($rows as $row) { $id = []; - /** @phpstan-var string $targetKeyColumn */ - foreach ($assoc['relationToTargetKeyColumns'] as $targetKeyJoinColumn => $targetKeyColumn) { + foreach (self::getRelationToTargetKeyColumns($assoc) as $targetKeyJoinColumn => $targetKeyColumn) { $joinKey = $row[$targetKeyJoinColumn]; $id[$targetKeyColumn] = $joinKey; } diff --git a/src/Collection/AuditedCollection.php b/src/Collection/AuditedCollection.php index fae06870..063e81ea 100644 --- a/src/Collection/AuditedCollection.php +++ b/src/Collection/AuditedCollection.php @@ -15,7 +15,8 @@ use Doctrine\Common\Collections\ArrayCollection; use Doctrine\Common\Collections\Collection; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\AssociationMapping; +use Doctrine\ORM\Mapping\ClassMetadata; use SimpleThings\EntityAudit\AuditConfiguration; use SimpleThings\EntityAudit\AuditReader; use SimpleThings\EntityAudit\Exception\AuditedCollectionException; @@ -58,19 +59,19 @@ class AuditedCollection implements Collection protected $initialized = false; /** - * @param string $class - * @param array $associationDefinition - * @param array $foreignKeys - * @param string|int $revision + * @param string $class + * @param array|AssociationMapping $associationDefinition + * @param array $foreignKeys + * @param string|int $revision * * @phpstan-param class-string $class - * @phpstan-param ClassMetadataInfo $metadata + * @phpstan-param ClassMetadata $metadata */ public function __construct( protected AuditReader $auditReader, protected $class, - protected ClassMetadataInfo $metadata, - protected array $associationDefinition, + protected ClassMetadata $metadata, + protected array|AssociationMapping $associationDefinition, protected array $foreignKeys, protected $revision ) { diff --git a/src/DeferredChangedManyToManyEntityRevisionToPersist.php b/src/DeferredChangedManyToManyEntityRevisionToPersist.php index 05182066..2b6f3448 100644 --- a/src/DeferredChangedManyToManyEntityRevisionToPersist.php +++ b/src/DeferredChangedManyToManyEntityRevisionToPersist.php @@ -14,6 +14,7 @@ namespace SimpleThings\EntityAudit; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; /** * @internal @@ -21,16 +22,16 @@ final class DeferredChangedManyToManyEntityRevisionToPersist { /** - * @param array $assoc - * @param array $entityData - * @param ClassMetadata $class - * @param ClassMetadata $targetClass + * @param array|ManyToManyOwningSideMapping $assoc + * @param array $entityData + * @param ClassMetadata $class + * @param ClassMetadata $targetClass */ public function __construct( private object $entity, private string $revType, private array $entityData, - private array $assoc, + private array|ManyToManyOwningSideMapping $assoc, private ClassMetadata $class, private ClassMetadata $targetClass ) { @@ -55,9 +56,9 @@ public function getEntityData(): array } /** - * @return array + * @return array|ManyToManyOwningSideMapping */ - public function getAssoc(): array + public function getAssoc(): array|ManyToManyOwningSideMapping { return $this->assoc; } diff --git a/src/EventListener/CreateSchemaListener.php b/src/EventListener/CreateSchemaListener.php index fbe5ea0d..0b06057a 100644 --- a/src/EventListener/CreateSchemaListener.php +++ b/src/EventListener/CreateSchemaListener.php @@ -19,19 +19,22 @@ use Doctrine\DBAL\Schema\Table; use Doctrine\DBAL\Types\Type; use Doctrine\DBAL\Types\Types; -use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Tools\Event\GenerateSchemaEventArgs; use Doctrine\ORM\Tools\Event\GenerateSchemaTableEventArgs; use Doctrine\ORM\Tools\ToolEvents; use SimpleThings\EntityAudit\AuditConfiguration; use SimpleThings\EntityAudit\AuditManager; use SimpleThings\EntityAudit\Metadata\MetadataFactory; +use SimpleThings\EntityAudit\Utils\ORMCompatibilityTrait; /** * NEXT_MAJOR: do not implement EventSubscriber interface anymore. */ class CreateSchemaListener implements EventSubscriber { + use ORMCompatibilityTrait; + private AuditConfiguration $config; private MetadataFactory $metadataFactory; @@ -61,6 +64,9 @@ public function getSubscribedEvents() ]; } + /** + * @psalm-suppress TypeDoesNotContainType, NoValue + */ public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $eventArgs): void { $cm = $eventArgs->getClassMetadata(); @@ -95,7 +101,7 @@ public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $eventArgs) } $revisionTable->addColumn($this->config->getRevisionFieldName(), $this->config->getRevisionIdFieldType()); $revisionTable->addColumn($this->config->getRevisionTypeFieldName(), Types::STRING, ['length' => 4]); - if (!\in_array($cm->inheritanceType, [ClassMetadataInfo::INHERITANCE_TYPE_NONE, ClassMetadataInfo::INHERITANCE_TYPE_JOINED, ClassMetadataInfo::INHERITANCE_TYPE_SINGLE_TABLE], true)) { + if (!\in_array($cm->inheritanceType, [ClassMetadata::INHERITANCE_TYPE_NONE, ClassMetadata::INHERITANCE_TYPE_JOINED, ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE], true)) { throw new \Exception(sprintf('Inheritance type "%s" is not yet supported', $cm->inheritanceType)); } @@ -108,13 +114,11 @@ public function postGenerateSchemaTable(GenerateSchemaTableEventArgs $eventArgs) $revisionTable->addIndex([$this->config->getRevisionFieldName()], $revIndexName); foreach ($cm->associationMappings as $associationMapping) { - if ($associationMapping['isOwningSide'] && isset($associationMapping['joinTable'])) { - if (isset($associationMapping['joinTable']['name'])) { - if ($schema->hasTable($associationMapping['joinTable']['name'])) { - $this->createRevisionJoinTableForJoinTable($schema, $associationMapping['joinTable']['name']); - } else { - $this->defferedJoinTablesToCreate[] = $associationMapping['joinTable']['name']; - } + if (self::isManyToManyOwningSideMapping($associationMapping)) { + if ($schema->hasTable(self::getMappingJoinTableNameValue($associationMapping))) { + $this->createRevisionJoinTableForJoinTable($schema, self::getMappingJoinTableNameValue($associationMapping)); + } else { + $this->defferedJoinTablesToCreate[] = self::getMappingJoinTableNameValue($associationMapping); } } } diff --git a/src/EventListener/LogRevisionsListener.php b/src/EventListener/LogRevisionsListener.php index f0eac433..af4d3c40 100644 --- a/src/EventListener/LogRevisionsListener.php +++ b/src/EventListener/LogRevisionsListener.php @@ -25,6 +25,7 @@ use Doctrine\ORM\Event\PostUpdateEventArgs; use Doctrine\ORM\Events; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; use Doctrine\ORM\Persisters\Entity\EntityPersister; use Doctrine\ORM\UnitOfWork; use Doctrine\ORM\Utility\PersisterHelper; @@ -34,12 +35,15 @@ use SimpleThings\EntityAudit\AuditManager; use SimpleThings\EntityAudit\DeferredChangedManyToManyEntityRevisionToPersist; use SimpleThings\EntityAudit\Metadata\MetadataFactory; +use SimpleThings\EntityAudit\Utils\ORMCompatibilityTrait; /** * NEXT_MAJOR: do not implement EventSubscriber interface anymore. */ class LogRevisionsListener implements EventSubscriber { + use ORMCompatibilityTrait; + private AuditConfiguration $config; private MetadataFactory $metadataFactory; @@ -153,11 +157,9 @@ public function postFlush(PostFlushEventArgs $eventArgs): void foreach ($meta->associationMappings as $mapping) { if (isset($mapping['joinColumns'])) { foreach ($mapping['joinColumns'] as $definition) { - if ($definition['name'] === $column) { - /** @var class-string $targetEntity */ - $targetEntity = $mapping['targetEntity']; - $targetTable = $em->getClassMetadata($targetEntity); - $type = $targetTable->getTypeOfField($targetTable->getFieldForColumn($definition['referencedColumnName'])); + if (self::getMappingNameValue($definition) === $column) { + $targetTable = $em->getClassMetadata(self::getMappingTargetEntityValue($mapping)); + $type = $targetTable->getTypeOfField($targetTable->getFieldForColumn(self::getMappingValue($definition, 'referencedColumnName'))); } } } @@ -177,12 +179,10 @@ public function postFlush(PostFlushEventArgs $eventArgs): void foreach ($meta->identifier as $idField) { if (isset($meta->fieldMappings[$idField])) { - /** @phpstan-var literal-string $columnName */ - $columnName = $meta->fieldMappings[$idField]['columnName']; - $types[] = $meta->fieldMappings[$idField]['type']; + $columnName = self::getMappingColumnNameValue($meta->fieldMappings[$idField]); + $types[] = self::getMappingValue($meta->fieldMappings[$idField], 'type'); } elseif (isset($meta->associationMappings[$idField]['joinColumns'])) { - /** @phpstan-var literal-string $columnName */ - $columnName = $meta->associationMappings[$idField]['joinColumns'][0]['name']; + $columnName = self::getMappingNameValue($meta->associationMappings[$idField]['joinColumns'][0]); $types[] = $meta->associationMappings[$idField]['type']; } else { throw new \RuntimeException('column name not found for'.$idField); @@ -349,7 +349,7 @@ private function getManyToManyRelations(EntityManagerInterface $em, object $enti $data = []; $class = $em->getClassMetadata($entity::class); foreach ($class->associationMappings as $field => $assoc) { - if (($assoc['type'] & ClassMetadata::MANY_TO_MANY) > 0 && $assoc['isOwningSide']) { + if (self::isManyToManyOwningSideMapping($assoc)) { $reflField = $class->reflFields[$field]; \assert(null !== $reflField); $data[$field] = $reflField->getValue($entity); @@ -396,8 +396,6 @@ private function getRevisionId(Connection $conn) * @throws Exception * * @return literal-string - * - * @psalm-suppress MoreSpecificReturnType,PropertyTypeCoercion,LessSpecificReturnStatement https://github.com/vimeo/psalm/issues/10909 */ private function getInsertRevisionSQL(EntityManagerInterface $em, ClassMetadata $class): string { @@ -415,13 +413,8 @@ private function getInsertRevisionSQL(EntityManagerInterface $em, ClassMetadata continue; } - if ( - ($assoc['type'] & ClassMetadata::TO_ONE) > 0 - && true === $assoc['isOwningSide'] - && isset($assoc['targetToSourceKeyColumns']) - ) { - /** @phpstan-var literal-string $sourceCol */ - foreach ($assoc['targetToSourceKeyColumns'] as $sourceCol) { + if (self::isToOneOwningSide($assoc)) { + foreach (self::getTargetToSourceKeyColumns($assoc) as $sourceCol) { $fields[$sourceCol] = true; $sql .= ', '.$sourceCol; $placeholders[] = '?'; @@ -442,15 +435,11 @@ private function getInsertRevisionSQL(EntityManagerInterface $em, ClassMetadata } $platform = $em->getConnection()->getDatabasePlatform(); - $type = Type::getType($class->fieldMappings[$field]['type']); + $type = Type::getType(self::getMappingValue($class->fieldMappings[$field], 'type')); - if (true === ($class->fieldMappings[$field]['requireSQLConversion'] ?? false)) { - /** @phpstan-var literal-string $placeholder */ - $placeholder = $type->convertToDatabaseValueSQL('?', $platform); - $placeholders[] = $placeholder; - } else { - $placeholders[] = '?'; - } + /** @phpstan-var literal-string $placeholder */ + $placeholder = $type->convertToDatabaseValueSQL('?', $platform); + $placeholders[] = $placeholder; /** @phpstan-var literal-string $columnName */ $columnName = $em->getConfiguration()->getQuoteStrategy()->getColumnName($field, $class, $platform); @@ -464,8 +453,7 @@ private function getInsertRevisionSQL(EntityManagerInterface $em, ClassMetadata ) && null !== $class->discriminatorColumn ) { - /** @var literal-string $discriminatorColumnName */ - $discriminatorColumnName = $class->discriminatorColumn['name']; + $discriminatorColumnName = self::getMappingNameValue($class->discriminatorColumn); $sql .= ', '.$discriminatorColumnName; $placeholders[] = '?'; } @@ -479,40 +467,37 @@ private function getInsertRevisionSQL(EntityManagerInterface $em, ClassMetadata } /** - * @param ClassMetadata $class - * @param ClassMetadata $targetClass - * @param array $assoc + * @param ClassMetadata $class + * @param ClassMetadata $targetClass + * @param array|ManyToManyOwningSideMapping $assoc * * @return literal-string - * - * @psalm-suppress MoreSpecificReturnType,PropertyTypeCoercion,LessSpecificReturnStatement https://github.com/vimeo/psalm/issues/10909 */ - private function getInsertJoinTableRevisionSQL(ClassMetadata $class, ClassMetadata $targetClass, array $assoc): string - { - $cacheKey = $class->name.'.'.$targetClass->name.'.'.$assoc['joinTable']['name']; + private function getInsertJoinTableRevisionSQL( + ClassMetadata $class, + ClassMetadata $targetClass, + array|ManyToManyOwningSideMapping $assoc + ): string { + $joinTableName = self::getMappingJoinTableNameValue($assoc); + $cacheKey = $class->name.'.'.$targetClass->name.'.'.$joinTableName; if ( !isset($this->insertJoinTableRevisionSQL[$cacheKey]) - && isset($assoc['relationToSourceKeyColumns'], $assoc['relationToTargetKeyColumns'], $assoc['joinTable']['name']) ) { $placeholders = ['?', '?']; - /** @phpstan-var literal-string $joinTableName */ - $joinTableName = $assoc['joinTable']['name']; $tableName = $this->config->getTablePrefix().$joinTableName.$this->config->getTableSuffix(); - /** @psalm-trace $sql */ $sql = 'INSERT INTO '.$tableName .' ('.$this->config->getRevisionFieldName(). ', '.$this->config->getRevisionTypeFieldName(); - /** @phpstan-var literal-string $sourceColumn */ - foreach ($assoc['relationToSourceKeyColumns'] as $sourceColumn => $targetColumn) { + foreach (self::getRelationToSourceKeyColumns($assoc) as $sourceColumn => $targetColumn) { $sql .= ', '.$sourceColumn; $placeholders[] = '?'; } - /** @phpstan-var literal-string $sourceColumn */ - foreach ($assoc['relationToTargetKeyColumns'] as $sourceColumn => $targetColumn) { + + foreach (self::getRelationToTargetKeyColumns($assoc) as $sourceColumn => $targetColumn) { $sql .= ', '.$sourceColumn; $placeholders[] = '?'; } @@ -544,9 +529,8 @@ private function saveRevisionEntityData(EntityManagerInterface $em, ClassMetadat continue; } - if ($assoc['isOwningSide']) { - if (0 !== ($assoc['type'] & ClassMetadata::TO_ONE) - && isset($assoc['sourceToTargetKeyColumns'])) { + if (self::isOwningSide($assoc)) { + if (self::isToOneOwningSide($assoc)) { $data = $entityData[$field] ?? null; $relatedId = []; @@ -554,11 +538,9 @@ private function saveRevisionEntityData(EntityManagerInterface $em, ClassMetadat $relatedId = $uow->getEntityIdentifier($data); } - /** @var class-string $targetEntity */ - $targetEntity = $assoc['targetEntity']; - $targetClass = $em->getClassMetadata($targetEntity); + $targetClass = $em->getClassMetadata(self::getMappingTargetEntityValue($assoc)); - foreach ($assoc['sourceToTargetKeyColumns'] as $sourceColumn => $targetColumn) { + foreach (self::getSourceToTargetKeyColumns($assoc) as $sourceColumn => $targetColumn) { $fields[$sourceColumn] = true; if (null === $data) { $params[] = null; @@ -568,9 +550,8 @@ private function saveRevisionEntityData(EntityManagerInterface $em, ClassMetadat $types[] = $targetClass->getTypeOfField($targetClass->getFieldForColumn($targetColumn)); } } - } elseif (($assoc['type'] & ClassMetadata::MANY_TO_MANY) > 0 - && isset($assoc['relationToSourceKeyColumns'], $assoc['relationToTargetKeyColumns'])) { - $targetClass = $em->getClassMetadata($assoc['targetEntity']); + } elseif (self::isManyToManyOwningSideMapping($assoc)) { + $targetClass = $em->getClassMetadata(self::getMappingTargetEntityValue($assoc)); $collection = $entityData[$assoc['fieldName']]; if (null !== $collection) { @@ -601,7 +582,7 @@ private function saveRevisionEntityData(EntityManagerInterface $em, ClassMetadat } $params[] = $entityData[$field] ?? null; - $types[] = $class->fieldMappings[$field]['type']; + $types[] = self::getMappingValue($class->fieldMappings[$field], 'type'); } if ( @@ -609,21 +590,21 @@ private function saveRevisionEntityData(EntityManagerInterface $em, ClassMetadat && null !== $class->discriminatorColumn ) { $params[] = $class->discriminatorValue; - $types[] = $class->discriminatorColumn['type']; + $types[] = self::getMappingValue($class->discriminatorColumn, 'type'); } elseif ( $class->isInheritanceTypeJoined() && $class->name === $class->rootEntityName && null !== $class->discriminatorColumn ) { - $params[] = $entityData[$class->discriminatorColumn['name']]; - $types[] = $class->discriminatorColumn['type']; + $params[] = $entityData[self::getMappingNameValue($class->discriminatorColumn)]; + $types[] = self::getMappingValue($class->discriminatorColumn, 'type'); } if ( $class->isInheritanceTypeJoined() && $class->name !== $class->rootEntityName && null !== $class->discriminatorColumn ) { - $entityData[$class->discriminatorColumn['name']] = $class->discriminatorValue; + $entityData[self::getMappingNameValue($class->discriminatorColumn)] = $class->discriminatorValue; $this->saveRevisionEntityData( $em, $em->getClassMetadata($class->rootEntityName), @@ -642,21 +623,30 @@ private function saveRevisionEntityData(EntityManagerInterface $em, ClassMetadat } /** - * @param array $assoc - * @param array $entityData - * @param ClassMetadata $class - * @param ClassMetadata $targetClass + * @param array|ManyToManyOwningSideMapping $assoc + * @param array $entityData + * @param ClassMetadata $class + * @param ClassMetadata $targetClass */ - private function recordRevisionForManyToManyEntity(object $relatedEntity, EntityManagerInterface $em, string $revType, array $entityData, array $assoc, ClassMetadata $class, ClassMetadata $targetClass): void - { + private function recordRevisionForManyToManyEntity( + object $relatedEntity, + EntityManagerInterface $em, + string $revType, + array $entityData, + array|ManyToManyOwningSideMapping $assoc, + ClassMetadata $class, + ClassMetadata $targetClass + ): void { $conn = $em->getConnection(); $joinTableParams = [$this->getRevisionId($conn), $revType]; $joinTableTypes = [\PDO::PARAM_INT, \PDO::PARAM_STR]; - foreach ($assoc['relationToSourceKeyColumns'] as $targetColumn) { + + foreach (self::getRelationToSourceKeyColumns($assoc) as $targetColumn) { $joinTableParams[] = $entityData[$class->fieldNames[$targetColumn]]; $joinTableTypes[] = PersisterHelper::getTypeOfColumn($targetColumn, $class, $em); } - foreach ($assoc['relationToTargetKeyColumns'] as $targetColumn) { + + foreach (self::getRelationToTargetKeyColumns($assoc) as $targetColumn) { $reflField = $targetClass->reflFields[$targetClass->fieldNames[$targetColumn]]; \assert(null !== $reflField); $joinTableParams[] = $reflField->getValue($relatedEntity); @@ -720,7 +710,7 @@ private function prepareUpdateData(EntityManagerInterface $em, EntityPersister $ $newVal = $change[1]; if (!isset($classMetadata->associationMappings[$field])) { - $columnName = $classMetadata->fieldMappings[$field]['columnName']; + $columnName = self::getMappingColumnNameValue($classMetadata->fieldMappings[$field]); $result[$persister->getOwningTable($field)][$columnName] = $newVal; continue; @@ -729,10 +719,7 @@ private function prepareUpdateData(EntityManagerInterface $em, EntityPersister $ $assoc = $classMetadata->associationMappings[$field]; // Only owning side of x-1 associations can have a FK column. - if ( - 0 === ($assoc['type'] & ClassMetadata::TO_ONE) - || false === $assoc['isOwningSide'] - || !isset($assoc['joinColumns'])) { + if (!self::isToOneOwningSide($assoc)) { continue; } @@ -752,14 +739,12 @@ private function prepareUpdateData(EntityManagerInterface $em, EntityPersister $ $newValId = $uow->getEntityIdentifier($newVal); } - /** @var class-string $targetEntity */ - $targetEntity = $assoc['targetEntity']; - $targetClass = $em->getClassMetadata($targetEntity); + $targetClass = $em->getClassMetadata(self::getMappingTargetEntityValue($assoc)); $owningTable = $persister->getOwningTable($field); foreach ($assoc['joinColumns'] as $joinColumn) { - $sourceColumn = $joinColumn['name']; - $targetColumn = $joinColumn['referencedColumnName']; + $sourceColumn = self::getMappingNameValue($joinColumn); + $targetColumn = self::getMappingValue($joinColumn, 'referencedColumnName'); $result[$owningTable][$sourceColumn] = null !== $newValId ? $newValId[$targetClass->getFieldForColumn($targetColumn)] diff --git a/src/Utils/ORMCompatibilityTrait.php b/src/Utils/ORMCompatibilityTrait.php new file mode 100644 index 00000000..2c9b2539 --- /dev/null +++ b/src/Utils/ORMCompatibilityTrait.php @@ -0,0 +1,243 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace SimpleThings\EntityAudit\Utils; + +use Doctrine\ORM\Mapping\AssociationMapping; +use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Mapping\DiscriminatorColumnMapping; +use Doctrine\ORM\Mapping\EmbeddedClassMapping; +use Doctrine\ORM\Mapping\FieldMapping; +use Doctrine\ORM\Mapping\JoinColumnMapping; +use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping; +use Doctrine\ORM\Mapping\ToOneOwningSideMapping; + +/** + * @internal + */ +trait ORMCompatibilityTrait +{ + /** + * @param array|AssociationMapping|EmbeddedClassMapping|FieldMapping|JoinColumnMapping|DiscriminatorColumnMapping $mapping + */ + final protected static function getMappingValue(array|AssociationMapping|EmbeddedClassMapping|FieldMapping|JoinColumnMapping|DiscriminatorColumnMapping $mapping, string $key): mixed + { + if ($mapping instanceof AssociationMapping || $mapping instanceof EmbeddedClassMapping || $mapping instanceof FieldMapping || $mapping instanceof JoinColumnMapping || $mapping instanceof DiscriminatorColumnMapping) { + /* @phpstan-ignore property.dynamicName */ + return $mapping->$key; + } + + return $mapping[$key] ?? null; + } + + /** + * @param array|AssociationMapping|FieldMapping|DiscriminatorColumnMapping $mapping + * + * @return literal-string + */ + final protected static function getMappingFieldNameValue(array|AssociationMapping|EmbeddedClassMapping|FieldMapping|DiscriminatorColumnMapping $mapping): string + { + if ($mapping instanceof AssociationMapping || $mapping instanceof FieldMapping || $mapping instanceof DiscriminatorColumnMapping) { + /* @phpstan-ignore return.type */ + return $mapping->fieldName; + } + + /* @phpstan-ignore return.type */ + return $mapping['fieldName']; + } + + /** + * @param array|JoinColumnMapping|DiscriminatorColumnMapping $mapping + * + * @return literal-string + */ + final protected static function getMappingNameValue(array|JoinColumnMapping|DiscriminatorColumnMapping $mapping): string + { + if ($mapping instanceof JoinColumnMapping || $mapping instanceof DiscriminatorColumnMapping) { + /* @phpstan-ignore return.type */ + return $mapping->name; + } + + /* @phpstan-ignore return.type */ + return $mapping['name']; + } + + /** + * @param array|FieldMapping $mapping + * + * @return literal-string + */ + final protected static function getMappingColumnNameValue(array|FieldMapping $mapping): string + { + if ($mapping instanceof FieldMapping) { + /* @phpstan-ignore return.type */ + return $mapping->columnName; + } + + /* @phpstan-ignore return.type */ + return $mapping['columnName']; + } + + /** + * @param array|ManyToManyOwningSideMapping $mapping + * + * @return literal-string + */ + final protected static function getMappingJoinTableNameValue(array|ManyToManyOwningSideMapping $mapping): string + { + if ($mapping instanceof ManyToManyOwningSideMapping) { + /* @phpstan-ignore return.type */ + return $mapping->joinTable->name; + } + + /* @phpstan-ignore return.type */ + return $mapping['joinTable']['name']; + } + + /** + * @param array|AssociationMapping $mapping + * + * @phpstan-assert-if-true ManyToManyOwningSideMapping $mapping + */ + final protected static function isManyToManyOwningSideMapping(array|AssociationMapping $mapping): bool + { + if ($mapping instanceof AssociationMapping) { + return $mapping->isManyToMany() && $mapping->isOwningSide(); + } + + return true === $mapping['isOwningSide'] && ($mapping['type'] & ClassMetadata::MANY_TO_MANY) > 0; + } + + /** + * @param array|AssociationMapping $mapping + * + * @phpstan-assert-if-true ToOneOwningSideMapping $mapping + */ + final protected static function isToOneOwningSide(array|AssociationMapping $mapping): bool + { + if ($mapping instanceof AssociationMapping) { + return $mapping->isToOneOwningSide(); + } + + return ($mapping['type'] & ClassMetadata::TO_ONE) > 0 && true === $mapping['isOwningSide']; + } + + /** + * @param array|AssociationMapping $mapping + */ + final protected static function isToOne(array|AssociationMapping $mapping): bool + { + if ($mapping instanceof AssociationMapping) { + return $mapping->isToOne(); + } + + return ($mapping['type'] & ClassMetadata::TO_ONE) > 0; + } + + /** + * @param array|AssociationMapping $mapping + */ + final protected static function isManyToMany(array|AssociationMapping $mapping): bool + { + if ($mapping instanceof AssociationMapping) { + return $mapping->isManyToMany(); + } + + return ($mapping['type'] & ClassMetadata::MANY_TO_MANY) > 0; + } + + /** + * @param array|ToOneOwningSideMapping $mapping + * + * @return array + */ + final protected static function getTargetToSourceKeyColumns(array|ToOneOwningSideMapping $mapping): array + { + if ($mapping instanceof ToOneOwningSideMapping) { + /* @phpstan-ignore return.type */ + return $mapping->targetToSourceKeyColumns; + } + + return $mapping['targetToSourceKeyColumns']; + } + + /** + * @param array|ToOneOwningSideMapping $mapping + * + * @return array + */ + final protected static function getSourceToTargetKeyColumns(array|ToOneOwningSideMapping $mapping): array + { + if ($mapping instanceof ToOneOwningSideMapping) { + return $mapping->sourceToTargetKeyColumns; + } + + return $mapping['sourceToTargetKeyColumns']; + } + + /** + * @param array|ManyToManyOwningSideMapping $mapping + * + * @return array + */ + final protected static function getRelationToSourceKeyColumns(array|ManyToManyOwningSideMapping $mapping): array + { + if ($mapping instanceof ManyToManyOwningSideMapping) { + /* @phpstan-ignore return.type */ + return $mapping->relationToSourceKeyColumns; + } + + return $mapping['relationToSourceKeyColumns']; + } + + /** + * @param array|ManyToManyOwningSideMapping $mapping + * + * @return array + */ + final protected static function getRelationToTargetKeyColumns(array|ManyToManyOwningSideMapping $mapping): array + { + if ($mapping instanceof ManyToManyOwningSideMapping) { + /* @phpstan-ignore return.type */ + return $mapping->relationToTargetKeyColumns; + } + + return $mapping['relationToTargetKeyColumns']; + } + + /** + * @param array|AssociationMapping $mapping + * + * @phpstan-return class-string + */ + final protected static function getMappingTargetEntityValue(array|AssociationMapping $mapping): string + { + if ($mapping instanceof AssociationMapping) { + return $mapping->targetEntity; + } + + return $mapping['targetEntity']; + } + + /** + * @param array|AssociationMapping $mapping + */ + final protected static function isOwningSide(array|AssociationMapping $mapping): bool + { + if ($mapping instanceof AssociationMapping) { + return $mapping->isOwningSide(); + } + + return true === $mapping['isOwningSide']; + } +} diff --git a/tests/BaseTest.php b/tests/BaseTest.php index 8b520778..bb3e2aab 100644 --- a/tests/BaseTest.php +++ b/tests/BaseTest.php @@ -16,6 +16,7 @@ use Doctrine\Common\EventManager; use Doctrine\DBAL\Connection; use Doctrine\DBAL\DriverManager; +use Doctrine\DBAL\Schema\DefaultSchemaManagerFactory; use Doctrine\DBAL\Types\Type; use Doctrine\ORM\Configuration; use Doctrine\ORM\EntityManager; @@ -94,6 +95,7 @@ protected function getEntityManager(): EntityManager } $config = ORMSetup::createAttributeMetadataConfiguration($mappingPaths, true); + $config->setSchemaManagerFactory(new DefaultSchemaManagerFactory()); $connection = $this->_getConnection($config); $this->em = new EntityManager($connection, $config, new EventManager());