diff --git a/lib/Doctrine/ORM/Configuration.php b/lib/Doctrine/ORM/Configuration.php index 7a7b1792d..cddc8672c 100644 --- a/lib/Doctrine/ORM/Configuration.php +++ b/lib/Doctrine/ORM/Configuration.php @@ -495,7 +495,32 @@ class Configuration extends \Doctrine\DBAL\Configuration } return $this->_attributes['classMetadataFactoryName']; } - + + /** + * Add a filter to the list of possible filters. + * + * @param string $name The name of the filter. + * @param string $className The class name of the filter. + */ + public function addFilter($name, $className) + { + $this->_attributes['filters'][$name] = $className; + } + + /** + * Gets the class name for a given filter name. + * + * @param string $name The name of the filter. + * + * @return string The class name of the filter, or null of it is not + * defined. + */ + public function getFilterClassName($name) + { + return isset($this->_attributes['filters'][$name]) ? + $this->_attributes['filters'][$name] : null; + } + /** * Set default repository class. * @@ -523,4 +548,4 @@ class Configuration extends \Doctrine\DBAL\Configuration return isset($this->_attributes['defaultRepositoryClassName']) ? $this->_attributes['defaultRepositoryClassName'] : 'Doctrine\ORM\EntityRepository'; } -} \ No newline at end of file +} diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index cec076d8d..ab3e4c7ee 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -27,7 +27,8 @@ use Closure, Exception, Doctrine\ORM\Mapping\ClassMetadata, Doctrine\ORM\Mapping\ClassMetadataFactory, Doctrine\ORM\Query\ResultSetMapping, - Doctrine\ORM\Proxy\ProxyFactory; + Doctrine\ORM\Proxy\ProxyFactory, + Doctrine\ORM\Query\FilterCollection; /** * The EntityManager is the central access point to ORM functionality. @@ -110,6 +111,13 @@ class EntityManager implements ObjectManager */ private $closed = false; + /** + * Collection of query filters. + * + * @var Doctrine\ORM\Query\FilterCollection + */ + private $filterCollection; + /** * Creates a new EntityManager that operates on the given database connection * and uses the given Configuration and EventManager implementations. @@ -788,4 +796,39 @@ class EntityManager implements ObjectManager return new EntityManager($conn, $config, $conn->getEventManager()); } + + /** + * Gets the enabled filters. + * + * @return FilterCollection The active filter collection. + */ + public function getFilters() + { + if (null === $this->filterCollection) { + $this->filterCollection = new FilterCollection($this); + } + + return $this->filterCollection; + } + + /** + * Checks whether the state of the filter collection is clean. + * + * @return boolean True, if the filter collection is clean. + */ + public function isFiltersStateClean() + { + return null === $this->filterCollection + || $this->filterCollection->isClean(); + } + + /** + * Checks whether the Entity Manager has filters. + * + * @return True, if the EM has a filter collection. + */ + public function hasFilters() + { + return null !== $this->filterCollection; + } } diff --git a/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php b/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php index f7d5031cd..3765fbd93 100644 --- a/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php +++ b/lib/Doctrine/ORM/Mapping/Builder/ClassMetadataBuilder.php @@ -298,7 +298,7 @@ class ClassMetadataBuilder $builder = $this->createManyToOne($name, $targetEntity); if ($inversedBy) { - $builder->setInversedBy($inversedBy); + $builder->inversedBy($inversedBy); } return $builder->build(); @@ -355,7 +355,7 @@ class ClassMetadataBuilder public function addInverseOneToOne($name, $targetEntity, $mappedBy) { $builder = $this->createOneToOne($name, $targetEntity); - $builder->setMappedBy($mappedBy); + $builder->mappedBy($mappedBy); return $builder->build(); } @@ -373,7 +373,7 @@ class ClassMetadataBuilder $builder = $this->createOneToOne($name, $targetEntity); if ($inversedBy) { - $builder->setInversedBy($inversedBy); + $builder->inversedBy($inversedBy); } return $builder->build(); @@ -411,7 +411,7 @@ class ClassMetadataBuilder $builder = $this->createManyToMany($name, $targetEntity); if ($inversedBy) { - $builder->setInversedBy($inversedBy); + $builder->inversedBy($inversedBy); } return $builder->build(); @@ -428,7 +428,7 @@ class ClassMetadataBuilder public function addInverseManyToMany($name, $targetEntity, $mappedBy) { $builder = $this->createManyToMany($name, $targetEntity); - $builder->setMappedBy($mappedBy); + $builder->mappedBy($mappedBy); return $builder->build(); } @@ -463,7 +463,7 @@ class ClassMetadataBuilder public function addOneToMany($name, $targetEntity, $mappedBy) { $builder = $this->createOneToMany($name, $targetEntity); - $builder->setMappedBy($mappedBy); + $builder->mappedBy($mappedBy); return $builder->build(); } diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php index 82ff14531..8801344ff 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataFactory.php @@ -371,6 +371,10 @@ class ClassMetadataFactory implements ClassMetadataFactoryInterface // second condition is necessary for mapped superclasses in the middle of an inheritance hierachy throw MappingException::noInheritanceOnMappedSuperClass($class->name); } + + if ($class->usesIdGenerator() && $class->isIdentifierComposite) { + throw MappingException::compositeKeyAssignedIdGeneratorRequired($class->name); + } } /** diff --git a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php index 0c3a339eb..d845e6f59 100644 --- a/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php +++ b/lib/Doctrine/ORM/Mapping/ClassMetadataInfo.php @@ -1849,21 +1849,6 @@ class ClassMetadataInfo implements ClassMetadata } } - /** - * Refine an association targetEntity class pointer to be consumed through loadMetadata event. - * - * @param string $assoc - * @param string $class - */ - public function setAssociationTargetClass($assocName, $class) - { - if ( ! isset($this->associationMappings[$assocName])) { - throw new \InvalidArgumentException("Association name expected, '" . $assocName ."' is not an association."); - } - - $this->associationMappings[$assocName]['targetEntity'] = $class; - } - /** * Sets whether this class is to be versioned for optimistic locking. * diff --git a/lib/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php index 457b7cda7..a92432db8 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AbstractFileDriver.php @@ -118,6 +118,9 @@ abstract class AbstractFileDriver implements Driver { $result = $this->_loadMappingFile($this->_findMappingFile($className)); + if(!isset($result[$className])){ + throw MappingException::invalidMappingFile($className, str_replace('\\', '.', $className) . $this->_fileExtension); + } return $result[$className]; } diff --git a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php index d469fd66e..bee84159f 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/AnnotationDriver.php @@ -192,7 +192,14 @@ class AnnotationDriver implements Driver if (isset($classAnnotations['Doctrine\ORM\Mapping\NamedQueries'])) { $namedQueriesAnnot = $classAnnotations['Doctrine\ORM\Mapping\NamedQueries']; + if (!is_array($namedQueriesAnnot->value)) { + throw new \UnexpectedValueException("@NamedQueries should contain an array of @NamedQuery annotations."); + } + foreach ($namedQueriesAnnot->value as $namedQuery) { + if (!($namedQuery instanceof \Doctrine\ORM\Mapping\NamedQuery)) { + throw new \UnexpectedValueException("@NamedQueries should contain an array of @NamedQuery annotations."); + } $metadata->addNamedQuery(array( 'name' => $namedQuery->name, 'query' => $namedQuery->query diff --git a/lib/Doctrine/ORM/Mapping/MappingException.php b/lib/Doctrine/ORM/Mapping/MappingException.php index 014714bf8..cdc856381 100644 --- a/lib/Doctrine/ORM/Mapping/MappingException.php +++ b/lib/Doctrine/ORM/Mapping/MappingException.php @@ -67,6 +67,11 @@ class MappingException extends \Doctrine\ORM\ORMException { return new self("No mapping file found named '$fileName' for class '$entityName'."); } + + public static function invalidMappingFile($entityName, $fileName) + { + return new self("Invalid mapping file '$fileName' for class '$entityName'."); + } public static function mappingNotFound($className, $fieldName) { @@ -314,4 +319,9 @@ class MappingException extends \Doctrine\ORM\ORMException { return new self("Entity '" . $className . "' has a mapping with invalid fetch mode '" . $annotation . "'"); } + + public static function compositeKeyAssignedIdGeneratorRequired($className) + { + return new self("Entity '". $className . "' has a composite identifier but uses an ID generator other than manually assigning (Identity, Sequence). This is not supported."); + } } diff --git a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php index 08f316fb3..6845d984a 100644 --- a/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php +++ b/lib/Doctrine/ORM/Persisters/BasicEntityPersister.php @@ -72,6 +72,7 @@ use PDO, * @author Roman Borschel * @author Giorgio Sironi * @author Benjamin Eberlei + * @author Alexander * @since 2.0 */ class BasicEntityPersister @@ -900,9 +901,19 @@ class BasicEntityPersister $lockSql = ' ' . $this->_platform->getWriteLockSql(); } + $alias = $this->_getSQLTableAlias($this->_class->name); + + if ($filterSql = $this->generateFilterConditionSQL($this->_class, $alias)) { + if ($conditionSql) { + $conditionSql .= ' AND '; + } + + $conditionSql .= $filterSql; + } + return $this->_platform->modifyLimitQuery('SELECT ' . $this->_getSelectColumnListSQL() . $this->_platform->appendLockHint(' FROM ' . $this->_class->getQuotedTableName($this->_platform) . ' ' - . $this->_getSQLTableAlias($this->_class->name), $lockMode) + . $alias, $lockMode) . $this->_selectJoinSql . $joinSql . ($conditionSql ? ' WHERE ' . $conditionSql : '') . $orderBySql, $limit, $offset) @@ -1014,14 +1025,20 @@ class BasicEntityPersister $this->_selectJoinSql .= ' ' . $this->getJoinSQLForJoinColumns($assoc['joinColumns']); $this->_selectJoinSql .= ' ' . $eagerEntity->getQuotedTableName($this->_platform) . ' ' . $this->_getSQLTableAlias($eagerEntity->name, $assocAlias) .' ON '; + $tableAlias = $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias); foreach ($assoc['sourceToTargetKeyColumns'] AS $sourceCol => $targetCol) { if ( ! $first) { $this->_selectJoinSql .= ' AND '; } $this->_selectJoinSql .= $this->_getSQLTableAlias($assoc['sourceEntity']) . '.' . $sourceCol . ' = ' - . $this->_getSQLTableAlias($assoc['targetEntity'], $assocAlias) . '.' . $targetCol; + . $tableAlias . '.' . $targetCol; $first = false; } + + // Add filter SQL + if ($filterSql = $this->generateFilterConditionSQL($eagerEntity, $tableAlias)) { + $this->_selectJoinSql .= ' AND ' . $filterSql; + } } else { $eagerEntity = $this->_em->getClassMetadata($assoc['targetEntity']); $owningAssoc = $eagerEntity->getAssociationMapping($assoc['mappedBy']); @@ -1521,10 +1538,16 @@ class BasicEntityPersister $criteria = array_merge($criteria, $extraConditions); } + $alias = $this->_getSQLTableAlias($this->_class->name); + $sql = 'SELECT 1 ' . $this->getLockTablesSql() . ' WHERE ' . $this->_getSelectConditionSQL($criteria); + if ($filterSql = $this->generateFilterConditionSQL($this->_class, $alias)) { + $sql .= ' AND ' . $filterSql; + } + list($params, $types) = $this->expandParameters($criteria); return (bool) $this->_conn->fetchColumn($sql, $params); @@ -1539,8 +1562,8 @@ class BasicEntityPersister 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']){ + foreach ($joinColumns as $joinColumn) { + if (!isset($joinColumn['nullable']) || $joinColumn['nullable']) { return 'LEFT JOIN'; } } @@ -1562,4 +1585,26 @@ class BasicEntityPersister substr($columnName . $this->_sqlAliasCounter++, -$this->_platform->getMaxIdentifierLength()) ); } + + /** + * 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" + } } diff --git a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php index 795ab4c3e..350ced98b 100644 --- a/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php +++ b/lib/Doctrine/ORM/Persisters/JoinedSubclassPersister.php @@ -31,6 +31,7 @@ use Doctrine\ORM\ORMException, * * @author Roman Borschel * @author Benjamin Eberlei + * @author Alexander * @since 2.0 * @see http://martinfowler.com/eaaCatalog/classTableInheritance.html */ @@ -374,6 +375,15 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $conditionSql = $this->_getSelectConditionSQL($criteria, $assoc); + // If the current class in the root entity, add the filters + if ($filterSql = $this->generateFilterConditionSQL($this->_em->getClassMetadata($this->_class->rootEntityName), $this->_getSQLTableAlias($this->_class->rootEntityName))) { + if ($conditionSql) { + $conditionSql .= ' AND '; + } + + $conditionSql .= $filterSql; + } + $orderBy = ($assoc !== null && isset($assoc['orderBy'])) ? $assoc['orderBy'] : $orderBy; $orderBySql = $orderBy ? $this->_getOrderBySQL($orderBy, $baseTableAlias) : ''; @@ -473,4 +483,5 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister $value = $this->fetchVersionValue($this->_getVersionedClassMetadata(), $id); $this->_class->setFieldValue($entity, $this->_class->versionField, $value); } + } diff --git a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php index bd147343f..2d9105709 100644 --- a/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/ManyToManyPersister.php @@ -21,7 +21,8 @@ namespace Doctrine\ORM\Persisters; -use Doctrine\ORM\PersistentCollection, +use Doctrine\ORM\Mapping\ClassMetadata, + Doctrine\ORM\PersistentCollection, Doctrine\ORM\UnitOfWork; /** @@ -29,6 +30,7 @@ use Doctrine\ORM\PersistentCollection, * * @author Roman Borschel * @author Guilherme Blanco + * @author Alexander * @since 2.0 */ class ManyToManyPersister extends AbstractCollectionPersister @@ -215,10 +217,16 @@ class ManyToManyPersister extends AbstractCollectionPersister ? $id[$class->getFieldForColumn($joinColumns[$joinTableColumn])] : $id[$class->fieldNames[$joinColumns[$joinTableColumn]]]; } - + + list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping); + if ($filterSql) { + $whereClauses[] = $filterSql; + } + $sql = 'SELECT COUNT(*)' - . ' FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()) - . ' WHERE ' . implode(' AND ', $whereClauses); + . ' FROM ' . $class->getQuotedJoinTableName($mapping, $this->_conn->getDatabasePlatform()) . ' t' + . $joinTargetEntitySQL + . ' WHERE ' . implode(' AND ', $whereClauses); return $this->_conn->fetchColumn($sql, $params); } @@ -250,7 +258,7 @@ class ManyToManyPersister extends AbstractCollectionPersister return false; } - list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element); + list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, true); $sql = 'SELECT 1 FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); @@ -271,7 +279,7 @@ class ManyToManyPersister extends AbstractCollectionPersister return false; } - list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element); + list($quotedJoinTable, $whereClauses, $params) = $this->getJoinTableRestrictions($coll, $element, false); $sql = 'DELETE FROM ' . $quotedJoinTable . ' WHERE ' . implode(' AND ', $whereClauses); @@ -281,9 +289,10 @@ class ManyToManyPersister extends AbstractCollectionPersister /** * @param \Doctrine\ORM\PersistentCollection $coll * @param object $element + * @param boolean $addFilters Whether the filter SQL should be included or not. * @return array */ - private function getJoinTableRestrictions(PersistentCollection $coll, $element) + private function getJoinTableRestrictions(PersistentCollection $coll, $element, $addFilters) { $uow = $this->_em->getUnitOfWork(); $mapping = $coll->getMapping(); @@ -321,7 +330,73 @@ class ManyToManyPersister extends AbstractCollectionPersister ? $sourceId[$sourceClass->getFieldForColumn($mapping['relationToSourceKeyColumns'][$joinTableColumn])] : $sourceId[$sourceClass->fieldNames[$mapping['relationToSourceKeyColumns'][$joinTableColumn]]]; } + + if ($addFilters) { + list($joinTargetEntitySQL, $filterSql) = $this->getFilterSql($mapping); + if ($filterSql) { + $quotedJoinTable .= ' t ' . $joinTargetEntitySQL; + $whereClauses[] = $filterSql; + } + } return array($quotedJoinTable, $whereClauses, $params); } -} \ No newline at end of file + + /** + * Generates the filter SQL for a given mapping. + * + * This method is not used for actually grabbing the related entities + * but when the extra-lazy collection methods are called on a filtered + * association. This is why besides the many to many table we also + * have to join in the actual entities table leading to additional + * JOIN. + * + * @param array $targetEntity Array containing mapping information. + * + * @return string The SQL query part to add to a query. + */ + public function getFilterSql($mapping) + { + $targetClass = $this->_em->getClassMetadata($mapping['targetEntity']); + $targetClass = $this->_em->getClassMetadata($targetClass->rootEntityName); + + // A join is needed if there is filtering on the target entity + $joinTargetEntitySQL = ''; + if ($filterSql = $this->generateFilterConditionSQL($targetClass, 'te')) { + $joinTargetEntitySQL = ' JOIN ' + . $targetClass->getQuotedTableName($this->_conn->getDatabasePlatform()) . ' te' + . ' ON'; + + $joinTargetEntitySQLClauses = array(); + foreach ($mapping['relationToTargetKeyColumns'] as $joinTableColumn => $targetTableColumn) { + $joinTargetEntitySQLClauses[] = ' t.' . $joinTableColumn . ' = ' . 'te.' . $targetTableColumn; + } + + $joinTargetEntitySQL .= implode(' AND ', $joinTargetEntitySQLClauses); + } + + return array($joinTargetEntitySQL, $filterSql); + } + + /** + * 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 . ")" : ""; + } +} diff --git a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php index 7a7f29750..b78d0e561 100644 --- a/lib/Doctrine/ORM/Persisters/OneToManyPersister.php +++ b/lib/Doctrine/ORM/Persisters/OneToManyPersister.php @@ -1,7 +1,5 @@ * @author Guilherme Blanco + * @author Alexander * @since 2.0 */ class OneToManyPersister extends AbstractCollectionPersister @@ -120,10 +119,16 @@ class OneToManyPersister extends AbstractCollectionPersister : $id[$sourceClass->fieldNames[$joinColumn['referencedColumnName']]]; } + foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) { + if ($filterExpr = $filter->addFilterConstraint($targetClass, 't')) { + $whereClauses[] = '(' . $filterExpr . ')'; + } + } + $sql = 'SELECT count(*)' - . ' FROM ' . $targetClass->getQuotedTableName($this->_conn->getDatabasePlatform()) + . ' FROM ' . $targetClass->getQuotedTableName($this->_conn->getDatabasePlatform()) . ' t' . ' WHERE ' . implode(' AND ', $whereClauses); - + return $this->_conn->fetchColumn($sql, $params); } diff --git a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php index 0f1b9e3de..c2687c109 100644 --- a/lib/Doctrine/ORM/Persisters/SingleTablePersister.php +++ b/lib/Doctrine/ORM/Persisters/SingleTablePersister.php @@ -27,6 +27,7 @@ use Doctrine\ORM\Mapping\ClassMetadata; * * @author Roman Borschel * @author Benjamin Eberlei + * @author Alexander * @since 2.0 * @link http://martinfowler.com/eaaCatalog/singleTableInheritance.html */ @@ -131,4 +132,14 @@ class SingleTablePersister extends AbstractEntityInheritancePersister return $conditionSql; } + + /** {@inheritdoc} */ + protected function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) + { + // Ensure that the filters are applied to the root entity of the inheritance tree + $targetEntity = $this->_em->getClassMetadata($targetEntity->rootEntityName); + // we dont care about the $targetTableAlias, in a STI there is only one table. + + return parent::generateFilterConditionSQL($targetEntity, $targetTableAlias); + } } diff --git a/lib/Doctrine/ORM/Query.php b/lib/Doctrine/ORM/Query.php index d6f05e15b..252f9fa32 100644 --- a/lib/Doctrine/ORM/Query.php +++ b/lib/Doctrine/ORM/Query.php @@ -198,7 +198,10 @@ final class Query extends AbstractQuery */ private function _parse() { - if ($this->_state === self::STATE_CLEAN) { + // Return previous parser result if the query and the filter collection are both clean + if ($this->_state === self::STATE_CLEAN + && $this->_em->isFiltersStateClean() + ) { return $this->_parserResult; } @@ -623,6 +626,7 @@ final class Query extends AbstractQuery return md5( $this->getDql() . var_export($this->_hints, true) . + ($this->_em->hasFilters() ? $this->_em->getFilters()->getHash() : '') . '&firstResult=' . $this->_firstResult . '&maxResult=' . $this->_maxResults . '&hydrationMode='.$this->_hydrationMode.'DOCTRINE_QUERY_CACHE_SALT' ); diff --git a/lib/Doctrine/ORM/Query/Filter/SQLFilter.php b/lib/Doctrine/ORM/Query/Filter/SQLFilter.php new file mode 100644 index 000000000..a87dd841e --- /dev/null +++ b/lib/Doctrine/ORM/Query/Filter/SQLFilter.php @@ -0,0 +1,122 @@ +. + */ + +namespace Doctrine\ORM\Query\Filter; + +use Doctrine\ORM\EntityManager, + Doctrine\ORM\Mapping\ClassMetaData, + Doctrine\ORM\Query\ParameterTypeInferer; + +/** + * The base class that user defined filters should extend. + * + * Handles the setting and escaping of parameters. + * + * @author Alexander + * @author Benjamin Eberlei + * @abstract + */ +abstract class SQLFilter +{ + /** + * The entity manager. + * @var EntityManager + */ + private $em; + + /** + * Parameters for the filter. + * @var array + */ + private $parameters; + + /** + * Constructs the SQLFilter object. + * + * @param EntityManager $em The EM + */ + final public function __construct(EntityManager $em) + { + $this->em = $em; + } + + /** + * Sets a parameter that can be used by the filter. + * + * @param string $name Name of the parameter. + * @param string $value Value of the parameter. + * @param string $type The parameter type. If specified, the given value will be run through + * the type conversion of this type. This is usually not needed for + * strings and numeric types. + * + * @return SQLFilter The current SQL filter. + */ + final public function setParameter($name, $value, $type = null) + { + if (null === $type) { + $type = ParameterTypeInferer::inferType($value); + } + + $this->parameters[$name] = array('value' => $value, 'type' => $type); + + // Keep the parameters sorted for the hash + ksort($this->parameters); + + // The filter collection of the EM is now dirty + $this->em->getFilters()->setFiltersStateDirty(); + + return $this; + } + + /** + * Gets a parameter to use in a query. + * + * The function is responsible for the right output escaping to use the + * value in a query. + * + * @param string $name Name of the parameter. + * + * @return string The SQL escaped parameter to use in a query. + */ + final public function getParameter($name) + { + if (!isset($this->parameters[$name])) { + throw new \InvalidArgumentException("Parameter '" . $name . "' does not exist."); + } + + return $this->em->getConnection()->quote($this->parameters[$name]['value'], $this->parameters[$name]['type']); + } + + /** + * Returns as string representation of the SQLFilter parameters (the state). + * + * @return string String representation of the SQLFilter. + */ + final public function __toString() + { + return serialize($this->parameters); + } + + /** + * Gets the SQL query part to add to a query. + * + * @return string The constraint SQL if there is available, empty string otherwise + */ + abstract public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias); +} diff --git a/lib/Doctrine/ORM/Query/FilterCollection.php b/lib/Doctrine/ORM/Query/FilterCollection.php new file mode 100644 index 000000000..35c8d043c --- /dev/null +++ b/lib/Doctrine/ORM/Query/FilterCollection.php @@ -0,0 +1,198 @@ +. + */ + +namespace Doctrine\ORM\Query; + +use Doctrine\ORM\Configuration, + Doctrine\ORM\EntityManager; + +/** + * Collection class for all the query filters. + * + * @author Alexander + */ +class FilterCollection +{ + /* Filter STATES */ + /** + * A filter object is in CLEAN state when it has no changed parameters. + */ + const FILTERS_STATE_CLEAN = 1; + + /** + * A filter object is in DIRTY state when it has changed parameters. + */ + const FILTERS_STATE_DIRTY = 2; + + /** + * The used Configuration. + * + * @var Doctrine\ORM\Configuration + */ + private $config; + + /** + * The EntityManager that "owns" this FilterCollection instance. + * + * @var Doctrine\ORM\EntityManager + */ + private $em; + + /** + * Instances of enabled filters. + * + * @var array + */ + private $enabledFilters = array(); + + /** + * @var string The filter hash from the last time the query was parsed. + */ + private $filterHash; + + /** + * @var integer $state The current state of this filter + */ + private $filtersState = self::FILTERS_STATE_CLEAN; + + /** + * Constructor. + * + * @param EntityManager $em + */ + public function __construct(EntityManager $em) + { + $this->em = $em; + $this->config = $em->getConfiguration(); + } + + /** + * Get all the enabled filters. + * + * @return array The enabled filters. + */ + public function getEnabledFilters() + { + return $this->enabledFilters; + } + + /** + * Enables a filter from the collection. + * + * @param string $name Name of the filter. + * + * @throws \InvalidArgumentException If the filter does not exist. + * + * @return SQLFilter The enabled filter. + */ + public function enable($name) + { + if (null === $filterClass = $this->config->getFilterClassName($name)) { + throw new \InvalidArgumentException("Filter '" . $name . "' does not exist."); + } + + if (!isset($this->enabledFilters[$name])) { + $this->enabledFilters[$name] = new $filterClass($this->em); + + // Keep the enabled filters sorted for the hash + ksort($this->enabledFilters); + + // Now the filter collection is dirty + $this->filtersState = self::FILTERS_STATE_DIRTY; + } + + return $this->enabledFilters[$name]; + } + + /** + * Disables a filter. + * + * @param string $name Name of the filter. + * + * @return SQLFilter The disabled filter. + * + * @throws \InvalidArgumentException If the filter does not exist. + */ + public function disable($name) + { + // Get the filter to return it + $filter = $this->getFilter($name); + + unset($this->enabledFilters[$name]); + + // Now the filter collection is dirty + $this->filtersState = self::FILTERS_STATE_DIRTY; + + return $filter; + } + + /** + * Get an enabled filter from the collection. + * + * @param string $name Name of the filter. + * + * @return SQLFilter The filter. + * + * @throws \InvalidArgumentException If the filter is not enabled. + */ + public function getFilter($name) + { + if (!isset($this->enabledFilters[$name])) { + throw new \InvalidArgumentException("Filter '" . $name . "' is not enabled."); + } + + return $this->enabledFilters[$name]; + } + + /** + * @return boolean True, if the filter collection is clean. + */ + public function isClean() + { + return self::FILTERS_STATE_CLEAN === $this->filtersState; + } + + /** + * Generates a string of currently enabled filters to use for the cache id. + * + * @return string + */ + public function getHash() + { + // If there are only clean filters, the previous hash can be returned + if (self::FILTERS_STATE_CLEAN === $this->filtersState) { + return $this->filterHash; + } + + $filterHash = ''; + foreach ($this->enabledFilters as $name => $filter) { + $filterHash .= $name . $filter; + } + + return $filterHash; + } + + /** + * Set the filter state to dirty. + */ + public function setFiltersStateDirty() + { + $this->filtersState = self::FILTERS_STATE_DIRTY; + } +} diff --git a/lib/Doctrine/ORM/Query/SqlWalker.php b/lib/Doctrine/ORM/Query/SqlWalker.php index 1e01e0f8b..753669f92 100644 --- a/lib/Doctrine/ORM/Query/SqlWalker.php +++ b/lib/Doctrine/ORM/Query/SqlWalker.php @@ -33,6 +33,7 @@ use Doctrine\DBAL\LockMode, * @author Guilherme Blanco * @author Roman Borschel * @author Benjamin Eberlei + * @author Alexander * @since 2.0 * @todo Rename: SQLWalker */ @@ -267,6 +268,11 @@ class SqlWalker implements TreeWalker $sqlParts[] = $baseTableAlias . '.' . $columnName . ' = ' . $tableAlias . '.' . $columnName; } + // Add filters on the root class + if ($filterSql = $this->generateFilterConditionSQL($parentClass, $tableAlias)) { + $sqlParts[] = $filterSql; + } + $sql .= implode(' AND ', $sqlParts); } @@ -352,6 +358,50 @@ class SqlWalker implements TreeWalker return (count($sqlParts) > 1) ? '(' . $sql . ')' : $sql; } + /** + * 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. + */ + private function generateFilterConditionSQL(ClassMetadata $targetEntity, $targetTableAlias) + { + if (!$this->_em->hasFilters()) { + return ''; + } + + switch($targetEntity->inheritanceType) { + case ClassMetadata::INHERITANCE_TYPE_NONE: + break; + case ClassMetadata::INHERITANCE_TYPE_JOINED: + // The classes in the inheritance will be added to the query one by one, + // but only the root node is getting filtered + if ($targetEntity->name !== $targetEntity->rootEntityName) { + return ''; + } + break; + case ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE: + // With STI the table will only be queried once, make sure that the filters + // are added to the root entity + $targetEntity = $this->_em->getClassMetadata($targetEntity->rootEntityName); + break; + default: + //@todo: throw exception? + return ''; + break; + } + + $filterClauses = array(); + foreach ($this->_em->getFilters()->getEnabledFilters() as $filter) { + if ('' !== $filterExpr = $filter->addFilterConstraint($targetEntity, $targetTableAlias)) { + $filterClauses[] = '(' . $filterExpr . ')'; + } + } + + return implode(' AND ', $filterClauses); + } /** * Walks down a SelectStatement AST node, thereby generating the appropriate SQL. * @@ -802,6 +852,7 @@ class SqlWalker implements TreeWalker $sql .= $sourceTableAlias . '.' . $quotedTargetColumn . ' = ' . $targetTableAlias . '.' . $sourceColumn; } } + } else if ($assoc['type'] == ClassMetadata::MANY_TO_MANY) { // Join relation table $joinTable = $assoc['joinTable']; @@ -867,6 +918,11 @@ class SqlWalker implements TreeWalker } } + // Apply the filters + if ($filterExpr = $this->generateFilterConditionSQL($targetClass, $targetTableAlias)) { + $sql .= ' AND ' . $filterExpr; + } + // Handle WITH clause if (($condExpr = $join->conditionalExpression) !== null) { // Phase 2 AST optimization: Skip processment of ConditionalExpression @@ -922,17 +978,13 @@ class SqlWalker implements TreeWalker */ public function walkCoalesceExpression($coalesceExpression) { - $sql = 'COALESCE('; - $scalarExpressions = array(); foreach ($coalesceExpression->scalarExpressions as $scalarExpression) { $scalarExpressions[] = $this->walkSimpleArithmeticExpression($scalarExpression); } - $sql .= implode(', ', $scalarExpressions) . ')'; - - return $sql; + return 'COALESCE(' . implode(', ', $scalarExpressions) . ')'; } /** @@ -1467,6 +1519,26 @@ class SqlWalker implements TreeWalker $condSql = null !== $whereClause ? $this->walkConditionalExpression($whereClause->conditionalExpression) : ''; $discrSql = $this->_generateDiscriminatorColumnConditionSql($this->_rootAliases); + if ($this->_em->hasFilters()) { + $filterClauses = array(); + foreach ($this->_rootAliases as $dqlAlias) { + $class = $this->_queryComponents[$dqlAlias]['metadata']; + $tableAlias = $this->getSQLTableAlias($class->table['name'], $dqlAlias); + + if ($filterExpr = $this->generateFilterConditionSQL($class, $tableAlias)) { + $filterClauses[] = $filterExpr; + } + } + + if (count($filterClauses)) { + if ($condSql) { + $condSql .= ' AND '; + } + + $condSql .= implode(' AND ', $filterClauses); + } + } + if ($condSql) { return ' WHERE ' . (( ! $discrSql) ? $condSql : '(' . $condSql . ') AND ' . $discrSql); } diff --git a/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php b/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php new file mode 100644 index 000000000..621214fdd --- /dev/null +++ b/lib/Doctrine/ORM/Tools/ResolveTargetEntityListener.php @@ -0,0 +1,94 @@ +. + */ + +namespace Doctrine\ORM\Tools; + +use Doctrine\ORM\Event\LoadClassMetadataEventArgs; +use Doctrine\ORM\Mapping\ClassMetadata; + +/** + * ResolveTargetEntityListener + * + * Mechanism to overwrite interfaces or classes specified as association + * targets. + * + * @author Benjamin Eberlei + * @since 2.2 + */ +class ResolveTargetEntityListener +{ + /** + * @var array + */ + private $resolveTargetEntities = array(); + + /** + * Add a target-entity class name to resolve to a new class name. + * + * @param string $originalEntity + * @param string $newEntity + * @param array $mapping + * @return void + */ + public function addResolveTargetEntity($originalEntity, $newEntity, array $mapping) + { + $mapping['targetEntity'] = ltrim($newEntity, "\\"); + $this->resolveTargetEntities[ltrim($originalEntity, "\\")] = $mapping; + } + + /** + * Process event and resolve new target entity names. + * + * @param LoadClassMetadataEventArgs $args + * @return void + */ + public function loadClassMetadata(LoadClassMetadataEventArgs $args) + { + $cm = $args->getClassMetadata(); + foreach ($cm->associationMappings as $assocName => $mapping) { + if (isset($this->resolveTargetEntities[$mapping['targetEntity']])) { + $this->remapAssociation($cm, $mapping); + } + } + } + + private function remapAssociation($classMetadata, $mapping) + { + $newMapping = $this->resolveTargetEntities[$mapping['targetEntity']]; + $newMapping = array_replace_recursive($mapping, $newMapping); + $newMapping['fieldName'] = $mapping['fieldName']; + unset($classMetadata->associationMappings[$mapping['fieldName']]); + + switch ($mapping['type']) { + case ClassMetadata::MANY_TO_MANY: + $classMetadata->mapManyToMany($newMapping); + break; + case ClassMetadata::MANY_TO_ONE: + $classMetadata->mapManyToOne($newMapping); + break; + case ClassMetadata::ONE_TO_MANY: + $classMetadata->mapOneToMany($newMapping); + break; + case ClassMetadata::ONE_TO_ONE: + $classMetadata->mapOneToOne($newMapping); + break; + } + } +} + diff --git a/lib/Doctrine/ORM/Tools/SchemaValidator.php b/lib/Doctrine/ORM/Tools/SchemaValidator.php index cb3c9e515..f6bcadb72 100644 --- a/lib/Doctrine/ORM/Tools/SchemaValidator.php +++ b/lib/Doctrine/ORM/Tools/SchemaValidator.php @@ -21,6 +21,7 @@ namespace Doctrine\ORM\Tools; use Doctrine\ORM\EntityManager; use Doctrine\ORM\Mapping\ClassMetadataInfo; +use Doctrine\DBAL\Types\Type; /** * Performs strict validation of the mapping schema @@ -28,7 +29,6 @@ use Doctrine\ORM\Mapping\ClassMetadataInfo; * @license http://www.opensource.org/licenses/lgpl-license.php LGPL * @link www.doctrine-project.com * @since 1.0 - * @version $Revision$ * @author Benjamin Eberlei * @author Guilherme Blanco * @author Jonathan Wage @@ -88,6 +88,12 @@ class SchemaValidator $ce = array(); $cmf = $this->em->getMetadataFactory(); + foreach ($class->fieldMappings as $fieldName => $mapping) { + if (!Type::hasType($mapping['type'])) { + $ce[] = "The field '" . $class->name . "#" . $fieldName."' uses a non-existant type '" . $mapping['type'] . "'."; + } + } + foreach ($class->associationMappings AS $fieldName => $assoc) { if (!$cmf->hasMetadataFor($assoc['targetEntity'])) { $ce[] = "The target entity '" . $assoc['targetEntity'] . "' specified on " . $class->name . '#' . $fieldName . ' is unknown.'; @@ -139,6 +145,19 @@ class SchemaValidator $assoc['targetEntity'] . "#" . $assoc['inversedBy'] . " are ". "incosistent with each other."; } + + // Verify inverse side/owning side match each other + $targetAssoc = $targetMetadata->associationMappings[$assoc['inversedBy']]; + if ($assoc['type'] == ClassMetadataInfo::ONE_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::ONE_TO_ONE){ + $ce[] = "If association " . $class->name . "#" . $fieldName . " is one-to-one, then the inversed " . + "side " . $targetMetadata->name . "#" . $assoc['inversedBy'] . " has to be one-to-one as well."; + } else if ($assoc['type'] == ClassMetadataInfo::MANY_TO_ONE && $targetAssoc['type'] !== ClassMetadataInfo::ONE_TO_MANY){ + $ce[] = "If association " . $class->name . "#" . $fieldName . " is many-to-one, then the inversed " . + "side " . $targetMetadata->name . "#" . $assoc['inversedBy'] . " has to be one-to-many."; + } else if ($assoc['type'] == ClassMetadataInfo::MANY_TO_MANY && $targetAssoc['type'] !== ClassMetadataInfo::MANY_TO_MANY){ + $ce[] = "If association " . $class->name . "#" . $fieldName . " is many-to-many, then the inversed " . + "side " . $targetMetadata->name . "#" . $assoc['inversedBy'] . " has to be many-to-many as well."; + } } if ($assoc['isOwningSide']) { diff --git a/lib/Doctrine/ORM/UnitOfWork.php b/lib/Doctrine/ORM/UnitOfWork.php index a026d001a..1db6caa0c 100644 --- a/lib/Doctrine/ORM/UnitOfWork.php +++ b/lib/Doctrine/ORM/UnitOfWork.php @@ -705,7 +705,6 @@ class UnitOfWork implements PropertyChangedListener foreach ($unwrappedValue as $key => $entry) { $state = $this->getEntityState($entry, self::STATE_NEW); - $oid = spl_object_hash($entry); switch ($state) { case self::STATE_NEW: @@ -2293,13 +2292,14 @@ class UnitOfWork implements PropertyChangedListener $id = array($class->identifier[0] => $idHash); } + $overrideLocalValues = true; + if (isset($this->identityMap[$class->rootEntityName][$idHash])) { $entity = $this->identityMap[$class->rootEntityName][$idHash]; $oid = spl_object_hash($entity); if ($entity instanceof Proxy && ! $entity->__isInitialized__) { $entity->__isInitialized__ = true; - $overrideLocalValues = true; if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); @@ -2308,7 +2308,7 @@ class UnitOfWork implements PropertyChangedListener $overrideLocalValues = isset($hints[Query::HINT_REFRESH]); // If only a specific entity is set to refresh, check that it's the one - if(isset($hints[Query::HINT_REFRESH_ENTITY])) { + if (isset($hints[Query::HINT_REFRESH_ENTITY])) { $overrideLocalValues = $hints[Query::HINT_REFRESH_ENTITY] === $entity; // inject ObjectManager into just loaded proxies. @@ -2333,8 +2333,6 @@ class UnitOfWork implements PropertyChangedListener if ($entity instanceof NotifyPropertyChanged) { $entity->addPropertyChangedListener($this); } - - $overrideLocalValues = true; } if ( ! $overrideLocalValues) { @@ -2362,6 +2360,10 @@ class UnitOfWork implements PropertyChangedListener foreach ($class->associationMappings as $field => $assoc) { // Check if the association is not among the fetch-joined associations already. if (isset($hints['fetchAlias']) && isset($hints['fetched'][$hints['fetchAlias']][$field])) { + // DDC-1545: Fetched associations must have original entity data set. + // Define NULL value right now, since next iteration may fill it with actual value. + $this->originalEntityData[$oid][$field] = null; + continue; } @@ -2382,12 +2384,15 @@ class UnitOfWork implements PropertyChangedListener foreach ($assoc['targetToSourceKeyColumns'] as $targetColumn => $srcColumn) { $joinColumnValue = isset($data[$srcColumn]) ? $data[$srcColumn] : null; - if ($joinColumnValue !== null) { - if ($targetClass->containsForeignIdentifier) { - $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; - } else { - $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; - } + // Skip is join column value is null + if ($joinColumnValue === null) { + continue; + } + + if ($targetClass->containsForeignIdentifier) { + $associatedId[$targetClass->getFieldForColumn($targetColumn)] = $joinColumnValue; + } else { + $associatedId[$targetClass->fieldNames[$targetColumn]] = $joinColumnValue; } } diff --git a/tests/Doctrine/Tests/Models/CMS/CmsArticle.php b/tests/Doctrine/Tests/Models/CMS/CmsArticle.php index 5cc516735..266cbc662 100644 --- a/tests/Doctrine/Tests/Models/CMS/CmsArticle.php +++ b/tests/Doctrine/Tests/Models/CMS/CmsArticle.php @@ -36,7 +36,7 @@ class CmsArticle * @Version @column(type="integer") */ public $version; - + public function setAuthor(CmsUser $author) { $this->user = $author; } diff --git a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php index 5fa55036c..8257176ab 100644 --- a/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php +++ b/tests/Doctrine/Tests/ORM/Functional/OneToOneEagerLoadingTest.php @@ -143,14 +143,27 @@ class OneToOneEagerLoadingTest extends \Doctrine\Tests\OrmFunctionalTestCase public function testEagerLoadWithNonNullableColumnsGeneratesInnerJoinOnOwningSide() { $waggon = new Waggon(); - $this->_em->persist($waggon); + + // It should have a train + $train = new Train(new TrainOwner("Alexander")); + $train->addWaggon($waggon); + + $this->_em->persist($train); $this->_em->flush(); $this->_em->clear(); $waggon = $this->_em->find(get_class($waggon), $waggon->id); + + // The last query is the eager loading of the owner of the train + $this->assertEquals( + "SELECT t0.id AS id1, t0.name AS name2, t3.id AS id4, t3.driver_id AS driver_id5, t3.owner_id AS owner_id6 FROM TrainOwner t0 LEFT JOIN Train t3 ON t3.owner_id = t0.id WHERE t0.id IN (?)", + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] + ); + + // The one before is the fetching of the waggon and train $this->assertEquals( "SELECT t0.id AS id1, t0.train_id AS train_id2, t3.id AS id4, t3.driver_id AS driver_id5, t3.owner_id AS owner_id6 FROM Waggon t0 INNER JOIN Train t3 ON t0.train_id = t3.id WHERE t0.id = ?", - $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery]['sql'] + $this->_sqlLoggerStack->queries[$this->_sqlLoggerStack->currentQuery - 1]['sql'] ); } @@ -189,6 +202,7 @@ class Train /** * Owning side * @OneToOne(targetEntity="TrainOwner", inversedBy="train", fetch="EAGER", cascade={"persist"}) + * @JoinColumn(nullable=false) */ public $owner; /** @@ -280,7 +294,10 @@ class Waggon { /** @id @generatedValue @column(type="integer") */ public $id; - /** @ManyToOne(targetEntity="Train", inversedBy="waggons", fetch="EAGER") */ + /** + * @ManyToOne(targetEntity="Train", inversedBy="waggons", fetch="EAGER") + * @JoinColumn(nullable=false) + */ public $train; public function setTrain($train) diff --git a/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php new file mode 100644 index 000000000..98b3fafd4 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/SQLFilterTest.php @@ -0,0 +1,749 @@ + + */ +class SQLFilterTest extends \Doctrine\Tests\OrmFunctionalTestCase +{ + private $userId, $userId2, $articleId, $articleId2; + private $groupId, $groupId2; + + public function setUp() + { + $this->useModelSet('cms'); + $this->useModelSet('company'); + parent::setUp(); + } + + public function tearDown() + { + parent::tearDown(); + + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_LAZY; + $class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_LAZY; + } + + public function testConfigureFilter() + { + $config = new \Doctrine\ORM\Configuration(); + + $config->addFilter("locale", "\Doctrine\Tests\ORM\Functional\MyLocaleFilter"); + + $this->assertEquals("\Doctrine\Tests\ORM\Functional\MyLocaleFilter", $config->getFilterClassName("locale")); + $this->assertNull($config->getFilterClassName("foo")); + } + + public function testEntityManagerEnableFilter() + { + $em = $this->_getEntityManager(); + $this->configureFilters($em); + + // Enable an existing filter + $filter = $em->getFilters()->enable("locale"); + $this->assertTrue($filter instanceof \Doctrine\Tests\ORM\Functional\MyLocaleFilter); + + // Enable the filter again + $filter2 = $em->getFilters()->enable("locale"); + $this->assertEquals($filter, $filter2); + + // Enable a non-existing filter + $exceptionThrown = false; + try { + $filter = $em->getFilters()->enable("foo"); + } catch (\InvalidArgumentException $e) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + } + + public function testEntityManagerEnabledFilters() + { + $em = $this->_getEntityManager(); + + // No enabled filters + $this->assertEquals(array(), $em->getFilters()->getEnabledFilters()); + + $this->configureFilters($em); + $filter = $em->getFilters()->enable("locale"); + $filter = $em->getFilters()->enable("soft_delete"); + + // Two enabled filters + $this->assertEquals(2, count($em->getFilters()->getEnabledFilters())); + + } + + public function testEntityManagerDisableFilter() + { + $em = $this->_getEntityManager(); + $this->configureFilters($em); + + // Enable the filter + $filter = $em->getFilters()->enable("locale"); + + // Disable it + $this->assertEquals($filter, $em->getFilters()->disable("locale")); + $this->assertEquals(0, count($em->getFilters()->getEnabledFilters())); + + // Disable a non-existing filter + $exceptionThrown = false; + try { + $filter = $em->getFilters()->disable("foo"); + } catch (\InvalidArgumentException $e) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + + // Disable a non-enabled filter + $exceptionThrown = false; + try { + $filter = $em->getFilters()->disable("locale"); + } catch (\InvalidArgumentException $e) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + } + + public function testEntityManagerGetFilter() + { + $em = $this->_getEntityManager(); + $this->configureFilters($em); + + // Enable the filter + $filter = $em->getFilters()->enable("locale"); + + // Get the filter + $this->assertEquals($filter, $em->getFilters()->getFilter("locale")); + + // Get a non-enabled filter + $exceptionThrown = false; + try { + $filter = $em->getFilters()->getFilter("soft_delete"); + } catch (\InvalidArgumentException $e) { + $exceptionThrown = true; + } + $this->assertTrue($exceptionThrown); + } + + protected function configureFilters($em) + { + // Add filters to the configuration of the EM + $config = $em->getConfiguration(); + $config->addFilter("locale", "\Doctrine\Tests\ORM\Functional\MyLocaleFilter"); + $config->addFilter("soft_delete", "\Doctrine\Tests\ORM\Functional\MySoftDeleteFilter"); + } + + protected function getMockConnection() + { + // Setup connection mock + $conn = $this->getMockBuilder('Doctrine\DBAL\Connection') + ->disableOriginalConstructor() + ->getMock(); + + return $conn; + } + + protected function getMockEntityManager() + { + // Setup connection mock + $em = $this->getMockBuilder('Doctrine\ORM\EntityManager') + ->disableOriginalConstructor() + ->getMock(); + + return $em; + } + + protected function addMockFilterCollection($em) + { + $filterCollection = $this->getMockBuilder('Doctrine\ORM\Query\FilterCollection') + ->disableOriginalConstructor() + ->getMock(); + + $em->expects($this->any()) + ->method('getFilters') + ->will($this->returnValue($filterCollection)); + + return $filterCollection; + } + + public function testSQLFilterGetSetParameter() + { + // Setup mock connection + $conn = $this->getMockConnection(); + $conn->expects($this->once()) + ->method('quote') + ->with($this->equalTo('en')) + ->will($this->returnValue("'en'")); + + $em = $this->getMockEntityManager($conn); + $em->expects($this->once()) + ->method('getConnection') + ->will($this->returnValue($conn)); + + $filterCollection = $this->addMockFilterCollection($em); + $filterCollection + ->expects($this->once()) + ->method('setFiltersStateDirty'); + + $filter = new MyLocaleFilter($em); + + $filter->setParameter('locale', 'en', DBALType::STRING); + + $this->assertEquals("'en'", $filter->getParameter('locale')); + } + + public function testSQLFilterSetParameterInfersType() + { + // Setup mock connection + $conn = $this->getMockConnection(); + $conn->expects($this->once()) + ->method('quote') + ->with($this->equalTo('en')) + ->will($this->returnValue("'en'")); + + $em = $this->getMockEntityManager($conn); + $em->expects($this->once()) + ->method('getConnection') + ->will($this->returnValue($conn)); + + $filterCollection = $this->addMockFilterCollection($em); + $filterCollection + ->expects($this->once()) + ->method('setFiltersStateDirty'); + + $filter = new MyLocaleFilter($em); + + $filter->setParameter('locale', 'en'); + + $this->assertEquals("'en'", $filter->getParameter('locale')); + } + + public function testSQLFilterAddConstraint() + { + // Set up metadata mock + $targetEntity = $this->getMockBuilder('Doctrine\ORM\Mapping\ClassMetadata') + ->disableOriginalConstructor() + ->getMock(); + + $filter = new MySoftDeleteFilter($this->getMockEntityManager()); + + // Test for an entity that gets extra filter data + $targetEntity->name = 'MyEntity\SoftDeleteNewsItem'; + $this->assertEquals('t1_.deleted = 0', $filter->addFilterConstraint($targetEntity, 't1_')); + + // Test for an entity that doesn't get extra filter data + $targetEntity->name = 'MyEntity\NoSoftDeleteNewsItem'; + $this->assertEquals('', $filter->addFilterConstraint($targetEntity, 't1_')); + + } + + public function testSQLFilterToString() + { + $em = $this->getMockEntityManager(); + $filterCollection = $this->addMockFilterCollection($em); + + $filter = new MyLocaleFilter($em); + $filter->setParameter('locale', 'en', DBALType::STRING); + $filter->setParameter('foo', 'bar', DBALType::STRING); + + $filter2 = new MyLocaleFilter($em); + $filter2->setParameter('foo', 'bar', DBALType::STRING); + $filter2->setParameter('locale', 'en', DBALType::STRING); + + $parameters = array( + 'foo' => array('value' => 'bar', 'type' => DBALType::STRING), + 'locale' => array('value' => 'en', 'type' => DBALType::STRING), + ); + + $this->assertEquals(serialize($parameters), ''.$filter); + $this->assertEquals(''.$filter, ''.$filter2); + } + + public function testQueryCache_DependsOnFilters() + { + $cacheDataReflection = new \ReflectionProperty("Doctrine\Common\Cache\ArrayCache", "data"); + $cacheDataReflection->setAccessible(true); + + $query = $this->_em->createQuery('select ux from Doctrine\Tests\Models\CMS\CmsUser ux'); + + $cache = new ArrayCache(); + $query->setQueryCacheDriver($cache); + + $query->getResult(); + $this->assertEquals(1, sizeof($cacheDataReflection->getValue($cache))); + + $conf = $this->_em->getConfiguration(); + $conf->addFilter("locale", "\Doctrine\Tests\ORM\Functional\MyLocaleFilter"); + $this->_em->getFilters()->enable("locale"); + + $query->getResult(); + $this->assertEquals(2, sizeof($cacheDataReflection->getValue($cache))); + + // Another time doesn't add another cache entry + $query->getResult(); + $this->assertEquals(2, sizeof($cacheDataReflection->getValue($cache))); + } + + public function testQueryGeneration_DependsOnFilters() + { + $query = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsAddress a'); + $firstSQLQuery = $query->getSQL(); + + $conf = $this->_em->getConfiguration(); + $conf->addFilter("country", "\Doctrine\Tests\ORM\Functional\CMSCountryFilter"); + $this->_em->getFilters()->enable("country") + ->setParameter("country", "en", DBALType::STRING); + + $this->assertNotEquals($firstSQLQuery, $query->getSQL()); + } + + public function testToOneFilter() + { + //$this->_em->getConnection()->getConfiguration()->setSQLLogger(new \Doctrine\DBAL\Logging\EchoSQLLogger); + $this->loadFixtureData(); + + $query = $this->_em->createQuery('select ux, ua from Doctrine\Tests\Models\CMS\CmsUser ux JOIN ux.address ua'); + + // We get two users before enabling the filter + $this->assertEquals(2, count($query->getResult())); + + $conf = $this->_em->getConfiguration(); + $conf->addFilter("country", "\Doctrine\Tests\ORM\Functional\CMSCountryFilter"); + $this->_em->getFilters()->enable("country")->setParameter("country", "Germany", DBALType::STRING); + + // We get one user after enabling the filter + $this->assertEquals(1, count($query->getResult())); + } + + public function testManyToManyFilter() + { + $this->loadFixtureData(); + $query = $this->_em->createQuery('select ux, ug from Doctrine\Tests\Models\CMS\CmsUser ux JOIN ux.groups ug'); + + // We get two users before enabling the filter + $this->assertEquals(2, count($query->getResult())); + + $conf = $this->_em->getConfiguration(); + $conf->addFilter("group_prefix", "\Doctrine\Tests\ORM\Functional\CMSGroupPrefixFilter"); + $this->_em->getFilters()->enable("group_prefix")->setParameter("prefix", "bar_%", DBALType::STRING); + + // We get one user after enabling the filter + $this->assertEquals(1, count($query->getResult())); + + } + + public function testWhereFilter() + { + $this->loadFixtureData(); + $query = $this->_em->createQuery('select ug from Doctrine\Tests\Models\CMS\CmsGroup ug WHERE 1=1'); + + // We get two users before enabling the filter + $this->assertEquals(2, count($query->getResult())); + + $conf = $this->_em->getConfiguration(); + $conf->addFilter("group_prefix", "\Doctrine\Tests\ORM\Functional\CMSGroupPrefixFilter"); + $this->_em->getFilters()->enable("group_prefix")->setParameter("prefix", "bar_%", DBALType::STRING); + + // We get one user after enabling the filter + $this->assertEquals(1, count($query->getResult())); + } + + + private function loadLazyFixtureData() + { + $class = $this->_em->getClassMetadata('Doctrine\Tests\Models\CMS\CmsUser'); + $class->associationMappings['articles']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; + $class->associationMappings['groups']['fetch'] = ClassMetadataInfo::FETCH_EXTRA_LAZY; + $this->loadFixtureData(); + } + + private function useCMSArticleTopicFilter() + { + $conf = $this->_em->getConfiguration(); + $conf->addFilter("article_topic", "\Doctrine\Tests\ORM\Functional\CMSArticleTopicFilter"); + $this->_em->getFilters()->enable("article_topic")->setParameter("topic", "Test1", DBALType::STRING); + } + + public function testOneToMany_ExtraLazyCountWithFilter() + { + $this->loadLazyFixtureData(); + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $this->assertFalse($user->articles->isInitialized()); + $this->assertEquals(2, count($user->articles)); + + $this->useCMSArticleTopicFilter(); + + $this->assertEquals(1, count($user->articles)); + } + + public function testOneToMany_ExtraLazyContainsWithFilter() + { + $this->loadLazyFixtureData(); + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + $filteredArticle = $this->_em->find('Doctrine\Tests\Models\CMS\CmsArticle', $this->articleId2); + + $this->assertFalse($user->articles->isInitialized()); + $this->assertTrue($user->articles->contains($filteredArticle)); + + $this->useCMSArticleTopicFilter(); + + $this->assertFalse($user->articles->contains($filteredArticle)); + } + + public function testOneToMany_ExtraLazySliceWithFilter() + { + $this->loadLazyFixtureData(); + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId); + + $this->assertFalse($user->articles->isInitialized()); + $this->assertEquals(2, count($user->articles->slice(0,10))); + + $this->useCMSArticleTopicFilter(); + + $this->assertEquals(1, count($user->articles->slice(0,10))); + } + + private function useCMSGroupPrefixFilter() + { + $conf = $this->_em->getConfiguration(); + $conf->addFilter("group_prefix", "\Doctrine\Tests\ORM\Functional\CMSGroupPrefixFilter"); + $this->_em->getFilters()->enable("group_prefix")->setParameter("prefix", "foo%", DBALType::STRING); + } + + public function testManyToMany_ExtraLazyCountWithFilter() + { + $this->loadLazyFixtureData(); + + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId2); + + $this->assertFalse($user->groups->isInitialized()); + $this->assertEquals(2, count($user->groups)); + + $this->useCMSGroupPrefixFilter(); + + $this->assertEquals(1, count($user->groups)); + } + + public function testManyToMany_ExtraLazyContainsWithFilter() + { + $this->loadLazyFixtureData(); + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId2); + $filteredArticle = $this->_em->find('Doctrine\Tests\Models\CMS\CmsGroup', $this->groupId2); + + $this->assertFalse($user->groups->isInitialized()); + $this->assertTrue($user->groups->contains($filteredArticle)); + + $this->useCMSGroupPrefixFilter(); + + $this->assertFalse($user->groups->contains($filteredArticle)); + } + + public function testManyToMany_ExtraLazySliceWithFilter() + { + $this->loadLazyFixtureData(); + $user = $this->_em->find('Doctrine\Tests\Models\CMS\CmsUser', $this->userId2); + + $this->assertFalse($user->groups->isInitialized()); + $this->assertEquals(2, count($user->groups->slice(0,10))); + + $this->useCMSGroupPrefixFilter(); + + $this->assertEquals(1, count($user->groups->slice(0,10))); + } + + private function loadFixtureData() + { + $user = new CmsUser; + $user->name = 'Roman'; + $user->username = 'romanb'; + $user->status = 'developer'; + + $address = new CmsAddress; + $address->country = 'Germany'; + $address->city = 'Berlin'; + $address->zip = '12345'; + + $user->address = $address; // inverse side + $address->user = $user; // owning side! + + $group = new CmsGroup; + $group->name = 'foo_group'; + $user->addGroup($group); + + $article1 = new CmsArticle; + $article1->topic = "Test1"; + $article1->text = "Test"; + $article1->setAuthor($user); + + $article2 = new CmsArticle; + $article2->topic = "Test2"; + $article2->text = "Test"; + $article2->setAuthor($user); + + $this->_em->persist($article1); + $this->_em->persist($article2); + + $this->_em->persist($user); + + $user2 = new CmsUser; + $user2->name = 'Guilherme'; + $user2->username = 'gblanco'; + $user2->status = 'developer'; + + $address2 = new CmsAddress; + $address2->country = 'France'; + $address2->city = 'Paris'; + $address2->zip = '12345'; + + $user->address = $address2; // inverse side + $address2->user = $user2; // owning side! + + $user2->addGroup($group); + $group2 = new CmsGroup; + $group2->name = 'bar_group'; + $user2->addGroup($group2); + + $this->_em->persist($user2); + $this->_em->flush(); + $this->_em->clear(); + + $this->userId = $user->getId(); + $this->userId2 = $user2->getId(); + $this->articleId = $article1->id; + $this->articleId2 = $article2->id; + $this->groupId = $group->id; + $this->groupId2 = $group2->id; + } + + public function testJoinSubclassPersister_FilterOnlyOnRootTableWhenFetchingSubEntity() + { + $this->loadCompanyJoinedSubclassFixtureData(); + // Persister + $this->assertEquals(2, count($this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyManager')->findAll())); + // SQLWalker + $this->assertEquals(2, count($this->_em->createQuery("SELECT cm FROM Doctrine\Tests\Models\Company\CompanyManager cm")->getResult())); + + // Enable the filter + $conf = $this->_em->getConfiguration(); + $conf->addFilter("person_name", "\Doctrine\Tests\ORM\Functional\CompanyPersonNameFilter"); + $this->_em->getFilters() + ->enable("person_name") + ->setParameter("name", "Guilh%", DBALType::STRING); + + $managers = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyManager')->findAll(); + $this->assertEquals(1, count($managers)); + $this->assertEquals("Guilherme", $managers[0]->getName()); + + $this->assertEquals(1, count($this->_em->createQuery("SELECT cm FROM Doctrine\Tests\Models\Company\CompanyManager cm")->getResult())); + } + + public function testJoinSubclassPersister_FilterOnlyOnRootTableWhenFetchingRootEntity() + { + $this->loadCompanyJoinedSubclassFixtureData(); + $this->assertEquals(3, count($this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyPerson')->findAll())); + $this->assertEquals(3, count($this->_em->createQuery("SELECT cp FROM Doctrine\Tests\Models\Company\CompanyPerson cp")->getResult())); + + // Enable the filter + $conf = $this->_em->getConfiguration(); + $conf->addFilter("person_name", "\Doctrine\Tests\ORM\Functional\CompanyPersonNameFilter"); + $this->_em->getFilters() + ->enable("person_name") + ->setParameter("name", "Guilh%", DBALType::STRING); + + $persons = $this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyPerson')->findAll(); + $this->assertEquals(1, count($persons)); + $this->assertEquals("Guilherme", $persons[0]->getName()); + + $this->assertEquals(1, count($this->_em->createQuery("SELECT cp FROM Doctrine\Tests\Models\Company\CompanyPerson cp")->getResult())); + } + + private function loadCompanyJoinedSubclassFixtureData() + { + $manager = new CompanyManager; + $manager->setName('Roman'); + $manager->setTitle('testlead'); + $manager->setSalary(42); + $manager->setDepartment('persisters'); + + $manager2 = new CompanyManager; + $manager2->setName('Guilherme'); + $manager2->setTitle('devlead'); + $manager2->setSalary(42); + $manager2->setDepartment('parsers'); + + $person = new CompanyPerson; + $person->setName('Benjamin'); + + $this->_em->persist($manager); + $this->_em->persist($manager2); + $this->_em->persist($person); + $this->_em->flush(); + $this->_em->clear(); + } + + public function testSingleTableInheritance_FilterOnlyOnRootTableWhenFetchingSubEntity() + { + $this->loadCompanySingleTableInheritanceFixtureData(); + // Persister + $this->assertEquals(2, count($this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyFlexUltraContract')->findAll())); + // SQLWalker + $this->assertEquals(2, count($this->_em->createQuery("SELECT cfc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract cfc")->getResult())); + + // Enable the filter + $conf = $this->_em->getConfiguration(); + $conf->addFilter("completed_contract", "\Doctrine\Tests\ORM\Functional\CompletedContractFilter"); + $this->_em->getFilters() + ->enable("completed_contract") + ->setParameter("completed", true, DBALType::BOOLEAN); + + $this->assertEquals(1, count($this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyFlexUltraContract')->findAll())); + $this->assertEquals(1, count($this->_em->createQuery("SELECT cfc FROM Doctrine\Tests\Models\Company\CompanyFlexUltraContract cfc")->getResult())); + } + + public function testSingleTableInheritance_FilterOnlyOnRootTableWhenFetchingRootEntity() + { + $this->loadCompanySingleTableInheritanceFixtureData(); + $this->assertEquals(4, count($this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyFlexContract')->findAll())); + $this->assertEquals(4, count($this->_em->createQuery("SELECT cfc FROM Doctrine\Tests\Models\Company\CompanyFlexContract cfc")->getResult())); + + // Enable the filter + $conf = $this->_em->getConfiguration(); + $conf->addFilter("completed_contract", "\Doctrine\Tests\ORM\Functional\CompletedContractFilter"); + $this->_em->getFilters() + ->enable("completed_contract") + ->setParameter("completed", true, DBALType::BOOLEAN); + + $this->assertEquals(2, count($this->_em->getRepository('Doctrine\Tests\Models\Company\CompanyFlexContract')->findAll())); + $this->assertEquals(2, count($this->_em->createQuery("SELECT cfc FROM Doctrine\Tests\Models\Company\CompanyFlexContract cfc")->getResult())); + } + + private function loadCompanySingleTableInheritanceFixtureData() + { + $contract1 = new CompanyFlexUltraContract; + $contract2 = new CompanyFlexUltraContract; + $contract2->markCompleted(); + + $contract3 = new CompanyFlexContract; + $contract4 = new CompanyFlexContract; + $contract4->markCompleted(); + + $this->_em->persist($contract1); + $this->_em->persist($contract2); + $this->_em->persist($contract3); + $this->_em->persist($contract4); + $this->_em->flush(); + $this->_em->clear(); + } +} + +class MySoftDeleteFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) + { + if ($targetEntity->name != "MyEntity\SoftDeleteNewsItem") { + return ""; + } + + return $targetTableAlias.'.deleted = 0'; + } +} + +class MyLocaleFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) + { + if (!in_array("LocaleAware", $targetEntity->reflClass->getInterfaceNames())) { + return ""; + } + + return $targetTableAlias.'.locale = ' . $this->getParameter('locale'); // getParam uses connection to quote the value. + } +} + +class CMSCountryFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) + { + if ($targetEntity->name != "Doctrine\Tests\Models\CMS\CmsAddress") { + return ""; + } + + return $targetTableAlias.'.country = ' . $this->getParameter('country'); // getParam uses connection to quote the value. + } +} + +class CMSGroupPrefixFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) + { + if ($targetEntity->name != "Doctrine\Tests\Models\CMS\CmsGroup") { + return ""; + } + + return $targetTableAlias.'.name LIKE ' . $this->getParameter('prefix'); // getParam uses connection to quote the value. + } +} + +class CMSArticleTopicFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias) + { + if ($targetEntity->name != "Doctrine\Tests\Models\CMS\CmsArticle") { + return ""; + } + + return $targetTableAlias.'.topic = ' . $this->getParameter('topic'); // getParam uses connection to quote the value. + } +} + +class CompanyPersonNameFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias, $targetTable = '') + { + if ($targetEntity->name != "Doctrine\Tests\Models\Company\CompanyPerson") { + return ""; + } + + return $targetTableAlias.'.name LIKE ' . $this->getParameter('name'); + } +} + +class CompletedContractFilter extends SQLFilter +{ + public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias, $targetTable = '') + { + if ($targetEntity->name != "Doctrine\Tests\Models\Company\CompanyContract") { + return ""; + } + + return $targetTableAlias.'.completed = ' . $this->getParameter('completed'); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1545Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1545Test.php new file mode 100644 index 000000000..e7b55eb1f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC1545Test.php @@ -0,0 +1,203 @@ +useModelSet('cms'); + parent::setUp(); + } + + private function initDb($link) + { + $article = new CmsArticle(); + $article->topic = 'foo'; + $article->text = 'foo'; + + $user = new CmsUser(); + $user->status = 'foo'; + $user->username = 'foo'; + $user->name = 'foo'; + + $user2 = new CmsUser(); + $user2->status = 'bar'; + $user2->username = 'bar'; + $user2->name = 'bar'; + + if ($link) { + $article->user = $user; + } + + $this->_em->persist($article); + $this->_em->persist($user); + $this->_em->persist($user2); + $this->_em->flush(); + $this->_em->clear(); + + $this->articleId = $article->id; + $this->userId = $user->id; + $this->user2Id = $user2->id; + } + + public function testLinkObjects() + { + $this->initDb(false); + + // don't join association + $article = $this->_em->find('Doctrine\Tests\Models\Cms\CmsArticle', $this->articleId); + + $user = $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $this->userId); + + $article->user = $user; + + $this->_em->flush(); + $this->_em->clear(); + + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $this->assertNotNull($article->user); + $this->assertEquals($user->id, $article->user->id); + } + + public function testLinkObjectsWithAssociationLoaded() + { + $this->initDb(false); + + // join association + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $user = $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $this->userId); + + $article->user = $user; + + $this->_em->flush(); + $this->_em->clear(); + + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $this->assertNotNull($article->user); + $this->assertEquals($user->id, $article->user->id); + } + + public function testUnlinkObjects() + { + $this->initDb(true); + + // don't join association + $article = $this->_em->find('Doctrine\Tests\Models\Cms\CmsArticle', $this->articleId); + + $article->user = null; + + $this->_em->flush(); + $this->_em->clear(); + + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $this->assertNull($article->user); + } + + public function testUnlinkObjectsWithAssociationLoaded() + { + $this->initDb(true); + + // join association + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $article->user = null; + + $this->_em->flush(); + $this->_em->clear(); + + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $this->assertNull($article->user); + } + + public function testChangeLink() + { + $this->initDb(false); + + // don't join association + $article = $this->_em->find('Doctrine\Tests\Models\Cms\CmsArticle', $this->articleId); + + $user2 = $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $this->user2Id); + + $article->user = $user2; + + $this->_em->flush(); + $this->_em->clear(); + + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $this->assertNotNull($article->user); + $this->assertEquals($user2->id, $article->user->id); + } + + public function testChangeLinkWithAssociationLoaded() + { + $this->initDb(false); + + // join association + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $user2 = $this->_em->find('Doctrine\Tests\Models\Cms\CmsUser', $this->user2Id); + + $article->user = $user2; + + $this->_em->flush(); + $this->_em->clear(); + + $article = $this->_em + ->createQuery('SELECT a, u FROM Doctrine\Tests\Models\Cms\CmsArticle a LEFT JOIN a.user u WHERE a.id = :id') + ->setParameter('id', $this->articleId) + ->getOneOrNullResult(); + + $this->assertNotNull($article->user); + $this->assertEquals($user2->id, $article->user->id); + } +} \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php index fa7e0af2f..0dd24d50c 100644 --- a/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/DDC633Test.php @@ -99,4 +99,4 @@ class DDC633Patient * @OneToOne(targetEntity="DDC633Appointment", mappedBy="patient") */ public $appointment; -} \ No newline at end of file +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php index 9ba8748f4..980a720df 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php @@ -51,6 +51,17 @@ class XmlMappingDriverTest extends AbstractMappingDriverTest $this->assertTrue($class->associationMappings['article']['id']); } + /** + * @group DDC-1468 + * + * @expectedException Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Invalid mapping file 'Doctrine.Tests.Models.Generic.SerializationModel.dcm.xml' for class 'Doctrine\Tests\Models\Generic\SerializationModel'. + */ + public function testInvalidMappingFileException() + { + $this->createClassMetadata('Doctrine\Tests\Models\Generic\SerializationModel'); + } + /** * @param string $xmlMappingFile * @dataProvider dataValidSchema diff --git a/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php index 34c3fa810..2757259a4 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/YamlMappingDriverTest.php @@ -43,4 +43,15 @@ class YamlMappingDriverTest extends AbstractMappingDriverTest $this->assertEquals('Doctrine\Tests\Models\DirectoryTree\Directory', $classDirectory->associationMappings['parentDirectory']['sourceEntity']); } + /** + * @group DDC-1468 + * + * @expectedException Doctrine\ORM\Mapping\MappingException + * @expectedExceptionMessage Invalid mapping file 'Doctrine.Tests.Models.Generic.SerializationModel.dcm.yml' for class 'Doctrine\Tests\Models\Generic\SerializationModel'. + */ + public function testInvalidMappingFileException() + { + $this->createClassMetadata('Doctrine\Tests\Models\Generic\SerializationModel'); + } + } diff --git a/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Generic.SerializationModel.dcm.xml b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Generic.SerializationModel.dcm.xml new file mode 100644 index 000000000..36af855f2 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/xml/Doctrine.Tests.Models.Generic.SerializationModel.dcm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Generic.SerializationModel.dcm.yml b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Generic.SerializationModel.dcm.yml new file mode 100644 index 000000000..64f74b55f --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Mapping/yaml/Doctrine.Tests.Models.Generic.SerializationModel.dcm.yml @@ -0,0 +1,13 @@ +\stdClass: + type: entity + id: + id: + type: integer + unsigned: true + generator: + strategy: AUTO + fields: + array: + type: array + object: + type: object \ No newline at end of file diff --git a/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php b/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php new file mode 100644 index 000000000..b794474a8 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/ResolveTargetEntityListenerTest.php @@ -0,0 +1,129 @@ +createAnnotationDriver(); + + $this->em = $this->_getTestEntityManager(); + $this->em->getConfiguration()->setMetadataDriverImpl($annotationDriver); + $this->factory = new ClassMetadataFactory; + $this->factory->setEntityManager($this->em); + $this->listener = new ResolveTargetEntityListener; + } + + /** + * @group DDC-1544 + */ + public function testResolveTargetEntityListenerCanResolveTargetEntity() + { + $evm = $this->em->getEventManager(); + $this->listener->addResolveTargetEntity( + 'Doctrine\Tests\ORM\Tools\ResolveTargetInterface', + 'Doctrine\Tests\ORM\Tools\ResolveTargetEntity', + array() + ); + $this->listener->addResolveTargetEntity( + 'Doctrine\Tests\ORM\Tools\TargetInterface', + 'Doctrine\Tests\ORM\Tools\TargetEntity', + array() + ); + $evm->addEventListener(Events::loadClassMetadata, $this->listener); + $cm = $this->factory->getMetadataFor('Doctrine\Tests\ORM\Tools\ResolveTargetEntity'); + $meta = $cm->associationMappings; + $this->assertSame('Doctrine\Tests\ORM\Tools\TargetEntity', $meta['manyToMany']['targetEntity']); + $this->assertSame('Doctrine\Tests\ORM\Tools\ResolveTargetEntity', $meta['manyToOne']['targetEntity']); + $this->assertSame('Doctrine\Tests\ORM\Tools\ResolveTargetEntity', $meta['oneToMany']['targetEntity']); + $this->assertSame('Doctrine\Tests\ORM\Tools\TargetEntity', $meta['oneToOne']['targetEntity']); + } +} + +interface ResolveTargetInterface +{ + public function getId(); +} + +interface TargetInterface extends ResolveTargetInterface +{ +} + +/** + * @Entity + */ +class ResolveTargetEntity implements ResolveTargetInterface +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + private $id; + + /** + * @ManyToMany(targetEntity="Doctrine\Tests\ORM\Tools\TargetInterface") + */ + private $manyToMany; + + /** + * @ManyToOne(targetEntity="Doctrine\Tests\ORM\Tools\ResolveTargetInterface", inversedBy="oneToMany") + */ + private $manyToOne; + + /** + * @OneToMany(targetEntity="Doctrine\Tests\ORM\Tools\ResolveTargetInterface", mappedBy="manyToOne") + */ + private $oneToMany; + + /** + * @OneToOne(targetEntity="Doctrine\Tests\ORM\Tools\TargetInterface") + * @JoinColumn(name="target_entity_id", referencedColumnName="id") + */ + private $oneToOne; + + public function getId() + { + return $this->id; + } +} + +/** + * @Entity + */ +class TargetEntity implements TargetInterface +{ + /** + * @Id + * @Column(type="integer") + * @GeneratedValue(strategy="AUTO") + */ + private $id; + + public function getId() + { + return $this->id; + } +} diff --git a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php index 0fc3bb636..1d68a9bcb 100644 --- a/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php +++ b/tests/Doctrine/Tests/ORM/Tools/SchemaValidatorTest.php @@ -141,13 +141,11 @@ class InvalidEntity2 { /** * @Id @Column - * @GeneratedValue(strategy="AUTO") */ protected $key3; /** * @Id @Column - * @GeneratedValue(strategy="AUTO") */ protected $key4; @@ -155,4 +153,4 @@ class InvalidEntity2 * @ManyToOne(targetEntity="InvalidEntity1") */ protected $assoc; -} \ No newline at end of file +}