Файл: concrete5.7.5.6/concrete/vendor/doctrine/orm/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php
Строк: 2617
<?php
/*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*
* This software consists of voluntary contributions made by many individuals
* and is licensed under the MIT license. For more information, see
* <http://www.doctrine-project.org>.
*/
namespace DoctrineORMPersisters;
use DoctrineDBALLockMode;
use DoctrineDBALTypesType;
use DoctrineDBALConnection;
use DoctrineORMORMException;
use DoctrineORMOptimisticLockException;
use DoctrineORMEntityManager;
use DoctrineORMUnitOfWork;
use DoctrineORMQuery;
use DoctrineORMPersistentCollection;
use DoctrineORMMappingMappingException;
use DoctrineORMMappingClassMetadata;
use DoctrineCommonUtilClassUtils;
use DoctrineCommonCollectionsCriteria;
use DoctrineCommonCollectionsExprComparison;
/**
* 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.
*
* @author Roman Borschel <roman@code-factory.org>
* @author Giorgio Sironi <piccoloprincipeazzurro@gmail.com>
* @author Benjamin Eberlei <kontakt@beberlei.de>
* @author Alexander <iam.asm89@gmail.com>
* @author Fabio B. Silva <fabio.bat.silva@gmail.com>
* @since 2.0
*/
class BasicEntityPersister
{
/**
* @var array
*/
static private $comparisonMap = array(
Comparison::EQ => '= %s',
Comparison::IS => '= %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',
);
/**
* Metadata object that describes the mapping of the mapped entity class.
*
* @var DoctrineORMMappingClassMetadata
*/
protected $class;
/**
* The underlying DBAL Connection of the used EntityManager.
*
* @var DoctrineDBALConnection $conn
*/
protected $conn;
/**
* The database platform.
*
* @var DoctrineDBALPlatformsAbstractPlatform
*/
protected $platform;
/**
* The EntityManager instance.
*
* @var DoctrineORMEntityManager
*/
protected $em;
/**
* Queued inserts.
*
* @var array
*/
protected $queuedInserts = array();
/**
* ResultSetMapping that is used for all queries. Is generated lazily once per request.
*
* TODO: Evaluate Caching in combination with the other cached SQL snippets.
*
* @var QueryResultSetMapping
*/
protected $rsm;
/**
* The map of column names to DBAL mapping types of all prepared columns used
* when INSERTing or UPDATEing an entity.
*
* @var array
*
* @see prepareInsertData($entity)
* @see prepareUpdateData($entity)
*/
protected $columnTypes = array();
/**
* The map of quoted column names.
*
* @var array
*
* @see prepareInsertData($entity)
* @see prepareUpdateData($entity)
*/
protected $quotedColumns = array();
/**
* The INSERT SQL statement used for entities handled by this persister.
* This SQL is only generated once per request, if at all.
*
* @var string
*/
private $insertSql;
/**
* The SELECT column list SQL fragment used for querying entities by this persister.
* This SQL fragment is only generated once per request, if at all.
*
* @var string
*/
protected $selectColumnListSql;
/**
* The JOIN SQL fragment used to eagerly load all many-to-one and one-to-one
* associations configured as FETCH_EAGER, as well as all inverse one-to-one associations.
*
* @var string
*/
protected $selectJoinSql;
/**
* Counter for creating unique SQL table and column aliases.
*
* @var integer
*/
protected $sqlAliasCounter = 0;
/**
* Map from class names (FQCN) to the corresponding generated SQL table aliases.
*
* @var array
*/
protected $sqlTableAliases = array();
/**
* The quote strategy.
*
* @var DoctrineORMMappingQuoteStrategy
*/
protected $quoteStrategy;
/**
* Initializes a new <tt>BasicEntityPersister</tt> that uses the given EntityManager
* and persists instances of the class described by the given ClassMetadata descriptor.
*
* @param DoctrineORMEntityManager $em
* @param DoctrineORMMappingClassMetadata $class
*/
public function __construct(EntityManager $em, ClassMetadata $class)
{
$this->em = $em;
$this->class = $class;
$this->conn = $em->getConnection();
$this->platform = $this->conn->getDatabasePlatform();
$this->quoteStrategy = $em->getConfiguration()->getQuoteStrategy();
}
/**
* @return DoctrineORMMappingClassMetadata
*/
public function getClassMetadata()
{
return $this->class;
}
/**
* Adds an entity to the queued insertions.
* The entity remains queued until {@link executeInserts} is invoked.
*
* @param object $entity The entity to queue for insertion.
*
* @return void
*/
public function addInsert($entity)
{
$this->queuedInserts[spl_object_hash($entity)] = $entity;
}
/**
* Executes all queued entity insertions and returns any generated post-insert
* identifiers that were created as a result of the insertions.
*
* If no inserts are queued, invoking this method is a NOOP.
*
* @return array An array of any generated post-insert IDs. This will be an empty array
* if the entity class does not use the IDENTITY generation strategy.
*/
public function executeInserts()
{
if ( ! $this->queuedInserts) {
return array();
}
$postInsertIds = array();
$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->execute();
if ($isPostInsertId) {
$id = $idGenerator->generate($this->em, $entity);
$postInsertIds[$id] = $entity;
} else {
$id = $this->class->getIdentifierValues($entity);
}
if ($this->class->isVersioned) {
$this->assignDefaultVersionValue($entity, $id);
}
}
$stmt->closeCursor();
$this->queuedInserts = array();
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.
*
* @param object $entity
* @param mixed $id
*
* @return void
*/
protected function assignDefaultVersionValue($entity, $id)
{
$value = $this->fetchVersionValue($this->class, $id);
$this->class->setFieldValue($entity, $this->class->versionField, $value);
}
/**
* Fetches the current version value of a versioned entity.
*
* @param DoctrineORMMappingClassMetadata $versionedClass
* @param mixed $id
*
* @return mixed
*/
protected function fetchVersionValue($versionedClass, $id)
{
$versionField = $versionedClass->versionField;
$tableName = $this->quoteStrategy->getTableName($versionedClass, $this->platform);
$identifier = $this->quoteStrategy->getIdentifierColumnNames($versionedClass, $this->platform);
$columnName = $this->quoteStrategy->getColumnName($versionField, $versionedClass, $this->platform);
//FIXME: Order with composite keys might not be correct
$sql = 'SELECT ' . $columnName
. ' FROM ' . $tableName
. ' WHERE ' . implode(' = ? AND ', $identifier) . ' = ?';
$value = $this->conn->fetchColumn($sql, array_values((array) $id));
return Type::getType($versionedClass->fieldMappings[$versionField]['type'])->convertToPHPValue($value, $this->platform);
}
/**
* Updates a managed entity. The entity is updated according to its current changeset
* in the running UnitOfWork. If there is no changeset, nothing is updated.
*
* The data to update is retrieved through {@link prepareUpdateData}.
* Subclasses that override this method are supposed to obtain the update data
* in the same way, through {@link prepareUpdateData}.
*
* Subclasses are also supposed to take care of versioning when overriding this method,
* if necessary. The {@link updateTable} method can be used to apply the data retrieved
* from {@prepareUpdateData} on the target tables, thereby optionally applying versioning.
*
* @param object $entity The entity to update.
*
* @return void
*/
public function update($entity)
{
$tableName = $this->class->getTableName();
$updateData = $this->prepareUpdateData($entity);
if ( ! isset($updateData[$tableName]) || ! ($data = $updateData[$tableName])) {
return;
}
$isVersioned = $this->class->isVersioned;
$quotedTableName = $this->quoteStrategy->getTableName($this->class, $this->platform);
$this->updateTable($entity, $quotedTableName, $data, $isVersioned);
if ($isVersioned) {
$id = $this->em->getUnitOfWork()->getEntityIdentifier($entity);
$this->assignDefaultVersionValue($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 array $updateData The map of columns to update (column => value).
* @param boolean $versioned Whether the UPDATE should be versioned.
*
* @return void
*
* @throws DoctrineORMORMException
* @throws DoctrineORMOptimisticLockException
*/
protected final function updateTable($entity, $quotedTableName, array $updateData, $versioned = false)
{
$set = array();
$types = array();
$params = array();
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 = array();
$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->class->associationMappings[$idField]['joinColumns'][0]['name'];
$targetMapping = $this->em->getClassMetadata($this->class->associationMappings[$idField]['targetEntity']);
switch (true) {
case (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])):
$types[] = $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
break;
case (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])):
$types[] = $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
break;
default:
throw ORMException::unrecognizedField($targetMapping->identifier[0]);
}
}
if ($versioned) {
$versionField = $this->class->versionField;
$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 Type::INTEGER:
$set[] = $versionColumn . ' = ' . $versionColumn . ' + 1';
break;
case Type::DATETIME:
$set[] = $versionColumn . ' = CURRENT_TIMESTAMP';
break;
}
}
$sql = 'UPDATE ' . $quotedTableName
. ' SET ' . implode(', ', $set)
. ' WHERE ' . implode(' = ? AND ', $where) . ' = ?';
$result = $this->conn->executeUpdate($sql, $params, $types);
if ($versioned && ! $result) {
throw OptimisticLockException::lockFailed($entity);
}
}
/**
* @todo Add check for platform if it supports foreign keys/cascading.
*
* @param array $identifier
*
* @return void
*/
protected function deleteJoinTableRecords($identifier)
{
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 = array();
$otherKeys = array();
$keys = array();
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));
if ($selfReferential) {
$this->conn->delete($joinTableName, array_combine($otherKeys, $identifier));
}
}
}
/**
* Deletes a managed entity.
*
* The entity to delete must be managed and have a persistent identifier.
* The deletion happens instantaneously.
*
* Subclasses may override this method to customize the semantics of entity deletion.
*
* @param object $entity The entity to delete.
*
* @return void
*/
public function delete($entity)
{
$class = $this->class;
$em = $this->em;
$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 = array_map(function ($identifier) use ($class, $em) {
if (isset($class->fieldMappings[$identifier])) {
return $class->fieldMappings[$identifier]['type'];
}
$targetMapping = $em->getClassMetadata($class->associationMappings[$identifier]['targetEntity']);
if (isset($targetMapping->fieldMappings[$targetMapping->identifier[0]])) {
return $targetMapping->fieldMappings[$targetMapping->identifier[0]]['type'];
}
if (isset($targetMapping->associationMappings[$targetMapping->identifier[0]])) {
return $targetMapping->associationMappings[$targetMapping->identifier[0]]['type'];
}
throw ORMException::unrecognizedField($targetMapping->identifier[0]);
}, $class->identifier);
$this->deleteJoinTableRecords($identifier);
$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.
*
* @return array The prepared data.
*/
protected function prepareUpdateData($entity)
{
$result = array();
$uow = $this->em->getUnitOfWork();
if (($versioned = $this->class->isVersioned) != false) {
$versionField = $this->class->versionField;
}
foreach ($uow->getEntityChangeSet($entity) as $field => $change) {
if ($versioned && $versionField == $field) {
continue;
}
$newVal = $change[1];
if ( ! isset($this->class->associationMappings[$field])) {
$columnName = $this->class->columnNames[$field];
$this->columnTypes[$columnName] = $this->class->fieldMappings[$field]['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_hash($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, array($field => array(null, $newVal)));
$newVal = 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] = $targetClass->getTypeOfColumn($targetColumn);
switch (true) {
case $newVal === null:
$value = null;
break;
case $targetClass->containsForeignIdentifier:
$value = $newValId[$targetClass->getFieldForColumn($targetColumn)];
break;
default:
$value = $newValId[$targetClass->fieldNames[$targetColumn]];
break;
}
$result[$owningTable][$sourceColumn] = $value;
}
}
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.
*
* @param object $entity The entity for which to prepare the data.
*
* @return array The prepared data for the tables to update.
*
* @see prepareUpdateData
*/
protected function prepareInsertData($entity)
{
return $this->prepareUpdateData($entity);
}
/**
* Gets the name of the table that owns the column the given field is mapped to.
*
* The default implementation in BasicEntityPersister always returns the name
* of the table the entity type of this persister is mapped to, since an entity
* is always persisted to a single table with a BasicEntityPersister.
*
* @param string $fieldName The field name.
*
* @return string The table name.
*/
public function getOwningTable($fieldName)
{
return $this->class->getTableName();
}
/**
* Loads an entity by a list of field criteria.
*
* @param array $criteria The criteria by which to load the entity.
* @param object|null $entity The entity to load the data into. If not specified, a new entity is created.
* @param array|null $assoc The association that connects the entity to load to another entity, if any.
* @param array $hints Hints for entity creation.
* @param int $lockMode
* @param int|null $limit Limit number of results.
* @param array|null $orderBy Criteria to order by.
*
* @return object|null The loaded and managed entity instance or NULL if the entity can not be found.
*
* @todo Check identity map? loadById method? Try to guess whether $criteria is the id?
*/
public function load(array $criteria, $entity = null, $assoc = null, array $hints = array(), $lockMode = 0, $limit = null, array $orderBy = null)
{
$sql = $this->getSelectSQL($criteria, $assoc, $lockMode, $limit, null, $orderBy);
list($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->selectJoinSql ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
$entities = $hydrator->hydrateAll($stmt, $this->rsm, $hints);
return $entities ? $entities[0] : null;
}
/**
* Loads an entity of this persister's mapped class as part of a single-valued
* association from another entity.
*
* @param array $assoc The association to load.
* @param object $sourceEntity The entity that owns the association (not necessarily the "owning side").
* @param array $identifier The identifier of the entity to load. Must be provided if
* the association to load represents the owning side, otherwise
* the identifier is derived from the $sourceEntity.
*
* @return object The loaded and managed entity instance or NULL if the entity can not be found.
*
* @throws DoctrineORMMappingMappingException
*/
public function loadOneToOneEntity(array $assoc, $sourceEntity, array $identifier = array())
{
if (($foundEntity = $this->em->getUnitOfWork()->tryGetById($identifier, $assoc['targetEntity'])) != 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 = array();
if ($isInverseSingleValued) {
$hints['fetched']["r"][$assoc['inversedBy']] = true;
}
/* cascade read-only status
if ($this->em->getUnitOfWork()->isReadOnly($sourceEntity)) {
$hints[Query::HINT_READ_ONLY] = 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']);
// 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
);
}
// unset the old value and set the new sql aliased value here. By definition
// unset($identifier[$targetKeyColumn] works here with how UnitOfWork::createEntity() calls this method.
$identifier[$this->getSQLTableAlias($targetClass->name) . "." . $targetKeyColumn] =
$sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
unset($identifier[$targetKeyColumn]);
}
$targetEntity = $this->load($identifier, null, $assoc);
if ($targetEntity !== null) {
$targetClass->setFieldValue($targetEntity, $assoc['mappedBy'], $sourceEntity);
}
return $targetEntity;
}
/**
* Refreshes a managed entity.
*
* @param array $id The identifier of the entity as an associative array from
* column or field names to values.
* @param object $entity The entity to refresh.
* @param int $lockMode
*
* @return void
*/
public function refresh(array $id, $entity, $lockMode = 0)
{
$sql = $this->getSelectSQL($id, null, $lockMode);
list($params, $types) = $this->expandParameters($id);
$stmt = $this->conn->executeQuery($sql, $params, $types);
$hydrator = $this->em->newHydrator(Query::HYDRATE_OBJECT);
$hydrator->hydrateAll($stmt, $this->rsm, array(Query::HINT_REFRESH => true));
}
/**
* Loads Entities matching the given Criteria object.
*
* @param DoctrineCommonCollectionsCriteria $criteria
*
* @return array
*/
public function loadCriteria(Criteria $criteria)
{
$orderBy = $criteria->getOrderings();
$limit = $criteria->getMaxResults();
$offset = $criteria->getFirstResult();
$query = $this->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy);
list($params, $types) = $this->expandCriteriaParameters($criteria);
$stmt = $this->conn->executeQuery($query, $params, $types);
$hydrator = $this->em->newHydrator(($this->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
return $hydrator->hydrateAll($stmt, $this->rsm, array(UnitOfWork::HINT_DEFEREAGERLOAD => true));
}
/**
* Expands Criteria Parameters by walking the expressions and grabbing all
* parameters and types from it.
*
* @param DoctrineCommonCollectionsCriteria $criteria
*
* @return array(array(), array())
*/
private function expandCriteriaParameters(Criteria $criteria)
{
$expression = $criteria->getWhereExpression();
if ($expression === null) {
return array(array(), array());
}
$valueVisitor = new SqlValueVisitor();
$valueVisitor->dispatch($expression);
list($values, $types) = $valueVisitor->getParamsAndTypes();
$sqlValues = array();
foreach ($values as $value) {
$sqlValues[] = $this->getValue($value);
}
$sqlTypes = array();
foreach ($types as $type) {
list($field, $value) = $type;
$sqlTypes[] = $this->getType($field, $value);
}
return array($sqlValues, $sqlTypes);
}
/**
* Loads a list of entities by a list of field criteria.
*
* @param array $criteria
* @param array|null $orderBy
* @param int|null $limit
* @param int|null $offset
*
* @return array
*/
public function loadAll(array $criteria = array(), array $orderBy = null, $limit = null, $offset = null)
{
$sql = $this->getSelectSQL($criteria, null, 0, $limit, $offset, $orderBy);
list($params, $types) = $this->expandParameters($criteria);
$stmt = $this->conn->executeQuery($sql, $params, $types);
$hydrator = $this->em->newHydrator(($this->selectJoinSql) ? Query::HYDRATE_OBJECT : Query::HYDRATE_SIMPLEOBJECT);
return $hydrator->hydrateAll($stmt, $this->rsm, array(UnitOfWork::HINT_DEFEREAGERLOAD => true));
}
/**
* Gets (sliced or full) elements of the given collection.
*
* @param array $assoc
* @param object $sourceEntity
* @param int|null $offset
* @param int|null $limit
*
* @return array
*/
public function getManyToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
$stmt = $this->getManyToManyStatement($assoc, $sourceEntity, $offset, $limit);
return $this->loadArrayFromStatement($assoc, $stmt);
}
/**
* Loads an array of entities from a given DBAL statement.
*
* @param array $assoc
* @param DoctrineDBALStatement $stmt
*
* @return array
*/
private function loadArrayFromStatement($assoc, $stmt)
{
$rsm = $this->rsm;
$hints = array(UnitOfWork::HINT_DEFEREAGERLOAD => true);
if (isset($assoc['indexBy'])) {
$rsm = clone ($this->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 array $assoc
* @param DoctrineDBALStatement $stmt
* @param PersistentCollection $coll
*
* @return array
*/
private function loadCollectionFromStatement($assoc, $stmt, $coll)
{
$rsm = $this->rsm;
$hints = array(
UnitOfWork::HINT_DEFEREAGERLOAD => true,
'collection' => $coll
);
if (isset($assoc['indexBy'])) {
$rsm = clone ($this->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);
}
/**
* Loads a collection of entities of a many-to-many association.
*
* @param array $assoc The association mapping of the association being loaded.
* @param object $sourceEntity The entity that owns the collection.
* @param PersistentCollection $coll The collection to fill.
*
* @return array
*/
public function loadManyToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
{
$stmt = $this->getManyToManyStatement($assoc, $sourceEntity);
return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
}
/**
* @param array $assoc
* @param object $sourceEntity
* @param int|null $offset
* @param int|null $limit
*
* @return DoctrineDBALDriverStatement
*
* @throws DoctrineORMMappingMappingException
*/
private function getManyToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
$sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
$class = $sourceClass;
$association = $assoc;
$criteria = array();
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;
}
$sql = $this->getSelectSQL($criteria, $assoc, 0, $limit, $offset);
list($params, $types) = $this->expandParameters($criteria);
return $this->conn->executeQuery($sql, $params, $types);
}
/**
* Gets the SELECT SQL to select one or more entities by a set of field criteria.
*
* @param array|DoctrineCommonCollectionsCriteria $criteria
* @param array|null $assoc
* @param int $lockMode
* @param int|null $limit
* @param int|null $offset
* @param array|null $orderBy
*
* @return string
*/
protected function getSelectSQL($criteria, $assoc = null, $lockMode = 0, $limit = null, $offset = null, array $orderBy = null)
{
$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->selectJoinSql . $joinSql;
$where = ($conditionSql ? ' WHERE ' . $conditionSql : '');
$lock = $this->platform->appendLockHint($from, $lockMode);
$query = $select
. $lock
. $join
. $where
. $orderBySql;
return $this->platform->modifyLimitQuery($query, $limit, $offset) . $lockSql;
}
/**
* Gets the ORDER BY SQL snippet for ordered collections.
*
* @param array $orderBy
* @param string $baseTableAlias
*
* @return string
*
* @throws DoctrineORMORMException
*/
protected final function getOrderBySQL(array $orderBy, $baseTableAlias)
{
$orderByList = array();
foreach ($orderBy as $fieldName => $orientation) {
$orientation = strtoupper(trim($orientation));
if ($orientation != 'ASC' && $orientation != 'DESC') {
throw ORMException::invalidOrientation($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 ORMException::invalidFindByInverseAssociation($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 ORMException::unrecognizedField($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->selectColumnListSql !== null) {
return $this->selectColumnListSql;
}
$columnList = array();
$this->rsm = new QueryResultSetMapping();
$this->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->selectJoinSql = '';
$eagerAliasCounter = 0;
foreach ($this->class->associationMappings as $assocField => $assoc) {
$assocColumnSQL = $this->getSelectColumnAssociationSQL($assocField, $assoc, $this->class);
if ($assocColumnSQL) {
$columnList[] = $assocColumnSQL;
}
if ( ! (($assoc['type'] & ClassMetadata::TO_ONE) && ($assoc['fetch'] == ClassMetadata::FETCH_EAGER || !$assoc['isOwningSide']))) {
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->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 = array();
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->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
if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) {
$joinCondition[] = $filterSql;
}
} else {
$this->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->selectJoinSql .= ' ' . $joinTableName . ' ' . $joinTableAlias . ' ON ';
$this->selectJoinSql .= implode(' AND ', $joinCondition);
}
$this->selectColumnListSql = implode(', ', $columnList);
return $this->selectColumnListSql;
}
/**
* Gets the SQL join fragment used when selecting entities from an association.
*
* @param string $field
* @param array $assoc
* @param ClassMetadata $class
* @param string $alias
*
* @return string
*/
protected function getSelectColumnAssociationSQL($field, $assoc, ClassMetadata $class, $alias = 'r')
{
if ( ! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE) ) {
return '';
}
$columnList = array();
$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
foreach ($assoc['joinColumns'] as $joinColumn) {
$type = null;
$isIdentifier = isset($assoc['id']) && $assoc['id'] === true;
$quotedColumn = $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
$resultColumnName = $this->getSQLColumnAlias($joinColumn['name']);
$columnList[] = $this->getSQLTableAlias($class->name, ($alias == 'r' ? '' : $alias) )
. '.' . $quotedColumn . ' AS ' . $resultColumnName;
if (isset($targetClass->fieldNames[$joinColumn['referencedColumnName']])) {
$type = $targetClass->fieldMappings[$targetClass->fieldNames[$joinColumn['referencedColumnName']]]['type'];
}
$this->rsm->addMetaResult($alias, $resultColumnName, $quotedColumn, $isIdentifier, $type);
}
return implode(', ', $columnList);
}
/**
* Gets the SQL join fragment used when selecting entities from a
* many-to-many association.
*
* @param array $manyToMany
*
* @return string
*/
protected function getSelectManyToManyJoinSQL(array $manyToMany)
{
$conditions = array();
$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);
}
/**
* Gets the INSERT SQL used by the persister to persist a new entity.
*
* @return string
*/
protected 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 = array();
$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 array The list of columns.
*/
protected function getInsertColumnList()
{
$columns = array();
foreach ($this->class->reflFields as $name => $field) {
if ($this->class->isVersioned && $this->class->versionField == $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->generatorType != ClassMetadata::GENERATOR_TYPE_IDENTITY || $this->class->identifier[0] != $name) {
$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);
$columnName = $this->quoteStrategy->getColumnName($field, $class, $this->platform);
$sql = $tableAlias . '.' . $columnName;
$columnAlias = $this->getSQLColumnAlias($class->columnNames[$field]);
$this->rsm->addFieldResult($alias, $columnAlias, $field);
if (isset($class->fieldMappings[$field]['requireSQLConversion'])) {
$type = Type::getType($class->getTypeOfField($field));
$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->sqlTableAliases[$className])) {
return $this->sqlTableAliases[$className];
}
$tableAlias = 't' . $this->sqlAliasCounter++;
$this->sqlTableAliases[$className] = $tableAlias;
return $tableAlias;
}
/**
* Locks all rows of this entity matching the given criteria with the specified pessimistic lock mode.
*
* @param array $criteria
* @param int $lockMode
*
* @return void
*/
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;
list($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 integer $lockMode One of the DoctrineDBALLockMode::* constants.
*
* @return string
*/
protected function getLockTablesSql($lockMode)
{
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.
*
* @param DoctrineCommonCollectionsCriteria $criteria
*
* @return string
*/
protected function getSelectConditionCriteriaSQL(Criteria $criteria)
{
$expression = $criteria->getWhereExpression();
if ($expression === null) {
return '';
}
$visitor = new SqlExpressionVisitor($this, $this->class);
return $visitor->dispatch($expression);
}
/**
* Gets the SQL WHERE condition for matching a field with a given value.
*
* @param string $field
* @param mixed $value
* @param array|null $assoc
* @param string|null $comparison
*
* @return string
*/
public function getSelectConditionStatementSQL($field, $value, $assoc = null, $comparison = null)
{
$placeholder = '?';
$condition = $this->getSelectConditionStatementColumnSQL($field, $assoc);
if (isset($this->class->fieldMappings[$field]['requireSQLConversion'])) {
$placeholder = Type::getType($this->class->getTypeOfField($field))->convertToDatabaseValueSQL($placeholder, $this->platform);
}
if ($comparison !== null) {
// special case null value handling
if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) {
return $condition . ' IS NULL';
} else if ($comparison === Comparison::NEQ && $value === null) {
return $condition . ' IS NOT NULL';
}
return $condition . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder);
}
if (is_array($value)) {
return sprintf('%s IN (%s)' , $condition, $placeholder);
}
if ($value === null) {
return sprintf('%s IS NULL' , $condition);
}
return sprintf('%s = %s' , $condition, $placeholder);
}
/**
* Builds the left-hand-side of a where condition statement.
*
* @param string $field
* @param array|null $assoc
*
* @return string
*
* @throws DoctrineORMORMException
*/
protected function getSelectConditionStatementColumnSQL($field, $assoc = null)
{
if (isset($this->class->columnNames[$field])) {
$className = (isset($this->class->fieldMappings[$field]['inherited']))
? $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])) {
if ( ! $this->class->associationMappings[$field]['isOwningSide']) {
throw ORMException::invalidFindByInverseAssociation($this->class->name, $field);
}
$joinColumn = $this->class->associationMappings[$field]['joinColumns'][0];
$className = (isset($this->class->associationMappings[$field]['inherited']))
? $this->class->associationMappings[$field]['inherited']
: $this->class->name;
return $this->getSQLTableAlias($className) . '.' . $this->quoteStrategy->getJoinColumnName($joinColumn, $this->class, $this->platform);
}
if ($assoc !== null && strpos($field, " ") === false && strpos($field, "(") === false) {
// 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 ORMException::unrecognizedField($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 array $criteria
* @param array|null $assoc
*
* @return string
*/
protected function getSelectConditionSQL(array $criteria, $assoc = null)
{
$conditions = array();
foreach ($criteria as $field => $value) {
$conditions[] = $this->getSelectConditionStatementSQL($field, $value, $assoc);
}
return implode(' AND ', $conditions);
}
/**
* Returns an array with (sliced or full list) of elements in the specified collection.
*
* @param array $assoc
* @param object $sourceEntity
* @param int|null $offset
* @param int|null $limit
*
* @return array
*/
public function getOneToManyCollection(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
$stmt = $this->getOneToManyStatement($assoc, $sourceEntity, $offset, $limit);
return $this->loadArrayFromStatement($assoc, $stmt);
}
/**
* Loads a collection of entities in a one-to-many association.
*
* @param array $assoc
* @param object $sourceEntity
* @param PersistentCollection $coll The collection to load/fill.
*
* @return array
*/
public function loadOneToManyCollection(array $assoc, $sourceEntity, PersistentCollection $coll)
{
$stmt = $this->getOneToManyStatement($assoc, $sourceEntity);
return $this->loadCollectionFromStatement($assoc, $stmt, $coll);
}
/**
* Builds criteria and execute SQL statement to fetch the one to many entities from.
*
* @param array $assoc
* @param object $sourceEntity
* @param int|null $offset
* @param int|null $limit
*
* @return DoctrineDBALStatement
*/
private function getOneToManyStatement(array $assoc, $sourceEntity, $offset = null, $limit = null)
{
$criteria = array();
$owningAssoc = $this->class->associationMappings[$assoc['mappedBy']];
$sourceClass = $this->em->getClassMetadata($assoc['sourceEntity']);
$tableAlias = $this->getSQLTableAlias(isset($owningAssoc['inherited']) ? $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;
continue;
}
$criteria[$tableAlias . "." . $targetKeyColumn] = $sourceClass->reflFields[$sourceClass->fieldNames[$sourceKeyColumn]]->getValue($sourceEntity);
}
$sql = $this->getSelectSQL($criteria, $assoc, 0, $limit, $offset);
list($params, $types) = $this->expandParameters($criteria);
return $this->conn->executeQuery($sql, $params, $types);
}
/**
* Expands the parameters from the given criteria and use the correct binding types if found.
*
* @param array $criteria
*
* @return array
*/
private function expandParameters($criteria)
{
$params = array();
$types = array();
foreach ($criteria as $field => $value) {
if ($value === null) {
continue; // skip null values.
}
$types[] = $this->getType($field, $value);
$params[] = $this->getValue($value);
}
return array($params, $types);
}
/**
* Infers field type to be used by parameter type casting.
*
* @param string $field
* @param mixed $value
*
* @return integer
*
* @throws DoctrineORMQueryQueryException
*/
private function getType($field, $value)
{
switch (true) {
case (isset($this->class->fieldMappings[$field])):
$type = $this->class->fieldMappings[$field]['type'];
break;
case (isset($this->class->associationMappings[$field])):
$assoc = $this->class->associationMappings[$field];
if (count($assoc['sourceToTargetKeyColumns']) > 1) {
throw QueryQueryException::associationPathCompositeKeyNotSupported();
}
$targetClass = $this->em->getClassMetadata($assoc['targetEntity']);
$targetColumn = $assoc['joinColumns'][0]['referencedColumnName'];
$type = null;
if (isset($targetClass->fieldNames[$targetColumn])) {
$type = $targetClass->fieldMappings[$targetClass->fieldNames[$targetColumn]]['type'];
}
break;
default:
$type = null;
}
if (is_array($value)) {
$type = Type::getType($type)->getBindingType();
$type += Connection::ARRAY_PARAM_OFFSET;
}
return $type;
}
/**
* Retrieves parameter value.
*
* @param mixed $value
*
* @return mixed
*/
private function getValue($value)
{
if ( ! is_array($value)) {
return $this->getIndividualValue($value);
}
$newValue = array();
foreach ($value as $itemValue) {
$newValue[] = $this->getIndividualValue($itemValue);
}
return $newValue;
}
/**
* Retrieves an individual parameter value.
*
* @param mixed $value
*
* @return mixed
*/
private function getIndividualValue($value)
{
if ( ! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
return $value;
}
return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
}
/**
* Checks whether the given managed entity exists in the database.
*
* @param object $entity
* @param array $extraConditions
*
* @return boolean TRUE if the entity exists in the database, FALSE otherwise.
*/
public function exists($entity, array $extraConditions = array())
{
$criteria = $this->class->getIdentifierValues($entity);
if ( ! $criteria) {
return false;
}
if ($extraConditions) {
$criteria = array_merge($criteria, $extraConditions);
}
$alias = $this->getSQLTableAlias($this->class->name);
$sql = 'SELECT 1 '
. $this->getLockTablesSql(null)
. ' WHERE ' . $this->getSelectConditionSQL($criteria);
if ($filterSql = $this->generateFilterConditionSQL($this->class, $alias)) {
$sql .= ' AND ' . $filterSql;
}
list($params) = $this->expandParameters($criteria);
return (bool) $this->conn->fetchColumn($sql, $params);
}
/**
* Generates the appropriate join SQL for the given join column.
*
* @param array $joinColumns The join columns definition of an association.
*
* @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';
}
/**
* Gets an SQL column alias for a column name.
*
* @param string $columnName
*
* @return string
*/
public function getSQLColumnAlias($columnName)
{
return $this->quoteStrategy->getColumnAlias($columnName, $this->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 = array();
foreach ($this->em->getFilters()->getEnabledFilters() as $filter) {
if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) {
$filterClauses[] = '(' . $filterExpr . ')';
}
}
$sql = implode(' AND ', $filterClauses);
return $sql ? "(" . $sql . ")" : ""; // Wrap again to avoid "X or Y and FilterConditionSQL"
}
}