diff options
Diffstat (limited to 'vendor/doctrine/orm/src/Persisters')
17 files changed, 4685 insertions, 0 deletions
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php new file mode 100644 index 0000000..26f0b9e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/AbstractCollectionPersister.php | |||
| @@ -0,0 +1,50 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Connection; | ||
| 8 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 9 | use Doctrine\ORM\EntityManagerInterface; | ||
| 10 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 11 | use Doctrine\ORM\UnitOfWork; | ||
| 12 | |||
| 13 | /** | ||
| 14 | * Base class for all collection persisters. | ||
| 15 | */ | ||
| 16 | abstract class AbstractCollectionPersister implements CollectionPersister | ||
| 17 | { | ||
| 18 | protected Connection $conn; | ||
| 19 | protected UnitOfWork $uow; | ||
| 20 | protected AbstractPlatform $platform; | ||
| 21 | protected QuoteStrategy $quoteStrategy; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Initializes a new instance of a class derived from AbstractCollectionPersister. | ||
| 25 | */ | ||
| 26 | public function __construct( | ||
| 27 | protected EntityManagerInterface $em, | ||
| 28 | ) { | ||
| 29 | $this->uow = $em->getUnitOfWork(); | ||
| 30 | $this->conn = $em->getConnection(); | ||
| 31 | $this->platform = $this->conn->getDatabasePlatform(); | ||
| 32 | $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 33 | } | ||
| 34 | |||
| 35 | /** | ||
| 36 | * Check if entity is in a valid state for operations. | ||
| 37 | */ | ||
| 38 | protected function isValidEntityState(object $entity): bool | ||
| 39 | { | ||
| 40 | $entityState = $this->uow->getEntityState($entity, UnitOfWork::STATE_NEW); | ||
| 41 | |||
| 42 | if ($entityState === UnitOfWork::STATE_NEW) { | ||
| 43 | return false; | ||
| 44 | } | ||
| 45 | |||
| 46 | // If Entity is scheduled for inclusion, it is not in this collection. | ||
| 47 | // We can assure that because it would have return true before on array check | ||
| 48 | return ! ($entityState === UnitOfWork::STATE_MANAGED && $this->uow->isScheduledForInsert($entity)); | ||
| 49 | } | ||
| 50 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php new file mode 100644 index 0000000..07c4eaf --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/CollectionPersister.php | |||
| @@ -0,0 +1,59 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\ORM\PersistentCollection; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * Define the behavior that should be implemented by all collection persisters. | ||
| 12 | */ | ||
| 13 | interface CollectionPersister | ||
| 14 | { | ||
| 15 | /** | ||
| 16 | * Deletes the persistent state represented by the given collection. | ||
| 17 | */ | ||
| 18 | public function delete(PersistentCollection $collection): void; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Updates the given collection, synchronizing its state with the database | ||
| 22 | * by inserting, updating and deleting individual elements. | ||
| 23 | */ | ||
| 24 | public function update(PersistentCollection $collection): void; | ||
| 25 | |||
| 26 | /** | ||
| 27 | * Counts the size of this persistent collection. | ||
| 28 | */ | ||
| 29 | public function count(PersistentCollection $collection): int; | ||
| 30 | |||
| 31 | /** | ||
| 32 | * Slices elements. | ||
| 33 | * | ||
| 34 | * @return mixed[] | ||
| 35 | */ | ||
| 36 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Checks for existence of an element. | ||
| 40 | */ | ||
| 41 | public function contains(PersistentCollection $collection, object $element): bool; | ||
| 42 | |||
| 43 | /** | ||
| 44 | * Checks for existence of a key. | ||
| 45 | */ | ||
| 46 | public function containsKey(PersistentCollection $collection, mixed $key): bool; | ||
| 47 | |||
| 48 | /** | ||
| 49 | * Gets an element by key. | ||
| 50 | */ | ||
| 51 | public function get(PersistentCollection $collection, mixed $index): mixed; | ||
| 52 | |||
| 53 | /** | ||
| 54 | * Loads association entities matching the given Criteria object. | ||
| 55 | * | ||
| 56 | * @return mixed[] | ||
| 57 | */ | ||
| 58 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array; | ||
| 59 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php new file mode 100644 index 0000000..7cf993d --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/ManyToManyPersister.php | |||
| @@ -0,0 +1,770 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 10 | use Doctrine\DBAL\Exception as DBALException; | ||
| 11 | use Doctrine\DBAL\LockMode; | ||
| 12 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 14 | use Doctrine\ORM\Mapping\InverseSideMapping; | ||
| 15 | use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; | ||
| 16 | use Doctrine\ORM\PersistentCollection; | ||
| 17 | use Doctrine\ORM\Persisters\SqlValueVisitor; | ||
| 18 | use Doctrine\ORM\Query; | ||
| 19 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 20 | |||
| 21 | use function array_fill; | ||
| 22 | use function array_pop; | ||
| 23 | use function assert; | ||
| 24 | use function count; | ||
| 25 | use function implode; | ||
| 26 | use function in_array; | ||
| 27 | use function reset; | ||
| 28 | use function sprintf; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Persister for many-to-many collections. | ||
| 32 | */ | ||
| 33 | class ManyToManyPersister extends AbstractCollectionPersister | ||
| 34 | { | ||
| 35 | public function delete(PersistentCollection $collection): void | ||
| 36 | { | ||
| 37 | $mapping = $this->getMapping($collection); | ||
| 38 | |||
| 39 | if (! $mapping->isOwningSide()) { | ||
| 40 | return; // ignore inverse side | ||
| 41 | } | ||
| 42 | |||
| 43 | assert($mapping->isManyToManyOwningSide()); | ||
| 44 | |||
| 45 | $types = []; | ||
| 46 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 47 | |||
| 48 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 49 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); | ||
| 50 | } | ||
| 51 | |||
| 52 | $this->conn->executeStatement($this->getDeleteSQL($collection), $this->getDeleteSQLParameters($collection), $types); | ||
| 53 | } | ||
| 54 | |||
| 55 | public function update(PersistentCollection $collection): void | ||
| 56 | { | ||
| 57 | $mapping = $this->getMapping($collection); | ||
| 58 | |||
| 59 | if (! $mapping->isOwningSide()) { | ||
| 60 | return; // ignore inverse side | ||
| 61 | } | ||
| 62 | |||
| 63 | [$deleteSql, $deleteTypes] = $this->getDeleteRowSQL($collection); | ||
| 64 | [$insertSql, $insertTypes] = $this->getInsertRowSQL($collection); | ||
| 65 | |||
| 66 | foreach ($collection->getDeleteDiff() as $element) { | ||
| 67 | $this->conn->executeStatement( | ||
| 68 | $deleteSql, | ||
| 69 | $this->getDeleteRowSQLParameters($collection, $element), | ||
| 70 | $deleteTypes, | ||
| 71 | ); | ||
| 72 | } | ||
| 73 | |||
| 74 | foreach ($collection->getInsertDiff() as $element) { | ||
| 75 | $this->conn->executeStatement( | ||
| 76 | $insertSql, | ||
| 77 | $this->getInsertRowSQLParameters($collection, $element), | ||
| 78 | $insertTypes, | ||
| 79 | ); | ||
| 80 | } | ||
| 81 | } | ||
| 82 | |||
| 83 | public function get(PersistentCollection $collection, mixed $index): object|null | ||
| 84 | { | ||
| 85 | $mapping = $this->getMapping($collection); | ||
| 86 | |||
| 87 | if (! $mapping->isIndexed()) { | ||
| 88 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 89 | } | ||
| 90 | |||
| 91 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 92 | $mappedKey = $mapping->isOwningSide() | ||
| 93 | ? $mapping->inversedBy | ||
| 94 | : $mapping->mappedBy; | ||
| 95 | |||
| 96 | assert($mappedKey !== null); | ||
| 97 | |||
| 98 | return $persister->load( | ||
| 99 | [$mappedKey => $collection->getOwner(), $mapping->indexBy() => $index], | ||
| 100 | null, | ||
| 101 | $mapping, | ||
| 102 | [], | ||
| 103 | LockMode::NONE, | ||
| 104 | 1, | ||
| 105 | ); | ||
| 106 | } | ||
| 107 | |||
| 108 | public function count(PersistentCollection $collection): int | ||
| 109 | { | ||
| 110 | $conditions = []; | ||
| 111 | $params = []; | ||
| 112 | $types = []; | ||
| 113 | $mapping = $this->getMapping($collection); | ||
| 114 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 115 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 116 | $association = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 117 | |||
| 118 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $sourceClass, $this->platform); | ||
| 119 | $joinColumns = ! $mapping->isOwningSide() | ||
| 120 | ? $association->joinTable->inverseJoinColumns | ||
| 121 | : $association->joinTable->joinColumns; | ||
| 122 | |||
| 123 | foreach ($joinColumns as $joinColumn) { | ||
| 124 | $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $sourceClass, $this->platform); | ||
| 125 | $referencedName = $joinColumn->referencedColumnName; | ||
| 126 | $conditions[] = 't.' . $columnName . ' = ?'; | ||
| 127 | $params[] = $id[$sourceClass->getFieldForColumn($referencedName)]; | ||
| 128 | $types[] = PersisterHelper::getTypeOfColumn($referencedName, $sourceClass, $this->em); | ||
| 129 | } | ||
| 130 | |||
| 131 | [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($mapping); | ||
| 132 | |||
| 133 | if ($filterSql) { | ||
| 134 | $conditions[] = $filterSql; | ||
| 135 | } | ||
| 136 | |||
| 137 | // If there is a provided criteria, make part of conditions | ||
| 138 | // @todo Fix this. Current SQL returns something like: | ||
| 139 | /*if ($criteria && ($expression = $criteria->getWhereExpression()) !== null) { | ||
| 140 | // A join is needed on the target entity | ||
| 141 | $targetTableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 142 | $targetJoinSql = ' JOIN ' . $targetTableName . ' te' | ||
| 143 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($association)); | ||
| 144 | |||
| 145 | // And criteria conditions needs to be added | ||
| 146 | $persister = $this->uow->getEntityPersister($targetClass->name); | ||
| 147 | $visitor = new SqlExpressionVisitor($persister, $targetClass); | ||
| 148 | $conditions[] = $visitor->dispatch($expression); | ||
| 149 | |||
| 150 | $joinTargetEntitySQL = $targetJoinSql . $joinTargetEntitySQL; | ||
| 151 | }*/ | ||
| 152 | |||
| 153 | $sql = 'SELECT COUNT(*)' | ||
| 154 | . ' FROM ' . $joinTableName . ' t' | ||
| 155 | . $joinTargetEntitySQL | ||
| 156 | . ' WHERE ' . implode(' AND ', $conditions); | ||
| 157 | |||
| 158 | return (int) $this->conn->fetchOne($sql, $params, $types); | ||
| 159 | } | ||
| 160 | |||
| 161 | /** | ||
| 162 | * {@inheritDoc} | ||
| 163 | */ | ||
| 164 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array | ||
| 165 | { | ||
| 166 | $mapping = $this->getMapping($collection); | ||
| 167 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 168 | |||
| 169 | return $persister->getManyToManyCollection($mapping, $collection->getOwner(), $offset, $length); | ||
| 170 | } | ||
| 171 | |||
| 172 | public function containsKey(PersistentCollection $collection, mixed $key): bool | ||
| 173 | { | ||
| 174 | $mapping = $this->getMapping($collection); | ||
| 175 | |||
| 176 | if (! $mapping->isIndexed()) { | ||
| 177 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 178 | } | ||
| 179 | |||
| 180 | [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictionsWithKey( | ||
| 181 | $collection, | ||
| 182 | (string) $key, | ||
| 183 | true, | ||
| 184 | ); | ||
| 185 | |||
| 186 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); | ||
| 187 | |||
| 188 | return (bool) $this->conn->fetchOne($sql, $params, $types); | ||
| 189 | } | ||
| 190 | |||
| 191 | public function contains(PersistentCollection $collection, object $element): bool | ||
| 192 | { | ||
| 193 | if (! $this->isValidEntityState($element)) { | ||
| 194 | return false; | ||
| 195 | } | ||
| 196 | |||
| 197 | [$quotedJoinTable, $whereClauses, $params, $types] = $this->getJoinTableRestrictions( | ||
| 198 | $collection, | ||
| 199 | $element, | ||
| 200 | true, | ||
| 201 | ); | ||
| 202 | |||
| 203 | $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); | ||
| 204 | |||
| 205 | return (bool) $this->conn->fetchOne($sql, $params, $types); | ||
| 206 | } | ||
| 207 | |||
| 208 | /** | ||
| 209 | * {@inheritDoc} | ||
| 210 | */ | ||
| 211 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array | ||
| 212 | { | ||
| 213 | $mapping = $this->getMapping($collection); | ||
| 214 | $owner = $collection->getOwner(); | ||
| 215 | $ownerMetadata = $this->em->getClassMetadata($owner::class); | ||
| 216 | $id = $this->uow->getEntityIdentifier($owner); | ||
| 217 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 218 | $onConditions = $this->getOnConditionSQL($mapping); | ||
| 219 | $whereClauses = $params = []; | ||
| 220 | $paramTypes = []; | ||
| 221 | |||
| 222 | if (! $mapping->isOwningSide()) { | ||
| 223 | assert($mapping instanceof InverseSideMapping); | ||
| 224 | $associationSourceClass = $targetClass; | ||
| 225 | $sourceRelationMode = 'relationToTargetKeyColumns'; | ||
| 226 | } else { | ||
| 227 | $associationSourceClass = $ownerMetadata; | ||
| 228 | $sourceRelationMode = 'relationToSourceKeyColumns'; | ||
| 229 | } | ||
| 230 | |||
| 231 | $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 232 | |||
| 233 | foreach ($mapping->$sourceRelationMode as $key => $value) { | ||
| 234 | $whereClauses[] = sprintf('t.%s = ?', $key); | ||
| 235 | $params[] = $ownerMetadata->containsForeignIdentifier | ||
| 236 | ? $id[$ownerMetadata->getFieldForColumn($value)] | ||
| 237 | : $id[$ownerMetadata->fieldNames[$value]]; | ||
| 238 | $paramTypes[] = PersisterHelper::getTypeOfColumn($value, $ownerMetadata, $this->em); | ||
| 239 | } | ||
| 240 | |||
| 241 | $parameters = $this->expandCriteriaParameters($criteria); | ||
| 242 | |||
| 243 | foreach ($parameters as $parameter) { | ||
| 244 | [$name, $value, $operator] = $parameter; | ||
| 245 | |||
| 246 | $field = $this->quoteStrategy->getColumnName($name, $targetClass, $this->platform); | ||
| 247 | |||
| 248 | if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { | ||
| 249 | $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); | ||
| 250 | } else { | ||
| 251 | $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); | ||
| 252 | $params[] = $value; | ||
| 253 | $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; | ||
| 254 | } | ||
| 255 | } | ||
| 256 | |||
| 257 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 258 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform); | ||
| 259 | |||
| 260 | $rsm = new Query\ResultSetMappingBuilder($this->em); | ||
| 261 | $rsm->addRootEntityFromClassMetadata($targetClass->name, 'te'); | ||
| 262 | |||
| 263 | $sql = 'SELECT ' . $rsm->generateSelectClause() | ||
| 264 | . ' FROM ' . $tableName . ' te' | ||
| 265 | . ' JOIN ' . $joinTable . ' t ON' | ||
| 266 | . implode(' AND ', $onConditions) | ||
| 267 | . ' WHERE ' . implode(' AND ', $whereClauses); | ||
| 268 | |||
| 269 | $sql .= $this->getOrderingSql($criteria, $targetClass); | ||
| 270 | |||
| 271 | $sql .= $this->getLimitSql($criteria); | ||
| 272 | |||
| 273 | $stmt = $this->conn->executeQuery($sql, $params, $paramTypes); | ||
| 274 | |||
| 275 | return $this | ||
| 276 | ->em | ||
| 277 | ->newHydrator(Query::HYDRATE_OBJECT) | ||
| 278 | ->hydrateAll($stmt, $rsm); | ||
| 279 | } | ||
| 280 | |||
| 281 | /** | ||
| 282 | * Generates the filter SQL for a given mapping. | ||
| 283 | * | ||
| 284 | * This method is not used for actually grabbing the related entities | ||
| 285 | * but when the extra-lazy collection methods are called on a filtered | ||
| 286 | * association. This is why besides the many to many table we also | ||
| 287 | * have to join in the actual entities table leading to additional | ||
| 288 | * JOIN. | ||
| 289 | * | ||
| 290 | * @param AssociationMapping $mapping Array containing mapping information. | ||
| 291 | * | ||
| 292 | * @return string[] ordered tuple: | ||
| 293 | * - JOIN condition to add to the SQL | ||
| 294 | * - WHERE condition to add to the SQL | ||
| 295 | * @psalm-return array{0: string, 1: string} | ||
| 296 | */ | ||
| 297 | public function getFilterSql(AssociationMapping $mapping): array | ||
| 298 | { | ||
| 299 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 300 | $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); | ||
| 301 | $filterSql = $this->generateFilterConditionSQL($rootClass, 'te'); | ||
| 302 | |||
| 303 | if ($filterSql === '') { | ||
| 304 | return ['', '']; | ||
| 305 | } | ||
| 306 | |||
| 307 | // A join is needed if there is filtering on the target entity | ||
| 308 | $tableName = $this->quoteStrategy->getTableName($rootClass, $this->platform); | ||
| 309 | $joinSql = ' JOIN ' . $tableName . ' te' | ||
| 310 | . ' ON' . implode(' AND ', $this->getOnConditionSQL($mapping)); | ||
| 311 | |||
| 312 | return [$joinSql, $filterSql]; | ||
| 313 | } | ||
| 314 | |||
| 315 | /** | ||
| 316 | * Generates the filter SQL for a given entity and table alias. | ||
| 317 | * | ||
| 318 | * @param ClassMetadata $targetEntity Metadata of the target entity. | ||
| 319 | * @param string $targetTableAlias The table alias of the joined/selected table. | ||
| 320 | * | ||
| 321 | * @return string The SQL query part to add to a query. | ||
| 322 | */ | ||
| 323 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string | ||
| 324 | { | ||
| 325 | $filterClauses = []; | ||
| 326 | |||
| 327 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
| 328 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
| 329 | if ($filterExpr) { | ||
| 330 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
| 331 | } | ||
| 332 | } | ||
| 333 | |||
| 334 | return $filterClauses | ||
| 335 | ? '(' . implode(' AND ', $filterClauses) . ')' | ||
| 336 | : ''; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * Generate ON condition | ||
| 341 | * | ||
| 342 | * @return string[] | ||
| 343 | * @psalm-return list<string> | ||
| 344 | */ | ||
| 345 | protected function getOnConditionSQL(AssociationMapping $mapping): array | ||
| 346 | { | ||
| 347 | $association = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 348 | $joinColumns = $mapping->isOwningSide() | ||
| 349 | ? $association->joinTable->inverseJoinColumns | ||
| 350 | : $association->joinTable->joinColumns; | ||
| 351 | |||
| 352 | $conditions = []; | ||
| 353 | |||
| 354 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 355 | foreach ($joinColumns as $joinColumn) { | ||
| 356 | $joinColumnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 357 | $refColumnName = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 358 | |||
| 359 | $conditions[] = ' t.' . $joinColumnName . ' = te.' . $refColumnName; | ||
| 360 | } | ||
| 361 | |||
| 362 | return $conditions; | ||
| 363 | } | ||
| 364 | |||
| 365 | protected function getDeleteSQL(PersistentCollection $collection): string | ||
| 366 | { | ||
| 367 | $columns = []; | ||
| 368 | $mapping = $this->getMapping($collection); | ||
| 369 | assert($mapping->isManyToManyOwningSide()); | ||
| 370 | $class = $this->em->getClassMetadata($collection->getOwner()::class); | ||
| 371 | $joinTable = $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform); | ||
| 372 | |||
| 373 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 374 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 375 | } | ||
| 376 | |||
| 377 | return 'DELETE FROM ' . $joinTable | ||
| 378 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; | ||
| 379 | } | ||
| 380 | |||
| 381 | /** | ||
| 382 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteSql. | ||
| 383 | * | ||
| 384 | * @return list<mixed> | ||
| 385 | */ | ||
| 386 | protected function getDeleteSQLParameters(PersistentCollection $collection): array | ||
| 387 | { | ||
| 388 | $mapping = $this->getMapping($collection); | ||
| 389 | assert($mapping->isManyToManyOwningSide()); | ||
| 390 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 391 | |||
| 392 | // Optimization for single column identifier | ||
| 393 | if (count($mapping->relationToSourceKeyColumns) === 1) { | ||
| 394 | return [reset($identifier)]; | ||
| 395 | } | ||
| 396 | |||
| 397 | // Composite identifier | ||
| 398 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 399 | $params = []; | ||
| 400 | |||
| 401 | foreach ($mapping->relationToSourceKeyColumns as $columnName => $refColumnName) { | ||
| 402 | $params[] = isset($sourceClass->fieldNames[$refColumnName]) | ||
| 403 | ? $identifier[$sourceClass->fieldNames[$refColumnName]] | ||
| 404 | : $identifier[$sourceClass->getFieldForColumn($refColumnName)]; | ||
| 405 | } | ||
| 406 | |||
| 407 | return $params; | ||
| 408 | } | ||
| 409 | |||
| 410 | /** | ||
| 411 | * Gets the SQL statement used for deleting a row from the collection. | ||
| 412 | * | ||
| 413 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array | ||
| 414 | * of types for bound parameters | ||
| 415 | * @psalm-return array{0: string, 1: list<string>} | ||
| 416 | */ | ||
| 417 | protected function getDeleteRowSQL(PersistentCollection $collection): array | ||
| 418 | { | ||
| 419 | $mapping = $this->getMapping($collection); | ||
| 420 | assert($mapping->isManyToManyOwningSide()); | ||
| 421 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 422 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 423 | $columns = []; | ||
| 424 | $types = []; | ||
| 425 | |||
| 426 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 427 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 428 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); | ||
| 429 | } | ||
| 430 | |||
| 431 | foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) { | ||
| 432 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 433 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 434 | } | ||
| 435 | |||
| 436 | return [ | ||
| 437 | 'DELETE FROM ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) | ||
| 438 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?', | ||
| 439 | $types, | ||
| 440 | ]; | ||
| 441 | } | ||
| 442 | |||
| 443 | /** | ||
| 444 | * Gets the SQL parameters for the corresponding SQL statement to delete the given | ||
| 445 | * element from the given collection. | ||
| 446 | * | ||
| 447 | * Internal note: Order of the parameters must be the same as the order of the columns in getDeleteRowSql. | ||
| 448 | * | ||
| 449 | * @return mixed[] | ||
| 450 | * @psalm-return list<mixed> | ||
| 451 | */ | ||
| 452 | protected function getDeleteRowSQLParameters(PersistentCollection $collection, object $element): array | ||
| 453 | { | ||
| 454 | return $this->collectJoinTableColumnParameters($collection, $element); | ||
| 455 | } | ||
| 456 | |||
| 457 | /** | ||
| 458 | * Gets the SQL statement used for inserting a row in the collection. | ||
| 459 | * | ||
| 460 | * @return string[]|string[][] ordered tuple containing the SQL to be executed and an array | ||
| 461 | * of types for bound parameters | ||
| 462 | * @psalm-return array{0: string, 1: list<string>} | ||
| 463 | */ | ||
| 464 | protected function getInsertRowSQL(PersistentCollection $collection): array | ||
| 465 | { | ||
| 466 | $columns = []; | ||
| 467 | $types = []; | ||
| 468 | $mapping = $this->getMapping($collection); | ||
| 469 | assert($mapping->isManyToManyOwningSide()); | ||
| 470 | $class = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 471 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 472 | |||
| 473 | foreach ($mapping->joinTable->joinColumns as $joinColumn) { | ||
| 474 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 475 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $class, $this->em); | ||
| 476 | } | ||
| 477 | |||
| 478 | foreach ($mapping->joinTable->inverseJoinColumns as $joinColumn) { | ||
| 479 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 480 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 481 | } | ||
| 482 | |||
| 483 | return [ | ||
| 484 | 'INSERT INTO ' . $this->quoteStrategy->getJoinTableName($mapping, $class, $this->platform) | ||
| 485 | . ' (' . implode(', ', $columns) . ')' | ||
| 486 | . ' VALUES' | ||
| 487 | . ' (' . implode(', ', array_fill(0, count($columns), '?')) . ')', | ||
| 488 | $types, | ||
| 489 | ]; | ||
| 490 | } | ||
| 491 | |||
| 492 | /** | ||
| 493 | * Gets the SQL parameters for the corresponding SQL statement to insert the given | ||
| 494 | * element of the given collection into the database. | ||
| 495 | * | ||
| 496 | * Internal note: Order of the parameters must be the same as the order of the columns in getInsertRowSql. | ||
| 497 | * | ||
| 498 | * @return mixed[] | ||
| 499 | * @psalm-return list<mixed> | ||
| 500 | */ | ||
| 501 | protected function getInsertRowSQLParameters(PersistentCollection $collection, object $element): array | ||
| 502 | { | ||
| 503 | return $this->collectJoinTableColumnParameters($collection, $element); | ||
| 504 | } | ||
| 505 | |||
| 506 | /** | ||
| 507 | * Collects the parameters for inserting/deleting on the join table in the order | ||
| 508 | * of the join table columns as specified in ManyToManyMapping#joinTableColumns. | ||
| 509 | * | ||
| 510 | * @return mixed[] | ||
| 511 | * @psalm-return list<mixed> | ||
| 512 | */ | ||
| 513 | private function collectJoinTableColumnParameters( | ||
| 514 | PersistentCollection $collection, | ||
| 515 | object $element, | ||
| 516 | ): array { | ||
| 517 | $params = []; | ||
| 518 | $mapping = $this->getMapping($collection); | ||
| 519 | assert($mapping->isManyToManyOwningSide()); | ||
| 520 | $isComposite = count($mapping->joinTableColumns) > 2; | ||
| 521 | |||
| 522 | $identifier1 = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 523 | $identifier2 = $this->uow->getEntityIdentifier($element); | ||
| 524 | |||
| 525 | $class1 = $class2 = null; | ||
| 526 | if ($isComposite) { | ||
| 527 | $class1 = $this->em->getClassMetadata($collection->getOwner()::class); | ||
| 528 | $class2 = $collection->getTypeClass(); | ||
| 529 | } | ||
| 530 | |||
| 531 | foreach ($mapping->joinTableColumns as $joinTableColumn) { | ||
| 532 | $isRelationToSource = isset($mapping->relationToSourceKeyColumns[$joinTableColumn]); | ||
| 533 | |||
| 534 | if (! $isComposite) { | ||
| 535 | $params[] = $isRelationToSource ? array_pop($identifier1) : array_pop($identifier2); | ||
| 536 | |||
| 537 | continue; | ||
| 538 | } | ||
| 539 | |||
| 540 | if ($isRelationToSource) { | ||
| 541 | $params[] = $identifier1[$class1->getFieldForColumn($mapping->relationToSourceKeyColumns[$joinTableColumn])]; | ||
| 542 | |||
| 543 | continue; | ||
| 544 | } | ||
| 545 | |||
| 546 | $params[] = $identifier2[$class2->getFieldForColumn($mapping->relationToTargetKeyColumns[$joinTableColumn])]; | ||
| 547 | } | ||
| 548 | |||
| 549 | return $params; | ||
| 550 | } | ||
| 551 | |||
| 552 | /** | ||
| 553 | * @param bool $addFilters Whether the filter SQL should be included or not. | ||
| 554 | * | ||
| 555 | * @return mixed[] ordered vector: | ||
| 556 | * - quoted join table name | ||
| 557 | * - where clauses to be added for filtering | ||
| 558 | * - parameters to be bound for filtering | ||
| 559 | * - types of the parameters to be bound for filtering | ||
| 560 | * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>} | ||
| 561 | */ | ||
| 562 | private function getJoinTableRestrictionsWithKey( | ||
| 563 | PersistentCollection $collection, | ||
| 564 | string $key, | ||
| 565 | bool $addFilters, | ||
| 566 | ): array { | ||
| 567 | $filterMapping = $this->getMapping($collection); | ||
| 568 | $mapping = $filterMapping; | ||
| 569 | $indexBy = $mapping->indexBy(); | ||
| 570 | $id = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 571 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 572 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 573 | |||
| 574 | if (! $mapping->isOwningSide()) { | ||
| 575 | assert($mapping instanceof InverseSideMapping); | ||
| 576 | $associationSourceClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 577 | $mapping = $associationSourceClass->associationMappings[$mapping->mappedBy]; | ||
| 578 | assert($mapping->isManyToManyOwningSide()); | ||
| 579 | $joinColumns = $mapping->joinTable->joinColumns; | ||
| 580 | $sourceRelationMode = 'relationToTargetKeyColumns'; | ||
| 581 | $targetRelationMode = 'relationToSourceKeyColumns'; | ||
| 582 | } else { | ||
| 583 | assert($mapping->isManyToManyOwningSide()); | ||
| 584 | $associationSourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 585 | $joinColumns = $mapping->joinTable->inverseJoinColumns; | ||
| 586 | $sourceRelationMode = 'relationToSourceKeyColumns'; | ||
| 587 | $targetRelationMode = 'relationToTargetKeyColumns'; | ||
| 588 | } | ||
| 589 | |||
| 590 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $associationSourceClass, $this->platform) . ' t'; | ||
| 591 | $whereClauses = []; | ||
| 592 | $params = []; | ||
| 593 | $types = []; | ||
| 594 | |||
| 595 | $joinNeeded = ! in_array($indexBy, $targetClass->identifier, true); | ||
| 596 | |||
| 597 | if ($joinNeeded) { // extra join needed if indexBy is not a @id | ||
| 598 | $joinConditions = []; | ||
| 599 | |||
| 600 | foreach ($joinColumns as $joinTableColumn) { | ||
| 601 | $joinConditions[] = 't.' . $joinTableColumn->name . ' = tr.' . $joinTableColumn->referencedColumnName; | ||
| 602 | } | ||
| 603 | |||
| 604 | $tableName = $this->quoteStrategy->getTableName($targetClass, $this->platform); | ||
| 605 | $quotedJoinTable .= ' JOIN ' . $tableName . ' tr ON ' . implode(' AND ', $joinConditions); | ||
| 606 | $columnName = $targetClass->getColumnName($indexBy); | ||
| 607 | |||
| 608 | $whereClauses[] = 'tr.' . $columnName . ' = ?'; | ||
| 609 | $params[] = $key; | ||
| 610 | $types[] = PersisterHelper::getTypeOfColumn($columnName, $targetClass, $this->em); | ||
| 611 | } | ||
| 612 | |||
| 613 | foreach ($mapping->joinTableColumns as $joinTableColumn) { | ||
| 614 | if (isset($mapping->{$sourceRelationMode}[$joinTableColumn])) { | ||
| 615 | $column = $mapping->{$sourceRelationMode}[$joinTableColumn]; | ||
| 616 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; | ||
| 617 | $params[] = $sourceClass->containsForeignIdentifier | ||
| 618 | ? $id[$sourceClass->getFieldForColumn($column)] | ||
| 619 | : $id[$sourceClass->fieldNames[$column]]; | ||
| 620 | $types[] = PersisterHelper::getTypeOfColumn($column, $sourceClass, $this->em); | ||
| 621 | } elseif (! $joinNeeded) { | ||
| 622 | $column = $mapping->{$targetRelationMode}[$joinTableColumn]; | ||
| 623 | |||
| 624 | $whereClauses[] = 't.' . $joinTableColumn . ' = ?'; | ||
| 625 | $params[] = $key; | ||
| 626 | $types[] = PersisterHelper::getTypeOfColumn($column, $targetClass, $this->em); | ||
| 627 | } | ||
| 628 | } | ||
| 629 | |||
| 630 | if ($addFilters) { | ||
| 631 | [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping); | ||
| 632 | |||
| 633 | if ($filterSql) { | ||
| 634 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; | ||
| 635 | $whereClauses[] = $filterSql; | ||
| 636 | } | ||
| 637 | } | ||
| 638 | |||
| 639 | return [$quotedJoinTable, $whereClauses, $params, $types]; | ||
| 640 | } | ||
| 641 | |||
| 642 | /** | ||
| 643 | * @param bool $addFilters Whether the filter SQL should be included or not. | ||
| 644 | * | ||
| 645 | * @return mixed[] ordered vector: | ||
| 646 | * - quoted join table name | ||
| 647 | * - where clauses to be added for filtering | ||
| 648 | * - parameters to be bound for filtering | ||
| 649 | * - types of the parameters to be bound for filtering | ||
| 650 | * @psalm-return array{0: string, 1: list<string>, 2: list<mixed>, 3: list<string>} | ||
| 651 | */ | ||
| 652 | private function getJoinTableRestrictions( | ||
| 653 | PersistentCollection $collection, | ||
| 654 | object $element, | ||
| 655 | bool $addFilters, | ||
| 656 | ): array { | ||
| 657 | $filterMapping = $this->getMapping($collection); | ||
| 658 | $mapping = $filterMapping; | ||
| 659 | |||
| 660 | if (! $mapping->isOwningSide()) { | ||
| 661 | $sourceClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 662 | $targetClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 663 | $sourceId = $this->uow->getEntityIdentifier($element); | ||
| 664 | $targetId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 665 | } else { | ||
| 666 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 667 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 668 | $sourceId = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 669 | $targetId = $this->uow->getEntityIdentifier($element); | ||
| 670 | } | ||
| 671 | |||
| 672 | $mapping = $this->em->getMetadataFactory()->getOwningSide($mapping); | ||
| 673 | |||
| 674 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($mapping, $sourceClass, $this->platform); | ||
| 675 | $whereClauses = []; | ||
| 676 | $params = []; | ||
| 677 | $types = []; | ||
| 678 | |||
| 679 | foreach ($mapping->joinTableColumns as $joinTableColumn) { | ||
| 680 | $whereClauses[] = ($addFilters ? 't.' : '') . $joinTableColumn . ' = ?'; | ||
| 681 | |||
| 682 | if (isset($mapping->relationToTargetKeyColumns[$joinTableColumn])) { | ||
| 683 | $targetColumn = $mapping->relationToTargetKeyColumns[$joinTableColumn]; | ||
| 684 | $params[] = $targetId[$targetClass->getFieldForColumn($targetColumn)]; | ||
| 685 | $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); | ||
| 686 | |||
| 687 | continue; | ||
| 688 | } | ||
| 689 | |||
| 690 | // relationToSourceKeyColumns | ||
| 691 | $targetColumn = $mapping->relationToSourceKeyColumns[$joinTableColumn]; | ||
| 692 | $params[] = $sourceId[$sourceClass->getFieldForColumn($targetColumn)]; | ||
| 693 | $types[] = PersisterHelper::getTypeOfColumn($targetColumn, $sourceClass, $this->em); | ||
| 694 | } | ||
| 695 | |||
| 696 | if ($addFilters) { | ||
| 697 | $quotedJoinTable .= ' t'; | ||
| 698 | |||
| 699 | [$joinTargetEntitySQL, $filterSql] = $this->getFilterSql($filterMapping); | ||
| 700 | |||
| 701 | if ($filterSql) { | ||
| 702 | $quotedJoinTable .= ' ' . $joinTargetEntitySQL; | ||
| 703 | $whereClauses[] = $filterSql; | ||
| 704 | } | ||
| 705 | } | ||
| 706 | |||
| 707 | return [$quotedJoinTable, $whereClauses, $params, $types]; | ||
| 708 | } | ||
| 709 | |||
| 710 | /** | ||
| 711 | * Expands Criteria Parameters by walking the expressions and grabbing all | ||
| 712 | * parameters and types from it. | ||
| 713 | * | ||
| 714 | * @return mixed[][] | ||
| 715 | */ | ||
| 716 | private function expandCriteriaParameters(Criteria $criteria): array | ||
| 717 | { | ||
| 718 | $expression = $criteria->getWhereExpression(); | ||
| 719 | |||
| 720 | if ($expression === null) { | ||
| 721 | return []; | ||
| 722 | } | ||
| 723 | |||
| 724 | $valueVisitor = new SqlValueVisitor(); | ||
| 725 | |||
| 726 | $valueVisitor->dispatch($expression); | ||
| 727 | |||
| 728 | [, $types] = $valueVisitor->getParamsAndTypes(); | ||
| 729 | |||
| 730 | return $types; | ||
| 731 | } | ||
| 732 | |||
| 733 | private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string | ||
| 734 | { | ||
| 735 | $orderings = $criteria->orderings(); | ||
| 736 | if ($orderings) { | ||
| 737 | $orderBy = []; | ||
| 738 | foreach ($orderings as $name => $direction) { | ||
| 739 | $field = $this->quoteStrategy->getColumnName( | ||
| 740 | $name, | ||
| 741 | $targetClass, | ||
| 742 | $this->platform, | ||
| 743 | ); | ||
| 744 | $orderBy[] = $field . ' ' . $direction->value; | ||
| 745 | } | ||
| 746 | |||
| 747 | return ' ORDER BY ' . implode(', ', $orderBy); | ||
| 748 | } | ||
| 749 | |||
| 750 | return ''; | ||
| 751 | } | ||
| 752 | |||
| 753 | /** @throws DBALException */ | ||
| 754 | private function getLimitSql(Criteria $criteria): string | ||
| 755 | { | ||
| 756 | $limit = $criteria->getMaxResults(); | ||
| 757 | $offset = $criteria->getFirstResult(); | ||
| 758 | |||
| 759 | return $this->platform->modifyLimitQuery('', $limit, $offset ?? 0); | ||
| 760 | } | ||
| 761 | |||
| 762 | private function getMapping(PersistentCollection $collection): AssociationMapping&ManyToManyAssociationMapping | ||
| 763 | { | ||
| 764 | $mapping = $collection->getMapping(); | ||
| 765 | |||
| 766 | assert($mapping instanceof ManyToManyAssociationMapping); | ||
| 767 | |||
| 768 | return $mapping; | ||
| 769 | } | ||
| 770 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php new file mode 100644 index 0000000..0727b1f --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Collection/OneToManyPersister.php | |||
| @@ -0,0 +1,264 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Collection; | ||
| 6 | |||
| 7 | use BadMethodCallException; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\DBAL\Exception as DBALException; | ||
| 10 | use Doctrine\DBAL\Types\Type; | ||
| 11 | use Doctrine\ORM\EntityNotFoundException; | ||
| 12 | use Doctrine\ORM\Mapping\MappingException; | ||
| 13 | use Doctrine\ORM\Mapping\OneToManyAssociationMapping; | ||
| 14 | use Doctrine\ORM\PersistentCollection; | ||
| 15 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 16 | |||
| 17 | use function array_reverse; | ||
| 18 | use function array_values; | ||
| 19 | use function assert; | ||
| 20 | use function implode; | ||
| 21 | use function is_int; | ||
| 22 | use function is_string; | ||
| 23 | |||
| 24 | /** | ||
| 25 | * Persister for one-to-many collections. | ||
| 26 | */ | ||
| 27 | class OneToManyPersister extends AbstractCollectionPersister | ||
| 28 | { | ||
| 29 | public function delete(PersistentCollection $collection): void | ||
| 30 | { | ||
| 31 | // The only valid case here is when you have weak entities. In this | ||
| 32 | // scenario, you have @OneToMany with orphanRemoval=true, and replacing | ||
| 33 | // the entire collection with a new would trigger this operation. | ||
| 34 | $mapping = $this->getMapping($collection); | ||
| 35 | |||
| 36 | if (! $mapping->orphanRemoval) { | ||
| 37 | // Handling non-orphan removal should never happen, as @OneToMany | ||
| 38 | // can only be inverse side. For owning side one to many, it is | ||
| 39 | // required to have a join table, which would classify as a ManyToManyPersister. | ||
| 40 | return; | ||
| 41 | } | ||
| 42 | |||
| 43 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 44 | |||
| 45 | $targetClass->isInheritanceTypeJoined() | ||
| 46 | ? $this->deleteJoinedEntityCollection($collection) | ||
| 47 | : $this->deleteEntityCollection($collection); | ||
| 48 | } | ||
| 49 | |||
| 50 | public function update(PersistentCollection $collection): void | ||
| 51 | { | ||
| 52 | // This can never happen. One to many can only be inverse side. | ||
| 53 | // For owning side one to many, it is required to have a join table, | ||
| 54 | // then classifying it as a ManyToManyPersister. | ||
| 55 | return; | ||
| 56 | } | ||
| 57 | |||
| 58 | public function get(PersistentCollection $collection, mixed $index): object|null | ||
| 59 | { | ||
| 60 | $mapping = $this->getMapping($collection); | ||
| 61 | |||
| 62 | if (! $mapping->isIndexed()) { | ||
| 63 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 64 | } | ||
| 65 | |||
| 66 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 67 | |||
| 68 | return $persister->load( | ||
| 69 | [ | ||
| 70 | $mapping->mappedBy => $collection->getOwner(), | ||
| 71 | $mapping->indexBy() => $index, | ||
| 72 | ], | ||
| 73 | null, | ||
| 74 | $mapping, | ||
| 75 | [], | ||
| 76 | null, | ||
| 77 | 1, | ||
| 78 | ); | ||
| 79 | } | ||
| 80 | |||
| 81 | public function count(PersistentCollection $collection): int | ||
| 82 | { | ||
| 83 | $mapping = $this->getMapping($collection); | ||
| 84 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 85 | |||
| 86 | // only works with single id identifier entities. Will throw an | ||
| 87 | // exception in Entity Persisters if that is not the case for the | ||
| 88 | // 'mappedBy' field. | ||
| 89 | $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
| 90 | |||
| 91 | return $persister->count($criteria); | ||
| 92 | } | ||
| 93 | |||
| 94 | /** | ||
| 95 | * {@inheritDoc} | ||
| 96 | */ | ||
| 97 | public function slice(PersistentCollection $collection, int $offset, int|null $length = null): array | ||
| 98 | { | ||
| 99 | $mapping = $this->getMapping($collection); | ||
| 100 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 101 | |||
| 102 | return $persister->getOneToManyCollection($mapping, $collection->getOwner(), $offset, $length); | ||
| 103 | } | ||
| 104 | |||
| 105 | public function containsKey(PersistentCollection $collection, mixed $key): bool | ||
| 106 | { | ||
| 107 | $mapping = $this->getMapping($collection); | ||
| 108 | |||
| 109 | if (! $mapping->isIndexed()) { | ||
| 110 | throw new BadMethodCallException('Selecting a collection by index is only supported on indexed collections.'); | ||
| 111 | } | ||
| 112 | |||
| 113 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 114 | |||
| 115 | // only works with single id identifier entities. Will throw an | ||
| 116 | // exception in Entity Persisters if that is not the case for the | ||
| 117 | // 'mappedBy' field. | ||
| 118 | $criteria = new Criteria(); | ||
| 119 | |||
| 120 | $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
| 121 | $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key)); | ||
| 122 | |||
| 123 | return (bool) $persister->count($criteria); | ||
| 124 | } | ||
| 125 | |||
| 126 | public function contains(PersistentCollection $collection, object $element): bool | ||
| 127 | { | ||
| 128 | if (! $this->isValidEntityState($element)) { | ||
| 129 | return false; | ||
| 130 | } | ||
| 131 | |||
| 132 | $mapping = $this->getMapping($collection); | ||
| 133 | $persister = $this->uow->getEntityPersister($mapping->targetEntity); | ||
| 134 | |||
| 135 | // only works with single id identifier entities. Will throw an | ||
| 136 | // exception in Entity Persisters if that is not the case for the | ||
| 137 | // 'mappedBy' field. | ||
| 138 | $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); | ||
| 139 | |||
| 140 | return $persister->exists($element, $criteria); | ||
| 141 | } | ||
| 142 | |||
| 143 | /** | ||
| 144 | * {@inheritDoc} | ||
| 145 | */ | ||
| 146 | public function loadCriteria(PersistentCollection $collection, Criteria $criteria): array | ||
| 147 | { | ||
| 148 | throw new BadMethodCallException('Filtering a collection by Criteria is not supported by this CollectionPersister.'); | ||
| 149 | } | ||
| 150 | |||
| 151 | /** | ||
| 152 | * @throws DBALException | ||
| 153 | * @throws EntityNotFoundException | ||
| 154 | * @throws MappingException | ||
| 155 | */ | ||
| 156 | private function deleteEntityCollection(PersistentCollection $collection): int | ||
| 157 | { | ||
| 158 | $mapping = $this->getMapping($collection); | ||
| 159 | $identifier = $this->uow->getEntityIdentifier($collection->getOwner()); | ||
| 160 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 161 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 162 | $columns = []; | ||
| 163 | $parameters = []; | ||
| 164 | $types = []; | ||
| 165 | |||
| 166 | foreach ($this->em->getMetadataFactory()->getOwningSide($mapping)->joinColumns as $joinColumn) { | ||
| 167 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $targetClass, $this->platform); | ||
| 168 | $parameters[] = $identifier[$sourceClass->getFieldForColumn($joinColumn->referencedColumnName)]; | ||
| 169 | $types[] = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $sourceClass, $this->em); | ||
| 170 | } | ||
| 171 | |||
| 172 | $statement = 'DELETE FROM ' . $this->quoteStrategy->getTableName($targetClass, $this->platform) | ||
| 173 | . ' WHERE ' . implode(' = ? AND ', $columns) . ' = ?'; | ||
| 174 | |||
| 175 | if ($targetClass->isInheritanceTypeSingleTable()) { | ||
| 176 | $discriminatorColumn = $targetClass->getDiscriminatorColumn(); | ||
| 177 | $statement .= ' AND ' . $discriminatorColumn->name . ' = ?'; | ||
| 178 | $parameters[] = $targetClass->discriminatorValue; | ||
| 179 | $types[] = $discriminatorColumn->type; | ||
| 180 | } | ||
| 181 | |||
| 182 | $numAffected = $this->conn->executeStatement($statement, $parameters, $types); | ||
| 183 | |||
| 184 | assert(is_int($numAffected)); | ||
| 185 | |||
| 186 | return $numAffected; | ||
| 187 | } | ||
| 188 | |||
| 189 | /** | ||
| 190 | * Delete Class Table Inheritance entities. | ||
| 191 | * A temporary table is needed to keep IDs to be deleted in both parent and child class' tables. | ||
| 192 | * | ||
| 193 | * Thanks Steve Ebersole (Hibernate) for idea on how to tackle reliably this scenario, we owe him a beer! =) | ||
| 194 | * | ||
| 195 | * @throws DBALException | ||
| 196 | */ | ||
| 197 | private function deleteJoinedEntityCollection(PersistentCollection $collection): int | ||
| 198 | { | ||
| 199 | $mapping = $this->getMapping($collection); | ||
| 200 | $sourceClass = $this->em->getClassMetadata($mapping->sourceEntity); | ||
| 201 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 202 | $rootClass = $this->em->getClassMetadata($targetClass->rootEntityName); | ||
| 203 | |||
| 204 | // 1) Build temporary table DDL | ||
| 205 | $tempTable = $this->platform->getTemporaryTableName($rootClass->getTemporaryIdTableName()); | ||
| 206 | $idColumnNames = $rootClass->getIdentifierColumnNames(); | ||
| 207 | $idColumnList = implode(', ', $idColumnNames); | ||
| 208 | $columnDefinitions = []; | ||
| 209 | |||
| 210 | foreach ($idColumnNames as $idColumnName) { | ||
| 211 | $columnDefinitions[$idColumnName] = [ | ||
| 212 | 'name' => $idColumnName, | ||
| 213 | 'notnull' => true, | ||
| 214 | 'type' => Type::getType(PersisterHelper::getTypeOfColumn($idColumnName, $rootClass, $this->em)), | ||
| 215 | ]; | ||
| 216 | } | ||
| 217 | |||
| 218 | $statement = $this->platform->getCreateTemporaryTableSnippetSQL() . ' ' . $tempTable | ||
| 219 | . ' (' . $this->platform->getColumnDeclarationListSQL($columnDefinitions) . ')'; | ||
| 220 | |||
| 221 | $this->conn->executeStatement($statement); | ||
| 222 | |||
| 223 | // 2) Build insert table records into temporary table | ||
| 224 | $query = $this->em->createQuery( | ||
| 225 | ' SELECT t0.' . implode(', t0.', $rootClass->getIdentifierFieldNames()) | ||
| 226 | . ' FROM ' . $targetClass->name . ' t0 WHERE t0.' . $mapping->mappedBy . ' = :owner', | ||
| 227 | )->setParameter('owner', $collection->getOwner()); | ||
| 228 | |||
| 229 | $sql = $query->getSQL(); | ||
| 230 | assert(is_string($sql)); | ||
| 231 | $statement = 'INSERT INTO ' . $tempTable . ' (' . $idColumnList . ') ' . $sql; | ||
| 232 | $parameters = array_values($sourceClass->getIdentifierValues($collection->getOwner())); | ||
| 233 | $numDeleted = $this->conn->executeStatement($statement, $parameters); | ||
| 234 | |||
| 235 | // 3) Delete records on each table in the hierarchy | ||
| 236 | $classNames = [...$targetClass->parentClasses, ...[$targetClass->name], ...$targetClass->subClasses]; | ||
| 237 | |||
| 238 | foreach (array_reverse($classNames) as $className) { | ||
| 239 | $tableName = $this->quoteStrategy->getTableName($this->em->getClassMetadata($className), $this->platform); | ||
| 240 | $statement = 'DELETE FROM ' . $tableName . ' WHERE (' . $idColumnList . ')' | ||
| 241 | . ' IN (SELECT ' . $idColumnList . ' FROM ' . $tempTable . ')'; | ||
| 242 | |||
| 243 | $this->conn->executeStatement($statement); | ||
| 244 | } | ||
| 245 | |||
| 246 | // 4) Drop temporary table | ||
| 247 | $statement = $this->platform->getDropTemporaryTableSQL($tempTable); | ||
| 248 | |||
| 249 | $this->conn->executeStatement($statement); | ||
| 250 | |||
| 251 | assert(is_int($numDeleted)); | ||
| 252 | |||
| 253 | return $numDeleted; | ||
| 254 | } | ||
| 255 | |||
| 256 | private function getMapping(PersistentCollection $collection): OneToManyAssociationMapping | ||
| 257 | { | ||
| 258 | $mapping = $collection->getMapping(); | ||
| 259 | |||
| 260 | assert($mapping->isOneToMany()); | ||
| 261 | |||
| 262 | return $mapping; | ||
| 263 | } | ||
| 264 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php b/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php new file mode 100644 index 0000000..cf8a74e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/AbstractEntityInheritancePersister.php | |||
| @@ -0,0 +1,66 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\DBAL\Types\Type; | ||
| 8 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Base class for entity persisters that implement a certain inheritance mapping strategy. | ||
| 14 | * All these persisters are assumed to use a discriminator column to discriminate entity | ||
| 15 | * types in the hierarchy. | ||
| 16 | */ | ||
| 17 | abstract class AbstractEntityInheritancePersister extends BasicEntityPersister | ||
| 18 | { | ||
| 19 | /** | ||
| 20 | * {@inheritDoc} | ||
| 21 | */ | ||
| 22 | protected function prepareInsertData(object $entity): array | ||
| 23 | { | ||
| 24 | $data = parent::prepareInsertData($entity); | ||
| 25 | |||
| 26 | // Populate the discriminator column | ||
| 27 | $discColumn = $this->class->getDiscriminatorColumn(); | ||
| 28 | $this->columnTypes[$discColumn->name] = $discColumn->type; | ||
| 29 | $data[$this->getDiscriminatorColumnTableName()][$discColumn->name] = $this->class->discriminatorValue; | ||
| 30 | |||
| 31 | return $data; | ||
| 32 | } | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Gets the name of the table that contains the discriminator column. | ||
| 36 | */ | ||
| 37 | abstract protected function getDiscriminatorColumnTableName(): string; | ||
| 38 | |||
| 39 | protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string | ||
| 40 | { | ||
| 41 | $tableAlias = $alias === 'r' ? '' : $alias; | ||
| 42 | $fieldMapping = $class->fieldMappings[$field]; | ||
| 43 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
| 44 | $sql = sprintf( | ||
| 45 | '%s.%s', | ||
| 46 | $this->getSQLTableAlias($class->name, $tableAlias), | ||
| 47 | $this->quoteStrategy->getColumnName($field, $class, $this->platform), | ||
| 48 | ); | ||
| 49 | |||
| 50 | $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->name); | ||
| 51 | |||
| 52 | $type = Type::getType($fieldMapping->type); | ||
| 53 | $sql = $type->convertToPHPValueSQL($sql, $this->platform); | ||
| 54 | |||
| 55 | return $sql . ' AS ' . $columnAlias; | ||
| 56 | } | ||
| 57 | |||
| 58 | protected function getSelectJoinColumnSQL(string $tableAlias, string $joinColumnName, string $quotedColumnName, string $type): string | ||
| 59 | { | ||
| 60 | $columnAlias = $this->getSQLColumnAlias($joinColumnName); | ||
| 61 | |||
| 62 | $this->currentPersisterContext->rsm->addMetaResult('r', $columnAlias, $joinColumnName, false, $type); | ||
| 63 | |||
| 64 | return $tableAlias . '.' . $quotedColumnName . ' AS ' . $columnAlias; | ||
| 65 | } | ||
| 66 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php new file mode 100644 index 0000000..377e03c --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/BasicEntityPersister.php | |||
| @@ -0,0 +1,2085 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use BackedEnum; | ||
| 8 | use Doctrine\Common\Collections\Criteria; | ||
| 9 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 10 | use Doctrine\Common\Collections\Order; | ||
| 11 | use Doctrine\DBAL\ArrayParameterType; | ||
| 12 | use Doctrine\DBAL\Connection; | ||
| 13 | use Doctrine\DBAL\LockMode; | ||
| 14 | use Doctrine\DBAL\ParameterType; | ||
| 15 | use Doctrine\DBAL\Platforms\AbstractPlatform; | ||
| 16 | use Doctrine\DBAL\Result; | ||
| 17 | use Doctrine\DBAL\Types\Type; | ||
| 18 | use Doctrine\DBAL\Types\Types; | ||
| 19 | use Doctrine\ORM\EntityManagerInterface; | ||
| 20 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 21 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 22 | use Doctrine\ORM\Mapping\JoinColumnMapping; | ||
| 23 | use Doctrine\ORM\Mapping\ManyToManyAssociationMapping; | ||
| 24 | use Doctrine\ORM\Mapping\MappingException; | ||
| 25 | use Doctrine\ORM\Mapping\OneToManyAssociationMapping; | ||
| 26 | use Doctrine\ORM\Mapping\QuoteStrategy; | ||
| 27 | use Doctrine\ORM\OptimisticLockException; | ||
| 28 | use Doctrine\ORM\PersistentCollection; | ||
| 29 | use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys; | ||
| 30 | use Doctrine\ORM\Persisters\Exception\InvalidOrientation; | ||
| 31 | use Doctrine\ORM\Persisters\Exception\UnrecognizedField; | ||
| 32 | use Doctrine\ORM\Persisters\SqlExpressionVisitor; | ||
| 33 | use Doctrine\ORM\Persisters\SqlValueVisitor; | ||
| 34 | use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; | ||
| 35 | use Doctrine\ORM\Query; | ||
| 36 | use Doctrine\ORM\Query\QueryException; | ||
| 37 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 38 | use Doctrine\ORM\Repository\Exception\InvalidFindByCall; | ||
| 39 | use Doctrine\ORM\UnitOfWork; | ||
| 40 | use Doctrine\ORM\Utility\IdentifierFlattener; | ||
| 41 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
| 42 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 43 | use LengthException; | ||
| 44 | |||
| 45 | use function array_combine; | ||
| 46 | use function array_keys; | ||
| 47 | use function array_map; | ||
| 48 | use function array_merge; | ||
| 49 | use function array_search; | ||
| 50 | use function array_unique; | ||
| 51 | use function array_values; | ||
| 52 | use function assert; | ||
| 53 | use function count; | ||
| 54 | use function implode; | ||
| 55 | use function is_array; | ||
| 56 | use function is_object; | ||
| 57 | use function reset; | ||
| 58 | use function spl_object_id; | ||
| 59 | use function sprintf; | ||
| 60 | use function str_contains; | ||
| 61 | use function strtoupper; | ||
| 62 | use function trim; | ||
| 63 | |||
| 64 | /** | ||
| 65 | * A BasicEntityPersister maps an entity to a single table in a relational database. | ||
| 66 | * | ||
| 67 | * A persister is always responsible for a single entity type. | ||
| 68 | * | ||
| 69 | * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent | ||
| 70 | * state of entities onto a relational database when the UnitOfWork is committed, | ||
| 71 | * as well as for basic querying of entities and their associations (not DQL). | ||
| 72 | * | ||
| 73 | * The persisting operations that are invoked during a commit of a UnitOfWork to | ||
| 74 | * persist the persistent entity state are: | ||
| 75 | * | ||
| 76 | * - {@link addInsert} : To schedule an entity for insertion. | ||
| 77 | * - {@link executeInserts} : To execute all scheduled insertions. | ||
| 78 | * - {@link update} : To update the persistent state of an entity. | ||
| 79 | * - {@link delete} : To delete the persistent state of an entity. | ||
| 80 | * | ||
| 81 | * As can be seen from the above list, insertions are batched and executed all at once | ||
| 82 | * for increased efficiency. | ||
| 83 | * | ||
| 84 | * The querying operations invoked during a UnitOfWork, either through direct find | ||
| 85 | * requests or lazy-loading, are the following: | ||
| 86 | * | ||
| 87 | * - {@link load} : Loads (the state of) a single, managed entity. | ||
| 88 | * - {@link loadAll} : Loads multiple, managed entities. | ||
| 89 | * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading). | ||
| 90 | * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading). | ||
| 91 | * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading). | ||
| 92 | * | ||
| 93 | * The BasicEntityPersister implementation provides the default behavior for | ||
| 94 | * persisting and querying entities that are mapped to a single database table. | ||
| 95 | * | ||
| 96 | * Subclasses can be created to provide custom persisting and querying strategies, | ||
| 97 | * i.e. spanning multiple tables. | ||
| 98 | */ | ||
| 99 | class BasicEntityPersister implements EntityPersister | ||
| 100 | { | ||
| 101 | use LockSqlHelper; | ||
| 102 | |||
| 103 | /** @var array<string,string> */ | ||
| 104 | private static array $comparisonMap = [ | ||
| 105 | Comparison::EQ => '= %s', | ||
| 106 | Comparison::NEQ => '!= %s', | ||
| 107 | Comparison::GT => '> %s', | ||
| 108 | Comparison::GTE => '>= %s', | ||
| 109 | Comparison::LT => '< %s', | ||
| 110 | Comparison::LTE => '<= %s', | ||
| 111 | Comparison::IN => 'IN (%s)', | ||
| 112 | Comparison::NIN => 'NOT IN (%s)', | ||
| 113 | Comparison::CONTAINS => 'LIKE %s', | ||
| 114 | Comparison::STARTS_WITH => 'LIKE %s', | ||
| 115 | Comparison::ENDS_WITH => 'LIKE %s', | ||
| 116 | ]; | ||
| 117 | |||
| 118 | /** | ||
| 119 | * The underlying DBAL Connection of the used EntityManager. | ||
| 120 | */ | ||
| 121 | protected Connection $conn; | ||
| 122 | |||
| 123 | /** | ||
| 124 | * The database platform. | ||
| 125 | */ | ||
| 126 | protected AbstractPlatform $platform; | ||
| 127 | |||
| 128 | /** | ||
| 129 | * Queued inserts. | ||
| 130 | * | ||
| 131 | * @psalm-var array<int, object> | ||
| 132 | */ | ||
| 133 | protected array $queuedInserts = []; | ||
| 134 | |||
| 135 | /** | ||
| 136 | * The map of column names to DBAL mapping types of all prepared columns used | ||
| 137 | * when INSERTing or UPDATEing an entity. | ||
| 138 | * | ||
| 139 | * @see prepareInsertData($entity) | ||
| 140 | * @see prepareUpdateData($entity) | ||
| 141 | * | ||
| 142 | * @var mixed[] | ||
| 143 | */ | ||
| 144 | protected array $columnTypes = []; | ||
| 145 | |||
| 146 | /** | ||
| 147 | * The map of quoted column names. | ||
| 148 | * | ||
| 149 | * @see prepareInsertData($entity) | ||
| 150 | * @see prepareUpdateData($entity) | ||
| 151 | * | ||
| 152 | * @var mixed[] | ||
| 153 | */ | ||
| 154 | protected array $quotedColumns = []; | ||
| 155 | |||
| 156 | /** | ||
| 157 | * The INSERT SQL statement used for entities handled by this persister. | ||
| 158 | * This SQL is only generated once per request, if at all. | ||
| 159 | */ | ||
| 160 | private string|null $insertSql = null; | ||
| 161 | |||
| 162 | /** | ||
| 163 | * The quote strategy. | ||
| 164 | */ | ||
| 165 | protected QuoteStrategy $quoteStrategy; | ||
| 166 | |||
| 167 | /** | ||
| 168 | * The IdentifierFlattener used for manipulating identifiers | ||
| 169 | */ | ||
| 170 | protected readonly IdentifierFlattener $identifierFlattener; | ||
| 171 | |||
| 172 | protected CachedPersisterContext $currentPersisterContext; | ||
| 173 | private readonly CachedPersisterContext $limitsHandlingContext; | ||
| 174 | private readonly CachedPersisterContext $noLimitsContext; | ||
| 175 | |||
| 176 | /** | ||
| 177 | * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager | ||
| 178 | * and persists instances of the class described by the given ClassMetadata descriptor. | ||
| 179 | * | ||
| 180 | * @param ClassMetadata $class Metadata object that describes the mapping of the mapped entity class. | ||
| 181 | */ | ||
| 182 | public function __construct( | ||
| 183 | protected EntityManagerInterface $em, | ||
| 184 | protected ClassMetadata $class, | ||
| 185 | ) { | ||
| 186 | $this->conn = $em->getConnection(); | ||
| 187 | $this->platform = $this->conn->getDatabasePlatform(); | ||
| 188 | $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy(); | ||
| 189 | $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory()); | ||
| 190 | $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext( | ||
| 191 | $class, | ||
| 192 | new Query\ResultSetMapping(), | ||
| 193 | false, | ||
| 194 | ); | ||
| 195 | $this->limitsHandlingContext = new CachedPersisterContext( | ||
| 196 | $class, | ||
| 197 | new Query\ResultSetMapping(), | ||
| 198 | true, | ||
| 199 | ); | ||
| 200 | } | ||
| 201 | |||
| 202 | public function getClassMetadata(): ClassMetadata | ||
| 203 | { | ||
| 204 | return $this->class; | ||
| 205 | } | ||
| 206 | |||
| 207 | public function getResultSetMapping(): ResultSetMapping | ||
| 208 | { | ||
| 209 | return $this->currentPersisterContext->rsm; | ||
| 210 | } | ||
| 211 | |||
| 212 | public function addInsert(object $entity): void | ||
| 213 | { | ||
| 214 | $this->queuedInserts[spl_object_id($entity)] = $entity; | ||
| 215 | } | ||
| 216 | |||
| 217 | /** | ||
| 218 | * {@inheritDoc} | ||
| 219 | */ | ||
| 220 | public function getInserts(): array | ||
| 221 | { | ||
| 222 | return $this->queuedInserts; | ||
| 223 | } | ||
| 224 | |||
| 225 | public function executeInserts(): void | ||
| 226 | { | ||
| 227 | if (! $this->queuedInserts) { | ||
| 228 | return; | ||
| 229 | } | ||
| 230 | |||
| 231 | $uow = $this->em->getUnitOfWork(); | ||
| 232 | $idGenerator = $this->class->idGenerator; | ||
| 233 | $isPostInsertId = $idGenerator->isPostInsertGenerator(); | ||
| 234 | |||
| 235 | $stmt = $this->conn->prepare($this->getInsertSQL()); | ||
| 236 | $tableName = $this->class->getTableName(); | ||
| 237 | |||
| 238 | foreach ($this->queuedInserts as $key => $entity) { | ||
| 239 | $insertData = $this->prepareInsertData($entity); | ||
| 240 | |||
| 241 | if (isset($insertData[$tableName])) { | ||
| 242 | $paramIndex = 1; | ||
| 243 | |||
| 244 | foreach ($insertData[$tableName] as $column => $value) { | ||
| 245 | $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]); | ||
| 246 | } | ||
| 247 | } | ||
| 248 | |||
| 249 | $stmt->executeStatement(); | ||
| 250 | |||
| 251 | if ($isPostInsertId) { | ||
| 252 | $generatedId = $idGenerator->generateId($this->em, $entity); | ||
| 253 | $id = [$this->class->identifier[0] => $generatedId]; | ||
| 254 | |||
| 255 | $uow->assignPostInsertId($entity, $generatedId); | ||
| 256 | } else { | ||
| 257 | $id = $this->class->getIdentifierValues($entity); | ||
| 258 | } | ||
| 259 | |||
| 260 | if ($this->class->requiresFetchAfterChange) { | ||
| 261 | $this->assignDefaultVersionAndUpsertableValues($entity, $id); | ||
| 262 | } | ||
| 263 | |||
| 264 | // Unset this queued insert, so that the prepareUpdateData() method knows right away | ||
| 265 | // (for the next entity already) that the current entity has been written to the database | ||
| 266 | // and no extra updates need to be scheduled to refer to it. | ||
| 267 | // | ||
| 268 | // In \Doctrine\ORM\UnitOfWork::executeInserts(), the UoW already removed entities | ||
| 269 | // from its own list (\Doctrine\ORM\UnitOfWork::$entityInsertions) right after they | ||
| 270 | // were given to our addInsert() method. | ||
| 271 | unset($this->queuedInserts[$key]); | ||
| 272 | } | ||
| 273 | } | ||
| 274 | |||
| 275 | /** | ||
| 276 | * Retrieves the default version value which was created | ||
| 277 | * by the preceding INSERT statement and assigns it back in to the | ||
| 278 | * entities version field if the given entity is versioned. | ||
| 279 | * Also retrieves values of columns marked as 'non insertable' and / or | ||
| 280 | * 'not updatable' and assigns them back to the entities corresponding fields. | ||
| 281 | * | ||
| 282 | * @param mixed[] $id | ||
| 283 | */ | ||
| 284 | protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void | ||
| 285 | { | ||
| 286 | $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id); | ||
| 287 | |||
| 288 | foreach ($values as $field => $value) { | ||
| 289 | $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform); | ||
| 290 | |||
| 291 | $this->class->setFieldValue($entity, $field, $value); | ||
| 292 | } | ||
| 293 | } | ||
| 294 | |||
| 295 | /** | ||
| 296 | * Fetches the current version value of a versioned entity and / or the values of fields | ||
| 297 | * marked as 'not insertable' and / or 'not updatable'. | ||
| 298 | * | ||
| 299 | * @param mixed[] $id | ||
| 300 | */ | ||
| 301 | protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed | ||
| 302 | { | ||
| 303 | $columnNames = []; | ||
| 304 | foreach ($this->class->fieldMappings as $key => $column) { | ||
| 305 | if (isset($column->generated) || ($this->class->isVersioned && $key === $versionedClass->versionField)) { | ||
| 306 | $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform); | ||
| 307 | } | ||
| 308 | } | ||
| 309 | |||
| 310 | $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); | ||
| 311 | $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); | ||
| 312 | |||
| 313 | // FIXME: Order with composite keys might not be correct | ||
| 314 | $sql = 'SELECT ' . implode(', ', $columnNames) | ||
| 315 | . ' FROM ' . $tableName | ||
| 316 | . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; | ||
| 317 | |||
| 318 | $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); | ||
| 319 | |||
| 320 | $values = $this->conn->fetchNumeric( | ||
| 321 | $sql, | ||
| 322 | array_values($flatId), | ||
| 323 | $this->extractIdentifierTypes($id, $versionedClass), | ||
| 324 | ); | ||
| 325 | |||
| 326 | if ($values === false) { | ||
| 327 | throw new LengthException('Unexpected empty result for database query.'); | ||
| 328 | } | ||
| 329 | |||
| 330 | $values = array_combine(array_keys($columnNames), $values); | ||
| 331 | |||
| 332 | if (! $values) { | ||
| 333 | throw new LengthException('Unexpected number of database columns.'); | ||
| 334 | } | ||
| 335 | |||
| 336 | return $values; | ||
| 337 | } | ||
| 338 | |||
| 339 | /** | ||
| 340 | * @param mixed[] $id | ||
| 341 | * | ||
| 342 | * @return list<ParameterType|int|string> | ||
| 343 | * @psalm-return list<ParameterType::*|ArrayParameterType::*|string> | ||
| 344 | */ | ||
| 345 | final protected function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array | ||
| 346 | { | ||
| 347 | $types = []; | ||
| 348 | |||
| 349 | foreach ($id as $field => $value) { | ||
| 350 | $types = [...$types, ...$this->getTypes($field, $value, $versionedClass)]; | ||
| 351 | } | ||
| 352 | |||
| 353 | return $types; | ||
| 354 | } | ||
| 355 | |||
| 356 | public function update(object $entity): void | ||
| 357 | { | ||
| 358 | $tableName = $this->class->getTableName(); | ||
| 359 | $updateData = $this->prepareUpdateData($entity); | ||
| 360 | |||
| 361 | if (! isset($updateData[$tableName])) { | ||
| 362 | return; | ||
| 363 | } | ||
| 364 | |||
| 365 | $data = $updateData[$tableName]; | ||
| 366 | |||
| 367 | if (! $data) { | ||
| 368 | return; | ||
| 369 | } | ||
| 370 | |||
| 371 | $isVersioned = $this->class->isVersioned; | ||
| 372 | $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 373 | |||
| 374 | $this->updateTable($entity, $quotedTableName, $data, $isVersioned); | ||
| 375 | |||
| 376 | if ($this->class->requiresFetchAfterChange) { | ||
| 377 | $id = $this->class->getIdentifierValues($entity); | ||
| 378 | |||
| 379 | $this->assignDefaultVersionAndUpsertableValues($entity, $id); | ||
| 380 | } | ||
| 381 | } | ||
| 382 | |||
| 383 | /** | ||
| 384 | * Performs an UPDATE statement for an entity on a specific table. | ||
| 385 | * The UPDATE can optionally be versioned, which requires the entity to have a version field. | ||
| 386 | * | ||
| 387 | * @param object $entity The entity object being updated. | ||
| 388 | * @param string $quotedTableName The quoted name of the table to apply the UPDATE on. | ||
| 389 | * @param mixed[] $updateData The map of columns to update (column => value). | ||
| 390 | * @param bool $versioned Whether the UPDATE should be versioned. | ||
| 391 | * | ||
| 392 | * @throws UnrecognizedField | ||
| 393 | * @throws OptimisticLockException | ||
| 394 | */ | ||
| 395 | final protected function updateTable( | ||
| 396 | object $entity, | ||
| 397 | string $quotedTableName, | ||
| 398 | array $updateData, | ||
| 399 | bool $versioned = false, | ||
| 400 | ): void { | ||
| 401 | $set = []; | ||
| 402 | $types = []; | ||
| 403 | $params = []; | ||
| 404 | |||
| 405 | foreach ($updateData as $columnName => $value) { | ||
| 406 | $placeholder = '?'; | ||
| 407 | $column = $columnName; | ||
| 408 | |||
| 409 | switch (true) { | ||
| 410 | case isset($this->class->fieldNames[$columnName]): | ||
| 411 | $fieldName = $this->class->fieldNames[$columnName]; | ||
| 412 | $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); | ||
| 413 | |||
| 414 | if (isset($this->class->fieldMappings[$fieldName])) { | ||
| 415 | $type = Type::getType($this->columnTypes[$columnName]); | ||
| 416 | $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); | ||
| 417 | } | ||
| 418 | |||
| 419 | break; | ||
| 420 | |||
| 421 | case isset($this->quotedColumns[$columnName]): | ||
| 422 | $column = $this->quotedColumns[$columnName]; | ||
| 423 | |||
| 424 | break; | ||
| 425 | } | ||
| 426 | |||
| 427 | $params[] = $value; | ||
| 428 | $set[] = $column . ' = ' . $placeholder; | ||
| 429 | $types[] = $this->columnTypes[$columnName]; | ||
| 430 | } | ||
| 431 | |||
| 432 | $where = []; | ||
| 433 | $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 434 | |||
| 435 | foreach ($this->class->identifier as $idField) { | ||
| 436 | if (! isset($this->class->associationMappings[$idField])) { | ||
| 437 | $params[] = $identifier[$idField]; | ||
| 438 | $types[] = $this->class->fieldMappings[$idField]->type; | ||
| 439 | $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform); | ||
| 440 | |||
| 441 | continue; | ||
| 442 | } | ||
| 443 | |||
| 444 | assert($this->class->associationMappings[$idField]->isToOneOwningSide()); | ||
| 445 | |||
| 446 | $params[] = $identifier[$idField]; | ||
| 447 | $where[] = $this->quoteStrategy->getJoinColumnName( | ||
| 448 | $this->class->associationMappings[$idField]->joinColumns[0], | ||
| 449 | $this->class, | ||
| 450 | $this->platform, | ||
| 451 | ); | ||
| 452 | |||
| 453 | $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]->targetEntity); | ||
| 454 | $targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em); | ||
| 455 | |||
| 456 | if ($targetType === []) { | ||
| 457 | throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]); | ||
| 458 | } | ||
| 459 | |||
| 460 | $types[] = reset($targetType); | ||
| 461 | } | ||
| 462 | |||
| 463 | if ($versioned) { | ||
| 464 | $versionField = $this->class->versionField; | ||
| 465 | assert($versionField !== null); | ||
| 466 | $versionFieldType = $this->class->fieldMappings[$versionField]->type; | ||
| 467 | $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform); | ||
| 468 | |||
| 469 | $where[] = $versionColumn; | ||
| 470 | $types[] = $this->class->fieldMappings[$versionField]->type; | ||
| 471 | $params[] = $this->class->reflFields[$versionField]->getValue($entity); | ||
| 472 | |||
| 473 | switch ($versionFieldType) { | ||
| 474 | case Types::SMALLINT: | ||
| 475 | case Types::INTEGER: | ||
| 476 | case Types::BIGINT: | ||
| 477 | $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1'; | ||
| 478 | break; | ||
| 479 | |||
| 480 | case Types::DATETIME_MUTABLE: | ||
| 481 | $set[] = $versionColumn . ' = CURRENT_TIMESTAMP'; | ||
| 482 | break; | ||
| 483 | } | ||
| 484 | } | ||
| 485 | |||
| 486 | $sql = 'UPDATE ' . $quotedTableName | ||
| 487 | . ' SET ' . implode(', ', $set) | ||
| 488 | . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?'; | ||
| 489 | |||
| 490 | $result = $this->conn->executeStatement($sql, $params, $types); | ||
| 491 | |||
| 492 | if ($versioned && ! $result) { | ||
| 493 | throw OptimisticLockException::lockFailed($entity); | ||
| 494 | } | ||
| 495 | } | ||
| 496 | |||
| 497 | /** | ||
| 498 | * @param array<mixed> $identifier | ||
| 499 | * @param string[] $types | ||
| 500 | * | ||
| 501 | * @todo Add check for platform if it supports foreign keys/cascading. | ||
| 502 | */ | ||
| 503 | protected function deleteJoinTableRecords(array $identifier, array $types): void | ||
| 504 | { | ||
| 505 | foreach ($this->class->associationMappings as $mapping) { | ||
| 506 | if (! $mapping->isManyToMany() || $mapping->isOnDeleteCascade) { | ||
| 507 | continue; | ||
| 508 | } | ||
| 509 | |||
| 510 | // @Todo this only covers scenarios with no inheritance or of the same level. Is there something | ||
| 511 | // like self-referential relationship between different levels of an inheritance hierarchy? I hope not! | ||
| 512 | $selfReferential = ($mapping->targetEntity === $mapping->sourceEntity); | ||
| 513 | $class = $this->class; | ||
| 514 | $association = $mapping; | ||
| 515 | $otherColumns = []; | ||
| 516 | $otherKeys = []; | ||
| 517 | $keys = []; | ||
| 518 | |||
| 519 | if (! $mapping->isOwningSide()) { | ||
| 520 | $class = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 521 | } | ||
| 522 | |||
| 523 | $association = $this->em->getMetadataFactory()->getOwningSide($association); | ||
| 524 | $joinColumns = $mapping->isOwningSide() | ||
| 525 | ? $association->joinTable->joinColumns | ||
| 526 | : $association->joinTable->inverseJoinColumns; | ||
| 527 | |||
| 528 | if ($selfReferential) { | ||
| 529 | $otherColumns = ! $mapping->isOwningSide() | ||
| 530 | ? $association->joinTable->joinColumns | ||
| 531 | : $association->joinTable->inverseJoinColumns; | ||
| 532 | } | ||
| 533 | |||
| 534 | foreach ($joinColumns as $joinColumn) { | ||
| 535 | $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 536 | } | ||
| 537 | |||
| 538 | foreach ($otherColumns as $joinColumn) { | ||
| 539 | $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 540 | } | ||
| 541 | |||
| 542 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform); | ||
| 543 | |||
| 544 | $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types); | ||
| 545 | |||
| 546 | if ($selfReferential) { | ||
| 547 | $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types); | ||
| 548 | } | ||
| 549 | } | ||
| 550 | } | ||
| 551 | |||
| 552 | public function delete(object $entity): bool | ||
| 553 | { | ||
| 554 | $class = $this->class; | ||
| 555 | $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 556 | $tableName = $this->quoteStrategy->getTableName($class, $this->platform); | ||
| 557 | $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform); | ||
| 558 | $id = array_combine($idColumns, $identifier); | ||
| 559 | $types = $this->getClassIdentifiersTypes($class); | ||
| 560 | |||
| 561 | $this->deleteJoinTableRecords($identifier, $types); | ||
| 562 | |||
| 563 | return (bool) $this->conn->delete($tableName, $id, $types); | ||
| 564 | } | ||
| 565 | |||
| 566 | /** | ||
| 567 | * Prepares the changeset of an entity for database insertion (UPDATE). | ||
| 568 | * | ||
| 569 | * The changeset is obtained from the currently running UnitOfWork. | ||
| 570 | * | ||
| 571 | * During this preparation the array that is passed as the second parameter is filled with | ||
| 572 | * <columnName> => <value> pairs, grouped by table name. | ||
| 573 | * | ||
| 574 | * Example: | ||
| 575 | * <code> | ||
| 576 | * array( | ||
| 577 | * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...), | ||
| 578 | * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...), | ||
| 579 | * ... | ||
| 580 | * ) | ||
| 581 | * </code> | ||
| 582 | * | ||
| 583 | * @param object $entity The entity for which to prepare the data. | ||
| 584 | * @param bool $isInsert Whether the data to be prepared refers to an insert statement. | ||
| 585 | * | ||
| 586 | * @return mixed[][] The prepared data. | ||
| 587 | * @psalm-return array<string, array<array-key, mixed|null>> | ||
| 588 | */ | ||
| 589 | protected function prepareUpdateData(object $entity, bool $isInsert = false): array | ||
| 590 | { | ||
| 591 | $versionField = null; | ||
| 592 | $result = []; | ||
| 593 | $uow = $this->em->getUnitOfWork(); | ||
| 594 | |||
| 595 | $versioned = $this->class->isVersioned; | ||
| 596 | if ($versioned !== false) { | ||
| 597 | $versionField = $this->class->versionField; | ||
| 598 | } | ||
| 599 | |||
| 600 | foreach ($uow->getEntityChangeSet($entity) as $field => $change) { | ||
| 601 | if (isset($versionField) && $versionField === $field) { | ||
| 602 | continue; | ||
| 603 | } | ||
| 604 | |||
| 605 | if (isset($this->class->embeddedClasses[$field])) { | ||
| 606 | continue; | ||
| 607 | } | ||
| 608 | |||
| 609 | $newVal = $change[1]; | ||
| 610 | |||
| 611 | if (! isset($this->class->associationMappings[$field])) { | ||
| 612 | $fieldMapping = $this->class->fieldMappings[$field]; | ||
| 613 | $columnName = $fieldMapping->columnName; | ||
| 614 | |||
| 615 | if (! $isInsert && isset($fieldMapping->notUpdatable)) { | ||
| 616 | continue; | ||
| 617 | } | ||
| 618 | |||
| 619 | if ($isInsert && isset($fieldMapping->notInsertable)) { | ||
| 620 | continue; | ||
| 621 | } | ||
| 622 | |||
| 623 | $this->columnTypes[$columnName] = $fieldMapping->type; | ||
| 624 | |||
| 625 | $result[$this->getOwningTable($field)][$columnName] = $newVal; | ||
| 626 | |||
| 627 | continue; | ||
| 628 | } | ||
| 629 | |||
| 630 | $assoc = $this->class->associationMappings[$field]; | ||
| 631 | |||
| 632 | // Only owning side of x-1 associations can have a FK column. | ||
| 633 | if (! $assoc->isToOneOwningSide()) { | ||
| 634 | continue; | ||
| 635 | } | ||
| 636 | |||
| 637 | if ($newVal !== null) { | ||
| 638 | $oid = spl_object_id($newVal); | ||
| 639 | |||
| 640 | // If the associated entity $newVal is not yet persisted and/or does not yet have | ||
| 641 | // an ID assigned, we must set $newVal = null. This will insert a null value and | ||
| 642 | // schedule an extra update on the UnitOfWork. | ||
| 643 | // | ||
| 644 | // This gives us extra time to a) possibly obtain a database-generated identifier | ||
| 645 | // value for $newVal, and b) insert $newVal into the database before the foreign | ||
| 646 | // key reference is being made. | ||
| 647 | // | ||
| 648 | // When looking at $this->queuedInserts and $uow->isScheduledForInsert, be aware | ||
| 649 | // of the implementation details that our own executeInserts() method will remove | ||
| 650 | // entities from the former as soon as the insert statement has been executed and | ||
| 651 | // a post-insert ID has been assigned (if necessary), and that the UnitOfWork has | ||
| 652 | // already removed entities from its own list at the time they were passed to our | ||
| 653 | // addInsert() method. | ||
| 654 | // | ||
| 655 | // Then, there is one extra exception we can make: An entity that references back to itself | ||
| 656 | // _and_ uses an application-provided ID (the "NONE" generator strategy) also does not | ||
| 657 | // need the extra update, although it is still in the list of insertions itself. | ||
| 658 | // This looks like a minor optimization at first, but is the capstone for being able to | ||
| 659 | // use non-NULLable, self-referencing associations in applications that provide IDs (like UUIDs). | ||
| 660 | if ( | ||
| 661 | (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) | ||
| 662 | && ! ($newVal === $entity && $this->class->isIdentifierNatural()) | ||
| 663 | ) { | ||
| 664 | $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]); | ||
| 665 | |||
| 666 | $newVal = null; | ||
| 667 | } | ||
| 668 | } | ||
| 669 | |||
| 670 | $newValId = null; | ||
| 671 | |||
| 672 | if ($newVal !== null) { | ||
| 673 | $newValId = $uow->getEntityIdentifier($newVal); | ||
| 674 | } | ||
| 675 | |||
| 676 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 677 | $owningTable = $this->getOwningTable($field); | ||
| 678 | |||
| 679 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 680 | $sourceColumn = $joinColumn->name; | ||
| 681 | $targetColumn = $joinColumn->referencedColumnName; | ||
| 682 | $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 683 | |||
| 684 | $this->quotedColumns[$sourceColumn] = $quotedColumn; | ||
| 685 | $this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em); | ||
| 686 | $result[$owningTable][$sourceColumn] = $newValId | ||
| 687 | ? $newValId[$targetClass->getFieldForColumn($targetColumn)] | ||
| 688 | : null; | ||
| 689 | } | ||
| 690 | } | ||
| 691 | |||
| 692 | return $result; | ||
| 693 | } | ||
| 694 | |||
| 695 | /** | ||
| 696 | * Prepares the data changeset of a managed entity for database insertion (initial INSERT). | ||
| 697 | * The changeset of the entity is obtained from the currently running UnitOfWork. | ||
| 698 | * | ||
| 699 | * The default insert data preparation is the same as for updates. | ||
| 700 | * | ||
| 701 | * @see prepareUpdateData | ||
| 702 | * | ||
| 703 | * @param object $entity The entity for which to prepare the data. | ||
| 704 | * | ||
| 705 | * @return mixed[][] The prepared data for the tables to update. | ||
| 706 | * @psalm-return array<string, mixed[]> | ||
| 707 | */ | ||
| 708 | protected function prepareInsertData(object $entity): array | ||
| 709 | { | ||
| 710 | return $this->prepareUpdateData($entity, true); | ||
| 711 | } | ||
| 712 | |||
| 713 | public function getOwningTable(string $fieldName): string | ||
| 714 | { | ||
| 715 | return $this->class->getTableName(); | ||
| 716 | } | ||
| 717 | |||
| 718 | /** | ||
| 719 | * {@inheritDoc} | ||
| 720 | */ | ||
| 721 | public function load( | ||
| 722 | array $criteria, | ||
| 723 | object|null $entity = null, | ||
| 724 | AssociationMapping|null $assoc = null, | ||
| 725 | array $hints = [], | ||
| 726 | LockMode|int|null $lockMode = null, | ||
| 727 | int|null $limit = null, | ||
| 728 | array|null $orderBy = null, | ||
| 729 | ): object|null { | ||
| 730 | $this->switchPersisterContext(null, $limit); | ||
| 731 | |||
| 732 | $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy); | ||
| 733 | [$params, $types] = $this->expandParameters($criteria); | ||
| 734 | $stmt = $this->conn->executeQuery($sql, $params, $types); | ||
| 735 | |||
| 736 | if ($entity !== null) { | ||
| 737 | $hints[Query::HINT_REFRESH] = true; | ||
| 738 | $hints[Query::HINT_REFRESH_ENTITY] = $entity; | ||
| 739 | } | ||
| 740 | |||
| 741 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); | ||
| 742 | $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints); | ||
| 743 | |||
| 744 | return $entities ? $entities[0] : null; | ||
| 745 | } | ||
| 746 | |||
| 747 | /** | ||
| 748 | * {@inheritDoc} | ||
| 749 | */ | ||
| 750 | public function loadById(array $identifier, object|null $entity = null): object|null | ||
| 751 | { | ||
| 752 | return $this->load($identifier, $entity); | ||
| 753 | } | ||
| 754 | |||
| 755 | /** | ||
| 756 | * {@inheritDoc} | ||
| 757 | */ | ||
| 758 | public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null | ||
| 759 | { | ||
| 760 | $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc->targetEntity); | ||
| 761 | if ($foundEntity !== false) { | ||
| 762 | return $foundEntity; | ||
| 763 | } | ||
| 764 | |||
| 765 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 766 | |||
| 767 | if ($assoc->isOwningSide()) { | ||
| 768 | $isInverseSingleValued = $assoc->inversedBy !== null && ! $targetClass->isCollectionValuedAssociation($assoc->inversedBy); | ||
| 769 | |||
| 770 | // Mark inverse side as fetched in the hints, otherwise the UoW would | ||
| 771 | // try to load it in a separate query (remember: to-one inverse sides can not be lazy). | ||
| 772 | $hints = []; | ||
| 773 | |||
| 774 | if ($isInverseSingleValued) { | ||
| 775 | $hints['fetched']['r'][$assoc->inversedBy] = true; | ||
| 776 | } | ||
| 777 | |||
| 778 | $targetEntity = $this->load($identifier, null, $assoc, $hints); | ||
| 779 | |||
| 780 | // Complete bidirectional association, if necessary | ||
| 781 | if ($targetEntity !== null && $isInverseSingleValued) { | ||
| 782 | $targetClass->reflFields[$assoc->inversedBy]->setValue($targetEntity, $sourceEntity); | ||
| 783 | } | ||
| 784 | |||
| 785 | return $targetEntity; | ||
| 786 | } | ||
| 787 | |||
| 788 | assert(isset($assoc->mappedBy)); | ||
| 789 | $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); | ||
| 790 | $owningAssoc = $targetClass->getAssociationMapping($assoc->mappedBy); | ||
| 791 | assert($owningAssoc->isOneToOneOwningSide()); | ||
| 792 | |||
| 793 | $computedIdentifier = []; | ||
| 794 | |||
| 795 | // TRICKY: since the association is specular source and target are flipped | ||
| 796 | foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { | ||
| 797 | if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) { | ||
| 798 | throw MappingException::joinColumnMustPointToMappedField( | ||
| 799 | $sourceClass->name, | ||
| 800 | $sourceKeyColumn, | ||
| 801 | ); | ||
| 802 | } | ||
| 803 | |||
| 804 | $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] = | ||
| 805 | $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity); | ||
| 806 | } | ||
| 807 | |||
| 808 | $targetEntity = $this->load($computedIdentifier, null, $assoc); | ||
| 809 | |||
| 810 | if ($targetEntity !== null) { | ||
| 811 | $targetClass->setFieldValue($targetEntity, $assoc->mappedBy, $sourceEntity); | ||
| 812 | } | ||
| 813 | |||
| 814 | return $targetEntity; | ||
| 815 | } | ||
| 816 | |||
| 817 | /** | ||
| 818 | * {@inheritDoc} | ||
| 819 | */ | ||
| 820 | public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void | ||
| 821 | { | ||
| 822 | $sql = $this->getSelectSQL($id, null, $lockMode); | ||
| 823 | [$params, $types] = $this->expandParameters($id); | ||
| 824 | $stmt = $this->conn->executeQuery($sql, $params, $types); | ||
| 825 | |||
| 826 | $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT); | ||
| 827 | $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]); | ||
| 828 | } | ||
| 829 | |||
| 830 | public function count(array|Criteria $criteria = []): int | ||
| 831 | { | ||
| 832 | $sql = $this->getCountSQL($criteria); | ||
| 833 | |||
| 834 | [$params, $types] = $criteria instanceof Criteria | ||
| 835 | ? $this->expandCriteriaParameters($criteria) | ||
| 836 | : $this->expandParameters($criteria); | ||
| 837 | |||
| 838 | return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne(); | ||
| 839 | } | ||
| 840 | |||
| 841 | /** | ||
| 842 | * {@inheritDoc} | ||
| 843 | */ | ||
| 844 | public function loadCriteria(Criteria $criteria): array | ||
| 845 | { | ||
| 846 | $orderBy = array_map( | ||
| 847 | static fn (Order $order): string => $order->value, | ||
| 848 | $criteria->orderings(), | ||
| 849 | ); | ||
| 850 | $limit = $criteria->getMaxResults(); | ||
| 851 | $offset = $criteria->getFirstResult(); | ||
| 852 | $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); | ||
| 853 | |||
| 854 | [$params, $types] = $this->expandCriteriaParameters($criteria); | ||
| 855 | |||
| 856 | $stmt = $this->conn->executeQuery($query, $params, $types); | ||
| 857 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); | ||
| 858 | |||
| 859 | return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]); | ||
| 860 | } | ||
| 861 | |||
| 862 | /** | ||
| 863 | * {@inheritDoc} | ||
| 864 | */ | ||
| 865 | public function expandCriteriaParameters(Criteria $criteria): array | ||
| 866 | { | ||
| 867 | $expression = $criteria->getWhereExpression(); | ||
| 868 | $sqlParams = []; | ||
| 869 | $sqlTypes = []; | ||
| 870 | |||
| 871 | if ($expression === null) { | ||
| 872 | return [$sqlParams, $sqlTypes]; | ||
| 873 | } | ||
| 874 | |||
| 875 | $valueVisitor = new SqlValueVisitor(); | ||
| 876 | |||
| 877 | $valueVisitor->dispatch($expression); | ||
| 878 | |||
| 879 | [, $types] = $valueVisitor->getParamsAndTypes(); | ||
| 880 | |||
| 881 | foreach ($types as $type) { | ||
| 882 | [$field, $value, $operator] = $type; | ||
| 883 | |||
| 884 | if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { | ||
| 885 | continue; | ||
| 886 | } | ||
| 887 | |||
| 888 | $sqlParams = [...$sqlParams, ...$this->getValues($value)]; | ||
| 889 | $sqlTypes = [...$sqlTypes, ...$this->getTypes($field, $value, $this->class)]; | ||
| 890 | } | ||
| 891 | |||
| 892 | return [$sqlParams, $sqlTypes]; | ||
| 893 | } | ||
| 894 | |||
| 895 | /** | ||
| 896 | * {@inheritDoc} | ||
| 897 | */ | ||
| 898 | public function loadAll( | ||
| 899 | array $criteria = [], | ||
| 900 | array|null $orderBy = null, | ||
| 901 | int|null $limit = null, | ||
| 902 | int|null $offset = null, | ||
| 903 | ): array { | ||
| 904 | $this->switchPersisterContext($offset, $limit); | ||
| 905 | |||
| 906 | $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy); | ||
| 907 | [$params, $types] = $this->expandParameters($criteria); | ||
| 908 | $stmt = $this->conn->executeQuery($sql, $params, $types); | ||
| 909 | |||
| 910 | $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT); | ||
| 911 | |||
| 912 | return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]); | ||
| 913 | } | ||
| 914 | |||
| 915 | /** | ||
| 916 | * {@inheritDoc} | ||
| 917 | */ | ||
| 918 | public function getManyToManyCollection( | ||
| 919 | AssociationMapping $assoc, | ||
| 920 | object $sourceEntity, | ||
| 921 | int|null $offset = null, | ||
| 922 | int|null $limit = null, | ||
| 923 | ): array { | ||
| 924 | assert($assoc->isManyToMany()); | ||
| 925 | $this->switchPersisterContext($offset, $limit); | ||
| 926 | |||
| 927 | $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit); | ||
| 928 | |||
| 929 | return $this->loadArrayFromResult($assoc, $stmt); | ||
| 930 | } | ||
| 931 | |||
| 932 | /** | ||
| 933 | * Loads an array of entities from a given DBAL statement. | ||
| 934 | * | ||
| 935 | * @return mixed[] | ||
| 936 | */ | ||
| 937 | private function loadArrayFromResult(AssociationMapping $assoc, Result $stmt): array | ||
| 938 | { | ||
| 939 | $rsm = $this->currentPersisterContext->rsm; | ||
| 940 | $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true]; | ||
| 941 | |||
| 942 | if ($assoc->isIndexed()) { | ||
| 943 | $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed. | ||
| 944 | $rsm->addIndexBy('r', $assoc->indexBy()); | ||
| 945 | } | ||
| 946 | |||
| 947 | return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints); | ||
| 948 | } | ||
| 949 | |||
| 950 | /** | ||
| 951 | * Hydrates a collection from a given DBAL statement. | ||
| 952 | * | ||
| 953 | * @return mixed[] | ||
| 954 | */ | ||
| 955 | private function loadCollectionFromStatement( | ||
| 956 | AssociationMapping $assoc, | ||
| 957 | Result $stmt, | ||
| 958 | PersistentCollection $coll, | ||
| 959 | ): array { | ||
| 960 | $rsm = $this->currentPersisterContext->rsm; | ||
| 961 | $hints = [ | ||
| 962 | UnitOfWork::HINT_DEFEREAGERLOAD => true, | ||
| 963 | 'collection' => $coll, | ||
| 964 | ]; | ||
| 965 | |||
| 966 | if ($assoc->isIndexed()) { | ||
| 967 | $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed. | ||
| 968 | $rsm->addIndexBy('r', $assoc->indexBy()); | ||
| 969 | } | ||
| 970 | |||
| 971 | return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints); | ||
| 972 | } | ||
| 973 | |||
| 974 | /** | ||
| 975 | * {@inheritDoc} | ||
| 976 | */ | ||
| 977 | public function loadManyToManyCollection(AssociationMapping $assoc, object $sourceEntity, PersistentCollection $collection): array | ||
| 978 | { | ||
| 979 | assert($assoc->isManyToMany()); | ||
| 980 | $stmt = $this->getManyToManyStatement($assoc, $sourceEntity); | ||
| 981 | |||
| 982 | return $this->loadCollectionFromStatement($assoc, $stmt, $collection); | ||
| 983 | } | ||
| 984 | |||
| 985 | /** @throws MappingException */ | ||
| 986 | private function getManyToManyStatement( | ||
| 987 | AssociationMapping&ManyToManyAssociationMapping $assoc, | ||
| 988 | object $sourceEntity, | ||
| 989 | int|null $offset = null, | ||
| 990 | int|null $limit = null, | ||
| 991 | ): Result { | ||
| 992 | $this->switchPersisterContext($offset, $limit); | ||
| 993 | |||
| 994 | $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); | ||
| 995 | $class = $sourceClass; | ||
| 996 | $association = $assoc; | ||
| 997 | $criteria = []; | ||
| 998 | $parameters = []; | ||
| 999 | |||
| 1000 | if (! $assoc->isOwningSide()) { | ||
| 1001 | $class = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1002 | } | ||
| 1003 | |||
| 1004 | $association = $this->em->getMetadataFactory()->getOwningSide($assoc); | ||
| 1005 | $joinColumns = $assoc->isOwningSide() | ||
| 1006 | ? $association->joinTable->joinColumns | ||
| 1007 | : $association->joinTable->inverseJoinColumns; | ||
| 1008 | |||
| 1009 | $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); | ||
| 1010 | |||
| 1011 | foreach ($joinColumns as $joinColumn) { | ||
| 1012 | $sourceKeyColumn = $joinColumn->referencedColumnName; | ||
| 1013 | $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 1014 | |||
| 1015 | switch (true) { | ||
| 1016 | case $sourceClass->containsForeignIdentifier: | ||
| 1017 | $field = $sourceClass->getFieldForColumn($sourceKeyColumn); | ||
| 1018 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1019 | |||
| 1020 | if (isset($sourceClass->associationMappings[$field])) { | ||
| 1021 | $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); | ||
| 1022 | $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]]; | ||
| 1023 | } | ||
| 1024 | |||
| 1025 | break; | ||
| 1026 | |||
| 1027 | case isset($sourceClass->fieldNames[$sourceKeyColumn]): | ||
| 1028 | $field = $sourceClass->fieldNames[$sourceKeyColumn]; | ||
| 1029 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1030 | |||
| 1031 | break; | ||
| 1032 | |||
| 1033 | default: | ||
| 1034 | throw MappingException::joinColumnMustPointToMappedField( | ||
| 1035 | $sourceClass->name, | ||
| 1036 | $sourceKeyColumn, | ||
| 1037 | ); | ||
| 1038 | } | ||
| 1039 | |||
| 1040 | $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value; | ||
| 1041 | $parameters[] = [ | ||
| 1042 | 'value' => $value, | ||
| 1043 | 'field' => $field, | ||
| 1044 | 'class' => $sourceClass, | ||
| 1045 | ]; | ||
| 1046 | } | ||
| 1047 | |||
| 1048 | $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); | ||
| 1049 | [$params, $types] = $this->expandToManyParameters($parameters); | ||
| 1050 | |||
| 1051 | return $this->conn->executeQuery($sql, $params, $types); | ||
| 1052 | } | ||
| 1053 | |||
| 1054 | public function getSelectSQL( | ||
| 1055 | array|Criteria $criteria, | ||
| 1056 | AssociationMapping|null $assoc = null, | ||
| 1057 | LockMode|int|null $lockMode = null, | ||
| 1058 | int|null $limit = null, | ||
| 1059 | int|null $offset = null, | ||
| 1060 | array|null $orderBy = null, | ||
| 1061 | ): string { | ||
| 1062 | $this->switchPersisterContext($offset, $limit); | ||
| 1063 | |||
| 1064 | $joinSql = ''; | ||
| 1065 | $orderBySql = ''; | ||
| 1066 | |||
| 1067 | if ($assoc !== null && $assoc->isManyToMany()) { | ||
| 1068 | $joinSql = $this->getSelectManyToManyJoinSQL($assoc); | ||
| 1069 | } | ||
| 1070 | |||
| 1071 | if ($assoc !== null && $assoc->isOrdered()) { | ||
| 1072 | $orderBy = $assoc->orderBy(); | ||
| 1073 | } | ||
| 1074 | |||
| 1075 | if ($orderBy) { | ||
| 1076 | $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name)); | ||
| 1077 | } | ||
| 1078 | |||
| 1079 | $conditionSql = $criteria instanceof Criteria | ||
| 1080 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 1081 | : $this->getSelectConditionSQL($criteria, $assoc); | ||
| 1082 | |||
| 1083 | $lockSql = match ($lockMode) { | ||
| 1084 | LockMode::PESSIMISTIC_READ => ' ' . $this->getReadLockSQL($this->platform), | ||
| 1085 | LockMode::PESSIMISTIC_WRITE => ' ' . $this->getWriteLockSQL($this->platform), | ||
| 1086 | default => '', | ||
| 1087 | }; | ||
| 1088 | |||
| 1089 | $columnList = $this->getSelectColumnsSQL(); | ||
| 1090 | $tableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 1091 | $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); | ||
| 1092 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 1093 | |||
| 1094 | if ($filterSql !== '') { | ||
| 1095 | $conditionSql = $conditionSql | ||
| 1096 | ? $conditionSql . ' AND ' . $filterSql | ||
| 1097 | : $filterSql; | ||
| 1098 | } | ||
| 1099 | |||
| 1100 | $select = 'SELECT ' . $columnList; | ||
| 1101 | $from = ' FROM ' . $tableName . ' ' . $tableAlias; | ||
| 1102 | $join = $this->currentPersisterContext->selectJoinSql . $joinSql; | ||
| 1103 | $where = ($conditionSql ? ' WHERE ' . $conditionSql : ''); | ||
| 1104 | $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE); | ||
| 1105 | $query = $select | ||
| 1106 | . $lock | ||
| 1107 | . $join | ||
| 1108 | . $where | ||
| 1109 | . $orderBySql; | ||
| 1110 | |||
| 1111 | return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql; | ||
| 1112 | } | ||
| 1113 | |||
| 1114 | public function getCountSQL(array|Criteria $criteria = []): string | ||
| 1115 | { | ||
| 1116 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 1117 | $tableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 1118 | |||
| 1119 | $conditionSql = $criteria instanceof Criteria | ||
| 1120 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 1121 | : $this->getSelectConditionSQL($criteria); | ||
| 1122 | |||
| 1123 | $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias); | ||
| 1124 | |||
| 1125 | if ($filterSql !== '') { | ||
| 1126 | $conditionSql = $conditionSql | ||
| 1127 | ? $conditionSql . ' AND ' . $filterSql | ||
| 1128 | : $filterSql; | ||
| 1129 | } | ||
| 1130 | |||
| 1131 | return 'SELECT COUNT(*) ' | ||
| 1132 | . 'FROM ' . $tableName . ' ' . $tableAlias | ||
| 1133 | . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); | ||
| 1134 | } | ||
| 1135 | |||
| 1136 | /** | ||
| 1137 | * Gets the ORDER BY SQL snippet for ordered collections. | ||
| 1138 | * | ||
| 1139 | * @psalm-param array<string, string> $orderBy | ||
| 1140 | * | ||
| 1141 | * @throws InvalidOrientation | ||
| 1142 | * @throws InvalidFindByCall | ||
| 1143 | * @throws UnrecognizedField | ||
| 1144 | */ | ||
| 1145 | final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string | ||
| 1146 | { | ||
| 1147 | $orderByList = []; | ||
| 1148 | |||
| 1149 | foreach ($orderBy as $fieldName => $orientation) { | ||
| 1150 | $orientation = strtoupper(trim($orientation)); | ||
| 1151 | |||
| 1152 | if ($orientation !== 'ASC' && $orientation !== 'DESC') { | ||
| 1153 | throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName); | ||
| 1154 | } | ||
| 1155 | |||
| 1156 | if (isset($this->class->fieldMappings[$fieldName])) { | ||
| 1157 | $tableAlias = isset($this->class->fieldMappings[$fieldName]->inherited) | ||
| 1158 | ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]->inherited) | ||
| 1159 | : $baseTableAlias; | ||
| 1160 | |||
| 1161 | $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform); | ||
| 1162 | $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; | ||
| 1163 | |||
| 1164 | continue; | ||
| 1165 | } | ||
| 1166 | |||
| 1167 | if (isset($this->class->associationMappings[$fieldName])) { | ||
| 1168 | $association = $this->class->associationMappings[$fieldName]; | ||
| 1169 | if (! $association->isOwningSide()) { | ||
| 1170 | throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName); | ||
| 1171 | } | ||
| 1172 | |||
| 1173 | assert($association->isToOneOwningSide()); | ||
| 1174 | |||
| 1175 | $tableAlias = isset($association->inherited) | ||
| 1176 | ? $this->getSQLTableAlias($association->inherited) | ||
| 1177 | : $baseTableAlias; | ||
| 1178 | |||
| 1179 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1180 | $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1181 | $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation; | ||
| 1182 | } | ||
| 1183 | |||
| 1184 | continue; | ||
| 1185 | } | ||
| 1186 | |||
| 1187 | throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName); | ||
| 1188 | } | ||
| 1189 | |||
| 1190 | return ' ORDER BY ' . implode(', ', $orderByList); | ||
| 1191 | } | ||
| 1192 | |||
| 1193 | /** | ||
| 1194 | * Gets the SQL fragment with the list of columns to select when querying for | ||
| 1195 | * an entity in this persister. | ||
| 1196 | * | ||
| 1197 | * Subclasses should override this method to alter or change the select column | ||
| 1198 | * list SQL fragment. Note that in the implementation of BasicEntityPersister | ||
| 1199 | * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}. | ||
| 1200 | * Subclasses may or may not do the same. | ||
| 1201 | */ | ||
| 1202 | protected function getSelectColumnsSQL(): string | ||
| 1203 | { | ||
| 1204 | if ($this->currentPersisterContext->selectColumnListSql !== null) { | ||
| 1205 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 1206 | } | ||
| 1207 | |||
| 1208 | $columnList = []; | ||
| 1209 | $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root | ||
| 1210 | |||
| 1211 | // Add regular columns to select list | ||
| 1212 | foreach ($this->class->fieldNames as $field) { | ||
| 1213 | $columnList[] = $this->getSelectColumnSQL($field, $this->class); | ||
| 1214 | } | ||
| 1215 | |||
| 1216 | $this->currentPersisterContext->selectJoinSql = ''; | ||
| 1217 | $eagerAliasCounter = 0; | ||
| 1218 | |||
| 1219 | foreach ($this->class->associationMappings as $assocField => $assoc) { | ||
| 1220 | $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class); | ||
| 1221 | |||
| 1222 | if ($assocColumnSQL) { | ||
| 1223 | $columnList[] = $assocColumnSQL; | ||
| 1224 | } | ||
| 1225 | |||
| 1226 | $isAssocToOneInverseSide = $assoc->isToOne() && ! $assoc->isOwningSide(); | ||
| 1227 | $isAssocFromOneEager = $assoc->isToOne() && $assoc->fetch === ClassMetadata::FETCH_EAGER; | ||
| 1228 | |||
| 1229 | if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) { | ||
| 1230 | continue; | ||
| 1231 | } | ||
| 1232 | |||
| 1233 | if ($assoc->isToMany() && $this->currentPersisterContext->handlesLimits) { | ||
| 1234 | continue; | ||
| 1235 | } | ||
| 1236 | |||
| 1237 | $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1238 | |||
| 1239 | if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) { | ||
| 1240 | continue; // now this is why you shouldn't use inheritance | ||
| 1241 | } | ||
| 1242 | |||
| 1243 | $assocAlias = 'e' . ($eagerAliasCounter++); | ||
| 1244 | $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc->targetEntity, $assocAlias, 'r', $assocField); | ||
| 1245 | |||
| 1246 | foreach ($eagerEntity->fieldNames as $field) { | ||
| 1247 | $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias); | ||
| 1248 | } | ||
| 1249 | |||
| 1250 | foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) { | ||
| 1251 | $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL( | ||
| 1252 | $eagerAssocField, | ||
| 1253 | $eagerAssoc, | ||
| 1254 | $eagerEntity, | ||
| 1255 | $assocAlias, | ||
| 1256 | ); | ||
| 1257 | |||
| 1258 | if ($eagerAssocColumnSQL) { | ||
| 1259 | $columnList[] = $eagerAssocColumnSQL; | ||
| 1260 | } | ||
| 1261 | } | ||
| 1262 | |||
| 1263 | $association = $assoc; | ||
| 1264 | $joinCondition = []; | ||
| 1265 | |||
| 1266 | if ($assoc->isIndexed()) { | ||
| 1267 | assert($assoc->isToMany()); | ||
| 1268 | $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc->indexBy()); | ||
| 1269 | } | ||
| 1270 | |||
| 1271 | if (! $assoc->isOwningSide()) { | ||
| 1272 | $eagerEntity = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1273 | $association = $eagerEntity->getAssociationMapping($assoc->mappedBy); | ||
| 1274 | } | ||
| 1275 | |||
| 1276 | assert($association->isToOneOwningSide()); | ||
| 1277 | |||
| 1278 | $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias); | ||
| 1279 | $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform); | ||
| 1280 | |||
| 1281 | if ($assoc->isOwningSide()) { | ||
| 1282 | $tableAlias = $this->getSQLTableAlias($association->targetEntity, $assocAlias); | ||
| 1283 | $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association->joinColumns); | ||
| 1284 | |||
| 1285 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1286 | $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1287 | $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1288 | $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity) | ||
| 1289 | . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol; | ||
| 1290 | } | ||
| 1291 | |||
| 1292 | // Add filter SQL | ||
| 1293 | $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias); | ||
| 1294 | if ($filterSql) { | ||
| 1295 | $joinCondition[] = $filterSql; | ||
| 1296 | } | ||
| 1297 | } else { | ||
| 1298 | $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN'; | ||
| 1299 | |||
| 1300 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1301 | $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1302 | $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1303 | |||
| 1304 | $joinCondition[] = $this->getSQLTableAlias($association->sourceEntity, $assocAlias) . '.' . $sourceCol . ' = ' | ||
| 1305 | . $this->getSQLTableAlias($association->targetEntity) . '.' . $targetCol; | ||
| 1306 | } | ||
| 1307 | } | ||
| 1308 | |||
| 1309 | $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON '; | ||
| 1310 | $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition); | ||
| 1311 | } | ||
| 1312 | |||
| 1313 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); | ||
| 1314 | |||
| 1315 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 1316 | } | ||
| 1317 | |||
| 1318 | /** Gets the SQL join fragment used when selecting entities from an association. */ | ||
| 1319 | protected function getSelectColumnAssociationSQL( | ||
| 1320 | string $field, | ||
| 1321 | AssociationMapping $assoc, | ||
| 1322 | ClassMetadata $class, | ||
| 1323 | string $alias = 'r', | ||
| 1324 | ): string { | ||
| 1325 | if (! $assoc->isToOneOwningSide()) { | ||
| 1326 | return ''; | ||
| 1327 | } | ||
| 1328 | |||
| 1329 | $columnList = []; | ||
| 1330 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1331 | $isIdentifier = isset($assoc->id) && $assoc->id === true; | ||
| 1332 | $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias)); | ||
| 1333 | |||
| 1334 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 1335 | $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1336 | $resultColumnName = $this->getSQLColumnAlias($joinColumn->name); | ||
| 1337 | $type = PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em); | ||
| 1338 | |||
| 1339 | $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn->name, $isIdentifier, $type); | ||
| 1340 | |||
| 1341 | $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName); | ||
| 1342 | } | ||
| 1343 | |||
| 1344 | return implode(', ', $columnList); | ||
| 1345 | } | ||
| 1346 | |||
| 1347 | /** | ||
| 1348 | * Gets the SQL join fragment used when selecting entities from a | ||
| 1349 | * many-to-many association. | ||
| 1350 | */ | ||
| 1351 | protected function getSelectManyToManyJoinSQL(AssociationMapping&ManyToManyAssociationMapping $manyToMany): string | ||
| 1352 | { | ||
| 1353 | $conditions = []; | ||
| 1354 | $association = $manyToMany; | ||
| 1355 | $sourceTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 1356 | |||
| 1357 | $association = $this->em->getMetadataFactory()->getOwningSide($manyToMany); | ||
| 1358 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform); | ||
| 1359 | $joinColumns = $manyToMany->isOwningSide() | ||
| 1360 | ? $association->joinTable->inverseJoinColumns | ||
| 1361 | : $association->joinTable->joinColumns; | ||
| 1362 | |||
| 1363 | foreach ($joinColumns as $joinColumn) { | ||
| 1364 | $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1365 | $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1366 | $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn; | ||
| 1367 | } | ||
| 1368 | |||
| 1369 | return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions); | ||
| 1370 | } | ||
| 1371 | |||
| 1372 | public function getInsertSQL(): string | ||
| 1373 | { | ||
| 1374 | if ($this->insertSql !== null) { | ||
| 1375 | return $this->insertSql; | ||
| 1376 | } | ||
| 1377 | |||
| 1378 | $columns = $this->getInsertColumnList(); | ||
| 1379 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 1380 | |||
| 1381 | if (empty($columns)) { | ||
| 1382 | $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform); | ||
| 1383 | $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn); | ||
| 1384 | |||
| 1385 | return $this->insertSql; | ||
| 1386 | } | ||
| 1387 | |||
| 1388 | $values = []; | ||
| 1389 | $columns = array_unique($columns); | ||
| 1390 | |||
| 1391 | foreach ($columns as $column) { | ||
| 1392 | $placeholder = '?'; | ||
| 1393 | |||
| 1394 | if ( | ||
| 1395 | isset($this->class->fieldNames[$column]) | ||
| 1396 | && isset($this->columnTypes[$this->class->fieldNames[$column]]) | ||
| 1397 | && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]) | ||
| 1398 | ) { | ||
| 1399 | $type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]); | ||
| 1400 | $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform); | ||
| 1401 | } | ||
| 1402 | |||
| 1403 | $values[] = $placeholder; | ||
| 1404 | } | ||
| 1405 | |||
| 1406 | $columns = implode(', ', $columns); | ||
| 1407 | $values = implode(', ', $values); | ||
| 1408 | |||
| 1409 | $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values); | ||
| 1410 | |||
| 1411 | return $this->insertSql; | ||
| 1412 | } | ||
| 1413 | |||
| 1414 | /** | ||
| 1415 | * Gets the list of columns to put in the INSERT SQL statement. | ||
| 1416 | * | ||
| 1417 | * Subclasses should override this method to alter or change the list of | ||
| 1418 | * columns placed in the INSERT statements used by the persister. | ||
| 1419 | * | ||
| 1420 | * @psalm-return list<string> | ||
| 1421 | */ | ||
| 1422 | protected function getInsertColumnList(): array | ||
| 1423 | { | ||
| 1424 | $columns = []; | ||
| 1425 | |||
| 1426 | foreach ($this->class->reflFields as $name => $field) { | ||
| 1427 | if ($this->class->isVersioned && $this->class->versionField === $name) { | ||
| 1428 | continue; | ||
| 1429 | } | ||
| 1430 | |||
| 1431 | if (isset($this->class->embeddedClasses[$name])) { | ||
| 1432 | continue; | ||
| 1433 | } | ||
| 1434 | |||
| 1435 | if (isset($this->class->associationMappings[$name])) { | ||
| 1436 | $assoc = $this->class->associationMappings[$name]; | ||
| 1437 | |||
| 1438 | if ($assoc->isToOneOwningSide()) { | ||
| 1439 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 1440 | $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1441 | } | ||
| 1442 | } | ||
| 1443 | |||
| 1444 | continue; | ||
| 1445 | } | ||
| 1446 | |||
| 1447 | if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) { | ||
| 1448 | if (isset($this->class->fieldMappings[$name]->notInsertable)) { | ||
| 1449 | continue; | ||
| 1450 | } | ||
| 1451 | |||
| 1452 | $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); | ||
| 1453 | $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type; | ||
| 1454 | } | ||
| 1455 | } | ||
| 1456 | |||
| 1457 | return $columns; | ||
| 1458 | } | ||
| 1459 | |||
| 1460 | /** | ||
| 1461 | * Gets the SQL snippet of a qualified column name for the given field name. | ||
| 1462 | * | ||
| 1463 | * @param ClassMetadata $class The class that declares this field. The table this class is | ||
| 1464 | * mapped to must own the column for the given field. | ||
| 1465 | */ | ||
| 1466 | protected function getSelectColumnSQL(string $field, ClassMetadata $class, string $alias = 'r'): string | ||
| 1467 | { | ||
| 1468 | $root = $alias === 'r' ? '' : $alias; | ||
| 1469 | $tableAlias = $this->getSQLTableAlias($class->name, $root); | ||
| 1470 | $fieldMapping = $class->fieldMappings[$field]; | ||
| 1471 | $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform)); | ||
| 1472 | $columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName); | ||
| 1473 | |||
| 1474 | $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field); | ||
| 1475 | if (! empty($fieldMapping->enumType)) { | ||
| 1476 | $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping->enumType); | ||
| 1477 | } | ||
| 1478 | |||
| 1479 | $type = Type::getType($fieldMapping->type); | ||
| 1480 | $sql = $type->convertToPHPValueSQL($sql, $this->platform); | ||
| 1481 | |||
| 1482 | return $sql . ' AS ' . $columnAlias; | ||
| 1483 | } | ||
| 1484 | |||
| 1485 | /** | ||
| 1486 | * Gets the SQL table alias for the given class name. | ||
| 1487 | * | ||
| 1488 | * @todo Reconsider. Binding table aliases to class names is not such a good idea. | ||
| 1489 | */ | ||
| 1490 | protected function getSQLTableAlias(string $className, string $assocName = ''): string | ||
| 1491 | { | ||
| 1492 | if ($assocName) { | ||
| 1493 | $className .= '#' . $assocName; | ||
| 1494 | } | ||
| 1495 | |||
| 1496 | if (isset($this->currentPersisterContext->sqlTableAliases[$className])) { | ||
| 1497 | return $this->currentPersisterContext->sqlTableAliases[$className]; | ||
| 1498 | } | ||
| 1499 | |||
| 1500 | $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++; | ||
| 1501 | |||
| 1502 | $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias; | ||
| 1503 | |||
| 1504 | return $tableAlias; | ||
| 1505 | } | ||
| 1506 | |||
| 1507 | /** | ||
| 1508 | * {@inheritDoc} | ||
| 1509 | */ | ||
| 1510 | public function lock(array $criteria, LockMode|int $lockMode): void | ||
| 1511 | { | ||
| 1512 | $conditionSql = $this->getSelectConditionSQL($criteria); | ||
| 1513 | |||
| 1514 | $lockSql = match ($lockMode) { | ||
| 1515 | LockMode::PESSIMISTIC_READ => $this->getReadLockSQL($this->platform), | ||
| 1516 | LockMode::PESSIMISTIC_WRITE => $this->getWriteLockSQL($this->platform), | ||
| 1517 | default => '', | ||
| 1518 | }; | ||
| 1519 | |||
| 1520 | $lock = $this->getLockTablesSql($lockMode); | ||
| 1521 | $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' '; | ||
| 1522 | $sql = 'SELECT 1 ' | ||
| 1523 | . $lock | ||
| 1524 | . $where | ||
| 1525 | . $lockSql; | ||
| 1526 | |||
| 1527 | [$params, $types] = $this->expandParameters($criteria); | ||
| 1528 | |||
| 1529 | $this->conn->executeQuery($sql, $params, $types); | ||
| 1530 | } | ||
| 1531 | |||
| 1532 | /** | ||
| 1533 | * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister. | ||
| 1534 | * | ||
| 1535 | * @psalm-param LockMode::* $lockMode | ||
| 1536 | */ | ||
| 1537 | protected function getLockTablesSql(LockMode|int $lockMode): string | ||
| 1538 | { | ||
| 1539 | return $this->platform->appendLockHint( | ||
| 1540 | 'FROM ' | ||
| 1541 | . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' ' | ||
| 1542 | . $this->getSQLTableAlias($this->class->name), | ||
| 1543 | $lockMode, | ||
| 1544 | ); | ||
| 1545 | } | ||
| 1546 | |||
| 1547 | /** | ||
| 1548 | * Gets the Select Where Condition from a Criteria object. | ||
| 1549 | */ | ||
| 1550 | protected function getSelectConditionCriteriaSQL(Criteria $criteria): string | ||
| 1551 | { | ||
| 1552 | $expression = $criteria->getWhereExpression(); | ||
| 1553 | |||
| 1554 | if ($expression === null) { | ||
| 1555 | return ''; | ||
| 1556 | } | ||
| 1557 | |||
| 1558 | $visitor = new SqlExpressionVisitor($this, $this->class); | ||
| 1559 | |||
| 1560 | return $visitor->dispatch($expression); | ||
| 1561 | } | ||
| 1562 | |||
| 1563 | public function getSelectConditionStatementSQL( | ||
| 1564 | string $field, | ||
| 1565 | mixed $value, | ||
| 1566 | AssociationMapping|null $assoc = null, | ||
| 1567 | string|null $comparison = null, | ||
| 1568 | ): string { | ||
| 1569 | $selectedColumns = []; | ||
| 1570 | $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc); | ||
| 1571 | |||
| 1572 | if (count($columns) > 1 && $comparison === Comparison::IN) { | ||
| 1573 | /* | ||
| 1574 | * @todo try to support multi-column IN expressions. | ||
| 1575 | * Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B')) | ||
| 1576 | */ | ||
| 1577 | throw CantUseInOperatorOnCompositeKeys::create(); | ||
| 1578 | } | ||
| 1579 | |||
| 1580 | foreach ($columns as $column) { | ||
| 1581 | $placeholder = '?'; | ||
| 1582 | |||
| 1583 | if (isset($this->class->fieldMappings[$field])) { | ||
| 1584 | $type = Type::getType($this->class->fieldMappings[$field]->type); | ||
| 1585 | $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform); | ||
| 1586 | } | ||
| 1587 | |||
| 1588 | if ($comparison !== null) { | ||
| 1589 | // special case null value handling | ||
| 1590 | if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) { | ||
| 1591 | $selectedColumns[] = $column . ' IS NULL'; | ||
| 1592 | |||
| 1593 | continue; | ||
| 1594 | } | ||
| 1595 | |||
| 1596 | if ($comparison === Comparison::NEQ && $value === null) { | ||
| 1597 | $selectedColumns[] = $column . ' IS NOT NULL'; | ||
| 1598 | |||
| 1599 | continue; | ||
| 1600 | } | ||
| 1601 | |||
| 1602 | $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); | ||
| 1603 | |||
| 1604 | continue; | ||
| 1605 | } | ||
| 1606 | |||
| 1607 | if (is_array($value)) { | ||
| 1608 | $in = sprintf('%s IN (%s)', $column, $placeholder); | ||
| 1609 | |||
| 1610 | if (array_search(null, $value, true) !== false) { | ||
| 1611 | $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); | ||
| 1612 | |||
| 1613 | continue; | ||
| 1614 | } | ||
| 1615 | |||
| 1616 | $selectedColumns[] = $in; | ||
| 1617 | |||
| 1618 | continue; | ||
| 1619 | } | ||
| 1620 | |||
| 1621 | if ($value === null) { | ||
| 1622 | $selectedColumns[] = sprintf('%s IS NULL', $column); | ||
| 1623 | |||
| 1624 | continue; | ||
| 1625 | } | ||
| 1626 | |||
| 1627 | $selectedColumns[] = sprintf('%s = %s', $column, $placeholder); | ||
| 1628 | } | ||
| 1629 | |||
| 1630 | return implode(' AND ', $selectedColumns); | ||
| 1631 | } | ||
| 1632 | |||
| 1633 | /** | ||
| 1634 | * Builds the left-hand-side of a where condition statement. | ||
| 1635 | * | ||
| 1636 | * @return string[] | ||
| 1637 | * @psalm-return list<string> | ||
| 1638 | * | ||
| 1639 | * @throws InvalidFindByCall | ||
| 1640 | * @throws UnrecognizedField | ||
| 1641 | */ | ||
| 1642 | private function getSelectConditionStatementColumnSQL( | ||
| 1643 | string $field, | ||
| 1644 | AssociationMapping|null $assoc = null, | ||
| 1645 | ): array { | ||
| 1646 | if (isset($this->class->fieldMappings[$field])) { | ||
| 1647 | $className = $this->class->fieldMappings[$field]->inherited ?? $this->class->name; | ||
| 1648 | |||
| 1649 | return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)]; | ||
| 1650 | } | ||
| 1651 | |||
| 1652 | if (isset($this->class->associationMappings[$field])) { | ||
| 1653 | $association = $this->class->associationMappings[$field]; | ||
| 1654 | // Many-To-Many requires join table check for joinColumn | ||
| 1655 | $columns = []; | ||
| 1656 | $class = $this->class; | ||
| 1657 | |||
| 1658 | if ($association->isManyToMany()) { | ||
| 1659 | assert($assoc !== null); | ||
| 1660 | if (! $association->isOwningSide()) { | ||
| 1661 | $association = $assoc; | ||
| 1662 | } | ||
| 1663 | |||
| 1664 | assert($association->isManyToManyOwningSide()); | ||
| 1665 | |||
| 1666 | $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform); | ||
| 1667 | $joinColumns = $assoc->isOwningSide() | ||
| 1668 | ? $association->joinTable->joinColumns | ||
| 1669 | : $association->joinTable->inverseJoinColumns; | ||
| 1670 | |||
| 1671 | foreach ($joinColumns as $joinColumn) { | ||
| 1672 | $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform); | ||
| 1673 | } | ||
| 1674 | } else { | ||
| 1675 | if (! $association->isOwningSide()) { | ||
| 1676 | throw InvalidFindByCall::fromInverseSideUsage( | ||
| 1677 | $this->class->name, | ||
| 1678 | $field, | ||
| 1679 | ); | ||
| 1680 | } | ||
| 1681 | |||
| 1682 | assert($association->isToOneOwningSide()); | ||
| 1683 | |||
| 1684 | $className = $association->inherited ?? $this->class->name; | ||
| 1685 | |||
| 1686 | foreach ($association->joinColumns as $joinColumn) { | ||
| 1687 | $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform); | ||
| 1688 | } | ||
| 1689 | } | ||
| 1690 | |||
| 1691 | return $columns; | ||
| 1692 | } | ||
| 1693 | |||
| 1694 | if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) { | ||
| 1695 | // very careless developers could potentially open up this normally hidden api for userland attacks, | ||
| 1696 | // therefore checking for spaces and function calls which are not allowed. | ||
| 1697 | |||
| 1698 | // found a join column condition, not really a "field" | ||
| 1699 | return [$field]; | ||
| 1700 | } | ||
| 1701 | |||
| 1702 | throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field); | ||
| 1703 | } | ||
| 1704 | |||
| 1705 | /** | ||
| 1706 | * Gets the conditional SQL fragment used in the WHERE clause when selecting | ||
| 1707 | * entities in this persister. | ||
| 1708 | * | ||
| 1709 | * Subclasses are supposed to override this method if they intend to change | ||
| 1710 | * or alter the criteria by which entities are selected. | ||
| 1711 | * | ||
| 1712 | * @psalm-param array<string, mixed> $criteria | ||
| 1713 | */ | ||
| 1714 | protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string | ||
| 1715 | { | ||
| 1716 | $conditions = []; | ||
| 1717 | |||
| 1718 | foreach ($criteria as $field => $value) { | ||
| 1719 | $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc); | ||
| 1720 | } | ||
| 1721 | |||
| 1722 | return implode(' AND ', $conditions); | ||
| 1723 | } | ||
| 1724 | |||
| 1725 | /** | ||
| 1726 | * {@inheritDoc} | ||
| 1727 | */ | ||
| 1728 | public function getOneToManyCollection( | ||
| 1729 | AssociationMapping $assoc, | ||
| 1730 | object $sourceEntity, | ||
| 1731 | int|null $offset = null, | ||
| 1732 | int|null $limit = null, | ||
| 1733 | ): array { | ||
| 1734 | assert($assoc instanceof OneToManyAssociationMapping); | ||
| 1735 | $this->switchPersisterContext($offset, $limit); | ||
| 1736 | |||
| 1737 | $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit); | ||
| 1738 | |||
| 1739 | return $this->loadArrayFromResult($assoc, $stmt); | ||
| 1740 | } | ||
| 1741 | |||
| 1742 | public function loadOneToManyCollection( | ||
| 1743 | AssociationMapping $assoc, | ||
| 1744 | object $sourceEntity, | ||
| 1745 | PersistentCollection $collection, | ||
| 1746 | ): mixed { | ||
| 1747 | assert($assoc instanceof OneToManyAssociationMapping); | ||
| 1748 | $stmt = $this->getOneToManyStatement($assoc, $sourceEntity); | ||
| 1749 | |||
| 1750 | return $this->loadCollectionFromStatement($assoc, $stmt, $collection); | ||
| 1751 | } | ||
| 1752 | |||
| 1753 | /** Builds criteria and execute SQL statement to fetch the one to many entities from. */ | ||
| 1754 | private function getOneToManyStatement( | ||
| 1755 | OneToManyAssociationMapping $assoc, | ||
| 1756 | object $sourceEntity, | ||
| 1757 | int|null $offset = null, | ||
| 1758 | int|null $limit = null, | ||
| 1759 | ): Result { | ||
| 1760 | $this->switchPersisterContext($offset, $limit); | ||
| 1761 | |||
| 1762 | $criteria = []; | ||
| 1763 | $parameters = []; | ||
| 1764 | $owningAssoc = $this->class->associationMappings[$assoc->mappedBy]; | ||
| 1765 | $sourceClass = $this->em->getClassMetadata($assoc->sourceEntity); | ||
| 1766 | $tableAlias = $this->getSQLTableAlias($owningAssoc->inherited ?? $this->class->name); | ||
| 1767 | assert($owningAssoc->isManyToOne()); | ||
| 1768 | |||
| 1769 | foreach ($owningAssoc->targetToSourceKeyColumns as $sourceKeyColumn => $targetKeyColumn) { | ||
| 1770 | if ($sourceClass->containsForeignIdentifier) { | ||
| 1771 | $field = $sourceClass->getFieldForColumn($sourceKeyColumn); | ||
| 1772 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1773 | |||
| 1774 | if (isset($sourceClass->associationMappings[$field])) { | ||
| 1775 | $value = $this->em->getUnitOfWork()->getEntityIdentifier($value); | ||
| 1776 | $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]->targetEntity)->identifier[0]]; | ||
| 1777 | } | ||
| 1778 | |||
| 1779 | $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; | ||
| 1780 | $parameters[] = [ | ||
| 1781 | 'value' => $value, | ||
| 1782 | 'field' => $field, | ||
| 1783 | 'class' => $sourceClass, | ||
| 1784 | ]; | ||
| 1785 | |||
| 1786 | continue; | ||
| 1787 | } | ||
| 1788 | |||
| 1789 | $field = $sourceClass->fieldNames[$sourceKeyColumn]; | ||
| 1790 | $value = $sourceClass->reflFields[$field]->getValue($sourceEntity); | ||
| 1791 | |||
| 1792 | $criteria[$tableAlias . '.' . $targetKeyColumn] = $value; | ||
| 1793 | $parameters[] = [ | ||
| 1794 | 'value' => $value, | ||
| 1795 | 'field' => $field, | ||
| 1796 | 'class' => $sourceClass, | ||
| 1797 | ]; | ||
| 1798 | } | ||
| 1799 | |||
| 1800 | $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset); | ||
| 1801 | [$params, $types] = $this->expandToManyParameters($parameters); | ||
| 1802 | |||
| 1803 | return $this->conn->executeQuery($sql, $params, $types); | ||
| 1804 | } | ||
| 1805 | |||
| 1806 | /** | ||
| 1807 | * {@inheritDoc} | ||
| 1808 | */ | ||
| 1809 | public function expandParameters(array $criteria): array | ||
| 1810 | { | ||
| 1811 | $params = []; | ||
| 1812 | $types = []; | ||
| 1813 | |||
| 1814 | foreach ($criteria as $field => $value) { | ||
| 1815 | if ($value === null) { | ||
| 1816 | continue; // skip null values. | ||
| 1817 | } | ||
| 1818 | |||
| 1819 | $types = [...$types, ...$this->getTypes($field, $value, $this->class)]; | ||
| 1820 | $params = array_merge($params, $this->getValues($value)); | ||
| 1821 | } | ||
| 1822 | |||
| 1823 | return [$params, $types]; | ||
| 1824 | } | ||
| 1825 | |||
| 1826 | /** | ||
| 1827 | * Expands the parameters from the given criteria and use the correct binding types if found, | ||
| 1828 | * specialized for OneToMany or ManyToMany associations. | ||
| 1829 | * | ||
| 1830 | * @param mixed[][] $criteria an array of arrays containing following: | ||
| 1831 | * - field to which each criterion will be bound | ||
| 1832 | * - value to be bound | ||
| 1833 | * - class to which the field belongs to | ||
| 1834 | * | ||
| 1835 | * @return mixed[][] | ||
| 1836 | * @psalm-return array{0: array, 1: list<ParameterType::*|ArrayParameterType::*|string>} | ||
| 1837 | */ | ||
| 1838 | private function expandToManyParameters(array $criteria): array | ||
| 1839 | { | ||
| 1840 | $params = []; | ||
| 1841 | $types = []; | ||
| 1842 | |||
| 1843 | foreach ($criteria as $criterion) { | ||
| 1844 | if ($criterion['value'] === null) { | ||
| 1845 | continue; // skip null values. | ||
| 1846 | } | ||
| 1847 | |||
| 1848 | $types = [...$types, ...$this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])]; | ||
| 1849 | $params = array_merge($params, $this->getValues($criterion['value'])); | ||
| 1850 | } | ||
| 1851 | |||
| 1852 | return [$params, $types]; | ||
| 1853 | } | ||
| 1854 | |||
| 1855 | /** | ||
| 1856 | * Infers field types to be used by parameter type casting. | ||
| 1857 | * | ||
| 1858 | * @return list<ParameterType|ArrayParameterType|int|string> | ||
| 1859 | * @psalm-return list<ParameterType::*|ArrayParameterType::*|string> | ||
| 1860 | * | ||
| 1861 | * @throws QueryException | ||
| 1862 | */ | ||
| 1863 | private function getTypes(string $field, mixed $value, ClassMetadata $class): array | ||
| 1864 | { | ||
| 1865 | $types = []; | ||
| 1866 | |||
| 1867 | switch (true) { | ||
| 1868 | case isset($class->fieldMappings[$field]): | ||
| 1869 | $types = array_merge($types, [$class->fieldMappings[$field]->type]); | ||
| 1870 | break; | ||
| 1871 | |||
| 1872 | case isset($class->associationMappings[$field]): | ||
| 1873 | $assoc = $this->em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]); | ||
| 1874 | $class = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 1875 | |||
| 1876 | if ($assoc->isManyToManyOwningSide()) { | ||
| 1877 | $columns = $assoc->relationToTargetKeyColumns; | ||
| 1878 | } else { | ||
| 1879 | assert($assoc->isToOneOwningSide()); | ||
| 1880 | $columns = $assoc->sourceToTargetKeyColumns; | ||
| 1881 | } | ||
| 1882 | |||
| 1883 | foreach ($columns as $column) { | ||
| 1884 | $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em); | ||
| 1885 | } | ||
| 1886 | |||
| 1887 | break; | ||
| 1888 | |||
| 1889 | default: | ||
| 1890 | $types[] = ParameterType::STRING; | ||
| 1891 | break; | ||
| 1892 | } | ||
| 1893 | |||
| 1894 | if (is_array($value)) { | ||
| 1895 | return array_map($this->getArrayBindingType(...), $types); | ||
| 1896 | } | ||
| 1897 | |||
| 1898 | return $types; | ||
| 1899 | } | ||
| 1900 | |||
| 1901 | /** @psalm-return ArrayParameterType::* */ | ||
| 1902 | private function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int | ||
| 1903 | { | ||
| 1904 | if (! $type instanceof ParameterType) { | ||
| 1905 | $type = Type::getType((string) $type)->getBindingType(); | ||
| 1906 | } | ||
| 1907 | |||
| 1908 | return match ($type) { | ||
| 1909 | ParameterType::STRING => ArrayParameterType::STRING, | ||
| 1910 | ParameterType::INTEGER => ArrayParameterType::INTEGER, | ||
| 1911 | ParameterType::ASCII => ArrayParameterType::ASCII, | ||
| 1912 | }; | ||
| 1913 | } | ||
| 1914 | |||
| 1915 | /** | ||
| 1916 | * Retrieves the parameters that identifies a value. | ||
| 1917 | * | ||
| 1918 | * @return mixed[] | ||
| 1919 | */ | ||
| 1920 | private function getValues(mixed $value): array | ||
| 1921 | { | ||
| 1922 | if (is_array($value)) { | ||
| 1923 | $newValue = []; | ||
| 1924 | |||
| 1925 | foreach ($value as $itemValue) { | ||
| 1926 | $newValue = array_merge($newValue, $this->getValues($itemValue)); | ||
| 1927 | } | ||
| 1928 | |||
| 1929 | return [$newValue]; | ||
| 1930 | } | ||
| 1931 | |||
| 1932 | return $this->getIndividualValue($value); | ||
| 1933 | } | ||
| 1934 | |||
| 1935 | /** | ||
| 1936 | * Retrieves an individual parameter value. | ||
| 1937 | * | ||
| 1938 | * @psalm-return list<mixed> | ||
| 1939 | */ | ||
| 1940 | private function getIndividualValue(mixed $value): array | ||
| 1941 | { | ||
| 1942 | if (! is_object($value)) { | ||
| 1943 | return [$value]; | ||
| 1944 | } | ||
| 1945 | |||
| 1946 | if ($value instanceof BackedEnum) { | ||
| 1947 | return [$value->value]; | ||
| 1948 | } | ||
| 1949 | |||
| 1950 | $valueClass = DefaultProxyClassNameResolver::getClass($value); | ||
| 1951 | |||
| 1952 | if ($this->em->getMetadataFactory()->isTransient($valueClass)) { | ||
| 1953 | return [$value]; | ||
| 1954 | } | ||
| 1955 | |||
| 1956 | $class = $this->em->getClassMetadata($valueClass); | ||
| 1957 | |||
| 1958 | if ($class->isIdentifierComposite) { | ||
| 1959 | $newValue = []; | ||
| 1960 | |||
| 1961 | foreach ($class->getIdentifierValues($value) as $innerValue) { | ||
| 1962 | $newValue = array_merge($newValue, $this->getValues($innerValue)); | ||
| 1963 | } | ||
| 1964 | |||
| 1965 | return $newValue; | ||
| 1966 | } | ||
| 1967 | |||
| 1968 | return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)]; | ||
| 1969 | } | ||
| 1970 | |||
| 1971 | public function exists(object $entity, Criteria|null $extraConditions = null): bool | ||
| 1972 | { | ||
| 1973 | $criteria = $this->class->getIdentifierValues($entity); | ||
| 1974 | |||
| 1975 | if (! $criteria) { | ||
| 1976 | return false; | ||
| 1977 | } | ||
| 1978 | |||
| 1979 | $alias = $this->getSQLTableAlias($this->class->name); | ||
| 1980 | |||
| 1981 | $sql = 'SELECT 1 ' | ||
| 1982 | . $this->getLockTablesSql(LockMode::NONE) | ||
| 1983 | . ' WHERE ' . $this->getSelectConditionSQL($criteria); | ||
| 1984 | |||
| 1985 | [$params, $types] = $this->expandParameters($criteria); | ||
| 1986 | |||
| 1987 | if ($extraConditions !== null) { | ||
| 1988 | $sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions); | ||
| 1989 | [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions); | ||
| 1990 | |||
| 1991 | $params = [...$params, ...$criteriaParams]; | ||
| 1992 | $types = [...$types, ...$criteriaTypes]; | ||
| 1993 | } | ||
| 1994 | |||
| 1995 | $filterSql = $this->generateFilterConditionSQL($this->class, $alias); | ||
| 1996 | if ($filterSql) { | ||
| 1997 | $sql .= ' AND ' . $filterSql; | ||
| 1998 | } | ||
| 1999 | |||
| 2000 | return (bool) $this->conn->fetchOne($sql, $params, $types); | ||
| 2001 | } | ||
| 2002 | |||
| 2003 | /** | ||
| 2004 | * Generates the appropriate join SQL for the given join column. | ||
| 2005 | * | ||
| 2006 | * @param list<JoinColumnMapping> $joinColumns The join columns definition of an association. | ||
| 2007 | * | ||
| 2008 | * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise. | ||
| 2009 | */ | ||
| 2010 | protected function getJoinSQLForJoinColumns(array $joinColumns): string | ||
| 2011 | { | ||
| 2012 | // if one of the join columns is nullable, return left join | ||
| 2013 | foreach ($joinColumns as $joinColumn) { | ||
| 2014 | if (! isset($joinColumn->nullable) || $joinColumn->nullable) { | ||
| 2015 | return 'LEFT JOIN'; | ||
| 2016 | } | ||
| 2017 | } | ||
| 2018 | |||
| 2019 | return 'INNER JOIN'; | ||
| 2020 | } | ||
| 2021 | |||
| 2022 | public function getSQLColumnAlias(string $columnName): string | ||
| 2023 | { | ||
| 2024 | return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform); | ||
| 2025 | } | ||
| 2026 | |||
| 2027 | /** | ||
| 2028 | * Generates the filter SQL for a given entity and table alias. | ||
| 2029 | * | ||
| 2030 | * @param ClassMetadata $targetEntity Metadata of the target entity. | ||
| 2031 | * @param string $targetTableAlias The table alias of the joined/selected table. | ||
| 2032 | * | ||
| 2033 | * @return string The SQL query part to add to a query. | ||
| 2034 | */ | ||
| 2035 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string | ||
| 2036 | { | ||
| 2037 | $filterClauses = []; | ||
| 2038 | |||
| 2039 | foreach ($this->em->getFilters()->getEnabledFilters() as $filter) { | ||
| 2040 | $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias); | ||
| 2041 | if ($filterExpr !== '') { | ||
| 2042 | $filterClauses[] = '(' . $filterExpr . ')'; | ||
| 2043 | } | ||
| 2044 | } | ||
| 2045 | |||
| 2046 | $sql = implode(' AND ', $filterClauses); | ||
| 2047 | |||
| 2048 | return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL" | ||
| 2049 | } | ||
| 2050 | |||
| 2051 | /** | ||
| 2052 | * Switches persister context according to current query offset/limits | ||
| 2053 | * | ||
| 2054 | * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved | ||
| 2055 | */ | ||
| 2056 | protected function switchPersisterContext(int|null $offset, int|null $limit): void | ||
| 2057 | { | ||
| 2058 | if ($offset === null && $limit === null) { | ||
| 2059 | $this->currentPersisterContext = $this->noLimitsContext; | ||
| 2060 | |||
| 2061 | return; | ||
| 2062 | } | ||
| 2063 | |||
| 2064 | $this->currentPersisterContext = $this->limitsHandlingContext; | ||
| 2065 | } | ||
| 2066 | |||
| 2067 | /** | ||
| 2068 | * @return string[] | ||
| 2069 | * @psalm-return list<string> | ||
| 2070 | */ | ||
| 2071 | protected function getClassIdentifiersTypes(ClassMetadata $class): array | ||
| 2072 | { | ||
| 2073 | $entityManager = $this->em; | ||
| 2074 | |||
| 2075 | return array_map( | ||
| 2076 | static function ($fieldName) use ($class, $entityManager): string { | ||
| 2077 | $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager); | ||
| 2078 | assert(isset($types[0])); | ||
| 2079 | |||
| 2080 | return $types[0]; | ||
| 2081 | }, | ||
| 2082 | $class->identifier, | ||
| 2083 | ); | ||
| 2084 | } | ||
| 2085 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php b/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php new file mode 100644 index 0000000..03d053b --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/CachedPersisterContext.php | |||
| @@ -0,0 +1,60 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 8 | use Doctrine\Persistence\Mapping\ClassMetadata; | ||
| 9 | |||
| 10 | /** | ||
| 11 | * A swappable persister context to use as a container for the current | ||
| 12 | * generated query/resultSetMapping/type binding information. | ||
| 13 | * | ||
| 14 | * This class is a utility class to be used only by the persister API | ||
| 15 | * | ||
| 16 | * This object is highly mutable due to performance reasons. Same reasoning | ||
| 17 | * behind its properties being public. | ||
| 18 | */ | ||
| 19 | class CachedPersisterContext | ||
| 20 | { | ||
| 21 | /** | ||
| 22 | * The SELECT column list SQL fragment used for querying entities by this persister. | ||
| 23 | * This SQL fragment is only generated once per request, if at all. | ||
| 24 | */ | ||
| 25 | public string|null $selectColumnListSql = null; | ||
| 26 | |||
| 27 | /** | ||
| 28 | * The JOIN SQL fragment used to eagerly load all many-to-one and one-to-one | ||
| 29 | * associations configured as FETCH_EAGER, as well as all inverse one-to-one associations. | ||
| 30 | */ | ||
| 31 | public string|null $selectJoinSql = null; | ||
| 32 | |||
| 33 | /** | ||
| 34 | * Counter for creating unique SQL table and column aliases. | ||
| 35 | */ | ||
| 36 | public int $sqlAliasCounter = 0; | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Map from class names (FQCN) to the corresponding generated SQL table aliases. | ||
| 40 | * | ||
| 41 | * @var array<class-string, string> | ||
| 42 | */ | ||
| 43 | public array $sqlTableAliases = []; | ||
| 44 | |||
| 45 | public function __construct( | ||
| 46 | /** | ||
| 47 | * Metadata object that describes the mapping of the mapped entity class. | ||
| 48 | */ | ||
| 49 | public ClassMetadata $class, | ||
| 50 | /** | ||
| 51 | * ResultSetMapping that is used for all queries. Is generated lazily once per request. | ||
| 52 | */ | ||
| 53 | public ResultSetMapping $rsm, | ||
| 54 | /** | ||
| 55 | * Whether this persistent context is considering limit operations applied to the selection queries | ||
| 56 | */ | ||
| 57 | public bool $handlesLimits, | ||
| 58 | ) { | ||
| 59 | } | ||
| 60 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php new file mode 100644 index 0000000..6b278a7 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/EntityPersister.php | |||
| @@ -0,0 +1,298 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\DBAL\ArrayParameterType; | ||
| 9 | use Doctrine\DBAL\LockMode; | ||
| 10 | use Doctrine\DBAL\ParameterType; | ||
| 11 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 12 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 13 | use Doctrine\ORM\Mapping\MappingException; | ||
| 14 | use Doctrine\ORM\PersistentCollection; | ||
| 15 | use Doctrine\ORM\Query\ResultSetMapping; | ||
| 16 | |||
| 17 | /** | ||
| 18 | * Entity persister interface | ||
| 19 | * Define the behavior that should be implemented by all entity persisters. | ||
| 20 | */ | ||
| 21 | interface EntityPersister | ||
| 22 | { | ||
| 23 | public function getClassMetadata(): ClassMetadata; | ||
| 24 | |||
| 25 | /** | ||
| 26 | * Gets the ResultSetMapping used for hydration. | ||
| 27 | */ | ||
| 28 | public function getResultSetMapping(): ResultSetMapping; | ||
| 29 | |||
| 30 | /** | ||
| 31 | * Get all queued inserts. | ||
| 32 | * | ||
| 33 | * @return object[] | ||
| 34 | */ | ||
| 35 | public function getInserts(): array; | ||
| 36 | |||
| 37 | /** | ||
| 38 | * Gets the INSERT SQL used by the persister to persist a new entity. | ||
| 39 | * | ||
| 40 | * @TODO It should not be here. | ||
| 41 | * But its necessary since JoinedSubclassPersister#executeInserts invoke the root persister. | ||
| 42 | */ | ||
| 43 | public function getInsertSQL(): string; | ||
| 44 | |||
| 45 | /** | ||
| 46 | * Gets the SELECT SQL to select one or more entities by a set of field criteria. | ||
| 47 | * | ||
| 48 | * @param mixed[]|Criteria $criteria | ||
| 49 | * @param mixed[]|null $orderBy | ||
| 50 | * @psalm-param AssociationMapping|null $assoc | ||
| 51 | * @psalm-param LockMode::*|null $lockMode | ||
| 52 | */ | ||
| 53 | public function getSelectSQL( | ||
| 54 | array|Criteria $criteria, | ||
| 55 | AssociationMapping|null $assoc = null, | ||
| 56 | LockMode|int|null $lockMode = null, | ||
| 57 | int|null $limit = null, | ||
| 58 | int|null $offset = null, | ||
| 59 | array|null $orderBy = null, | ||
| 60 | ): string; | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Get the COUNT SQL to count entities (optionally based on a criteria) | ||
| 64 | * | ||
| 65 | * @param mixed[]|Criteria $criteria | ||
| 66 | */ | ||
| 67 | public function getCountSQL(array|Criteria $criteria = []): string; | ||
| 68 | |||
| 69 | /** | ||
| 70 | * Expands the parameters from the given criteria and use the correct binding types if found. | ||
| 71 | * | ||
| 72 | * @param string[] $criteria | ||
| 73 | * | ||
| 74 | * @psalm-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>} | ||
| 75 | */ | ||
| 76 | public function expandParameters(array $criteria): array; | ||
| 77 | |||
| 78 | /** | ||
| 79 | * Expands Criteria Parameters by walking the expressions and grabbing all parameters and types from it. | ||
| 80 | * | ||
| 81 | * @psalm-return array{list<mixed>, list<ParameterType::*|ArrayParameterType::*|string>} | ||
| 82 | */ | ||
| 83 | public function expandCriteriaParameters(Criteria $criteria): array; | ||
| 84 | |||
| 85 | /** Gets the SQL WHERE condition for matching a field with a given value. */ | ||
| 86 | public function getSelectConditionStatementSQL( | ||
| 87 | string $field, | ||
| 88 | mixed $value, | ||
| 89 | AssociationMapping|null $assoc = null, | ||
| 90 | string|null $comparison = null, | ||
| 91 | ): string; | ||
| 92 | |||
| 93 | /** | ||
| 94 | * Adds an entity to the queued insertions. | ||
| 95 | * The entity remains queued until {@link executeInserts} is invoked. | ||
| 96 | */ | ||
| 97 | public function addInsert(object $entity): void; | ||
| 98 | |||
| 99 | /** | ||
| 100 | * Executes all queued entity insertions. | ||
| 101 | * | ||
| 102 | * If no inserts are queued, invoking this method is a NOOP. | ||
| 103 | */ | ||
| 104 | public function executeInserts(): void; | ||
| 105 | |||
| 106 | /** | ||
| 107 | * Updates a managed entity. The entity is updated according to its current changeset | ||
| 108 | * in the running UnitOfWork. If there is no changeset, nothing is updated. | ||
| 109 | */ | ||
| 110 | public function update(object $entity): void; | ||
| 111 | |||
| 112 | /** | ||
| 113 | * Deletes a managed entity. | ||
| 114 | * | ||
| 115 | * The entity to delete must be managed and have a persistent identifier. | ||
| 116 | * The deletion happens instantaneously. | ||
| 117 | * | ||
| 118 | * Subclasses may override this method to customize the semantics of entity deletion. | ||
| 119 | * | ||
| 120 | * @return bool TRUE if the entity got deleted in the database, FALSE otherwise. | ||
| 121 | */ | ||
| 122 | public function delete(object $entity): bool; | ||
| 123 | |||
| 124 | /** | ||
| 125 | * Count entities (optionally filtered by a criteria) | ||
| 126 | * | ||
| 127 | * @param mixed[]|Criteria $criteria | ||
| 128 | */ | ||
| 129 | public function count(array|Criteria $criteria = []): int; | ||
| 130 | |||
| 131 | /** | ||
| 132 | * Gets the name of the table that owns the column the given field is mapped to. | ||
| 133 | * | ||
| 134 | * The default implementation in BasicEntityPersister always returns the name | ||
| 135 | * of the table the entity type of this persister is mapped to, since an entity | ||
| 136 | * is always persisted to a single table with a BasicEntityPersister. | ||
| 137 | */ | ||
| 138 | public function getOwningTable(string $fieldName): string; | ||
| 139 | |||
| 140 | /** | ||
| 141 | * Loads an entity by a list of field criteria. | ||
| 142 | * | ||
| 143 | * @param mixed[] $criteria The criteria by which to load the entity. | ||
| 144 | * @param object|null $entity The entity to load the data into. If not specified, | ||
| 145 | * a new entity is created. | ||
| 146 | * @param AssociationMapping|null $assoc The association that connects the entity | ||
| 147 | * to load to another entity, if any. | ||
| 148 | * @param mixed[] $hints Hints for entity creation. | ||
| 149 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 150 | * or NULL if no specific lock mode should be used | ||
| 151 | * for loading the entity. | ||
| 152 | * @param int|null $limit Limit number of results. | ||
| 153 | * @param string[]|null $orderBy Criteria to order by. | ||
| 154 | * @psalm-param array<string, mixed> $criteria | ||
| 155 | * @psalm-param array<string, mixed> $hints | ||
| 156 | * @psalm-param LockMode::*|null $lockMode | ||
| 157 | * @psalm-param array<string, string>|null $orderBy | ||
| 158 | * | ||
| 159 | * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. | ||
| 160 | * | ||
| 161 | * @todo Check identity map? loadById method? Try to guess whether $criteria is the id? | ||
| 162 | */ | ||
| 163 | public function load( | ||
| 164 | array $criteria, | ||
| 165 | object|null $entity = null, | ||
| 166 | AssociationMapping|null $assoc = null, | ||
| 167 | array $hints = [], | ||
| 168 | LockMode|int|null $lockMode = null, | ||
| 169 | int|null $limit = null, | ||
| 170 | array|null $orderBy = null, | ||
| 171 | ): object|null; | ||
| 172 | |||
| 173 | /** | ||
| 174 | * Loads an entity by identifier. | ||
| 175 | * | ||
| 176 | * @param object|null $entity The entity to load the data into. If not specified, a new entity is created. | ||
| 177 | * @psalm-param array<string, mixed> $identifier The entity identifier. | ||
| 178 | * | ||
| 179 | * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. | ||
| 180 | * | ||
| 181 | * @todo Check parameters | ||
| 182 | */ | ||
| 183 | public function loadById(array $identifier, object|null $entity = null): object|null; | ||
| 184 | |||
| 185 | /** | ||
| 186 | * Loads an entity of this persister's mapped class as part of a single-valued | ||
| 187 | * association from another entity. | ||
| 188 | * | ||
| 189 | * @param AssociationMapping $assoc The association to load. | ||
| 190 | * @param object $sourceEntity The entity that owns the association (not necessarily the "owning side"). | ||
| 191 | * @psalm-param array<string, mixed> $identifier The identifier of the entity to load. Must be provided if | ||
| 192 | * the association to load represents the owning side, otherwise | ||
| 193 | * the identifier is derived from the $sourceEntity. | ||
| 194 | * | ||
| 195 | * @return object|null The loaded and managed entity instance or NULL if the entity can not be found. | ||
| 196 | * | ||
| 197 | * @throws MappingException | ||
| 198 | */ | ||
| 199 | public function loadOneToOneEntity(AssociationMapping $assoc, object $sourceEntity, array $identifier = []): object|null; | ||
| 200 | |||
| 201 | /** | ||
| 202 | * Refreshes a managed entity. | ||
| 203 | * | ||
| 204 | * @param LockMode|int|null $lockMode One of the \Doctrine\DBAL\LockMode::* constants | ||
| 205 | * or NULL if no specific lock mode should be used | ||
| 206 | * for refreshing the managed entity. | ||
| 207 | * @psalm-param array<string, mixed> $id The identifier of the entity as an | ||
| 208 | * associative array from column or | ||
| 209 | * field names to values. | ||
| 210 | * @psalm-param LockMode::*|null $lockMode | ||
| 211 | */ | ||
| 212 | public function refresh(array $id, object $entity, LockMode|int|null $lockMode = null): void; | ||
| 213 | |||
| 214 | /** | ||
| 215 | * Loads Entities matching the given Criteria object. | ||
| 216 | * | ||
| 217 | * @return mixed[] | ||
| 218 | */ | ||
| 219 | public function loadCriteria(Criteria $criteria): array; | ||
| 220 | |||
| 221 | /** | ||
| 222 | * Loads a list of entities by a list of field criteria. | ||
| 223 | * | ||
| 224 | * @psalm-param array<string, string>|null $orderBy | ||
| 225 | * @psalm-param array<string, mixed> $criteria | ||
| 226 | * | ||
| 227 | * @return mixed[] | ||
| 228 | */ | ||
| 229 | public function loadAll( | ||
| 230 | array $criteria = [], | ||
| 231 | array|null $orderBy = null, | ||
| 232 | int|null $limit = null, | ||
| 233 | int|null $offset = null, | ||
| 234 | ): array; | ||
| 235 | |||
| 236 | /** | ||
| 237 | * Gets (sliced or full) elements of the given collection. | ||
| 238 | * | ||
| 239 | * @return mixed[] | ||
| 240 | */ | ||
| 241 | public function getManyToManyCollection( | ||
| 242 | AssociationMapping $assoc, | ||
| 243 | object $sourceEntity, | ||
| 244 | int|null $offset = null, | ||
| 245 | int|null $limit = null, | ||
| 246 | ): array; | ||
| 247 | |||
| 248 | /** | ||
| 249 | * Loads a collection of entities of a many-to-many association. | ||
| 250 | * | ||
| 251 | * @param AssociationMapping $assoc The association mapping of the association being loaded. | ||
| 252 | * @param object $sourceEntity The entity that owns the collection. | ||
| 253 | * @param PersistentCollection $collection The collection to fill. | ||
| 254 | * | ||
| 255 | * @return mixed[] | ||
| 256 | */ | ||
| 257 | public function loadManyToManyCollection( | ||
| 258 | AssociationMapping $assoc, | ||
| 259 | object $sourceEntity, | ||
| 260 | PersistentCollection $collection, | ||
| 261 | ): array; | ||
| 262 | |||
| 263 | /** | ||
| 264 | * Loads a collection of entities in a one-to-many association. | ||
| 265 | * | ||
| 266 | * @param PersistentCollection $collection The collection to load/fill. | ||
| 267 | */ | ||
| 268 | public function loadOneToManyCollection( | ||
| 269 | AssociationMapping $assoc, | ||
| 270 | object $sourceEntity, | ||
| 271 | PersistentCollection $collection, | ||
| 272 | ): mixed; | ||
| 273 | |||
| 274 | /** | ||
| 275 | * Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode. | ||
| 276 | * | ||
| 277 | * @psalm-param array<string, mixed> $criteria | ||
| 278 | * @psalm-param LockMode::* $lockMode | ||
| 279 | */ | ||
| 280 | public function lock(array $criteria, LockMode|int $lockMode): void; | ||
| 281 | |||
| 282 | /** | ||
| 283 | * Returns an array with (sliced or full list) of elements in the specified collection. | ||
| 284 | * | ||
| 285 | * @return mixed[] | ||
| 286 | */ | ||
| 287 | public function getOneToManyCollection( | ||
| 288 | AssociationMapping $assoc, | ||
| 289 | object $sourceEntity, | ||
| 290 | int|null $offset = null, | ||
| 291 | int|null $limit = null, | ||
| 292 | ): array; | ||
| 293 | |||
| 294 | /** | ||
| 295 | * Checks whether the given managed entity exists in the database. | ||
| 296 | */ | ||
| 297 | public function exists(object $entity, Criteria|null $extraConditions = null): bool; | ||
| 298 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php new file mode 100644 index 0000000..76719a2 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/JoinedSubclassPersister.php | |||
| @@ -0,0 +1,601 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\DBAL\LockMode; | ||
| 9 | use Doctrine\DBAL\Types\Type; | ||
| 10 | use Doctrine\DBAL\Types\Types; | ||
| 11 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 12 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 13 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 14 | use Doctrine\ORM\Utility\LockSqlHelper; | ||
| 15 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 16 | use LengthException; | ||
| 17 | |||
| 18 | use function array_combine; | ||
| 19 | use function array_keys; | ||
| 20 | use function array_values; | ||
| 21 | use function implode; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * The joined subclass persister maps a single entity instance to several tables in the | ||
| 25 | * database as it is defined by the <tt>Class Table Inheritance</tt> strategy. | ||
| 26 | * | ||
| 27 | * @see https://martinfowler.com/eaaCatalog/classTableInheritance.html | ||
| 28 | */ | ||
| 29 | class JoinedSubclassPersister extends AbstractEntityInheritancePersister | ||
| 30 | { | ||
| 31 | use LockSqlHelper; | ||
| 32 | use SQLResultCasing; | ||
| 33 | |||
| 34 | /** | ||
| 35 | * Map that maps column names to the table names that own them. | ||
| 36 | * This is mainly a temporary cache, used during a single request. | ||
| 37 | * | ||
| 38 | * @psalm-var array<string, string> | ||
| 39 | */ | ||
| 40 | private array $owningTableMap = []; | ||
| 41 | |||
| 42 | /** | ||
| 43 | * Map of table to quoted table names. | ||
| 44 | * | ||
| 45 | * @psalm-var array<string, string> | ||
| 46 | */ | ||
| 47 | private array $quotedTableMap = []; | ||
| 48 | |||
| 49 | protected function getDiscriminatorColumnTableName(): string | ||
| 50 | { | ||
| 51 | $class = $this->class->name !== $this->class->rootEntityName | ||
| 52 | ? $this->em->getClassMetadata($this->class->rootEntityName) | ||
| 53 | : $this->class; | ||
| 54 | |||
| 55 | return $class->getTableName(); | ||
| 56 | } | ||
| 57 | |||
| 58 | /** | ||
| 59 | * This function finds the ClassMetadata instance in an inheritance hierarchy | ||
| 60 | * that is responsible for enabling versioning. | ||
| 61 | */ | ||
| 62 | private function getVersionedClassMetadata(): ClassMetadata | ||
| 63 | { | ||
| 64 | if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) { | ||
| 65 | $definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited; | ||
| 66 | |||
| 67 | return $this->em->getClassMetadata($definingClassName); | ||
| 68 | } | ||
| 69 | |||
| 70 | return $this->class; | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Gets the name of the table that owns the column the given field is mapped to. | ||
| 75 | */ | ||
| 76 | public function getOwningTable(string $fieldName): string | ||
| 77 | { | ||
| 78 | if (isset($this->owningTableMap[$fieldName])) { | ||
| 79 | return $this->owningTableMap[$fieldName]; | ||
| 80 | } | ||
| 81 | |||
| 82 | $cm = match (true) { | ||
| 83 | isset($this->class->associationMappings[$fieldName]->inherited) | ||
| 84 | => $this->em->getClassMetadata($this->class->associationMappings[$fieldName]->inherited), | ||
| 85 | isset($this->class->fieldMappings[$fieldName]->inherited) | ||
| 86 | => $this->em->getClassMetadata($this->class->fieldMappings[$fieldName]->inherited), | ||
| 87 | default => $this->class, | ||
| 88 | }; | ||
| 89 | |||
| 90 | $tableName = $cm->getTableName(); | ||
| 91 | $quotedTableName = $this->quoteStrategy->getTableName($cm, $this->platform); | ||
| 92 | |||
| 93 | $this->owningTableMap[$fieldName] = $tableName; | ||
| 94 | $this->quotedTableMap[$tableName] = $quotedTableName; | ||
| 95 | |||
| 96 | return $tableName; | ||
| 97 | } | ||
| 98 | |||
| 99 | public function executeInserts(): void | ||
| 100 | { | ||
| 101 | if (! $this->queuedInserts) { | ||
| 102 | return; | ||
| 103 | } | ||
| 104 | |||
| 105 | $uow = $this->em->getUnitOfWork(); | ||
| 106 | $idGenerator = $this->class->idGenerator; | ||
| 107 | $isPostInsertId = $idGenerator->isPostInsertGenerator(); | ||
| 108 | $rootClass = $this->class->name !== $this->class->rootEntityName | ||
| 109 | ? $this->em->getClassMetadata($this->class->rootEntityName) | ||
| 110 | : $this->class; | ||
| 111 | |||
| 112 | // Prepare statement for the root table | ||
| 113 | $rootPersister = $this->em->getUnitOfWork()->getEntityPersister($rootClass->name); | ||
| 114 | $rootTableName = $rootClass->getTableName(); | ||
| 115 | $rootTableStmt = $this->conn->prepare($rootPersister->getInsertSQL()); | ||
| 116 | |||
| 117 | // Prepare statements for sub tables. | ||
| 118 | $subTableStmts = []; | ||
| 119 | |||
| 120 | if ($rootClass !== $this->class) { | ||
| 121 | $subTableStmts[$this->class->getTableName()] = $this->conn->prepare($this->getInsertSQL()); | ||
| 122 | } | ||
| 123 | |||
| 124 | foreach ($this->class->parentClasses as $parentClassName) { | ||
| 125 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 126 | $parentTableName = $parentClass->getTableName(); | ||
| 127 | |||
| 128 | if ($parentClass !== $rootClass) { | ||
| 129 | $parentPersister = $this->em->getUnitOfWork()->getEntityPersister($parentClassName); | ||
| 130 | $subTableStmts[$parentTableName] = $this->conn->prepare($parentPersister->getInsertSQL()); | ||
| 131 | } | ||
| 132 | } | ||
| 133 | |||
| 134 | // Execute all inserts. For each entity: | ||
| 135 | // 1) Insert on root table | ||
| 136 | // 2) Insert on sub tables | ||
| 137 | foreach ($this->queuedInserts as $entity) { | ||
| 138 | $insertData = $this->prepareInsertData($entity); | ||
| 139 | |||
| 140 | // Execute insert on root table | ||
| 141 | $paramIndex = 1; | ||
| 142 | |||
| 143 | foreach ($insertData[$rootTableName] as $columnName => $value) { | ||
| 144 | $rootTableStmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); | ||
| 145 | } | ||
| 146 | |||
| 147 | $rootTableStmt->executeStatement(); | ||
| 148 | |||
| 149 | if ($isPostInsertId) { | ||
| 150 | $generatedId = $idGenerator->generateId($this->em, $entity); | ||
| 151 | $id = [$this->class->identifier[0] => $generatedId]; | ||
| 152 | |||
| 153 | $uow->assignPostInsertId($entity, $generatedId); | ||
| 154 | } else { | ||
| 155 | $id = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 156 | } | ||
| 157 | |||
| 158 | // Execute inserts on subtables. | ||
| 159 | // The order doesn't matter because all child tables link to the root table via FK. | ||
| 160 | foreach ($subTableStmts as $tableName => $stmt) { | ||
| 161 | $paramIndex = 1; | ||
| 162 | $data = $insertData[$tableName] ?? []; | ||
| 163 | |||
| 164 | foreach ($id as $idName => $idVal) { | ||
| 165 | $type = $this->columnTypes[$idName] ?? Types::STRING; | ||
| 166 | |||
| 167 | $stmt->bindValue($paramIndex++, $idVal, $type); | ||
| 168 | } | ||
| 169 | |||
| 170 | foreach ($data as $columnName => $value) { | ||
| 171 | if (! isset($id[$columnName])) { | ||
| 172 | $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$columnName]); | ||
| 173 | } | ||
| 174 | } | ||
| 175 | |||
| 176 | $stmt->executeStatement(); | ||
| 177 | } | ||
| 178 | |||
| 179 | if ($this->class->requiresFetchAfterChange) { | ||
| 180 | $this->assignDefaultVersionAndUpsertableValues($entity, $id); | ||
| 181 | } | ||
| 182 | } | ||
| 183 | |||
| 184 | $this->queuedInserts = []; | ||
| 185 | } | ||
| 186 | |||
| 187 | public function update(object $entity): void | ||
| 188 | { | ||
| 189 | $updateData = $this->prepareUpdateData($entity); | ||
| 190 | |||
| 191 | if (! $updateData) { | ||
| 192 | return; | ||
| 193 | } | ||
| 194 | |||
| 195 | $isVersioned = $this->class->isVersioned; | ||
| 196 | |||
| 197 | $versionedClass = $this->getVersionedClassMetadata(); | ||
| 198 | $versionedTable = $versionedClass->getTableName(); | ||
| 199 | |||
| 200 | foreach ($updateData as $tableName => $data) { | ||
| 201 | $tableName = $this->quotedTableMap[$tableName]; | ||
| 202 | $versioned = $isVersioned && $versionedTable === $tableName; | ||
| 203 | |||
| 204 | $this->updateTable($entity, $tableName, $data, $versioned); | ||
| 205 | } | ||
| 206 | |||
| 207 | if ($this->class->requiresFetchAfterChange) { | ||
| 208 | // Make sure the table with the version column is updated even if no columns on that | ||
| 209 | // table were affected. | ||
| 210 | if ($isVersioned && ! isset($updateData[$versionedTable])) { | ||
| 211 | $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); | ||
| 212 | |||
| 213 | $this->updateTable($entity, $tableName, [], true); | ||
| 214 | } | ||
| 215 | |||
| 216 | $identifiers = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 217 | |||
| 218 | $this->assignDefaultVersionAndUpsertableValues($entity, $identifiers); | ||
| 219 | } | ||
| 220 | } | ||
| 221 | |||
| 222 | public function delete(object $entity): bool | ||
| 223 | { | ||
| 224 | $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity); | ||
| 225 | $id = array_combine($this->class->getIdentifierColumnNames(), $identifier); | ||
| 226 | $types = $this->getClassIdentifiersTypes($this->class); | ||
| 227 | |||
| 228 | $this->deleteJoinTableRecords($identifier, $types); | ||
| 229 | |||
| 230 | // Delete the row from the root table. Cascades do the rest. | ||
| 231 | $rootClass = $this->em->getClassMetadata($this->class->rootEntityName); | ||
| 232 | $rootTable = $this->quoteStrategy->getTableName($rootClass, $this->platform); | ||
| 233 | $rootTypes = $this->getClassIdentifiersTypes($rootClass); | ||
| 234 | |||
| 235 | return (bool) $this->conn->delete($rootTable, $id, $rootTypes); | ||
| 236 | } | ||
| 237 | |||
| 238 | public function getSelectSQL( | ||
| 239 | array|Criteria $criteria, | ||
| 240 | AssociationMapping|null $assoc = null, | ||
| 241 | LockMode|int|null $lockMode = null, | ||
| 242 | int|null $limit = null, | ||
| 243 | int|null $offset = null, | ||
| 244 | array|null $orderBy = null, | ||
| 245 | ): string { | ||
| 246 | $this->switchPersisterContext($offset, $limit); | ||
| 247 | |||
| 248 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 249 | $joinSql = $this->getJoinSql($baseTableAlias); | ||
| 250 | |||
| 251 | if ($assoc !== null && $assoc->isManyToMany()) { | ||
| 252 | $joinSql .= $this->getSelectManyToManyJoinSQL($assoc); | ||
| 253 | } | ||
| 254 | |||
| 255 | $conditionSql = $criteria instanceof Criteria | ||
| 256 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 257 | : $this->getSelectConditionSQL($criteria, $assoc); | ||
| 258 | |||
| 259 | $filterSql = $this->generateFilterConditionSQL( | ||
| 260 | $this->em->getClassMetadata($this->class->rootEntityName), | ||
| 261 | $this->getSQLTableAlias($this->class->rootEntityName), | ||
| 262 | ); | ||
| 263 | // If the current class in the root entity, add the filters | ||
| 264 | if ($filterSql) { | ||
| 265 | $conditionSql .= $conditionSql | ||
| 266 | ? ' AND ' . $filterSql | ||
| 267 | : $filterSql; | ||
| 268 | } | ||
| 269 | |||
| 270 | $orderBySql = ''; | ||
| 271 | |||
| 272 | if ($assoc !== null && $assoc->isOrdered()) { | ||
| 273 | $orderBy = $assoc->orderBy(); | ||
| 274 | } | ||
| 275 | |||
| 276 | if ($orderBy) { | ||
| 277 | $orderBySql = $this->getOrderBySQL($orderBy, $baseTableAlias); | ||
| 278 | } | ||
| 279 | |||
| 280 | $lockSql = ''; | ||
| 281 | |||
| 282 | switch ($lockMode) { | ||
| 283 | case LockMode::PESSIMISTIC_READ: | ||
| 284 | $lockSql = ' ' . $this->getReadLockSQL($this->platform); | ||
| 285 | |||
| 286 | break; | ||
| 287 | |||
| 288 | case LockMode::PESSIMISTIC_WRITE: | ||
| 289 | $lockSql = ' ' . $this->getWriteLockSQL($this->platform); | ||
| 290 | |||
| 291 | break; | ||
| 292 | } | ||
| 293 | |||
| 294 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 295 | $from = ' FROM ' . $tableName . ' ' . $baseTableAlias; | ||
| 296 | $where = $conditionSql !== '' ? ' WHERE ' . $conditionSql : ''; | ||
| 297 | $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE); | ||
| 298 | $columnList = $this->getSelectColumnsSQL(); | ||
| 299 | $query = 'SELECT ' . $columnList | ||
| 300 | . $lock | ||
| 301 | . $joinSql | ||
| 302 | . $where | ||
| 303 | . $orderBySql; | ||
| 304 | |||
| 305 | return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql; | ||
| 306 | } | ||
| 307 | |||
| 308 | public function getCountSQL(array|Criteria $criteria = []): string | ||
| 309 | { | ||
| 310 | $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform); | ||
| 311 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 312 | $joinSql = $this->getJoinSql($baseTableAlias); | ||
| 313 | |||
| 314 | $conditionSql = $criteria instanceof Criteria | ||
| 315 | ? $this->getSelectConditionCriteriaSQL($criteria) | ||
| 316 | : $this->getSelectConditionSQL($criteria); | ||
| 317 | |||
| 318 | $filterSql = $this->generateFilterConditionSQL($this->em->getClassMetadata($this->class->rootEntityName), $this->getSQLTableAlias($this->class->rootEntityName)); | ||
| 319 | |||
| 320 | if ($filterSql !== '') { | ||
| 321 | $conditionSql = $conditionSql | ||
| 322 | ? $conditionSql . ' AND ' . $filterSql | ||
| 323 | : $filterSql; | ||
| 324 | } | ||
| 325 | |||
| 326 | return 'SELECT COUNT(*) ' | ||
| 327 | . 'FROM ' . $tableName . ' ' . $baseTableAlias | ||
| 328 | . $joinSql | ||
| 329 | . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql); | ||
| 330 | } | ||
| 331 | |||
| 332 | protected function getLockTablesSql(LockMode|int $lockMode): string | ||
| 333 | { | ||
| 334 | $joinSql = ''; | ||
| 335 | $identifierColumns = $this->class->getIdentifierColumnNames(); | ||
| 336 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 337 | |||
| 338 | // INNER JOIN parent tables | ||
| 339 | foreach ($this->class->parentClasses as $parentClassName) { | ||
| 340 | $conditions = []; | ||
| 341 | $tableAlias = $this->getSQLTableAlias($parentClassName); | ||
| 342 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 343 | $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 344 | |||
| 345 | foreach ($identifierColumns as $idColumn) { | ||
| 346 | $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; | ||
| 347 | } | ||
| 348 | |||
| 349 | $joinSql .= implode(' AND ', $conditions); | ||
| 350 | } | ||
| 351 | |||
| 352 | return parent::getLockTablesSql($lockMode) . $joinSql; | ||
| 353 | } | ||
| 354 | |||
| 355 | /** | ||
| 356 | * Ensure this method is never called. This persister overrides getSelectEntitiesSQL directly. | ||
| 357 | */ | ||
| 358 | protected function getSelectColumnsSQL(): string | ||
| 359 | { | ||
| 360 | // Create the column list fragment only once | ||
| 361 | if ($this->currentPersisterContext->selectColumnListSql !== null) { | ||
| 362 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 363 | } | ||
| 364 | |||
| 365 | $columnList = []; | ||
| 366 | $discrColumn = $this->class->getDiscriminatorColumn(); | ||
| 367 | $discrColumnName = $discrColumn->name; | ||
| 368 | $discrColumnType = $discrColumn->type; | ||
| 369 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 370 | $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName); | ||
| 371 | |||
| 372 | $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); | ||
| 373 | $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName); | ||
| 374 | $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType); | ||
| 375 | |||
| 376 | // Add regular columns | ||
| 377 | foreach ($this->class->fieldMappings as $fieldName => $mapping) { | ||
| 378 | $class = isset($mapping->inherited) | ||
| 379 | ? $this->em->getClassMetadata($mapping->inherited) | ||
| 380 | : $this->class; | ||
| 381 | |||
| 382 | $columnList[] = $this->getSelectColumnSQL($fieldName, $class); | ||
| 383 | } | ||
| 384 | |||
| 385 | // Add foreign key columns | ||
| 386 | foreach ($this->class->associationMappings as $mapping) { | ||
| 387 | if (! $mapping->isToOneOwningSide()) { | ||
| 388 | continue; | ||
| 389 | } | ||
| 390 | |||
| 391 | $tableAlias = isset($mapping->inherited) | ||
| 392 | ? $this->getSQLTableAlias($mapping->inherited) | ||
| 393 | : $baseTableAlias; | ||
| 394 | |||
| 395 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 396 | |||
| 397 | foreach ($mapping->joinColumns as $joinColumn) { | ||
| 398 | $columnList[] = $this->getSelectJoinColumnSQL( | ||
| 399 | $tableAlias, | ||
| 400 | $joinColumn->name, | ||
| 401 | $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform), | ||
| 402 | PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), | ||
| 403 | ); | ||
| 404 | } | ||
| 405 | } | ||
| 406 | |||
| 407 | // Add discriminator column (DO NOT ALIAS, see AbstractEntityInheritancePersister#processSQLResult). | ||
| 408 | $tableAlias = $this->class->rootEntityName === $this->class->name | ||
| 409 | ? $baseTableAlias | ||
| 410 | : $this->getSQLTableAlias($this->class->rootEntityName); | ||
| 411 | |||
| 412 | $columnList[] = $tableAlias . '.' . $discrColumnName; | ||
| 413 | |||
| 414 | // sub tables | ||
| 415 | foreach ($this->class->subClasses as $subClassName) { | ||
| 416 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 417 | $tableAlias = $this->getSQLTableAlias($subClassName); | ||
| 418 | |||
| 419 | // Add subclass columns | ||
| 420 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
| 421 | if (isset($mapping->inherited)) { | ||
| 422 | continue; | ||
| 423 | } | ||
| 424 | |||
| 425 | $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass); | ||
| 426 | } | ||
| 427 | |||
| 428 | // Add join columns (foreign keys) | ||
| 429 | foreach ($subClass->associationMappings as $mapping) { | ||
| 430 | if (! $mapping->isToOneOwningSide() || isset($mapping->inherited)) { | ||
| 431 | continue; | ||
| 432 | } | ||
| 433 | |||
| 434 | $targetClass = $this->em->getClassMetadata($mapping->targetEntity); | ||
| 435 | |||
| 436 | foreach ($mapping->joinColumns as $joinColumn) { | ||
| 437 | $columnList[] = $this->getSelectJoinColumnSQL( | ||
| 438 | $tableAlias, | ||
| 439 | $joinColumn->name, | ||
| 440 | $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform), | ||
| 441 | PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), | ||
| 442 | ); | ||
| 443 | } | ||
| 444 | } | ||
| 445 | } | ||
| 446 | |||
| 447 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); | ||
| 448 | |||
| 449 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 450 | } | ||
| 451 | |||
| 452 | /** | ||
| 453 | * {@inheritDoc} | ||
| 454 | */ | ||
| 455 | protected function getInsertColumnList(): array | ||
| 456 | { | ||
| 457 | // Identifier columns must always come first in the column list of subclasses. | ||
| 458 | $columns = $this->class->parentClasses | ||
| 459 | ? $this->class->getIdentifierColumnNames() | ||
| 460 | : []; | ||
| 461 | |||
| 462 | foreach ($this->class->reflFields as $name => $field) { | ||
| 463 | if ( | ||
| 464 | isset($this->class->fieldMappings[$name]->inherited) | ||
| 465 | && ! isset($this->class->fieldMappings[$name]->id) | ||
| 466 | || isset($this->class->associationMappings[$name]->inherited) | ||
| 467 | || ($this->class->isVersioned && $this->class->versionField === $name) | ||
| 468 | || isset($this->class->embeddedClasses[$name]) | ||
| 469 | || isset($this->class->fieldMappings[$name]->notInsertable) | ||
| 470 | ) { | ||
| 471 | continue; | ||
| 472 | } | ||
| 473 | |||
| 474 | if (isset($this->class->associationMappings[$name])) { | ||
| 475 | $assoc = $this->class->associationMappings[$name]; | ||
| 476 | if ($assoc->isToOneOwningSide()) { | ||
| 477 | foreach ($assoc->targetToSourceKeyColumns as $sourceCol) { | ||
| 478 | $columns[] = $sourceCol; | ||
| 479 | } | ||
| 480 | } | ||
| 481 | } elseif ( | ||
| 482 | $this->class->name !== $this->class->rootEntityName || | ||
| 483 | ! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name | ||
| 484 | ) { | ||
| 485 | $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform); | ||
| 486 | $this->columnTypes[$name] = $this->class->fieldMappings[$name]->type; | ||
| 487 | } | ||
| 488 | } | ||
| 489 | |||
| 490 | // Add discriminator column if it is the topmost class. | ||
| 491 | if ($this->class->name === $this->class->rootEntityName) { | ||
| 492 | $columns[] = $this->class->getDiscriminatorColumn()->name; | ||
| 493 | } | ||
| 494 | |||
| 495 | return $columns; | ||
| 496 | } | ||
| 497 | |||
| 498 | /** | ||
| 499 | * {@inheritDoc} | ||
| 500 | */ | ||
| 501 | protected function assignDefaultVersionAndUpsertableValues(object $entity, array $id): void | ||
| 502 | { | ||
| 503 | $values = $this->fetchVersionAndNotUpsertableValues($this->getVersionedClassMetadata(), $id); | ||
| 504 | |||
| 505 | foreach ($values as $field => $value) { | ||
| 506 | $value = Type::getType($this->class->fieldMappings[$field]->type)->convertToPHPValue($value, $this->platform); | ||
| 507 | |||
| 508 | $this->class->setFieldValue($entity, $field, $value); | ||
| 509 | } | ||
| 510 | } | ||
| 511 | |||
| 512 | /** | ||
| 513 | * {@inheritDoc} | ||
| 514 | */ | ||
| 515 | protected function fetchVersionAndNotUpsertableValues(ClassMetadata $versionedClass, array $id): mixed | ||
| 516 | { | ||
| 517 | $columnNames = []; | ||
| 518 | foreach ($this->class->fieldMappings as $key => $column) { | ||
| 519 | $class = null; | ||
| 520 | if ($this->class->isVersioned && $key === $versionedClass->versionField) { | ||
| 521 | $class = $versionedClass; | ||
| 522 | } elseif (isset($column->generated)) { | ||
| 523 | $class = isset($column->inherited) | ||
| 524 | ? $this->em->getClassMetadata($column->inherited) | ||
| 525 | : $this->class; | ||
| 526 | } else { | ||
| 527 | continue; | ||
| 528 | } | ||
| 529 | |||
| 530 | $columnNames[$key] = $this->getSelectColumnSQL($key, $class); | ||
| 531 | } | ||
| 532 | |||
| 533 | $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform); | ||
| 534 | $baseTableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 535 | $joinSql = $this->getJoinSql($baseTableAlias); | ||
| 536 | $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform); | ||
| 537 | foreach ($identifier as $i => $idValue) { | ||
| 538 | $identifier[$i] = $baseTableAlias . '.' . $idValue; | ||
| 539 | } | ||
| 540 | |||
| 541 | $sql = 'SELECT ' . implode(', ', $columnNames) | ||
| 542 | . ' FROM ' . $tableName . ' ' . $baseTableAlias | ||
| 543 | . $joinSql | ||
| 544 | . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?'; | ||
| 545 | |||
| 546 | $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id); | ||
| 547 | $values = $this->conn->fetchNumeric( | ||
| 548 | $sql, | ||
| 549 | array_values($flatId), | ||
| 550 | $this->extractIdentifierTypes($id, $versionedClass), | ||
| 551 | ); | ||
| 552 | |||
| 553 | if ($values === false) { | ||
| 554 | throw new LengthException('Unexpected empty result for database query.'); | ||
| 555 | } | ||
| 556 | |||
| 557 | $values = array_combine(array_keys($columnNames), $values); | ||
| 558 | |||
| 559 | if (! $values) { | ||
| 560 | throw new LengthException('Unexpected number of database columns.'); | ||
| 561 | } | ||
| 562 | |||
| 563 | return $values; | ||
| 564 | } | ||
| 565 | |||
| 566 | private function getJoinSql(string $baseTableAlias): string | ||
| 567 | { | ||
| 568 | $joinSql = ''; | ||
| 569 | $identifierColumn = $this->class->getIdentifierColumnNames(); | ||
| 570 | |||
| 571 | // INNER JOIN parent tables | ||
| 572 | foreach ($this->class->parentClasses as $parentClassName) { | ||
| 573 | $conditions = []; | ||
| 574 | $parentClass = $this->em->getClassMetadata($parentClassName); | ||
| 575 | $tableAlias = $this->getSQLTableAlias($parentClassName); | ||
| 576 | $joinSql .= ' INNER JOIN ' . $this->quoteStrategy->getTableName($parentClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 577 | |||
| 578 | foreach ($identifierColumn as $idColumn) { | ||
| 579 | $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; | ||
| 580 | } | ||
| 581 | |||
| 582 | $joinSql .= implode(' AND ', $conditions); | ||
| 583 | } | ||
| 584 | |||
| 585 | // OUTER JOIN sub tables | ||
| 586 | foreach ($this->class->subClasses as $subClassName) { | ||
| 587 | $conditions = []; | ||
| 588 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 589 | $tableAlias = $this->getSQLTableAlias($subClassName); | ||
| 590 | $joinSql .= ' LEFT JOIN ' . $this->quoteStrategy->getTableName($subClass, $this->platform) . ' ' . $tableAlias . ' ON '; | ||
| 591 | |||
| 592 | foreach ($identifierColumn as $idColumn) { | ||
| 593 | $conditions[] = $baseTableAlias . '.' . $idColumn . ' = ' . $tableAlias . '.' . $idColumn; | ||
| 594 | } | ||
| 595 | |||
| 596 | $joinSql .= implode(' AND ', $conditions); | ||
| 597 | } | ||
| 598 | |||
| 599 | return $joinSql; | ||
| 600 | } | ||
| 601 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php b/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php new file mode 100644 index 0000000..4a4d999 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Entity/SingleTablePersister.php | |||
| @@ -0,0 +1,166 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Entity; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Criteria; | ||
| 8 | use Doctrine\ORM\Internal\SQLResultCasing; | ||
| 9 | use Doctrine\ORM\Mapping\AssociationMapping; | ||
| 10 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 11 | use Doctrine\ORM\Utility\PersisterHelper; | ||
| 12 | |||
| 13 | use function array_flip; | ||
| 14 | use function array_intersect; | ||
| 15 | use function array_map; | ||
| 16 | use function array_unshift; | ||
| 17 | use function implode; | ||
| 18 | use function strval; | ||
| 19 | |||
| 20 | /** | ||
| 21 | * Persister for entities that participate in a hierarchy mapped with the | ||
| 22 | * SINGLE_TABLE strategy. | ||
| 23 | * | ||
| 24 | * @link https://martinfowler.com/eaaCatalog/singleTableInheritance.html | ||
| 25 | */ | ||
| 26 | class SingleTablePersister extends AbstractEntityInheritancePersister | ||
| 27 | { | ||
| 28 | use SQLResultCasing; | ||
| 29 | |||
| 30 | protected function getDiscriminatorColumnTableName(): string | ||
| 31 | { | ||
| 32 | return $this->class->getTableName(); | ||
| 33 | } | ||
| 34 | |||
| 35 | protected function getSelectColumnsSQL(): string | ||
| 36 | { | ||
| 37 | $columnList = []; | ||
| 38 | if ($this->currentPersisterContext->selectColumnListSql !== null) { | ||
| 39 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 40 | } | ||
| 41 | |||
| 42 | $columnList[] = parent::getSelectColumnsSQL(); | ||
| 43 | |||
| 44 | $rootClass = $this->em->getClassMetadata($this->class->rootEntityName); | ||
| 45 | $tableAlias = $this->getSQLTableAlias($rootClass->name); | ||
| 46 | |||
| 47 | // Append discriminator column | ||
| 48 | $discrColumn = $this->class->getDiscriminatorColumn(); | ||
| 49 | $discrColumnName = $discrColumn->name; | ||
| 50 | $discrColumnType = $discrColumn->type; | ||
| 51 | |||
| 52 | $columnList[] = $tableAlias . '.' . $discrColumnName; | ||
| 53 | |||
| 54 | $resultColumnName = $this->getSQLResultCasing($this->platform, $discrColumnName); | ||
| 55 | |||
| 56 | $this->currentPersisterContext->rsm->setDiscriminatorColumn('r', $resultColumnName); | ||
| 57 | $this->currentPersisterContext->rsm->addMetaResult('r', $resultColumnName, $discrColumnName, false, $discrColumnType); | ||
| 58 | |||
| 59 | // Append subclass columns | ||
| 60 | foreach ($this->class->subClasses as $subClassName) { | ||
| 61 | $subClass = $this->em->getClassMetadata($subClassName); | ||
| 62 | |||
| 63 | // Regular columns | ||
| 64 | foreach ($subClass->fieldMappings as $fieldName => $mapping) { | ||
| 65 | if (isset($mapping->inherited)) { | ||
| 66 | continue; | ||
| 67 | } | ||
| 68 | |||
| 69 | $columnList[] = $this->getSelectColumnSQL($fieldName, $subClass); | ||
| 70 | } | ||
| 71 | |||
| 72 | // Foreign key columns | ||
| 73 | foreach ($subClass->associationMappings as $assoc) { | ||
| 74 | if (! $assoc->isToOneOwningSide() || isset($assoc->inherited)) { | ||
| 75 | continue; | ||
| 76 | } | ||
| 77 | |||
| 78 | $targetClass = $this->em->getClassMetadata($assoc->targetEntity); | ||
| 79 | |||
| 80 | foreach ($assoc->joinColumns as $joinColumn) { | ||
| 81 | $columnList[] = $this->getSelectJoinColumnSQL( | ||
| 82 | $tableAlias, | ||
| 83 | $joinColumn->name, | ||
| 84 | $this->quoteStrategy->getJoinColumnName($joinColumn, $subClass, $this->platform), | ||
| 85 | PersisterHelper::getTypeOfColumn($joinColumn->referencedColumnName, $targetClass, $this->em), | ||
| 86 | ); | ||
| 87 | } | ||
| 88 | } | ||
| 89 | } | ||
| 90 | |||
| 91 | $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList); | ||
| 92 | |||
| 93 | return $this->currentPersisterContext->selectColumnListSql; | ||
| 94 | } | ||
| 95 | |||
| 96 | /** | ||
| 97 | * {@inheritDoc} | ||
| 98 | */ | ||
| 99 | protected function getInsertColumnList(): array | ||
| 100 | { | ||
| 101 | $columns = parent::getInsertColumnList(); | ||
| 102 | |||
| 103 | // Add discriminator column to the INSERT SQL | ||
| 104 | $columns[] = $this->class->getDiscriminatorColumn()->name; | ||
| 105 | |||
| 106 | return $columns; | ||
| 107 | } | ||
| 108 | |||
| 109 | protected function getSQLTableAlias(string $className, string $assocName = ''): string | ||
| 110 | { | ||
| 111 | return parent::getSQLTableAlias($this->class->rootEntityName, $assocName); | ||
| 112 | } | ||
| 113 | |||
| 114 | /** | ||
| 115 | * {@inheritDoc} | ||
| 116 | */ | ||
| 117 | protected function getSelectConditionSQL(array $criteria, AssociationMapping|null $assoc = null): string | ||
| 118 | { | ||
| 119 | $conditionSql = parent::getSelectConditionSQL($criteria, $assoc); | ||
| 120 | |||
| 121 | if ($conditionSql) { | ||
| 122 | $conditionSql .= ' AND '; | ||
| 123 | } | ||
| 124 | |||
| 125 | return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL(); | ||
| 126 | } | ||
| 127 | |||
| 128 | protected function getSelectConditionCriteriaSQL(Criteria $criteria): string | ||
| 129 | { | ||
| 130 | $conditionSql = parent::getSelectConditionCriteriaSQL($criteria); | ||
| 131 | |||
| 132 | if ($conditionSql) { | ||
| 133 | $conditionSql .= ' AND '; | ||
| 134 | } | ||
| 135 | |||
| 136 | return $conditionSql . $this->getSelectConditionDiscriminatorValueSQL(); | ||
| 137 | } | ||
| 138 | |||
| 139 | protected function getSelectConditionDiscriminatorValueSQL(): string | ||
| 140 | { | ||
| 141 | $values = array_map($this->conn->quote(...), array_map( | ||
| 142 | strval(...), | ||
| 143 | array_flip(array_intersect($this->class->discriminatorMap, $this->class->subClasses)), | ||
| 144 | )); | ||
| 145 | |||
| 146 | if ($this->class->discriminatorValue !== null) { // discriminators can be 0 | ||
| 147 | array_unshift($values, $this->conn->quote((string) $this->class->discriminatorValue)); | ||
| 148 | } | ||
| 149 | |||
| 150 | $discColumnName = $this->class->getDiscriminatorColumn()->name; | ||
| 151 | |||
| 152 | $values = implode(', ', $values); | ||
| 153 | $tableAlias = $this->getSQLTableAlias($this->class->name); | ||
| 154 | |||
| 155 | return $tableAlias . '.' . $discColumnName . ' IN (' . $values . ')'; | ||
| 156 | } | ||
| 157 | |||
| 158 | protected function generateFilterConditionSQL(ClassMetadata $targetEntity, string $targetTableAlias): string | ||
| 159 | { | ||
| 160 | // Ensure that the filters are applied to the root entity of the inheritance tree | ||
| 161 | $targetEntity = $this->em->getClassMetadata($targetEntity->rootEntityName); | ||
| 162 | // we don't care about the $targetTableAlias, in a STI there is only one table. | ||
| 163 | |||
| 164 | return parent::generateFilterConditionSQL($targetEntity, $targetTableAlias); | ||
| 165 | } | ||
| 166 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php b/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php new file mode 100644 index 0000000..5c91312 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Exception/CantUseInOperatorOnCompositeKeys.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | class CantUseInOperatorOnCompositeKeys extends PersisterException | ||
| 10 | { | ||
| 11 | public static function create(): self | ||
| 12 | { | ||
| 13 | return new self("Can't use IN operator on entities that have composite keys."); | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php b/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php new file mode 100644 index 0000000..7532800 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Exception/InvalidOrientation.php | |||
| @@ -0,0 +1,15 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | class InvalidOrientation extends PersisterException | ||
| 10 | { | ||
| 11 | public static function fromClassNameAndField(string $className, string $field): self | ||
| 12 | { | ||
| 13 | return new self('Invalid order by orientation specified for ' . $className . '#' . $field); | ||
| 14 | } | ||
| 15 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php b/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php new file mode 100644 index 0000000..be7303e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/Exception/UnrecognizedField.php | |||
| @@ -0,0 +1,24 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters\Exception; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class UnrecognizedField extends PersisterException | ||
| 12 | { | ||
| 13 | /** @deprecated Use {@see byFullyQualifiedName()} instead. */ | ||
| 14 | public static function byName(string $field): self | ||
| 15 | { | ||
| 16 | return new self(sprintf('Unrecognized field: %s', $field)); | ||
| 17 | } | ||
| 18 | |||
| 19 | /** @param class-string $className */ | ||
| 20 | public static function byFullyQualifiedName(string $className, string $field): self | ||
| 21 | { | ||
| 22 | return new self(sprintf('Unrecognized field: %s::$%s', $className, $field)); | ||
| 23 | } | ||
| 24 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php b/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php new file mode 100644 index 0000000..4e7251e --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/MatchingAssociationFieldRequiresObject.php | |||
| @@ -0,0 +1,22 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\PersisterException; | ||
| 8 | |||
| 9 | use function sprintf; | ||
| 10 | |||
| 11 | final class MatchingAssociationFieldRequiresObject extends PersisterException | ||
| 12 | { | ||
| 13 | public static function fromClassAndAssociation(string $class, string $associationName): self | ||
| 14 | { | ||
| 15 | return new self(sprintf( | ||
| 16 | 'Cannot match on %s::%s with a non-object value. Matching objects by id is ' . | ||
| 17 | 'not compatible with matching on an in-memory collection, which compares objects by reference.', | ||
| 18 | $class, | ||
| 19 | $associationName, | ||
| 20 | )); | ||
| 21 | } | ||
| 22 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/PersisterException.php b/vendor/doctrine/orm/src/Persisters/PersisterException.php new file mode 100644 index 0000000..0016472 --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/PersisterException.php | |||
| @@ -0,0 +1,23 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\ORM\Exception\ORMException; | ||
| 8 | use Exception; | ||
| 9 | |||
| 10 | use function sprintf; | ||
| 11 | |||
| 12 | class PersisterException extends Exception implements ORMException | ||
| 13 | { | ||
| 14 | public static function matchingAssocationFieldRequiresObject(string $class, string $associationName): PersisterException | ||
| 15 | { | ||
| 16 | return new self(sprintf( | ||
| 17 | 'Cannot match on %s::%s with a non-object value. Matching objects by id is ' . | ||
| 18 | 'not compatible with matching on an in-memory collection, which compares objects by reference.', | ||
| 19 | $class, | ||
| 20 | $associationName, | ||
| 21 | )); | ||
| 22 | } | ||
| 23 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php b/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php new file mode 100644 index 0000000..df60f6a --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/SqlExpressionVisitor.php | |||
| @@ -0,0 +1,79 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 8 | use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
| 9 | use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
| 10 | use Doctrine\Common\Collections\Expr\Value; | ||
| 11 | use Doctrine\ORM\Mapping\ClassMetadata; | ||
| 12 | use Doctrine\ORM\Persisters\Entity\BasicEntityPersister; | ||
| 13 | use RuntimeException; | ||
| 14 | |||
| 15 | use function implode; | ||
| 16 | use function in_array; | ||
| 17 | use function is_object; | ||
| 18 | |||
| 19 | /** | ||
| 20 | * Visit Expressions and generate SQL WHERE conditions from them. | ||
| 21 | */ | ||
| 22 | class SqlExpressionVisitor extends ExpressionVisitor | ||
| 23 | { | ||
| 24 | public function __construct( | ||
| 25 | private readonly BasicEntityPersister $persister, | ||
| 26 | private readonly ClassMetadata $classMetadata, | ||
| 27 | ) { | ||
| 28 | } | ||
| 29 | |||
| 30 | /** Converts a comparison expression into the target query language output. */ | ||
| 31 | public function walkComparison(Comparison $comparison): string | ||
| 32 | { | ||
| 33 | $field = $comparison->getField(); | ||
| 34 | $value = $comparison->getValue()->getValue(); // shortcut for walkValue() | ||
| 35 | |||
| 36 | if ( | ||
| 37 | isset($this->classMetadata->associationMappings[$field]) && | ||
| 38 | $value !== null && | ||
| 39 | ! is_object($value) && | ||
| 40 | ! in_array($comparison->getOperator(), [Comparison::IN, Comparison::NIN], true) | ||
| 41 | ) { | ||
| 42 | throw MatchingAssociationFieldRequiresObject::fromClassAndAssociation( | ||
| 43 | $this->classMetadata->name, | ||
| 44 | $field, | ||
| 45 | ); | ||
| 46 | } | ||
| 47 | |||
| 48 | return $this->persister->getSelectConditionStatementSQL($field, $value, null, $comparison->getOperator()); | ||
| 49 | } | ||
| 50 | |||
| 51 | /** | ||
| 52 | * Converts a composite expression into the target query language output. | ||
| 53 | * | ||
| 54 | * @throws RuntimeException | ||
| 55 | */ | ||
| 56 | public function walkCompositeExpression(CompositeExpression $expr): string | ||
| 57 | { | ||
| 58 | $expressionList = []; | ||
| 59 | |||
| 60 | foreach ($expr->getExpressionList() as $child) { | ||
| 61 | $expressionList[] = $this->dispatch($child); | ||
| 62 | } | ||
| 63 | |||
| 64 | return match ($expr->getType()) { | ||
| 65 | CompositeExpression::TYPE_AND => '(' . implode(' AND ', $expressionList) . ')', | ||
| 66 | CompositeExpression::TYPE_OR => '(' . implode(' OR ', $expressionList) . ')', | ||
| 67 | CompositeExpression::TYPE_NOT => 'NOT (' . $expressionList[0] . ')', | ||
| 68 | default => throw new RuntimeException('Unknown composite ' . $expr->getType()), | ||
| 69 | }; | ||
| 70 | } | ||
| 71 | |||
| 72 | /** | ||
| 73 | * Converts a value expression into the target query language part. | ||
| 74 | */ | ||
| 75 | public function walkValue(Value $value): string | ||
| 76 | { | ||
| 77 | return '?'; | ||
| 78 | } | ||
| 79 | } | ||
diff --git a/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php b/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php new file mode 100644 index 0000000..7f987ad --- /dev/null +++ b/vendor/doctrine/orm/src/Persisters/SqlValueVisitor.php | |||
| @@ -0,0 +1,88 @@ | |||
| 1 | <?php | ||
| 2 | |||
| 3 | declare(strict_types=1); | ||
| 4 | |||
| 5 | namespace Doctrine\ORM\Persisters; | ||
| 6 | |||
| 7 | use Doctrine\Common\Collections\Expr\Comparison; | ||
| 8 | use Doctrine\Common\Collections\Expr\CompositeExpression; | ||
| 9 | use Doctrine\Common\Collections\Expr\ExpressionVisitor; | ||
| 10 | use Doctrine\Common\Collections\Expr\Value; | ||
| 11 | |||
| 12 | /** | ||
| 13 | * Extract the values from a criteria/expression | ||
| 14 | */ | ||
| 15 | class SqlValueVisitor extends ExpressionVisitor | ||
| 16 | { | ||
| 17 | /** @var mixed[] */ | ||
| 18 | private array $values = []; | ||
| 19 | |||
| 20 | /** @var mixed[][] */ | ||
| 21 | private array $types = []; | ||
| 22 | |||
| 23 | /** | ||
| 24 | * Converts a comparison expression into the target query language output. | ||
| 25 | * | ||
| 26 | * {@inheritDoc} | ||
| 27 | */ | ||
| 28 | public function walkComparison(Comparison $comparison) | ||
| 29 | { | ||
| 30 | $value = $this->getValueFromComparison($comparison); | ||
| 31 | |||
| 32 | $this->values[] = $value; | ||
| 33 | $this->types[] = [$comparison->getField(), $value, $comparison->getOperator()]; | ||
| 34 | |||
| 35 | return null; | ||
| 36 | } | ||
| 37 | |||
| 38 | /** | ||
| 39 | * Converts a composite expression into the target query language output. | ||
| 40 | * | ||
| 41 | * {@inheritDoc} | ||
| 42 | */ | ||
| 43 | public function walkCompositeExpression(CompositeExpression $expr) | ||
| 44 | { | ||
| 45 | foreach ($expr->getExpressionList() as $child) { | ||
| 46 | $this->dispatch($child); | ||
| 47 | } | ||
| 48 | |||
| 49 | return null; | ||
| 50 | } | ||
| 51 | |||
| 52 | /** | ||
| 53 | * Converts a value expression into the target query language part. | ||
| 54 | * | ||
| 55 | * {@inheritDoc} | ||
| 56 | */ | ||
| 57 | public function walkValue(Value $value) | ||
| 58 | { | ||
| 59 | return null; | ||
| 60 | } | ||
| 61 | |||
| 62 | /** | ||
| 63 | * Returns the Parameters and Types necessary for matching the last visited expression. | ||
| 64 | * | ||
| 65 | * @return mixed[][] | ||
| 66 | * @psalm-return array{0: array, 1: array<array<mixed>>} | ||
| 67 | */ | ||
| 68 | public function getParamsAndTypes(): array | ||
| 69 | { | ||
| 70 | return [$this->values, $this->types]; | ||
| 71 | } | ||
| 72 | |||
| 73 | /** | ||
| 74 | * Returns the value from a Comparison. In case of a CONTAINS comparison, | ||
| 75 | * the value is wrapped in %-signs, because it will be used in a LIKE clause. | ||
| 76 | */ | ||
| 77 | protected function getValueFromComparison(Comparison $comparison): mixed | ||
| 78 | { | ||
| 79 | $value = $comparison->getValue()->getValue(); | ||
| 80 | |||
| 81 | return match ($comparison->getOperator()) { | ||
| 82 | Comparison::CONTAINS => '%' . $value . '%', | ||
| 83 | Comparison::STARTS_WITH => $value . '%', | ||
| 84 | Comparison::ENDS_WITH => '%' . $value, | ||
| 85 | default => $value, | ||
| 86 | }; | ||
| 87 | } | ||
| 88 | } | ||
