vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php line 924
- <?php
- declare(strict_types=1);
- namespace Doctrine\ORM\Persisters\Entity;
- use BackedEnum;
- use Doctrine\Common\Collections\Criteria;
- use Doctrine\Common\Collections\Expr\Comparison;
- use Doctrine\Common\Util\ClassUtils;
- use Doctrine\DBAL\Connection;
- use Doctrine\DBAL\LockMode;
- use Doctrine\DBAL\Platforms\AbstractPlatform;
- use Doctrine\DBAL\Result;
- use Doctrine\DBAL\Types\Type;
- use Doctrine\DBAL\Types\Types;
- use Doctrine\Deprecations\Deprecation;
- use Doctrine\ORM\EntityManagerInterface;
- use Doctrine\ORM\Mapping\ClassMetadata;
- use Doctrine\ORM\Mapping\MappingException;
- use Doctrine\ORM\Mapping\QuoteStrategy;
- use Doctrine\ORM\OptimisticLockException;
- use Doctrine\ORM\PersistentCollection;
- use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
- use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
- use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
- use Doctrine\ORM\Persisters\SqlExpressionVisitor;
- use Doctrine\ORM\Persisters\SqlValueVisitor;
- use Doctrine\ORM\Query;
- use Doctrine\ORM\Query\QueryException;
- use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
- use Doctrine\ORM\UnitOfWork;
- use Doctrine\ORM\Utility\IdentifierFlattener;
- use Doctrine\ORM\Utility\PersisterHelper;
- use LengthException;
- use function array_combine;
- use function array_keys;
- use function array_map;
- use function array_merge;
- use function array_search;
- use function array_unique;
- use function array_values;
- use function assert;
- use function count;
- use function implode;
- use function is_array;
- use function is_object;
- use function reset;
- use function spl_object_id;
- use function sprintf;
- use function str_contains;
- use function strtoupper;
- use function trim;
- /**
- * A BasicEntityPersister maps an entity to a single table in a relational database.
- *
- * A persister is always responsible for a single entity type.
- *
- * EntityPersisters are used during a UnitOfWork to apply any changes to the persistent
- * state of entities onto a relational database when the UnitOfWork is committed,
- * as well as for basic querying of entities and their associations (not DQL).
- *
- * The persisting operations that are invoked during a commit of a UnitOfWork to
- * persist the persistent entity state are:
- *
- * - {@link addInsert} : To schedule an entity for insertion.
- * - {@link executeInserts} : To execute all scheduled insertions.
- * - {@link update} : To update the persistent state of an entity.
- * - {@link delete} : To delete the persistent state of an entity.
- *
- * As can be seen from the above list, insertions are batched and executed all at once
- * for increased efficiency.
- *
- * The querying operations invoked during a UnitOfWork, either through direct find
- * requests or lazy-loading, are the following:
- *
- * - {@link load} : Loads (the state of) a single, managed entity.
- * - {@link loadAll} : Loads multiple, managed entities.
- * - {@link loadOneToOneEntity} : Loads a one/many-to-one entity association (lazy-loading).
- * - {@link loadOneToManyCollection} : Loads a one-to-many entity association (lazy-loading).
- * - {@link loadManyToManyCollection} : Loads a many-to-many entity association (lazy-loading).
- *
- * The BasicEntityPersister implementation provides the default behavior for
- * persisting and querying entities that are mapped to a single database table.
- *
- * Subclasses can be created to provide custom persisting and querying strategies,
- * i.e. spanning multiple tables.
- *
- * @psalm-import-type AssociationMapping from ClassMetadata
- */
- class BasicEntityPersister implements EntityPersister
- {
- /** @var array<string,string> */
- private static $comparisonMap = [
- Comparison::EQ => '= %s',
- Comparison::NEQ => '!= %s',
- Comparison::GT => '> %s',
- Comparison::GTE => '>= %s',
- Comparison::LT => '< %s',
- Comparison::LTE => '<= %s',
- Comparison::IN => 'IN (%s)',
- Comparison::NIN => 'NOT IN (%s)',
- Comparison::CONTAINS => 'LIKE %s',
- Comparison::STARTS_WITH => 'LIKE %s',
- Comparison::ENDS_WITH => 'LIKE %s',
- ];
- /**
- * Metadata object that describes the mapping of the mapped entity class.
- *
- * @var ClassMetadata
- */
- protected $class;
- /**
- * The underlying DBAL Connection of the used EntityManager.
- *
- * @var Connection $conn
- */
- protected $conn;
- /**
- * The database platform.
- *
- * @var AbstractPlatform
- */
- protected $platform;
- /**
- * The EntityManager instance.
- *
- * @var EntityManagerInterface
- */
- protected $em;
- /**
- * Queued inserts.
- *
- * @psalm-var array<int, object>
- */
- protected $queuedInserts = [];
- /**
- * The map of column names to DBAL mapping types of all prepared columns used
- * when INSERTing or UPDATEing an entity.
- *
- * @see prepareInsertData($entity)
- * @see prepareUpdateData($entity)
- *
- * @var mixed[]
- */
- protected $columnTypes = [];
- /**
- * The map of quoted column names.
- *
- * @see prepareInsertData($entity)
- * @see prepareUpdateData($entity)
- *
- * @var mixed[]
- */
- protected $quotedColumns = [];
- /**
- * The INSERT SQL statement used for entities handled by this persister.
- * This SQL is only generated once per request, if at all.
- *
- * @var string|null
- */
- private $insertSql;
- /**
- * The quote strategy.
- *
- * @var QuoteStrategy
- */
- protected $quoteStrategy;
- /**
- * The IdentifierFlattener used for manipulating identifiers
- *
- * @var IdentifierFlattener
- */
- private $identifierFlattener;
- /** @var CachedPersisterContext */
- protected $currentPersisterContext;
- /** @var CachedPersisterContext */
- private $limitsHandlingContext;
- /** @var CachedPersisterContext */
- private $noLimitsContext;
- /**
- * Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
- * and persists instances of the class described by the given ClassMetadata descriptor.
- */
- public function __construct(EntityManagerInterface $em, ClassMetadata $class)
- {
- $this->em = $em;
- $this->class = $class;
- $this->conn = $em->getConnection();
- $this->platform = $this->conn->getDatabasePlatform();
- $this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
- $this->identifierFlattener = new IdentifierFlattener($em->getUnitOfWork(), $em->getMetadataFactory());
- $this->noLimitsContext = $this->currentPersisterContext = new CachedPersisterContext(
- $class,
- new Query\ResultSetMapping(),
- false
- );
- $this->limitsHandlingContext = new CachedPersisterContext(
- $class,
- new Query\ResultSetMapping(),
- true
- );
- }
- /**
- * {@inheritDoc}
- */
- public function getClassMetadata()
- {
- return $this->class;
- }
- /**
- * {@inheritDoc}
- */
- public function getResultSetMapping()
- {
- return $this->currentPersisterContext->rsm;
- }
- /**
- * {@inheritDoc}
- */
- public function addInsert($entity)
- {
- $this->queuedInserts[spl_object_id($entity)] = $entity;
- }
- /**
- * {@inheritDoc}
- */
- public function getInserts()
- {
- return $this->queuedInserts;
- }
- /**
- * {@inheritDoc}
- */
- public function executeInserts()
- {
- if (! $this->queuedInserts) {
- return [];
- }
- $postInsertIds = [];
- $idGenerator = $this->class->idGenerator;
- $isPostInsertId = $idGenerator->isPostInsertGenerator();
- $stmt = $this->conn->prepare($this->getInsertSQL());
- $tableName = $this->class->getTableName();
- foreach ($this->queuedInserts as $entity) {
- $insertData = $this->prepareInsertData($entity);
- if (isset($insertData[$tableName])) {
- $paramIndex = 1;
- foreach ($insertData[$tableName] as $column => $value) {
- $stmt->bindValue($paramIndex++, $value, $this->columnTypes[$column]);
- }
- }
- $stmt->executeStatement();
- if ($isPostInsertId) {
- $generatedId = $idGenerator->generateId($this->em, $entity);
- $id = [$this->class->identifier[0] => $generatedId];
- $postInsertIds[] = [
- 'generatedId' => $generatedId,
- 'entity' => $entity,
- ];
- } else {
- $id = $this->class->getIdentifierValues($entity);
- }
- if ($this->class->requiresFetchAfterChange) {
- $this->assignDefaultVersionAndUpsertableValues($entity, $id);
- }
- }
- $this->queuedInserts = [];
- return $postInsertIds;
- }
- /**
- * Retrieves the default version value which was created
- * by the preceding INSERT statement and assigns it back in to the
- * entities version field if the given entity is versioned.
- * Also retrieves values of columns marked as 'non insertable' and / or
- * 'not updatable' and assigns them back to the entities corresponding fields.
- *
- * @param object $entity
- * @param mixed[] $id
- *
- * @return void
- */
- protected function assignDefaultVersionAndUpsertableValues($entity, array $id)
- {
- $values = $this->fetchVersionAndNotUpsertableValues($this->class, $id);
- foreach ($values as $field => $value) {
- $value = Type::getType($this->class->fieldMappings[$field]['type'])->convertToPHPValue($value, $this->platform);
- $this->class->setFieldValue($entity, $field, $value);
- }
- }
- /**
- * Fetches the current version value of a versioned entity and / or the values of fields
- * marked as 'not insertable' and / or 'not updatable'.
- *
- * @param ClassMetadata $versionedClass
- * @param mixed[] $id
- *
- * @return mixed
- */
- protected function fetchVersionAndNotUpsertableValues($versionedClass, array $id)
- {
- $columnNames = [];
- foreach ($this->class->fieldMappings as $key => $column) {
- if (isset($column['generated']) || ($this->class->isVersioned && $key === $versionedClass->versionField)) {
- $columnNames[$key] = $this->quoteStrategy->getColumnName($key, $versionedClass, $this->platform);
- }
- }
- $tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
- $identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
- // FIXME: Order with composite keys might not be correct
- $sql = 'SELECT ' . implode(', ', $columnNames)
- . ' FROM ' . $tableName
- . ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
- $flatId = $this->identifierFlattener->flattenIdentifier($versionedClass, $id);
- $values = $this->conn->fetchNumeric(
- $sql,
- array_values($flatId),
- $this->extractIdentifierTypes($id, $versionedClass)
- );
- if ($values === false) {
- throw new LengthException('Unexpected empty result for database query.');
- }
- $values = array_combine(array_keys($columnNames), $values);
- if (! $values) {
- throw new LengthException('Unexpected number of database columns.');
- }
- return $values;
- }
- /**
- * @param mixed[] $id
- *
- * @return int[]|null[]|string[]
- * @psalm-return list<int|string|null>
- */
- private function extractIdentifierTypes(array $id, ClassMetadata $versionedClass): array
- {
- $types = [];
- foreach ($id as $field => $value) {
- $types = array_merge($types, $this->getTypes($field, $value, $versionedClass));
- }
- return $types;
- }
- /**
- * {@inheritDoc}
- */
- public function update($entity)
- {
- $tableName = $this->class->getTableName();
- $updateData = $this->prepareUpdateData($entity);
- if (! isset($updateData[$tableName])) {
- return;
- }
- $data = $updateData[$tableName];
- if (! $data) {
- return;
- }
- $isVersioned = $this->class->isVersioned;
- $quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
- $this->updateTable($entity, $quotedTableName, $data, $isVersioned);
- if ($this->class->requiresFetchAfterChange) {
- $id = $this->class->getIdentifierValues($entity);
- $this->assignDefaultVersionAndUpsertableValues($entity, $id);
- }
- }
- /**
- * Performs an UPDATE statement for an entity on a specific table.
- * The UPDATE can optionally be versioned, which requires the entity to have a version field.
- *
- * @param object $entity The entity object being updated.
- * @param string $quotedTableName The quoted name of the table to apply the UPDATE on.
- * @param mixed[] $updateData The map of columns to update (column => value).
- * @param bool $versioned Whether the UPDATE should be versioned.
- *
- * @throws UnrecognizedField
- * @throws OptimisticLockException
- */
- final protected function updateTable(
- $entity,
- $quotedTableName,
- array $updateData,
- $versioned = false
- ): void {
- $set = [];
- $types = [];
- $params = [];
- foreach ($updateData as $columnName => $value) {
- $placeholder = '?';
- $column = $columnName;
- switch (true) {
- case isset($this->class->fieldNames[$columnName]):
- $fieldName = $this->class->fieldNames[$columnName];
- $column = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
- if (isset($this->class->fieldMappings[$fieldName]['requireSQLConversion'])) {
- $type = Type::getType($this->columnTypes[$columnName]);
- $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
- }
- break;
- case isset($this->quotedColumns[$columnName]):
- $column = $this->quotedColumns[$columnName];
- break;
- }
- $params[] = $value;
- $set[] = $column . ' = ' . $placeholder;
- $types[] = $this->columnTypes[$columnName];
- }
- $where = [];
- $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
- foreach ($this->class->identifier as $idField) {
- if (! isset($this->class->associationMappings[$idField])) {
- $params[] = $identifier[$idField];
- $types[] = $this->class->fieldMappings[$idField]['type'];
- $where[] = $this->quoteStrategy->getColumnName($idField, $this->class, $this->platform);
- continue;
- }
- $params[] = $identifier[$idField];
- $where[] = $this->quoteStrategy->getJoinColumnName(
- $this->class->associationMappings[$idField]['joinColumns'][0],
- $this->class,
- $this->platform
- );
- $targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
- $targetType = PersisterHelper::getTypeOfField($targetMapping->identifier[0], $targetMapping, $this->em);
- if ($targetType === []) {
- throw UnrecognizedField::byFullyQualifiedName($this->class->name, $targetMapping->identifier[0]);
- }
- $types[] = reset($targetType);
- }
- if ($versioned) {
- $versionField = $this->class->versionField;
- assert($versionField !== null);
- $versionFieldType = $this->class->fieldMappings[$versionField]['type'];
- $versionColumn = $this->quoteStrategy->getColumnName($versionField, $this->class, $this->platform);
- $where[] = $versionColumn;
- $types[] = $this->class->fieldMappings[$versionField]['type'];
- $params[] = $this->class->reflFields[$versionField]->getValue($entity);
- switch ($versionFieldType) {
- case Types::SMALLINT:
- case Types::INTEGER:
- case Types::BIGINT:
- $set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
- break;
- case Types::DATETIME_MUTABLE:
- $set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
- break;
- }
- }
- $sql = 'UPDATE ' . $quotedTableName
- . ' SET ' . implode(', ', $set)
- . ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
- $result = $this->conn->executeStatement($sql, $params, $types);
- if ($versioned && ! $result) {
- throw OptimisticLockException::lockFailed($entity);
- }
- }
- /**
- * @param array<mixed> $identifier
- * @param string[] $types
- *
- * @todo Add check for platform if it supports foreign keys/cascading.
- */
- protected function deleteJoinTableRecords(array $identifier, array $types): void
- {
- foreach ($this->class->associationMappings as $mapping) {
- if ($mapping['type'] !== ClassMetadata::MANY_TO_MANY) {
- continue;
- }
- // @Todo this only covers scenarios with no inheritance or of the same level. Is there something
- // like self-referential relationship between different levels of an inheritance hierarchy? I hope not!
- $selfReferential = ($mapping['targetEntity'] === $mapping['sourceEntity']);
- $class = $this->class;
- $association = $mapping;
- $otherColumns = [];
- $otherKeys = [];
- $keys = [];
- if (! $mapping['isOwningSide']) {
- $class = $this->em->getClassMetadata($mapping['targetEntity']);
- $association = $class->associationMappings[$mapping['mappedBy']];
- }
- $joinColumns = $mapping['isOwningSide']
- ? $association['joinTable']['joinColumns']
- : $association['joinTable']['inverseJoinColumns'];
- if ($selfReferential) {
- $otherColumns = ! $mapping['isOwningSide']
- ? $association['joinTable']['joinColumns']
- : $association['joinTable']['inverseJoinColumns'];
- }
- foreach ($joinColumns as $joinColumn) {
- $keys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
- }
- foreach ($otherColumns as $joinColumn) {
- $otherKeys[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
- }
- if (isset($mapping['isOnDeleteCascade'])) {
- continue;
- }
- $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
- $this->conn->delete($joinTableName, array_combine($keys, $identifier), $types);
- if ($selfReferential) {
- $this->conn->delete($joinTableName, array_combine($otherKeys, $identifier), $types);
- }
- }
- }
- /**
- * {@inheritDoc}
- */
- public function delete($entity)
- {
- $class = $this->class;
- $identifier = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
- $tableName = $this->quoteStrategy->getTableName($class, $this->platform);
- $idColumns = $this->quoteStrategy->getIdentifierColumnNames($class, $this->platform);
- $id = array_combine($idColumns, $identifier);
- $types = $this->getClassIdentifiersTypes($class);
- $this->deleteJoinTableRecords($identifier, $types);
- return (bool) $this->conn->delete($tableName, $id, $types);
- }
- /**
- * Prepares the changeset of an entity for database insertion (UPDATE).
- *
- * The changeset is obtained from the currently running UnitOfWork.
- *
- * During this preparation the array that is passed as the second parameter is filled with
- * <columnName> => <value> pairs, grouped by table name.
- *
- * Example:
- * <code>
- * array(
- * 'foo_table' => array('column1' => 'value1', 'column2' => 'value2', ...),
- * 'bar_table' => array('columnX' => 'valueX', 'columnY' => 'valueY', ...),
- * ...
- * )
- * </code>
- *
- * @param object $entity The entity for which to prepare the data.
- * @param bool $isInsert Whether the data to be prepared refers to an insert statement.
- *
- * @return mixed[][] The prepared data.
- * @psalm-return array<string, array<array-key, mixed|null>>
- */
- protected function prepareUpdateData($entity, bool $isInsert = false)
- {
- $versionField = null;
- $result = [];
- $uow = $this->em->getUnitOfWork();
- $versioned = $this->class->isVersioned;
- if ($versioned !== false) {
- $versionField = $this->class->versionField;
- }
- foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
- if (isset($versionField) && $versionField === $field) {
- continue;
- }
- if (isset($this->class->embeddedClasses[$field])) {
- continue;
- }
- $newVal = $change[1];
- if (! isset($this->class->associationMappings[$field])) {
- $fieldMapping = $this->class->fieldMappings[$field];
- $columnName = $fieldMapping['columnName'];
- if (! $isInsert && isset($fieldMapping['notUpdatable'])) {
- continue;
- }
- if ($isInsert && isset($fieldMapping['notInsertable'])) {
- continue;
- }
- $this->columnTypes[$columnName] = $fieldMapping['type'];
- $result[$this->getOwningTable($field)][$columnName] = $newVal;
- continue;
- }
- $assoc = $this->class->associationMappings[$field];
- // Only owning side of x-1 associations can have a FK column.
- if (! $assoc['isOwningSide'] || ! ($assoc['type'] & ClassMetadata::TO_ONE)) {
- continue;
- }
- if ($newVal !== null) {
- $oid = spl_object_id($newVal);
- if (isset($this->queuedInserts[$oid]) || $uow->isScheduledForInsert($newVal)) {
- // The associated entity $newVal is not yet persisted, so we must
- // set $newVal = null, in order to insert a null value and schedule an
- // extra update on the UnitOfWork.
- $uow->scheduleExtraUpdate($entity, [$field => [null, $newVal]]);
- $newVal = null;
- }
- }
- $newValId = null;
- if ($newVal !== null) {
- $newValId = $uow->getEntityIdentifier($newVal);
- }
- $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
- $owningTable = $this->getOwningTable($field);
- foreach ($assoc['joinColumns'] as $joinColumn) {
- $sourceColumn = $joinColumn['name'];
- $targetColumn = $joinColumn['referencedColumnName'];
- $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- $this->quotedColumns[$sourceColumn] = $quotedColumn;
- $this->columnTypes[$sourceColumn] = PersisterHelper::getTypeOfColumn($targetColumn, $targetClass, $this->em);
- $result[$owningTable][$sourceColumn] = $newValId
- ? $newValId[$targetClass->getFieldForColumn($targetColumn)]
- : null;
- }
- }
- return $result;
- }
- /**
- * Prepares the data changeset of a managed entity for database insertion (initial INSERT).
- * The changeset of the entity is obtained from the currently running UnitOfWork.
- *
- * The default insert data preparation is the same as for updates.
- *
- * @see prepareUpdateData
- *
- * @param object $entity The entity for which to prepare the data.
- *
- * @return mixed[][] The prepared data for the tables to update.
- * @psalm-return array<string, mixed[]>
- */
- protected function prepareInsertData($entity)
- {
- return $this->prepareUpdateData($entity, true);
- }
- /**
- * {@inheritDoc}
- */
- public function getOwningTable($fieldName)
- {
- return $this->class->getTableName();
- }
- /**
- * {@inheritDoc}
- */
- public function load(array $criteria, $entity = null, $assoc = null, array $hints = [], $lockMode = null, $limit = null, ?array $orderBy = null)
- {
- $this->switchPersisterContext(null, $limit);
- $sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
- [$params, $types] = $this->expandParameters($criteria);
- $stmt = $this->conn->executeQuery($sql, $params, $types);
- if ($entity !== null) {
- $hints[Query::HINT_REFRESH] = true;
- $hints[Query::HINT_REFRESH_ENTITY] = $entity;
- }
- $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
- $entities = $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, $hints);
- return $entities ? $entities[0] : null;
- }
- /**
- * {@inheritDoc}
- */
- public function loadById(array $identifier, $entity = null)
- {
- return $this->load($identifier, $entity);
- }
- /**
- * {@inheritDoc}
- */
- public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = [])
- {
- $foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity']);
- if ($foundEntity !== false) {
- return $foundEntity;
- }
- $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
- if ($assoc['isOwningSide']) {
- $isInverseSingleValued = $assoc['inversedBy'] && ! $targetClass->isCollectionValuedAssociation($assoc['inversedBy']);
- // Mark inverse side as fetched in the hints, otherwise the UoW would
- // try to load it in a separate query (remember: to-one inverse sides can not be lazy).
- $hints = [];
- if ($isInverseSingleValued) {
- $hints['fetched']['r'][$assoc['inversedBy']] = true;
- }
- $targetEntity = $this->load($identifier, null, $assoc, $hints);
- // Complete bidirectional association, if necessary
- if ($targetEntity !== null && $isInverseSingleValued) {
- $targetClass->reflFields[$assoc['inversedBy']]->setValue($targetEntity, $sourceEntity);
- }
- return $targetEntity;
- }
- $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
- $owningAssoc = $targetClass->getAssociationMapping($assoc['mappedBy']);
- $computedIdentifier = [];
- // TRICKY: since the association is specular source and target are flipped
- foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
- if (! isset($sourceClass->fieldNames[$sourceKeyColumn])) {
- throw MappingException::joinColumnMustPointToMappedField(
- $sourceClass->name,
- $sourceKeyColumn
- );
- }
- $computedIdentifier[$targetClass->getFieldForColumn($targetKeyColumn)] =
- $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
- }
- $targetEntity = $this->load($computedIdentifier, null, $assoc);
- if ($targetEntity !== null) {
- $targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
- }
- return $targetEntity;
- }
- /**
- * {@inheritDoc}
- */
- public function refresh(array $id, $entity, $lockMode = null)
- {
- $sql = $this->getSelectSQL($id, null, $lockMode);
- [$params, $types] = $this->expandParameters($id);
- $stmt = $this->conn->executeQuery($sql, $params, $types);
- $hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
- $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [Query::HINT_REFRESH => true]);
- }
- /**
- * {@inheritDoc}
- */
- public function count($criteria = [])
- {
- $sql = $this->getCountSQL($criteria);
- [$params, $types] = $criteria instanceof Criteria
- ? $this->expandCriteriaParameters($criteria)
- : $this->expandParameters($criteria);
- return (int) $this->conn->executeQuery($sql, $params, $types)->fetchOne();
- }
- /**
- * {@inheritDoc}
- */
- public function loadCriteria(Criteria $criteria)
- {
- $orderBy = $criteria->getOrderings();
- $limit = $criteria->getMaxResults();
- $offset = $criteria->getFirstResult();
- $query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
- [$params, $types] = $this->expandCriteriaParameters($criteria);
- $stmt = $this->conn->executeQuery($query, $params, $types);
- $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
- return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
- }
- /**
- * {@inheritDoc}
- */
- public function expandCriteriaParameters(Criteria $criteria)
- {
- $expression = $criteria->getWhereExpression();
- $sqlParams = [];
- $sqlTypes = [];
- if ($expression === null) {
- return [$sqlParams, $sqlTypes];
- }
- $valueVisitor = new SqlValueVisitor();
- $valueVisitor->dispatch($expression);
- [, $types] = $valueVisitor->getParamsAndTypes();
- foreach ($types as $type) {
- [$field, $value, $operator] = $type;
- if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) {
- continue;
- }
- $sqlParams = array_merge($sqlParams, $this->getValues($value));
- $sqlTypes = array_merge($sqlTypes, $this->getTypes($field, $value, $this->class));
- }
- return [$sqlParams, $sqlTypes];
- }
- /**
- * {@inheritDoc}
- */
- public function loadAll(array $criteria = [], ?array $orderBy = null, $limit = null, $offset = null)
- {
- $this->switchPersisterContext($offset, $limit);
- $sql = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);
- [$params, $types] = $this->expandParameters($criteria);
- $stmt = $this->conn->executeQuery($sql, $params, $types);
- $hydrator = $this->em->newHydrator($this->currentPersisterContext->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
- return $hydrator->hydrateAll($stmt, $this->currentPersisterContext->rsm, [UnitOfWork::HINT_DEFEREAGERLOAD => true]);
- }
- /**
- * {@inheritDoc}
- */
- public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
- {
- $this->switchPersisterContext($offset, $limit);
- $stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
- return $this->loadArrayFromResult($assoc, $stmt);
- }
- /**
- * Loads an array of entities from a given DBAL statement.
- *
- * @param mixed[] $assoc
- *
- * @return mixed[]
- */
- private function loadArrayFromResult(array $assoc, Result $stmt): array
- {
- $rsm = $this->currentPersisterContext->rsm;
- $hints = [UnitOfWork::HINT_DEFEREAGERLOAD => true];
- if (isset($assoc['indexBy'])) {
- $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
- $rsm->addIndexBy('r', $assoc['indexBy']);
- }
- return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
- }
- /**
- * Hydrates a collection from a given DBAL statement.
- *
- * @param mixed[] $assoc
- *
- * @return mixed[]
- */
- private function loadCollectionFromStatement(
- array $assoc,
- Result $stmt,
- PersistentCollection $coll
- ): array {
- $rsm = $this->currentPersisterContext->rsm;
- $hints = [
- UnitOfWork::HINT_DEFEREAGERLOAD => true,
- 'collection' => $coll,
- ];
- if (isset($assoc['indexBy'])) {
- $rsm = clone $this->currentPersisterContext->rsm; // this is necessary because the "default rsm" should be changed.
- $rsm->addIndexBy('r', $assoc['indexBy']);
- }
- return $this->em->newHydrator(Query::HYDRATE_OBJECT)->hydrateAll($stmt, $rsm, $hints);
- }
- /**
- * {@inheritDoc}
- */
- public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
- {
- $stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
- return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
- }
- /**
- * @param object $sourceEntity
- * @psalm-param array<string, mixed> $assoc
- *
- * @return Result
- *
- * @throws MappingException
- */
- private function getManyToManyStatement(
- array $assoc,
- $sourceEntity,
- ?int $offset = null,
- ?int $limit = null
- ) {
- $this->switchPersisterContext($offset, $limit);
- $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
- $class = $sourceClass;
- $association = $assoc;
- $criteria = [];
- $parameters = [];
- if (! $assoc['isOwningSide']) {
- $class = $this->em->getClassMetadata($assoc['targetEntity']);
- $association = $class->associationMappings[$assoc['mappedBy']];
- }
- $joinColumns = $assoc['isOwningSide']
- ? $association['joinTable']['joinColumns']
- : $association['joinTable']['inverseJoinColumns'];
- $quotedJoinTable = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
- foreach ($joinColumns as $joinColumn) {
- $sourceKeyColumn = $joinColumn['referencedColumnName'];
- $quotedKeyColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
- switch (true) {
- case $sourceClass->containsForeignIdentifier:
- $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
- $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
- if (isset($sourceClass->associationMappings[$field])) {
- $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
- $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
- }
- break;
- case isset($sourceClass->fieldNames[$sourceKeyColumn]):
- $field = $sourceClass->fieldNames[$sourceKeyColumn];
- $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
- break;
- default:
- throw MappingException::joinColumnMustPointToMappedField(
- $sourceClass->name,
- $sourceKeyColumn
- );
- }
- $criteria[$quotedJoinTable . '.' . $quotedKeyColumn] = $value;
- $parameters[] = [
- 'value' => $value,
- 'field' => $field,
- 'class' => $sourceClass,
- ];
- }
- $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
- [$params, $types] = $this->expandToManyParameters($parameters);
- return $this->conn->executeQuery($sql, $params, $types);
- }
- /**
- * {@inheritDoc}
- */
- public function getSelectSQL($criteria, $assoc = null, $lockMode = null, $limit = null, $offset = null, ?array $orderBy = null)
- {
- $this->switchPersisterContext($offset, $limit);
- $lockSql = '';
- $joinSql = '';
- $orderBySql = '';
- if ($assoc !== null && $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
- $joinSql = $this->getSelectManyToManyJoinSQL($assoc);
- }
- if (isset($assoc['orderBy'])) {
- $orderBy = $assoc['orderBy'];
- }
- if ($orderBy) {
- $orderBySql = $this->getOrderBySQL($orderBy, $this->getSQLTableAlias($this->class->name));
- }
- $conditionSql = $criteria instanceof Criteria
- ? $this->getSelectConditionCriteriaSQL($criteria)
- : $this->getSelectConditionSQL($criteria, $assoc);
- switch ($lockMode) {
- case LockMode::PESSIMISTIC_READ:
- $lockSql = ' ' . $this->platform->getReadLockSQL();
- break;
- case LockMode::PESSIMISTIC_WRITE:
- $lockSql = ' ' . $this->platform->getWriteLockSQL();
- break;
- }
- $columnList = $this->getSelectColumnsSQL();
- $tableAlias = $this->getSQLTableAlias($this->class->name);
- $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
- $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
- if ($filterSql !== '') {
- $conditionSql = $conditionSql
- ? $conditionSql . ' AND ' . $filterSql
- : $filterSql;
- }
- $select = 'SELECT ' . $columnList;
- $from = ' FROM ' . $tableName . ' ' . $tableAlias;
- $join = $this->currentPersisterContext->selectJoinSql . $joinSql;
- $where = ($conditionSql ? ' WHERE ' . $conditionSql : '');
- $lock = $this->platform->appendLockHint($from, $lockMode ?? LockMode::NONE);
- $query = $select
- . $lock
- . $join
- . $where
- . $orderBySql;
- return $this->platform->modifyLimitQuery($query, $limit, $offset ?? 0) . $lockSql;
- }
- /**
- * {@inheritDoc}
- */
- public function getCountSQL($criteria = [])
- {
- $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
- $tableAlias = $this->getSQLTableAlias($this->class->name);
- $conditionSql = $criteria instanceof Criteria
- ? $this->getSelectConditionCriteriaSQL($criteria)
- : $this->getSelectConditionSQL($criteria);
- $filterSql = $this->generateFilterConditionSQL($this->class, $tableAlias);
- if ($filterSql !== '') {
- $conditionSql = $conditionSql
- ? $conditionSql . ' AND ' . $filterSql
- : $filterSql;
- }
- return 'SELECT COUNT(*) '
- . 'FROM ' . $tableName . ' ' . $tableAlias
- . (empty($conditionSql) ? '' : ' WHERE ' . $conditionSql);
- }
- /**
- * Gets the ORDER BY SQL snippet for ordered collections.
- *
- * @psalm-param array<string, string> $orderBy
- *
- * @throws InvalidOrientation
- * @throws InvalidFindByCall
- * @throws UnrecognizedField
- */
- final protected function getOrderBySQL(array $orderBy, string $baseTableAlias): string
- {
- $orderByList = [];
- foreach ($orderBy as $fieldName => $orientation) {
- $orientation = strtoupper(trim($orientation));
- if ($orientation !== 'ASC' && $orientation !== 'DESC') {
- throw InvalidOrientation::fromClassNameAndField($this->class->name, $fieldName);
- }
- if (isset($this->class->fieldMappings[$fieldName])) {
- $tableAlias = isset($this->class->fieldMappings[$fieldName]['inherited'])
- ? $this->getSQLTableAlias($this->class->fieldMappings[$fieldName]['inherited'])
- : $baseTableAlias;
- $columnName = $this->quoteStrategy->getColumnName($fieldName, $this->class, $this->platform);
- $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
- continue;
- }
- if (isset($this->class->associationMappings[$fieldName])) {
- if (! $this->class->associationMappings[$fieldName]['isOwningSide']) {
- throw InvalidFindByCall::fromInverseSideUsage($this->class->name, $fieldName);
- }
- $tableAlias = isset($this->class->associationMappings[$fieldName]['inherited'])
- ? $this->getSQLTableAlias($this->class->associationMappings[$fieldName]['inherited'])
- : $baseTableAlias;
- foreach ($this->class->associationMappings[$fieldName]['joinColumns'] as $joinColumn) {
- $columnName = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- $orderByList[] = $tableAlias . '.' . $columnName . ' ' . $orientation;
- }
- continue;
- }
- throw UnrecognizedField::byFullyQualifiedName($this->class->name, $fieldName);
- }
- return ' ORDER BY ' . implode(', ', $orderByList);
- }
- /**
- * Gets the SQL fragment with the list of columns to select when querying for
- * an entity in this persister.
- *
- * Subclasses should override this method to alter or change the select column
- * list SQL fragment. Note that in the implementation of BasicEntityPersister
- * the resulting SQL fragment is generated only once and cached in {@link selectColumnListSql}.
- * Subclasses may or may not do the same.
- *
- * @return string The SQL fragment.
- */
- protected function getSelectColumnsSQL()
- {
- if ($this->currentPersisterContext->selectColumnListSql !== null) {
- return $this->currentPersisterContext->selectColumnListSql;
- }
- $columnList = [];
- $this->currentPersisterContext->rsm->addEntityResult($this->class->name, 'r'); // r for root
- // Add regular columns to select list
- foreach ($this->class->fieldNames as $field) {
- $columnList[] = $this->getSelectColumnSQL($field, $this->class);
- }
- $this->currentPersisterContext->selectJoinSql = '';
- $eagerAliasCounter = 0;
- foreach ($this->class->associationMappings as $assocField => $assoc) {
- $assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
- if ($assocColumnSQL) {
- $columnList[] = $assocColumnSQL;
- }
- $isAssocToOneInverseSide = $assoc['type'] & ClassMetadata::TO_ONE && ! $assoc['isOwningSide'];
- $isAssocFromOneEager = $assoc['type'] !== ClassMetadata::MANY_TO_MANY && $assoc['fetch'] === ClassMetadata::FETCH_EAGER;
- if (! ($isAssocFromOneEager || $isAssocToOneInverseSide)) {
- continue;
- }
- if ((($assoc['type'] & ClassMetadata::TO_MANY) > 0) && $this->currentPersisterContext->handlesLimits) {
- continue;
- }
- $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
- if ($eagerEntity->inheritanceType !== ClassMetadata::INHERITANCE_TYPE_NONE) {
- continue; // now this is why you shouldn't use inheritance
- }
- $assocAlias = 'e' . ($eagerAliasCounter++);
- $this->currentPersisterContext->rsm->addJoinedEntityResult($assoc['targetEntity'], $assocAlias, 'r', $assocField);
- foreach ($eagerEntity->fieldNames as $field) {
- $columnList[] = $this->getSelectColumnSQL($field, $eagerEntity, $assocAlias);
- }
- foreach ($eagerEntity->associationMappings as $eagerAssocField => $eagerAssoc) {
- $eagerAssocColumnSQL = $this->getSelectColumnAssociationSQL(
- $eagerAssocField,
- $eagerAssoc,
- $eagerEntity,
- $assocAlias
- );
- if ($eagerAssocColumnSQL) {
- $columnList[] = $eagerAssocColumnSQL;
- }
- }
- $association = $assoc;
- $joinCondition = [];
- if (isset($assoc['indexBy'])) {
- $this->currentPersisterContext->rsm->addIndexBy($assocAlias, $assoc['indexBy']);
- }
- if (! $assoc['isOwningSide']) {
- $eagerEntity = $this->em->getClassMetadata($assoc['targetEntity']);
- $association = $eagerEntity->getAssociationMapping($assoc['mappedBy']);
- }
- $joinTableAlias = $this->getSQLTableAlias($eagerEntity->name, $assocAlias);
- $joinTableName = $this->quoteStrategy->getTableName($eagerEntity, $this->platform);
- if ($assoc['isOwningSide']) {
- $tableAlias = $this->getSQLTableAlias($association['targetEntity'], $assocAlias);
- $this->currentPersisterContext->selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($association['joinColumns']);
- foreach ($association['joinColumns'] as $joinColumn) {
- $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
- $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'])
- . '.' . $sourceCol . ' = ' . $tableAlias . '.' . $targetCol;
- }
- // Add filter SQL
- $filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias);
- if ($filterSql) {
- $joinCondition[] = $filterSql;
- }
- } else {
- $this->currentPersisterContext->selectJoinSql .= ' LEFT JOIN';
- foreach ($association['joinColumns'] as $joinColumn) {
- $sourceCol = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- $targetCol = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
- $joinCondition[] = $this->getSQLTableAlias($association['sourceEntity'], $assocAlias) . '.' . $sourceCol . ' = '
- . $this->getSQLTableAlias($association['targetEntity']) . '.' . $targetCol;
- }
- }
- $this->currentPersisterContext->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
- $this->currentPersisterContext->selectJoinSql .= implode(' AND ', $joinCondition);
- }
- $this->currentPersisterContext->selectColumnListSql = implode(', ', $columnList);
- return $this->currentPersisterContext->selectColumnListSql;
- }
- /**
- * Gets the SQL join fragment used when selecting entities from an association.
- *
- * @param string $field
- * @param AssociationMapping $assoc
- * @param string $alias
- *
- * @return string
- */
- protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
- {
- if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
- return '';
- }
- $columnList = [];
- $targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
- $isIdentifier = isset($assoc['id']) && $assoc['id'] === true;
- $sqlTableAlias = $this->getSQLTableAlias($class->name, ($alias === 'r' ? '' : $alias));
- foreach ($assoc['joinColumns'] as $joinColumn) {
- $quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- $resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
- $type = PersisterHelper::getTypeOfColumn($joinColumn['referencedColumnName'], $targetClass, $this->em);
- $this->currentPersisterContext->rsm->addMetaResult($alias, $resultColumnName, $joinColumn['name'], $isIdentifier, $type);
- $columnList[] = sprintf('%s.%s AS %s', $sqlTableAlias, $quotedColumn, $resultColumnName);
- }
- return implode(', ', $columnList);
- }
- /**
- * Gets the SQL join fragment used when selecting entities from a
- * many-to-many association.
- *
- * @psalm-param AssociationMapping $manyToMany
- *
- * @return string
- */
- protected function getSelectManyToManyJoinSQL(array $manyToMany)
- {
- $conditions = [];
- $association = $manyToMany;
- $sourceTableAlias = $this->getSQLTableAlias($this->class->name);
- if (! $manyToMany['isOwningSide']) {
- $targetEntity = $this->em->getClassMetadata($manyToMany['targetEntity']);
- $association = $targetEntity->associationMappings[$manyToMany['mappedBy']];
- }
- $joinTableName = $this->quoteStrategy->getJoinTableName($association, $this->class, $this->platform);
- $joinColumns = $manyToMany['isOwningSide']
- ? $association['joinTable']['inverseJoinColumns']
- : $association['joinTable']['joinColumns'];
- foreach ($joinColumns as $joinColumn) {
- $quotedSourceColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- $quotedTargetColumn = $this->quoteStrategy->getReferencedJoinColumnName($joinColumn, $this->class, $this->platform);
- $conditions[] = $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $joinTableName . '.' . $quotedSourceColumn;
- }
- return ' INNER JOIN ' . $joinTableName . ' ON ' . implode(' AND ', $conditions);
- }
- /**
- * {@inheritDoc}
- */
- public function getInsertSQL()
- {
- if ($this->insertSql !== null) {
- return $this->insertSql;
- }
- $columns = $this->getInsertColumnList();
- $tableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
- if (empty($columns)) {
- $identityColumn = $this->quoteStrategy->getColumnName($this->class->identifier[0], $this->class, $this->platform);
- $this->insertSql = $this->platform->getEmptyIdentityInsertSQL($tableName, $identityColumn);
- return $this->insertSql;
- }
- $values = [];
- $columns = array_unique($columns);
- foreach ($columns as $column) {
- $placeholder = '?';
- if (
- isset($this->class->fieldNames[$column])
- && isset($this->columnTypes[$this->class->fieldNames[$column]])
- && isset($this->class->fieldMappings[$this->class->fieldNames[$column]]['requireSQLConversion'])
- ) {
- $type = Type::getType($this->columnTypes[$this->class->fieldNames[$column]]);
- $placeholder = $type->convertToDatabaseValueSQL('?', $this->platform);
- }
- $values[] = $placeholder;
- }
- $columns = implode(', ', $columns);
- $values = implode(', ', $values);
- $this->insertSql = sprintf('INSERT INTO %s (%s) VALUES (%s)', $tableName, $columns, $values);
- return $this->insertSql;
- }
- /**
- * Gets the list of columns to put in the INSERT SQL statement.
- *
- * Subclasses should override this method to alter or change the list of
- * columns placed in the INSERT statements used by the persister.
- *
- * @return string[] The list of columns.
- * @psalm-return list<string>
- */
- protected function getInsertColumnList()
- {
- $columns = [];
- foreach ($this->class->reflFields as $name => $field) {
- if ($this->class->isVersioned && $this->class->versionField === $name) {
- continue;
- }
- if (isset($this->class->embeddedClasses[$name])) {
- continue;
- }
- if (isset($this->class->associationMappings[$name])) {
- $assoc = $this->class->associationMappings[$name];
- if ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) {
- foreach ($assoc['joinColumns'] as $joinColumn) {
- $columns[] = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- }
- }
- continue;
- }
- if (! $this->class->isIdGeneratorIdentity() || $this->class->identifier[0] !== $name) {
- if (isset($this->class->fieldMappings[$name]['notInsertable'])) {
- continue;
- }
- $columns[] = $this->quoteStrategy->getColumnName($name, $this->class, $this->platform);
- $this->columnTypes[$name] = $this->class->fieldMappings[$name]['type'];
- }
- }
- return $columns;
- }
- /**
- * Gets the SQL snippet of a qualified column name for the given field name.
- *
- * @param string $field The field name.
- * @param ClassMetadata $class The class that declares this field. The table this class is
- * mapped to must own the column for the given field.
- * @param string $alias
- *
- * @return string
- */
- protected function getSelectColumnSQL($field, ClassMetadata $class, $alias = 'r')
- {
- $root = $alias === 'r' ? '' : $alias;
- $tableAlias = $this->getSQLTableAlias($class->name, $root);
- $fieldMapping = $class->fieldMappings[$field];
- $sql = sprintf('%s.%s', $tableAlias, $this->quoteStrategy->getColumnName($field, $class, $this->platform));
- $columnAlias = $this->getSQLColumnAlias($fieldMapping['columnName']);
- $this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field);
- if (! empty($fieldMapping['enumType'])) {
- $this->currentPersisterContext->rsm->addEnumResult($columnAlias, $fieldMapping['enumType']);
- }
- if (isset($fieldMapping['requireSQLConversion'])) {
- $type = Type::getType($fieldMapping['type']);
- $sql = $type->convertToPHPValueSQL($sql, $this->platform);
- }
- return $sql . ' AS ' . $columnAlias;
- }
- /**
- * Gets the SQL table alias for the given class name.
- *
- * @param string $className
- * @param string $assocName
- *
- * @return string The SQL table alias.
- *
- * @todo Reconsider. Binding table aliases to class names is not such a good idea.
- */
- protected function getSQLTableAlias($className, $assocName = '')
- {
- if ($assocName) {
- $className .= '#' . $assocName;
- }
- if (isset($this->currentPersisterContext->sqlTableAliases[$className])) {
- return $this->currentPersisterContext->sqlTableAliases[$className];
- }
- $tableAlias = 't' . $this->currentPersisterContext->sqlAliasCounter++;
- $this->currentPersisterContext->sqlTableAliases[$className] = $tableAlias;
- return $tableAlias;
- }
- /**
- * {@inheritDoc}
- */
- public function lock(array $criteria, $lockMode)
- {
- $lockSql = '';
- $conditionSql = $this->getSelectConditionSQL($criteria);
- switch ($lockMode) {
- case LockMode::PESSIMISTIC_READ:
- $lockSql = $this->platform->getReadLockSQL();
- break;
- case LockMode::PESSIMISTIC_WRITE:
- $lockSql = $this->platform->getWriteLockSQL();
- break;
- }
- $lock = $this->getLockTablesSql($lockMode);
- $where = ($conditionSql ? ' WHERE ' . $conditionSql : '') . ' ';
- $sql = 'SELECT 1 '
- . $lock
- . $where
- . $lockSql;
- [$params, $types] = $this->expandParameters($criteria);
- $this->conn->executeQuery($sql, $params, $types);
- }
- /**
- * Gets the FROM and optionally JOIN conditions to lock the entity managed by this persister.
- *
- * @param int|null $lockMode One of the Doctrine\DBAL\LockMode::* constants.
- * @psalm-param LockMode::*|null $lockMode
- *
- * @return string
- */
- protected function getLockTablesSql($lockMode)
- {
- if ($lockMode === null) {
- Deprecation::trigger(
- 'doctrine/orm',
- 'https://github.com/doctrine/orm/pull/9466',
- 'Passing null as argument to %s is deprecated, pass LockMode::NONE instead.',
- __METHOD__
- );
- $lockMode = LockMode::NONE;
- }
- return $this->platform->appendLockHint(
- 'FROM '
- . $this->quoteStrategy->getTableName($this->class, $this->platform) . ' '
- . $this->getSQLTableAlias($this->class->name),
- $lockMode
- );
- }
- /**
- * Gets the Select Where Condition from a Criteria object.
- *
- * @return string
- */
- protected function getSelectConditionCriteriaSQL(Criteria $criteria)
- {
- $expression = $criteria->getWhereExpression();
- if ($expression === null) {
- return '';
- }
- $visitor = new SqlExpressionVisitor($this, $this->class);
- return $visitor->dispatch($expression);
- }
- /**
- * {@inheritDoc}
- */
- public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
- {
- $selectedColumns = [];
- $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc);
- if (count($columns) > 1 && $comparison === Comparison::IN) {
- /*
- * @todo try to support multi-column IN expressions.
- * Example: (col1, col2) IN (('val1A', 'val2A'), ('val1B', 'val2B'))
- */
- throw CantUseInOperatorOnCompositeKeys::create();
- }
- foreach ($columns as $column) {
- $placeholder = '?';
- if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
- $type = Type::getType($this->class->fieldMappings[$field]['type']);
- $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform);
- }
- if ($comparison !== null) {
- // special case null value handling
- if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
- $selectedColumns[] = $column . ' IS NULL';
- continue;
- }
- if ($comparison === Comparison::NEQ && $value === null) {
- $selectedColumns[] = $column . ' IS NOT NULL';
- continue;
- }
- $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
- continue;
- }
- if (is_array($value)) {
- $in = sprintf('%s IN (%s)', $column, $placeholder);
- if (array_search(null, $value, true) !== false) {
- $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column);
- continue;
- }
- $selectedColumns[] = $in;
- continue;
- }
- if ($value === null) {
- $selectedColumns[] = sprintf('%s IS NULL', $column);
- continue;
- }
- $selectedColumns[] = sprintf('%s = %s', $column, $placeholder);
- }
- return implode(' AND ', $selectedColumns);
- }
- /**
- * Builds the left-hand-side of a where condition statement.
- *
- * @psalm-param AssociationMapping|null $assoc
- *
- * @return string[]
- * @psalm-return list<string>
- *
- * @throws InvalidFindByCall
- * @throws UnrecognizedField
- */
- private function getSelectConditionStatementColumnSQL(
- string $field,
- ?array $assoc = null
- ): array {
- if (isset($this->class->fieldMappings[$field])) {
- $className = $this->class->fieldMappings[$field]['inherited'] ?? $this->class->name;
- return [$this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getColumnName($field, $this->class, $this->platform)];
- }
- if (isset($this->class->associationMappings[$field])) {
- $association = $this->class->associationMappings[$field];
- // Many-To-Many requires join table check for joinColumn
- $columns = [];
- $class = $this->class;
- if ($association['type'] === ClassMetadata::MANY_TO_MANY) {
- if (! $association['isOwningSide']) {
- $association = $assoc;
- }
- $joinTableName = $this->quoteStrategy->getJoinTableName($association, $class, $this->platform);
- $joinColumns = $assoc['isOwningSide']
- ? $association['joinTable']['joinColumns']
- : $association['joinTable']['inverseJoinColumns'];
- foreach ($joinColumns as $joinColumn) {
- $columns[] = $joinTableName . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $class, $this->platform);
- }
- } else {
- if (! $association['isOwningSide']) {
- throw InvalidFindByCall::fromInverseSideUsage(
- $this->class->name,
- $field
- );
- }
- $className = $association['inherited'] ?? $this->class->name;
- foreach ($association['joinColumns'] as $joinColumn) {
- $columns[] = $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
- }
- }
- return $columns;
- }
- if ($assoc !== null && ! str_contains($field, ' ') && ! str_contains($field, '(')) {
- // very careless developers could potentially open up this normally hidden api for userland attacks,
- // therefore checking for spaces and function calls which are not allowed.
- // found a join column condition, not really a "field"
- return [$field];
- }
- throw UnrecognizedField::byFullyQualifiedName($this->class->name, $field);
- }
- /**
- * Gets the conditional SQL fragment used in the WHERE clause when selecting
- * entities in this persister.
- *
- * Subclasses are supposed to override this method if they intend to change
- * or alter the criteria by which entities are selected.
- *
- * @param AssociationMapping|null $assoc
- * @psalm-param array<string, mixed> $criteria
- * @psalm-param array<string, mixed>|null $assoc
- *
- * @return string
- */
- protected function getSelectConditionSQL(array $criteria, $assoc = null)
- {
- $conditions = [];
- foreach ($criteria as $field => $value) {
- $conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
- }
- return implode(' AND ', $conditions);
- }
- /**
- * {@inheritDoc}
- */
- public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
- {
- $this->switchPersisterContext($offset, $limit);
- $stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
- return $this->loadArrayFromResult($assoc, $stmt);
- }
- /**
- * {@inheritDoc}
- */
- public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $collection)
- {
- $stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
- return $this->loadCollectionFromStatement($assoc, $stmt, $collection);
- }
- /**
- * Builds criteria and execute SQL statement to fetch the one to many entities from.
- *
- * @param object $sourceEntity
- * @psalm-param AssociationMapping $assoc
- */
- private function getOneToManyStatement(
- array $assoc,
- $sourceEntity,
- ?int $offset = null,
- ?int $limit = null
- ): Result {
- $this->switchPersisterContext($offset, $limit);
- $criteria = [];
- $parameters = [];
- $owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
- $sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
- $tableAlias = $this->getSQLTableAlias($owningAssoc['inherited'] ?? $this->class->name);
- foreach ($owningAssoc['targetToSourceKeyColumns'] as $sourceKeyColumn => $targetKeyColumn) {
- if ($sourceClass->containsForeignIdentifier) {
- $field = $sourceClass->getFieldForColumn($sourceKeyColumn);
- $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
- if (isset($sourceClass->associationMappings[$field])) {
- $value = $this->em->getUnitOfWork()->getEntityIdentifier($value);
- $value = $value[$this->em->getClassMetadata($sourceClass->associationMappings[$field]['targetEntity'])->identifier[0]];
- }
- $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
- $parameters[] = [
- 'value' => $value,
- 'field' => $field,
- 'class' => $sourceClass,
- ];
- continue;
- }
- $field = $sourceClass->fieldNames[$sourceKeyColumn];
- $value = $sourceClass->reflFields[$field]->getValue($sourceEntity);
- $criteria[$tableAlias . '.' . $targetKeyColumn] = $value;
- $parameters[] = [
- 'value' => $value,
- 'field' => $field,
- 'class' => $sourceClass,
- ];
- }
- $sql = $this->getSelectSQL($criteria, $assoc, null, $limit, $offset);
- [$params, $types] = $this->expandToManyParameters($parameters);
- return $this->conn->executeQuery($sql, $params, $types);
- }
- /**
- * {@inheritDoc}
- */
- public function expandParameters($criteria)
- {
- $params = [];
- $types = [];
- foreach ($criteria as $field => $value) {
- if ($value === null) {
- continue; // skip null values.
- }
- $types = array_merge($types, $this->getTypes($field, $value, $this->class));
- $params = array_merge($params, $this->getValues($value));
- }
- return [$params, $types];
- }
- /**
- * Expands the parameters from the given criteria and use the correct binding types if found,
- * specialized for OneToMany or ManyToMany associations.
- *
- * @param mixed[][] $criteria an array of arrays containing following:
- * - field to which each criterion will be bound
- * - value to be bound
- * - class to which the field belongs to
- *
- * @return mixed[][]
- * @psalm-return array{0: array, 1: list<int|string|null>}
- */
- private function expandToManyParameters(array $criteria): array
- {
- $params = [];
- $types = [];
- foreach ($criteria as $criterion) {
- if ($criterion['value'] === null) {
- continue; // skip null values.
- }
- $types = array_merge($types, $this->getTypes($criterion['field'], $criterion['value'], $criterion['class']));
- $params = array_merge($params, $this->getValues($criterion['value']));
- }
- return [$params, $types];
- }
- /**
- * Infers field types to be used by parameter type casting.
- *
- * @param mixed $value
- *
- * @return int[]|null[]|string[]
- * @psalm-return list<int|string|null>
- *
- * @throws QueryException
- */
- private function getTypes(string $field, $value, ClassMetadata $class): array
- {
- $types = [];
- switch (true) {
- case isset($class->fieldMappings[$field]):
- $types = array_merge($types, [$class->fieldMappings[$field]['type']]);
- break;
- case isset($class->associationMappings[$field]):
- $assoc = $class->associationMappings[$field];
- $class = $this->em->getClassMetadata($assoc['targetEntity']);
- if (! $assoc['isOwningSide']) {
- $assoc = $class->associationMappings[$assoc['mappedBy']];
- $class = $this->em->getClassMetadata($assoc['targetEntity']);
- }
- $columns = $assoc['type'] === ClassMetadata::MANY_TO_MANY
- ? $assoc['relationToTargetKeyColumns']
- : $assoc['sourceToTargetKeyColumns'];
- foreach ($columns as $column) {
- $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em);
- }
- break;
- default:
- $types[] = null;
- break;
- }
- if (is_array($value)) {
- return array_map(static function ($type) {
- $type = Type::getType($type);
- return $type->getBindingType() + Connection::ARRAY_PARAM_OFFSET;
- }, $types);
- }
- return $types;
- }
- /**
- * Retrieves the parameters that identifies a value.
- *
- * @param mixed $value
- *
- * @return mixed[]
- */
- private function getValues($value): array
- {
- if (is_array($value)) {
- $newValue = [];
- foreach ($value as $itemValue) {
- $newValue = array_merge($newValue, $this->getValues($itemValue));
- }
- return [$newValue];
- }
- return $this->getIndividualValue($value);
- }
- /**
- * Retrieves an individual parameter value.
- *
- * @param mixed $value
- *
- * @psalm-return list<mixed>
- */
- private function getIndividualValue($value): array
- {
- if (! is_object($value)) {
- return [$value];
- }
- if ($value instanceof BackedEnum) {
- return [$value->value];
- }
- $valueClass = ClassUtils::getClass($value);
- if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
- return [$value];
- }
- $class = $this->em->getClassMetadata($valueClass);
- if ($class->isIdentifierComposite) {
- $newValue = [];
- foreach ($class->getIdentifierValues($value) as $innerValue) {
- $newValue = array_merge($newValue, $this->getValues($innerValue));
- }
- return $newValue;
- }
- return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];
- }
- /**
- * {@inheritDoc}
- */
- public function exists($entity, ?Criteria $extraConditions = null)
- {
- $criteria = $this->class->getIdentifierValues($entity);
- if (! $criteria) {
- return false;
- }
- $alias = $this->getSQLTableAlias($this->class->name);
- $sql = 'SELECT 1 '
- . $this->getLockTablesSql(LockMode::NONE)
- . ' WHERE ' . $this->getSelectConditionSQL($criteria);
- [$params, $types] = $this->expandParameters($criteria);
- if ($extraConditions !== null) {
- $sql .= ' AND ' . $this->getSelectConditionCriteriaSQL($extraConditions);
- [$criteriaParams, $criteriaTypes] = $this->expandCriteriaParameters($extraConditions);
- $params = array_merge($params, $criteriaParams);
- $types = array_merge($types, $criteriaTypes);
- }
- $filterSql = $this->generateFilterConditionSQL($this->class, $alias);
- if ($filterSql) {
- $sql .= ' AND ' . $filterSql;
- }
- return (bool) $this->conn->fetchOne($sql, $params, $types);
- }
- /**
- * Generates the appropriate join SQL for the given join column.
- *
- * @param array[] $joinColumns The join columns definition of an association.
- * @psalm-param array<array<string, mixed>> $joinColumns
- *
- * @return string LEFT JOIN if one of the columns is nullable, INNER JOIN otherwise.
- */
- protected function getJoinSQLForJoinColumns($joinColumns)
- {
- // if one of the join columns is nullable, return left join
- foreach ($joinColumns as $joinColumn) {
- if (! isset($joinColumn['nullable']) || $joinColumn['nullable']) {
- return 'LEFT JOIN';
- }
- }
- return 'INNER JOIN';
- }
- /**
- * @param string $columnName
- *
- * @return string
- */
- public function getSQLColumnAlias($columnName)
- {
- return $this->quoteStrategy->getColumnAlias($columnName, $this->currentPersisterContext->sqlAliasCounter++, $this->platform);
- }
- /**
- * Generates the filter SQL for a given entity and table alias.
- *
- * @param ClassMetadata $targetEntity Metadata of the target entity.
- * @param string $targetTableAlias The table alias of the joined/selected table.
- *
- * @return string The SQL query part to add to a query.
- */
- protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias)
- {
- $filterClauses = [];
- foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
- $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias);
- if ($filterExpr !== '') {
- $filterClauses[] = '(' . $filterExpr . ')';
- }
- }
- $sql = implode(' AND ', $filterClauses);
- return $sql ? '(' . $sql . ')' : ''; // Wrap again to avoid "X or Y and FilterConditionSQL"
- }
- /**
- * Switches persister context according to current query offset/limits
- *
- * This is due to the fact that to-many associations cannot be fetch-joined when a limit is involved
- *
- * @param int|null $offset
- * @param int|null $limit
- *
- * @return void
- */
- protected function switchPersisterContext($offset, $limit)
- {
- if ($offset === null && $limit === null) {
- $this->currentPersisterContext = $this->noLimitsContext;
- return;
- }
- $this->currentPersisterContext = $this->limitsHandlingContext;
- }
- /**
- * @return string[]
- * @psalm-return list<string>
- */
- protected function getClassIdentifiersTypes(ClassMetadata $class): array
- {
- $entityManager = $this->em;
- return array_map(
- static function ($fieldName) use ($class, $entityManager): string {
- $types = PersisterHelper::getTypeOfField($fieldName, $class, $entityManager);
- assert(isset($types[0]));
- return $types[0];
- },
- $class->identifier
- );
- }
- }