diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 49b6c82ddeb..abe0f959598 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -24,4 +24,4 @@ on: jobs: coding-standards: - uses: "doctrine/.github/.github/workflows/coding-standards.yml@10.1.0" + uses: "doctrine/.github/.github/workflows/coding-standards.yml@12.1.0" diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9b80a6b09db..fcd53f2fba4 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -119,7 +119,7 @@ jobs: ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }} - name: "Upload coverage file" - uses: "actions/upload-artifact@v4" + uses: "actions/upload-artifact@v5" with: name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.native_lazy }}-coverage" path: "coverage*.xml" @@ -228,7 +228,7 @@ jobs: run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml" - name: "Upload coverage file" - uses: "actions/upload-artifact@v4" + uses: "actions/upload-artifact@v5" with: name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage" path: "coverage.xml" @@ -296,7 +296,7 @@ jobs: run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml" - name: "Upload coverage file" - uses: "actions/upload-artifact@v4" + uses: "actions/upload-artifact@v5" with: name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage" path: "coverage.xml" @@ -393,7 +393,7 @@ jobs: ENABLE_SECOND_LEVEL_CACHE: 1 - name: "Upload coverage files" - uses: "actions/upload-artifact@v4" + uses: "actions/upload-artifact@v5" with: name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage" path: "coverage*.xml" @@ -416,7 +416,7 @@ jobs: fetch-depth: 2 - name: "Download coverage files" - uses: "actions/download-artifact@v5" + uses: "actions/download-artifact@v6" with: path: "reports" diff --git a/.github/workflows/documentation.yml b/.github/workflows/documentation.yml index 9734c20e2c5..5a34474b36f 100644 --- a/.github/workflows/documentation.yml +++ b/.github/workflows/documentation.yml @@ -17,4 +17,4 @@ on: jobs: documentation: name: "Documentation" - uses: "doctrine/.github/.github/workflows/documentation.yml@10.1.0" + uses: "doctrine/.github/.github/workflows/documentation.yml@12.1.0" diff --git a/.github/workflows/release-on-milestone-closed.yml b/.github/workflows/release-on-milestone-closed.yml index c0426413cff..0fd9b0c6671 100644 --- a/.github/workflows/release-on-milestone-closed.yml +++ b/.github/workflows/release-on-milestone-closed.yml @@ -7,7 +7,7 @@ on: jobs: release: - uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@10.1.0" + uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.1.0" secrets: GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }} GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }} diff --git a/composer.json b/composer.json index 6231cb4ec46..eb9578fac2b 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,9 @@ {"name": "Jonathan Wage", "email": "jonwage@gmail.com"}, {"name": "Marco Pivetta", "email": "ocramius@gmail.com"} ], + "scripts": { + "docs": "composer --working-dir docs update && ./docs/vendor/bin/build-docs.sh @additional_args" + }, "config": { "allow-plugins": { "composer/package-versions-deprecated": true, diff --git a/docs/.gitignore b/docs/.gitignore index 7373d9ac652..3d283e7410a 100644 --- a/docs/.gitignore +++ b/docs/.gitignore @@ -1,3 +1,3 @@ composer.lock vendor/ -build/ +output/ diff --git a/docs/Makefile b/docs/Makefile deleted file mode 100644 index d17a941b97b..00000000000 --- a/docs/Makefile +++ /dev/null @@ -1,24 +0,0 @@ -# Makefile for Doctrine ORM documentation -# - -# You can set these variables from the command line. -DOCOPTS = -BUILD = vendor/bin/guides -BUILDDIR = build - -# Internal variables. -ALLGUIDESOPTS = $(DOCOPTS) en/ - -.PHONY: help clean html - -help: - @echo "Please use \`make ' where is one of" - @echo " html to make standalone HTML files" - -clean: - -rm -rf ./$(BUILDDIR)/* - -html: - $(BUILD) $(ALLGUIDESOPTS) --output=$(BUILDDIR)/html - @echo - @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." diff --git a/docs/README.md b/docs/README.md index d7717c0aba7..d1eeb194a56 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,19 +4,15 @@ The documentation is written in [ReStructured Text](https://docutils.sourceforge ## How to Generate: -In the `docs/` folder, run +In the project root, run - composer update + composer docs -Then compile the documentation with: - - make html - -This will generate the documentation into the `build` subdirectory. +This will generate the documentation into the `docs/output` subdirectory. To browse the documentation, you need to run a webserver: - cd build/html + cd docs/output php -S localhost:8000 Now the documentation is available at [http://localhost:8000](http://localhost:8000). diff --git a/docs/composer.json b/docs/composer.json index aba7fdfa320..a0fb4d4aed8 100644 --- a/docs/composer.json +++ b/docs/composer.json @@ -3,8 +3,7 @@ "description": "Documentation for the Object-Relational Mapper\"", "type": "library", "license": "MIT", - "require": { - "phpdocumentor/guides-cli": "1.7.1", - "phpdocumentor/filesystem": "1.7.1" + "require-dev": { + "doctrine/docs-builder": "^1.0" } } diff --git a/docs/en/cookbook/dql-custom-walkers.rst b/docs/en/cookbook/dql-custom-walkers.rst index 4320856eb49..578a32c7c68 100644 --- a/docs/en/cookbook/dql-custom-walkers.rst +++ b/docs/en/cookbook/dql-custom-walkers.rst @@ -101,8 +101,16 @@ The ``Paginate::count(Query $query)`` looks like: { static public function count(Query $query) { - /** @var Query $countQuery */ - $countQuery = clone $query; + /* + To avoid changing the $query passed into the method and to make sure a possibly existing + ResultSetMapping is discarded, we create a new query object any copy relevant data over. + */ + $countQuery = new Query($query->getEntityManager()); + $countQuery->setDQL($query->getDQL()); + $countQuery->setParameters(clone $query->getParameters()); + foreach ($query->getHints() as $name => $value) { + $countQuery->setHint($name, $value); + } $countQuery->setHint(Query::HINT_CUSTOM_TREE_WALKERS, array('DoctrineExtensions\Paginate\CountSqlWalker')); $countQuery->setFirstResult(null)->setMaxResults(null); @@ -111,7 +119,7 @@ The ``Paginate::count(Query $query)`` looks like: } } -It clones the query, resets the limit clause first and max results +This resets the limit clause first and max results and registers the ``CountSqlWalker`` custom tree walker which will modify the AST to execute a count query. The walkers implementation is: diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 02b24d06758..983c6782f35 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -304,7 +304,7 @@ requests. Connection ---------- -The ``$connection`` passed as the first argument to he constructor of +The ``$connection`` passed as the first argument to the constructor of ``EntityManager`` has to be an instance of ``Doctrine\DBAL\Connection``. You can use the factory ``Doctrine\DBAL\DriverManager::getConnection()`` to create such a connection. The DBAL configuration is explained in the diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index e4896bcb488..07ff7320a7a 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -168,7 +168,7 @@ recommended, at least not as long as an entity instance still holds references to proxy objects or is still managed by an EntityManager. By default, serializing proxy objects does not initialize them. On unserialization, resulting objects are detached from the entity -manager and cannot be initialiazed anymore. You can implement the +manager and cannot be initialized anymore. You can implement the ``__serialize()`` method if you want to change that behavior, but then you need to ensure that you won't generate large serialized object graphs and take care of circular associations. diff --git a/docs/en/reference/filters.rst b/docs/en/reference/filters.rst index 58cabbc48fd..ccbf7336c89 100644 --- a/docs/en/reference/filters.rst +++ b/docs/en/reference/filters.rst @@ -30,7 +30,7 @@ table alias of the SQL table of the entity. For the filter to correctly function, the following rules must be followed. Failure to do so will lead to unexpected results from the query cache. 1. Parameters for the query should be set on the filter object by ``SQLFilter#setParameter()`` before the filter is used by the ORM ( i.e. do not set parameters inside ``SQLFilter#addFilterConstraint()`` function ). - 2. The filter must be deterministic. Don't change the values base on external inputs. + 2. The filter must be deterministic. Don't change the values based on external inputs. The ``SQLFilter#getParameter()`` function takes care of the proper quoting of parameters. diff --git a/docs/en/reference/second-level-cache.rst b/docs/en/reference/second-level-cache.rst index a36d85530d1..a762b8574a2 100644 --- a/docs/en/reference/second-level-cache.rst +++ b/docs/en/reference/second-level-cache.rst @@ -44,7 +44,7 @@ Something like below for an entity region: If the entity holds a collection that also needs to be cached. -An collection region could look something like: +A collection region could look something like: .. code-block:: php @@ -518,7 +518,7 @@ DELETE / UPDATE queries DQL UPDATE / DELETE statements are ported directly into a database and bypass the second-level cache. Entities that are already cached will NOT be invalidated. -However the cached data could be evicted using the cache API or an special query hint. +However the cached data could be evicted using the cache API or a special query hint. Execute the ``UPDATE`` and invalidate ``all cache entries`` using ``Query::HINT_CACHE_EVICT`` diff --git a/docs/en/reference/security.rst b/docs/en/reference/security.rst index 53d2a87ab60..30a8e88a550 100644 --- a/docs/en/reference/security.rst +++ b/docs/en/reference/security.rst @@ -118,7 +118,7 @@ entity might look like this: } } -Now the possiblity of mass-assignment exists on this entity and can +Now the possibility of mass-assignment exists on this entity and can be exploited by attackers to set the "isAdmin" flag to true on any object when you pass the whole request data to this method like: diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index fecf7487974..6d3fd860c33 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2136,12 +2136,6 @@ parameters: count: 1 path: src/Persisters/Entity/BasicEntityPersister.php - - - message: '#^Strict comparison using \=\=\= between string and null will always evaluate to false\.$#' - identifier: identical.alwaysFalse - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\CachedPersisterContext\:\:__construct\(\) has parameter \$class with generic interface Doctrine\\Persistence\\Mapping\\ClassMetadata but does not specify its types\: T$#' identifier: missingType.generics @@ -3048,12 +3042,6 @@ parameters: count: 1 path: src/Tools/ResolveTargetEntityListener.php - - - message: '#^Parameter \#1 \$className of method Doctrine\\Persistence\\Mapping\\AbstractClassMetadataFactory\\:\:setMetadataFor\(\) expects class\-string, \(int\|string\) given\.$#' - identifier: argument.type - count: 1 - path: src/Tools/ResolveTargetEntityListener.php - - message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$joinColumns\.$#' identifier: property.notFound diff --git a/src/Persisters/Collection/ManyToManyPersister.php b/src/Persisters/Collection/ManyToManyPersister.php index 1f2c7629de2..e60dfeea9b8 100644 --- a/src/Persisters/Collection/ManyToManyPersister.php +++ b/src/Persisters/Collection/ManyToManyPersister.php @@ -19,6 +19,7 @@ use Doctrine\ORM\Utility\PersisterHelper; use function array_fill; +use function array_merge; use function array_pop; use function assert; use function count; @@ -247,14 +248,14 @@ public function loadCriteria(PersistentCollection $collection, Criteria $criteri if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); - } else { - if ($operator === Comparison::IN) { - $whereClauses[] = sprintf('te.%s IN (?)', $field); - } elseif ($operator === Comparison::NIN) { - $whereClauses[] = sprintf('te.%s NOT IN (?)', $field); - } else { - $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); + } elseif ($operator === Comparison::IN || $operator === Comparison::NIN) { + $whereClauses[] = sprintf('te.%s %s (%s)', $field, $operator === Comparison::IN ? 'IN' : 'NOT IN', implode(', ', array_fill(0, count($value), '?'))); + foreach ($value as $item) { + $params = array_merge($params, PersisterHelper::convertToParameterValue($item, $this->em)); + $paramTypes = array_merge($paramTypes, PersisterHelper::inferParameterTypes($name, $item, $targetClass, $this->em)); } + } else { + $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); $params = [...$params, ...PersisterHelper::convertToParameterValue($value, $this->em)]; $paramTypes = [...$paramTypes, ...PersisterHelper::inferParameterTypes($name, $value, $targetClass, $this->em)]; diff --git a/src/Persisters/Collection/OneToManyPersister.php b/src/Persisters/Collection/OneToManyPersister.php index d96be8deea8..de7df4f3031 100644 --- a/src/Persisters/Collection/OneToManyPersister.php +++ b/src/Persisters/Collection/OneToManyPersister.php @@ -89,7 +89,7 @@ public function count(PersistentCollection $collection): int // only works with single id identifier entities. Will throw an // exception in Entity Persisters if that is not the case for the // 'mappedBy' field. - $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); return $persister->count($criteria); } @@ -118,7 +118,7 @@ public function containsKey(PersistentCollection $collection, mixed $key): bool // only works with single id identifier entities. Will throw an // exception in Entity Persisters if that is not the case for the // 'mappedBy' field. - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->andWhere(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); $criteria->andWhere(Criteria::expr()->eq($mapping->indexBy(), $key)); @@ -138,7 +138,7 @@ public function contains(PersistentCollection $collection, object $element): boo // only works with single id identifier entities. Will throw an // exception in Entity Persisters if that is not the case for the // 'mappedBy' field. - $criteria = new Criteria(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq($mapping->mappedBy, $collection->getOwner())); return $persister->exists($element, $criteria); } diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index c84770170c9..2fe561aea74 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -41,9 +41,11 @@ use LengthException; use function array_combine; +use function array_diff_key; +use function array_fill; +use function array_flip; use function array_keys; use function array_map; -use function array_search; use function array_unique; use function array_values; use function assert; @@ -915,6 +917,29 @@ public function expandCriteriaParameters(Criteria $criteria): array continue; } + if ($operator === Comparison::IN || $operator === Comparison::NIN) { + if (! is_array($value)) { + $value = [$value]; + } + + foreach ($value as $item) { + if ($item === null) { + /* + * Compare this to how \Doctrine\ORM\Persisters\Entity\BasicEntityPersister::getSelectConditionStatementSQL + * creates the "[NOT] IN (...)" expression - for NULL values, it does _not_ insert a placeholder in the + * SQL and instead adds an extra ... OR ... IS NULL condition. So we need to skip NULL values here as + * well to create a parameters list that matches the SQL. + */ + continue; + } + + $sqlParams = [...$sqlParams, ...PersisterHelper::convertToParameterValue($item, $this->em)]; + $sqlTypes = [...$sqlTypes, ...PersisterHelper::inferParameterTypes($field, $item, $this->class, $this->em)]; + } + + continue; + } + $sqlParams = [...$sqlParams, ...PersisterHelper::convertToParameterValue($value, $this->em)]; $sqlTypes = [...$sqlTypes, ...PersisterHelper::inferParameterTypes($field, $value, $this->class, $this->em)]; } @@ -1604,6 +1629,8 @@ public function getSelectConditionStatementSQL( AssociationMapping|null $assoc = null, string|null $comparison = null, ): string { + $comparison ??= (is_array($value) ? Comparison::IN : Comparison::EQ); + $selectedColumns = []; $columns = $this->getSelectConditionStatementColumnSQL($field, $assoc); @@ -1623,46 +1650,45 @@ public function getSelectConditionStatementSQL( $placeholder = $type->convertToDatabaseValueSQL($placeholder, $this->platform); } - if ($comparison !== null) { - // special case null value handling - if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) { - $selectedColumns[] = $column . ' IS NULL'; - - continue; - } - - if ($comparison === Comparison::NEQ && $value === null) { - $selectedColumns[] = $column . ' IS NOT NULL'; - - continue; - } - - $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); + // special case null value handling + if (($comparison === Comparison::EQ || $comparison === Comparison::IS) && $value === null) { + $selectedColumns[] = $column . ' IS NULL'; continue; } - if (is_array($value)) { - $in = sprintf('%s IN (%s)', $column, $placeholder); + if ($comparison === Comparison::NEQ && $value === null) { + $selectedColumns[] = $column . ' IS NOT NULL'; - if (array_search(null, $value, true) !== false) { - $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); + continue; + } - continue; + if ($comparison === Comparison::IN || $comparison === Comparison::NIN) { + if (! is_array($value)) { + $value = [$value]; } - $selectedColumns[] = $in; + $nullKeys = array_keys($value, null, true); + $nonNullValues = array_diff_key($value, array_flip($nullKeys)); - continue; - } + $placeholders = implode(', ', array_fill(0, count($nonNullValues), $placeholder)); - if ($value === null) { - $selectedColumns[] = sprintf('%s IS NULL', $column); + $in = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholders); + + if ($nullKeys) { + if ($nonNullValues) { + $selectedColumns[] = sprintf('(%s OR %s IS NULL)', $in, $column); + } else { + $selectedColumns[] = $column . ' IS NULL'; + } + } else { + $selectedColumns[] = $in; + } continue; } - $selectedColumns[] = sprintf('%s = %s', $column, $placeholder); + $selectedColumns[] = $column . ' ' . sprintf(self::$comparisonMap[$comparison], $placeholder); } return implode(' AND ', $selectedColumns); @@ -1854,6 +1880,16 @@ public function expandParameters(array $criteria): array continue; // skip null values. } + if (is_array($value)) { + $nonNullValues = array_diff_key($value, array_flip(array_keys($value, null, true))); + foreach ($nonNullValues as $item) { + $types = [...$types, ...PersisterHelper::inferParameterTypes($field, $item, $this->class, $this->em)]; + $params = [...$params, ...PersisterHelper::convertToParameterValue($item, $this->em)]; + } + + continue; + } + $types = [...$types, ...PersisterHelper::inferParameterTypes($field, $value, $this->class, $this->em)]; $params = [...$params, ...PersisterHelper::convertToParameterValue($value, $this->em)]; } diff --git a/src/Persisters/Entity/EntityPersister.php b/src/Persisters/Entity/EntityPersister.php index 1c4da2f4c04..195b1ad5fe6 100644 --- a/src/Persisters/Entity/EntityPersister.php +++ b/src/Persisters/Entity/EntityPersister.php @@ -69,7 +69,7 @@ public function getCountSQL(array|Criteria $criteria = []): string; /** * Expands the parameters from the given criteria and use the correct binding types if found. * - * @param string[] $criteria + * @param array $criteria * * @phpstan-return array{list, list} */ diff --git a/src/Query/AST/EntityAsDtoArgumentExpression.php b/src/Query/AST/EntityAsDtoArgumentExpression.php index bcfa5e6dc3d..cc597884766 100644 --- a/src/Query/AST/EntityAsDtoArgumentExpression.php +++ b/src/Query/AST/EntityAsDtoArgumentExpression.php @@ -16,7 +16,11 @@ class EntityAsDtoArgumentExpression extends Node public function __construct( public mixed $expression, public string|null $identificationVariable, + public string|null $aliasVariable = null, ) { + if (! $aliasVariable) { + $this->aliasVariable = $expression; + } } public function dispatch(SqlWalker $walker): string diff --git a/src/Query/Parser.php b/src/Query/Parser.php index daf282c8b70..a58faf3cefb 100644 --- a/src/Query/Parser.php +++ b/src/Query/Parser.php @@ -1147,7 +1147,7 @@ public function EntityAsDtoArgumentExpression(): AST\EntityAsDtoArgumentExpressi ]; } - return new AST\EntityAsDtoArgumentExpression($expression, $identVariable); + return new AST\EntityAsDtoArgumentExpression($expression, $identVariable, $aliasResultVariable); } /** @@ -1895,6 +1895,7 @@ public function NewObjectArg(string|null &$fieldAlias = null): mixed $expression = $this->NewObjectExpression(); } elseif ($token->type === TokenType::T_IDENTIFIER && $peek->type !== TokenType::T_DOT && $peek->type !== TokenType::T_OPEN_PARENTHESIS) { $expression = $this->EntityAsDtoArgumentExpression(); + $fieldAlias = $expression->aliasVariable; } else { $expression = $this->ScalarExpression(); } diff --git a/src/Tools/Pagination/Paginator.php b/src/Tools/Pagination/Paginator.php index d74ab6c63f8..7601f001f11 100644 --- a/src/Tools/Pagination/Paginator.php +++ b/src/Tools/Pagination/Paginator.php @@ -202,7 +202,22 @@ private function appendTreeWalker(Query $query, string $walkerClass): void */ private function getCountQuery(): Query { - $countQuery = $this->cloneQuery($this->query); + /* + As opposed to using self::cloneQuery, the following code does not transfer + a potentially existing result set mapping (either set directly by the user, + or taken from the parser result from a previous invocation of Query::parse()) + to the new query object. This is fine, since we are going to completely change the + select clause, so a previously existing result set mapping (RSM) is probably wrong anyway. + In the case of using output walkers, we are even creating a new RSM down below. + In the case of using a tree walker, we want to have a new RSM created by the parser. + */ + $countQuery = new Query($this->query->getEntityManager()); + $countQuery->setDQL($this->query->getDQL()); + $countQuery->setParameters(clone $this->query->getParameters()); + $countQuery->setCacheable(false); + foreach ($this->query->getHints() as $name => $value) { + $countQuery->setHint($name, $value); + } if (! $countQuery->hasHint(CountWalker::HINT_DISTINCT)) { $countQuery->setHint(CountWalker::HINT_DISTINCT, true); diff --git a/src/Tools/ResolveTargetEntityListener.php b/src/Tools/ResolveTargetEntityListener.php index 9760abf00cf..ceec0698b00 100644 --- a/src/Tools/ResolveTargetEntityListener.php +++ b/src/Tools/ResolveTargetEntityListener.php @@ -75,12 +75,6 @@ public function loadClassMetadata(LoadClassMetadataEventArgs $args): void } } - foreach ($this->resolveTargetEntities as $interface => $data) { - if ($data['targetEntity'] === $cm->getName()) { - $args->getEntityManager()->getMetadataFactory()->setMetadataFor($interface, $cm); - } - } - foreach ($cm->discriminatorMap as $value => $class) { if (isset($this->resolveTargetEntities[$class])) { $cm->addDiscriminatorMapClass($value, $this->resolveTargetEntities[$class]['targetEntity']); diff --git a/tests/Tests/Models/CMS/CmsUserDTONamedArgs.php b/tests/Tests/Models/CMS/CmsUserDTONamedArgs.php index b9a7652911a..fd844e5351f 100644 --- a/tests/Tests/Models/CMS/CmsUserDTONamedArgs.php +++ b/tests/Tests/Models/CMS/CmsUserDTONamedArgs.php @@ -13,6 +13,8 @@ public function __construct( public int|null $phonenumbers = null, public CmsAddressDTO|null $addressDto = null, public CmsAddressDTONamedArgs|null $addressDtoNamedArgs = null, + public CmsDumbDTO|null $dumb = null, + public CmsAddress|null $addressEntity = null, ) { } } diff --git a/tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php b/tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php index 6420ca8b033..395738db2ba 100644 --- a/tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php +++ b/tests/Tests/Models/CMS/CmsUserDTOVariadicArg.php @@ -10,12 +10,14 @@ class CmsUserDTOVariadicArg public string|null $email = null; public string|null $address = null; public int|null $phonenumbers = null; + public array $otherProperties = []; public function __construct(...$args) { - $this->name = $args['name'] ?? null; - $this->email = $args['email'] ?? null; - $this->phonenumbers = $args['phonenumbers'] ?? null; - $this->address = $args['address'] ?? null; + $this->name = $args['name'] ?? null; + $this->email = $args['email'] ?? null; + $this->phonenumbers = $args['phonenumbers'] ?? null; + $this->address = $args['address'] ?? null; + $this->otherProperties = $args; } } diff --git a/tests/Tests/Models/ValueConversionType/InversedManyToManyEntity.php b/tests/Tests/Models/ValueConversionType/InversedManyToManyEntity.php index 50e575e3a56..721afaeb537 100644 --- a/tests/Tests/Models/ValueConversionType/InversedManyToManyEntity.php +++ b/tests/Tests/Models/ValueConversionType/InversedManyToManyEntity.php @@ -21,8 +21,12 @@ class InversedManyToManyEntity #[Id] public $id1; + /** @var string */ + #[Column(type: 'rot13', length: 255, nullable: true)] + public $field = null; + /** @phpstan-var Collection */ - #[ManyToMany(targetEntity: 'OwningManyToManyEntity', mappedBy: 'associatedEntities')] + #[ManyToMany(targetEntity: OwningManyToManyEntity::class, mappedBy: 'associatedEntities')] public $associatedEntities; public function __construct() diff --git a/tests/Tests/Models/ValueConversionType/InversedOneToManyEntity.php b/tests/Tests/Models/ValueConversionType/InversedOneToManyEntity.php index 9ffbf32b677..905b9f7a79a 100644 --- a/tests/Tests/Models/ValueConversionType/InversedOneToManyEntity.php +++ b/tests/Tests/Models/ValueConversionType/InversedOneToManyEntity.php @@ -26,7 +26,7 @@ class InversedOneToManyEntity public $associatedEntities; /** @var string */ - #[Column(type: 'string', name: 'some_property', length: 255)] + #[Column(type: 'string', name: 'some_property', length: 255, nullable: true)] public $someProperty; public function __construct() diff --git a/tests/Tests/Models/ValueConversionType/OwningManyToManyEntity.php b/tests/Tests/Models/ValueConversionType/OwningManyToManyEntity.php index c2b9534a0b1..80eea556f0c 100644 --- a/tests/Tests/Models/ValueConversionType/OwningManyToManyEntity.php +++ b/tests/Tests/Models/ValueConversionType/OwningManyToManyEntity.php @@ -24,6 +24,10 @@ class OwningManyToManyEntity #[Id] public $id2; + /** @var string */ + #[Column(type: 'rot13', length: 255, nullable: true)] + public $field = null; + /** @var Collection */ #[JoinTable(name: 'vct_xref_manytomany')] #[JoinColumn(name: 'owning_id', referencedColumnName: 'id2')] diff --git a/tests/Tests/Models/ValueConversionType/OwningManyToOneEntity.php b/tests/Tests/Models/ValueConversionType/OwningManyToOneEntity.php index 7896465571d..3a237a3aca0 100644 --- a/tests/Tests/Models/ValueConversionType/OwningManyToOneEntity.php +++ b/tests/Tests/Models/ValueConversionType/OwningManyToOneEntity.php @@ -20,8 +20,12 @@ class OwningManyToOneEntity #[Id] public $id2; + /** @var string */ + #[Column(type: 'rot13', length: 255, nullable: true)] + public $field = null; + /** @var InversedOneToManyEntity */ - #[ManyToOne(targetEntity: 'InversedOneToManyEntity', inversedBy: 'associatedEntities')] + #[ManyToOne(targetEntity: InversedOneToManyEntity::class, inversedBy: 'associatedEntities')] #[JoinColumn(name: 'associated_id', referencedColumnName: 'id1')] public $associatedEntity; } diff --git a/tests/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php b/tests/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php index c590f3a04e1..97723366dd2 100644 --- a/tests/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php +++ b/tests/Tests/ORM/Cache/Persister/Entity/EntityPersisterTestCase.php @@ -141,7 +141,7 @@ public function testInvokeExpandParameters(): void public function testInvokeExpandCriteriaParameters(): void { $persister = $this->createPersisterDefault(); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $this->entityPersister->expects(self::once()) ->method('expandCriteriaParameters') @@ -320,7 +320,7 @@ public function testInvokeLoadCriteria(): void $rsm = new ResultSetMappingBuilder($this->em); $persister = $this->createPersisterDefault(); $entity = new Country('Foo'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $this->em->getUnitOfWork()->registerManaged($entity, ['id' => 1], ['id' => 1, 'name' => 'Foo']); $rsm->addEntityResult(Country::class, 'c'); diff --git a/tests/Tests/ORM/Functional/ClassTableInheritanceTest.php b/tests/Tests/ORM/Functional/ClassTableInheritanceTest.php index 94f817a2353..298df26091f 100644 --- a/tests/Tests/ORM/Functional/ClassTableInheritanceTest.php +++ b/tests/Tests/ORM/Functional/ClassTableInheritanceTest.php @@ -494,13 +494,13 @@ public function testMatching(): void $this->_em->flush(); $repository = $this->_em->getRepository(CompanyEmployee::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('department', 'IT'), )); self::assertCount(1, $users); $repository = $this->_em->getRepository(CompanyManager::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('department', 'IT'), )); self::assertCount(1, $users); diff --git a/tests/Tests/ORM/Functional/EntityRepositoryCriteriaTest.php b/tests/Tests/ORM/Functional/EntityRepositoryCriteriaTest.php index 51e1dc3b557..58903382f95 100644 --- a/tests/Tests/ORM/Functional/EntityRepositoryCriteriaTest.php +++ b/tests/Tests/ORM/Functional/EntityRepositoryCriteriaTest.php @@ -66,7 +66,7 @@ public function testLteDateComparison(): void $this->loadFixture(); $repository = $this->_em->getRepository(DateTimeModel::class); - $dates = $repository->matching(new Criteria( + $dates = $repository->matching(Criteria::create(true)->where( Criteria::expr()->lte('datetime', new DateTime('today')), )); @@ -98,7 +98,7 @@ public function testIsNullComparison(): void $this->loadNullFieldFixtures(); $repository = $this->_em->getRepository(DateTimeModel::class); - $dates = $repository->matching(new Criteria( + $dates = $repository->matching(Criteria::create(true)->where( Criteria::expr()->isNull('time'), )); @@ -110,7 +110,7 @@ public function testEqNullComparison(): void $this->loadNullFieldFixtures(); $repository = $this->_em->getRepository(DateTimeModel::class); - $dates = $repository->matching(new Criteria( + $dates = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('time', null), )); @@ -122,7 +122,7 @@ public function testNotEqNullComparison(): void $this->loadNullFieldFixtures(); $repository = $this->_em->getRepository(DateTimeModel::class); - $dates = $repository->matching(new Criteria( + $dates = $repository->matching(Criteria::create(true)->where( Criteria::expr()->neq('time', null), )); @@ -134,14 +134,14 @@ public function testCanCountWithoutLoadingCollection(): void $this->loadFixture(); $repository = $this->_em->getRepository(DateTimeModel::class); - $dates = $repository->matching(new Criteria()); + $dates = $repository->matching(Criteria::create(true)); self::assertFalse($dates->isInitialized()); self::assertCount(3, $dates); self::assertFalse($dates->isInitialized()); // Test it can work even with a constraint - $dates = $repository->matching(new Criteria( + $dates = $repository->matching(Criteria::create(true)->where( Criteria::expr()->lte('datetime', new DateTime('today')), )); @@ -169,7 +169,7 @@ public function testCanContainsWithoutLoadingCollection(): void $this->_em->clear(); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->andWhere($criteria->expr()->contains('content', 'Criteria')); $user = $this->_em->find(User::class, $user->id); diff --git a/tests/Tests/ORM/Functional/EntityRepositoryTest.php b/tests/Tests/ORM/Functional/EntityRepositoryTest.php index 4ea391669f5..398265ab96b 100644 --- a/tests/Tests/ORM/Functional/EntityRepositoryTest.php +++ b/tests/Tests/ORM/Functional/EntityRepositoryTest.php @@ -661,7 +661,7 @@ public function testMatchingEmptyCriteria(): void $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria()); + $users = $repository->matching(Criteria::create(true)); self::assertCount(4, $users); } @@ -672,7 +672,7 @@ public function testMatchingCriteriaEqComparison(): void $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('username', 'beberlei'), )); @@ -685,7 +685,7 @@ public function testMatchingCriteriaNeqComparison(): void $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->neq('username', 'beberlei'), )); @@ -698,7 +698,7 @@ public function testMatchingCriteriaInComparison(): void $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->in('username', ['beberlei', 'gblanco']), )); @@ -711,7 +711,7 @@ public function testMatchingCriteriaNotInComparison(): void $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->notIn('username', ['beberlei', 'gblanco', 'asm89']), )); @@ -724,7 +724,7 @@ public function testMatchingCriteriaLtComparison(): void $firstUserId = $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->lt('id', $firstUserId + 1), )); @@ -737,7 +737,7 @@ public function testMatchingCriteriaLeComparison(): void $firstUserId = $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->lte('id', $firstUserId + 1), )); @@ -750,7 +750,7 @@ public function testMatchingCriteriaGtComparison(): void $firstUserId = $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->gt('id', $firstUserId), )); @@ -763,7 +763,7 @@ public function testMatchingCriteriaGteComparison(): void $firstUserId = $this->loadFixture(); $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria( + $users = $repository->matching(Criteria::create(true)->where( Criteria::expr()->gte('id', $firstUserId), )); @@ -777,7 +777,7 @@ public function testMatchingCriteriaAssocationByObjectInMemory(): void $user = $this->_em->find(CmsUser::class, $userId); - $criteria = new Criteria( + $criteria = Criteria::create(true)->where( Criteria::expr()->eq('user', $user), ); @@ -798,7 +798,7 @@ public function testMatchingCriteriaAssocationInWithArray(): void $user = $this->_em->find(CmsUser::class, $userId); - $criteria = new Criteria( + $criteria = Criteria::create(true)->where( Criteria::expr()->in('user', [$user]), ); @@ -818,13 +818,13 @@ public function testMatchingCriteriaContainsComparison(): void $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria(Criteria::expr()->contains('name', 'Foobar'))); + $users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('name', 'Foobar'))); self::assertCount(0, $users); - $users = $repository->matching(new Criteria(Criteria::expr()->contains('name', 'Rom'))); + $users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('name', 'Rom'))); self::assertCount(1, $users); - $users = $repository->matching(new Criteria(Criteria::expr()->contains('status', 'dev'))); + $users = $repository->matching(Criteria::create(true)->where(Criteria::expr()->contains('status', 'dev'))); self::assertCount(2, $users); } @@ -834,13 +834,19 @@ public function testMatchingCriteriaStartsWithComparison(): void $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria(Criteria::expr()->startsWith('name', 'Foo'))); + $users = $repository->matching(Criteria::create(true)->where( + Criteria::expr()->startsWith('name', 'Foo'), + )); self::assertCount(0, $users); - $users = $repository->matching(new Criteria(Criteria::expr()->startsWith('name', 'R'))); + $users = $repository->matching(Criteria::create(true)->where( + Criteria::expr()->startsWith('name', 'R'), + )); self::assertCount(1, $users); - $users = $repository->matching(new Criteria(Criteria::expr()->startsWith('status', 'de'))); + $users = $repository->matching(Criteria::create(true)->where( + Criteria::expr()->startsWith('status', 'de'), + )); self::assertCount(2, $users); } @@ -850,13 +856,19 @@ public function testMatchingCriteriaEndsWithComparison(): void $repository = $this->_em->getRepository(CmsUser::class); - $users = $repository->matching(new Criteria(Criteria::expr()->endsWith('name', 'foo'))); + $users = $repository->matching(Criteria::create(true)->where( + Criteria::expr()->endsWith('name', 'foo'), + )); self::assertCount(0, $users); - $users = $repository->matching(new Criteria(Criteria::expr()->endsWith('name', 'oman'))); + $users = $repository->matching(Criteria::create(true)->where( + Criteria::expr()->endsWith('name', 'oman'), + )); self::assertCount(1, $users); - $users = $repository->matching(new Criteria(Criteria::expr()->endsWith('status', 'ev'))); + $users = $repository->matching(Criteria::create(true)->where( + Criteria::expr()->endsWith('status', 'ev'), + )); self::assertCount(2, $users); } @@ -866,8 +878,8 @@ public function testMatchingCriteriaNullAssocComparison(): void $fixtures = $this->loadFixtureUserEmail(); $user = $this->_em->find(CmsUser::class, $fixtures[0]->id); $repository = $this->_em->getRepository(CmsUser::class); - $criteriaIsNull = Criteria::create()->where(Criteria::expr()->isNull('email')); - $criteriaEqNull = Criteria::create()->where(Criteria::expr()->eq('email', null)); + $criteriaIsNull = Criteria::create(true)->where(Criteria::expr()->isNull('email')); + $criteriaEqNull = Criteria::create(true)->where(Criteria::expr()->eq('email', null)); $user->setEmail(null); $this->_em->persist($user); @@ -924,7 +936,7 @@ public function testMatchingInjectionPrevented(): void $this->expectExceptionMessage('Unrecognized field: '); $repository = $this->_em->getRepository(CmsUser::class); - $result = $repository->matching(new Criteria( + $result = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('username = ?; DELETE FROM cms_users; SELECT 1 WHERE 1', 'beberlei'), )); diff --git a/tests/Tests/ORM/Functional/EnumTest.php b/tests/Tests/ORM/Functional/EnumTest.php index 3f81912d8f9..04b1fc883f1 100644 --- a/tests/Tests/ORM/Functional/EnumTest.php +++ b/tests/Tests/ORM/Functional/EnumTest.php @@ -558,7 +558,7 @@ public function testEnumCollectionMatchingOnOneToMany(Comparison $comparison): v $library = $this->_em->find(Library::class, $library->id); self::assertFalse($library->books->isInitialized(), 'Pre-condition: lazy collection'); - $result = $library->books->matching(Criteria::create()->where($comparison)); + $result = $library->books->matching(Criteria::create(true)->where($comparison)); self::assertCount(1, $result); self::assertSame($nonfictionBook->id, $result[0]->id); @@ -587,7 +587,7 @@ public function testEnumCollectionMatchingOnManyToMany(Comparison $comparison): $category = $this->_em->find(BookCategory::class, $category->id); self::assertFalse($category->books->isInitialized(), 'Pre-condition: lazy collection'); - $result = $category->books->matching(Criteria::create()->where($comparison)); + $result = $category->books->matching(Criteria::create(true)->where($comparison)); self::assertCount(1, $result); self::assertSame($nonfictionBook->id, $result[0]->id); diff --git a/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php b/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php index fdb97e38f0f..4826abe8537 100644 --- a/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php +++ b/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php @@ -437,7 +437,7 @@ public function testManyToManyOrderByIsNotIgnored(): void $user = $this->_em->find($user::class, $user->id); - $criteria = Criteria::create() + $criteria = Criteria::create(true) ->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]); self::assertEquals( @@ -477,7 +477,7 @@ public function testManyToManyOrderByHonorsFieldNameColumnNameAliases(): void $user = $this->_em->find($user::class, $user->id); - $criteria = Criteria::create() + $criteria = Criteria::create(true) ->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]); self::assertEquals( @@ -500,7 +500,7 @@ public function testMatchingWithLimit(): void $groups = $user->groups; self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); - $criteria = Criteria::create()->setMaxResults(1); + $criteria = Criteria::create(true)->setMaxResults(1); $result = $groups->matching($criteria); self::assertCount(1, $result); @@ -518,7 +518,7 @@ public function testMatchingWithOffset(): void $groups = $user->groups; self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); - $criteria = Criteria::create()->setFirstResult(1); + $criteria = Criteria::create(true)->setFirstResult(1); $result = $groups->matching($criteria); self::assertCount(1, $result); @@ -539,7 +539,7 @@ public function testMatchingWithLimitAndOffset(): void $groups = $user->groups; self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); - $criteria = Criteria::create()->setFirstResult(1)->setMaxResults(3); + $criteria = Criteria::create(true)->setFirstResult(1)->setMaxResults(3); $result = $groups->matching($criteria); self::assertCount(3, $result); @@ -563,7 +563,7 @@ public function testMatching(): void $groups = $user->groups; self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); - $criteria = Criteria::create()->where(Criteria::expr()->eq('name', (string) 'Developers_0')); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq('name', (string) 'Developers_0')); $result = $groups->matching($criteria); self::assertCount(1, $result); @@ -584,7 +584,7 @@ public function testMatchingWithInCondition(): void $groups = $user->groups; self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); - $criteria = Criteria::create()->where(Criteria::expr()->in('name', ['Developers_1'])); + $criteria = Criteria::create(true)->where(Criteria::expr()->in('name', ['Developers_1'])); $result = $groups->matching($criteria); self::assertCount(1, $result); @@ -603,7 +603,7 @@ public function testMatchingWithNotInCondition(): void $groups = $user->groups; self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); - $criteria = Criteria::create()->where(Criteria::expr()->notIn('name', ['Developers_0'])); + $criteria = Criteria::create(true)->where(Criteria::expr()->notIn('name', ['Developers_0'])); $result = $groups->matching($criteria); self::assertCount(1, $result); diff --git a/tests/Tests/ORM/Functional/NewOperatorTest.php b/tests/Tests/ORM/Functional/NewOperatorTest.php index d133dc7e06d..4339f669c93 100644 --- a/tests/Tests/ORM/Functional/NewOperatorTest.php +++ b/tests/Tests/ORM/Functional/NewOperatorTest.php @@ -1394,6 +1394,168 @@ public function testOnlyObjectInDto(): void self::assertSame($this->fixtures[2]->email->email, $result[2]->val2->val2); } + public function testOnlyObjectInNamedDto(): void + { + $dql = ' + SELECT + new named CmsUserDTOVariadicArg( + a, + new CmsDumbDTO( + u.name, + e.email + ) as dumb + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + LEFT JOIN + u.email e + LEFT JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]); + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]); + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]); + + self::assertInstanceOf(CmsAddress::class, $result[0]->otherProperties['a']); + self::assertInstanceOf(CmsAddress::class, $result[1]->otherProperties['a']); + self::assertInstanceOf(CmsAddress::class, $result[2]->otherProperties['a']); + + self::assertSame($this->fixtures[0]->address->city, $result[0]->otherProperties['a']->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]->otherProperties['a']->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]->otherProperties['a']->city); + + self::assertSame($this->fixtures[0]->address->country, $result[0]->otherProperties['a']->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]->otherProperties['a']->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]->otherProperties['a']->country); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]->otherProperties['dumb']); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]->otherProperties['dumb']); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]->otherProperties['dumb']); + + self::assertSame($this->fixtures[0]->name, $result[0]->otherProperties['dumb']->val1); + self::assertSame($this->fixtures[1]->name, $result[1]->otherProperties['dumb']->val1); + self::assertSame($this->fixtures[2]->name, $result[2]->otherProperties['dumb']->val1); + + self::assertSame($this->fixtures[0]->email->email, $result[0]->otherProperties['dumb']->val2); + self::assertSame($this->fixtures[1]->email->email, $result[1]->otherProperties['dumb']->val2); + self::assertSame($this->fixtures[2]->email->email, $result[2]->otherProperties['dumb']->val2); + } + + public function testOnlyObjectInNamedDtoWithAlias(): void + { + $dql = ' + SELECT + new named CmsUserDTOVariadicArg( + a as addr, + new CmsDumbDTO( + u.name, + e.email + ) as dumb + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + LEFT JOIN + u.email e + LEFT JOIN + u.address a + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[0]); + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[1]); + self::assertInstanceOf(CmsUserDTOVariadicArg::class, $result[2]); + + self::assertInstanceOf(CmsAddress::class, $result[0]->otherProperties['addr']); + self::assertInstanceOf(CmsAddress::class, $result[1]->otherProperties['addr']); + self::assertInstanceOf(CmsAddress::class, $result[2]->otherProperties['addr']); + + self::assertSame($this->fixtures[0]->address->city, $result[0]->otherProperties['addr']->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]->otherProperties['addr']->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]->otherProperties['addr']->city); + + self::assertSame($this->fixtures[0]->address->country, $result[0]->otherProperties['addr']->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]->otherProperties['addr']->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]->otherProperties['addr']->country); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]->otherProperties['dumb']); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]->otherProperties['dumb']); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]->otherProperties['dumb']); + + self::assertSame($this->fixtures[0]->name, $result[0]->otherProperties['dumb']->val1); + self::assertSame($this->fixtures[1]->name, $result[1]->otherProperties['dumb']->val1); + self::assertSame($this->fixtures[2]->name, $result[2]->otherProperties['dumb']->val1); + + self::assertSame($this->fixtures[0]->email->email, $result[0]->otherProperties['dumb']->val2); + self::assertSame($this->fixtures[1]->email->email, $result[1]->otherProperties['dumb']->val2); + self::assertSame($this->fixtures[2]->email->email, $result[2]->otherProperties['dumb']->val2); + } + + public function testOnlyObjectInNamedDtoWithSameNameAsTheProperties(): void + { + $dql = ' + SELECT + new named CmsUserDTONamedArgs( + addressEntity, + new CmsDumbDTO( + u.name, + e.email + ) as dumb + ) + FROM + Doctrine\Tests\Models\CMS\CmsUser u + LEFT JOIN + u.email e + LEFT JOIN + u.address addressEntity + ORDER BY + u.name'; + + $query = $this->getEntityManager()->createQuery($dql); + $result = $query->getResult(); + + self::assertCount(3, $result); + + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[0]); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[1]); + self::assertInstanceOf(CmsUserDTONamedArgs::class, $result[2]); + + self::assertInstanceOf(CmsAddress::class, $result[0]->addressEntity); + self::assertInstanceOf(CmsAddress::class, $result[1]->addressEntity); + self::assertInstanceOf(CmsAddress::class, $result[2]->addressEntity); + + self::assertSame($this->fixtures[0]->address->city, $result[0]->addressEntity->city); + self::assertSame($this->fixtures[1]->address->city, $result[1]->addressEntity->city); + self::assertSame($this->fixtures[2]->address->city, $result[2]->addressEntity->city); + + self::assertSame($this->fixtures[0]->address->country, $result[0]->addressEntity->country); + self::assertSame($this->fixtures[1]->address->country, $result[1]->addressEntity->country); + self::assertSame($this->fixtures[2]->address->country, $result[2]->addressEntity->country); + + self::assertInstanceOf(CmsDumbDTO::class, $result[0]->dumb); + self::assertInstanceOf(CmsDumbDTO::class, $result[1]->dumb); + self::assertInstanceOf(CmsDumbDTO::class, $result[2]->dumb); + + self::assertSame($this->fixtures[0]->name, $result[0]->dumb->val1); + self::assertSame($this->fixtures[1]->name, $result[1]->dumb->val1); + self::assertSame($this->fixtures[2]->name, $result[2]->dumb->val1); + + self::assertSame($this->fixtures[0]->email->email, $result[0]->dumb->val2); + self::assertSame($this->fixtures[1]->email->email, $result[1]->dumb->val2); + self::assertSame($this->fixtures[2]->email->email, $result[2]->dumb->val2); + } + public function testNamedArguments(): void { $dql = <<<'SQL' diff --git a/tests/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php b/tests/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php index ebd23a43635..047721d4aed 100644 --- a/tests/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php +++ b/tests/Tests/ORM/Functional/OneToManyBidirectionalAssociationTest.php @@ -164,14 +164,14 @@ public function testMatching(): void $product = $this->_em->find(ECommerceProduct::class, $this->product->getId()); $features = $product->getFeatures(); - $results = $features->matching(new Criteria( + $results = $features->matching(Criteria::create(true)->where( Criteria::expr()->eq('description', 'Model writing tutorial'), )); self::assertInstanceOf(Collection::class, $results); self::assertCount(1, $results); - $results = $features->matching(new Criteria()); + $results = $features->matching(Criteria::create(true)); self::assertInstanceOf(Collection::class, $results); self::assertCount(2, $results); @@ -190,7 +190,7 @@ public function testMatchingOnDirtyCollection(): void $features = $product->getFeatures(); $features->add($thirdFeature); - $results = $features->matching(new Criteria( + $results = $features->matching(Criteria::create(true)->where( Criteria::expr()->eq('description', 'Model writing tutorial'), )); @@ -208,14 +208,14 @@ public function testMatchingBis(): void $thirdFeature->setDescription('Third feature'); $product->addFeature($thirdFeature); - $results = $features->matching(new Criteria( + $results = $features->matching(Criteria::create(true)->where( Criteria::expr()->eq('description', 'Third feature'), )); self::assertInstanceOf(Collection::class, $results); self::assertCount(1, $results); - $results = $features->matching(new Criteria()); + $results = $features->matching(Criteria::create(true)); self::assertInstanceOf(Collection::class, $results); self::assertCount(3, $results); diff --git a/tests/Tests/ORM/Functional/PersistentCollectionCriteriaTest.php b/tests/Tests/ORM/Functional/PersistentCollectionCriteriaTest.php index fb4d03bde4b..d240c45d3fa 100644 --- a/tests/Tests/ORM/Functional/PersistentCollectionCriteriaTest.php +++ b/tests/Tests/ORM/Functional/PersistentCollectionCriteriaTest.php @@ -80,7 +80,7 @@ public function testCanCountWithoutLoadingPersistentCollection(): void $repository = $this->_em->getRepository(User::class); $user = $repository->findOneBy(['name' => 'ngal']); - $tweets = $user->tweets->matching(new Criteria()); + $tweets = $user->tweets->matching(Criteria::create(true)); self::assertInstanceOf(LazyCriteriaCollection::class, $tweets); self::assertFalse($tweets->isInitialized()); @@ -88,7 +88,7 @@ public function testCanCountWithoutLoadingPersistentCollection(): void self::assertFalse($tweets->isInitialized()); // Make sure it works with constraints - $tweets = $user->tweets->matching(new Criteria( + $tweets = $user->tweets->matching(Criteria::create(true)->where( Criteria::expr()->eq('content', 'Foo'), )); @@ -117,7 +117,7 @@ public function testCanHandleComplexTypesOnAssociation(): void $parent = $this->_em->find(OwningManyToManyExtraLazyEntity::class, $parent->id2); - $criteria = Criteria::create()->where(Criteria::expr()->eq('id1', 'Bob')); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq('id1', 'Bob')); $result = $parent->associatedEntities->matching($criteria); diff --git a/tests/Tests/ORM/Functional/PersistentCollectionTest.php b/tests/Tests/ORM/Functional/PersistentCollectionTest.php index 5191377524b..b4d8ec14278 100644 --- a/tests/Tests/ORM/Functional/PersistentCollectionTest.php +++ b/tests/Tests/ORM/Functional/PersistentCollectionTest.php @@ -89,7 +89,7 @@ public function testMatchingDoesNotModifyTheGivenCriteria(): void $this->_em->flush(); $this->_em->clear(); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $collectionHolder = $this->_em->find(PersistentCollectionHolder::class, $collectionHolder->getId()); $collectionHolder->getCollection()->matching($criteria); diff --git a/tests/Tests/ORM/Functional/SecondLevelCacheCriteriaTest.php b/tests/Tests/ORM/Functional/SecondLevelCacheCriteriaTest.php index 0fe074dfd9f..829d0b19d71 100644 --- a/tests/Tests/ORM/Functional/SecondLevelCacheCriteriaTest.php +++ b/tests/Tests/ORM/Functional/SecondLevelCacheCriteriaTest.php @@ -26,7 +26,7 @@ public function testMatchingPut(): void $repository = $this->_em->getRepository(Country::class); $this->getQueryLog()->reset()->enable(); $name = $this->countries[0]->getName(); - $result1 = $repository->matching(new Criteria( + $result1 = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $name), )); @@ -41,7 +41,7 @@ public function testMatchingPut(): void $this->_em->clear(); - $result2 = $repository->matching(new Criteria( + $result2 = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $name), )); @@ -65,7 +65,7 @@ public function testRepositoryMatching(): void $repository = $this->_em->getRepository(Country::class); $this->getQueryLog()->reset()->enable(); - $result1 = $repository->matching(new Criteria( + $result1 = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $this->countries[0]->getName()), )); @@ -79,7 +79,7 @@ public function testRepositoryMatching(): void $this->_em->clear(); - $result2 = $repository->matching(new Criteria( + $result2 = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $this->countries[0]->getName()), )); @@ -94,7 +94,7 @@ public function testRepositoryMatching(): void self::assertEquals($this->countries[0]->getId(), $result2[0]->getId()); self::assertEquals($this->countries[0]->getName(), $result2[0]->getName()); - $result3 = $repository->matching(new Criteria( + $result3 = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $this->countries[1]->getName()), )); @@ -109,7 +109,7 @@ public function testRepositoryMatching(): void self::assertEquals($this->countries[1]->getId(), $result3[0]->getId()); self::assertEquals($this->countries[1]->getName(), $result3[0]->getName()); - $result4 = $repository->matching(new Criteria( + $result4 = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $this->countries[1]->getName()), )); @@ -134,7 +134,7 @@ public function testCollectionMatching(): void $itemName = $this->states[0]->getCities()->get(0)->getName(); $this->getQueryLog()->reset()->enable(); $collection = $entity->getCities(); - $matching = $collection->matching(new Criteria( + $matching = $collection->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $itemName), )); @@ -147,7 +147,7 @@ public function testCollectionMatching(): void $entity = $this->_em->find(State::class, $this->states[0]->getId()); $this->getQueryLog()->reset()->enable(); $collection = $entity->getCities(); - $matching = $collection->matching(new Criteria( + $matching = $collection->matching(Criteria::create(true)->where( Criteria::expr()->eq('name', $itemName), )); diff --git a/tests/Tests/ORM/Functional/SingleTableInheritanceTest.php b/tests/Tests/ORM/Functional/SingleTableInheritanceTest.php index f3fa00560d2..1d5fdf9af46 100644 --- a/tests/Tests/ORM/Functional/SingleTableInheritanceTest.php +++ b/tests/Tests/ORM/Functional/SingleTableInheritanceTest.php @@ -354,13 +354,13 @@ public function testInheritanceMatching(): void $this->loadFullFixture(); $repository = $this->_em->getRepository(CompanyContract::class); - $contracts = $repository->matching(new Criteria( + $contracts = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('salesPerson', $this->salesPerson), )); self::assertCount(3, $contracts); $repository = $this->_em->getRepository(CompanyFixContract::class); - $contracts = $repository->matching(new Criteria( + $contracts = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('salesPerson', $this->salesPerson), )); self::assertCount(1, $contracts); @@ -376,7 +376,7 @@ public function testMatchingNonObjectOnAssocationThrowsException(): void $this->expectException(MatchingAssociationFieldRequiresObject::class); $this->expectExceptionMessage('annot match on Doctrine\Tests\Models\Company\CompanyContract::salesPerson with a non-object value.'); - $contracts = $repository->matching(new Criteria( + $contracts = $repository->matching(Criteria::create(true)->where( Criteria::expr()->eq('salesPerson', $this->salesPerson->getId()), )); diff --git a/tests/Tests/ORM/Functional/Ticket/DDC2106Test.php b/tests/Tests/ORM/Functional/Ticket/DDC2106Test.php index 10043467bb1..71a1310fe84 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC2106Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC2106Test.php @@ -39,7 +39,7 @@ public function testDetachedEntityAsId(): void $entityWithoutId = new DDC2106Entity(); $this->_em->persist($entityWithoutId); - $criteria = Criteria::create()->where(Criteria::expr()->eq('parent', $entityWithoutId)); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq('parent', $entityWithoutId)); self::assertCount(0, $entity->children->matching($criteria)); } diff --git a/tests/Tests/ORM/Functional/Ticket/DDC3719Test.php b/tests/Tests/ORM/Functional/Ticket/DDC3719Test.php index bdfd2078167..3e7a8a068e4 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC3719Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC3719Test.php @@ -44,7 +44,7 @@ public function testCriteriaOnNotOwningSide(): void $contracts = $manager->managedContracts; self::assertCount(2, $contracts); - $criteria = Criteria::create(); + $criteria = Criteria::create(true); $criteria->where(Criteria::expr()->eq('completed', true)); $completedContracts = $contracts->matching($criteria); diff --git a/tests/Tests/ORM/Functional/Ticket/GH12174Test.php b/tests/Tests/ORM/Functional/Ticket/GH12174Test.php new file mode 100644 index 00000000000..eb23856b31b --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH12174Test.php @@ -0,0 +1,80 @@ +expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/10431'); // make test fail on 2.x; in 3.x, it would even throw + + $resolveTargetEntity = new ResolveTargetEntityListener(); + + $resolveTargetEntity->addResolveTargetEntity(GH12174Smurf::class, GH12174BlueSmurf::class, []); + + $this->_em->getEventManager()->addEventSubscriber($resolveTargetEntity); + + $this->createSchemaForModels( + GH12174Smurf::class, + GH12174BlueSmurf::class, + GH12174PapaSmurf::class, + ); + } + + public function testMappedSuperclassNameCanBeUsedToResolveTargetEntityClass(): void + { + $smurf = $this->_em->getClassMetadata(GH12174Smurf::class); + self::assertTrue($smurf->isMappedSuperclass); + self::assertSame(GH12174Smurf::class, $smurf->getName()); + self::assertSame(GH12174BlueSmurf::class, $smurf->getAssociationMapping('children')['targetEntity']); + + $blue = $this->_em->getClassMetadata(GH12174BlueSmurf::class); + self::assertFalse($blue->isMappedSuperclass); + self::assertSame(GH12174BlueSmurf::class, $blue->getName()); + + $papa = $this->_em->getClassMetadata(GH12174PapaSmurf::class); + self::assertFalse($papa->isMappedSuperclass); + self::assertSame(GH12174PapaSmurf::class, $papa->getName()); + } +} + +#[ORM\MappedSuperclass] +class GH12174Smurf +{ + #[ORM\Id] + #[ORM\GeneratedValue] + #[ORM\Column(type: Types::INTEGER)] + public int $id; + + #[ManyToOne(inversedBy: 'children', targetEntity: self::class)] + private GH12174Smurf $parent; + + /** @var Collection */ + #[OneToMany(targetEntity: self::class, mappedBy: 'parent')] + private Collection $children; +} + +#[ORM\Entity] +class GH12174BlueSmurf extends GH12174Smurf +{ +} + +#[ORM\Entity] +class GH12174PapaSmurf extends GH12174Smurf +{ +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH12183Test.php b/tests/Tests/ORM/Functional/Ticket/GH12183Test.php new file mode 100644 index 00000000000..15c7846ecde --- /dev/null +++ b/tests/Tests/ORM/Functional/Ticket/GH12183Test.php @@ -0,0 +1,46 @@ +useModelSet('cms'); + + parent::setUp(); + + $article = new CmsArticle(); + + $article->topic = 'Loomings'; + $article->text = 'Call me Ishmael.'; + + $this->_em->persist($article); + $this->_em->flush(); + $this->_em->clear(); + } + + public function testPaginatorCountWithTreeWalkerAfterQueryHasBeenExecuted(): void + { + $query = $this->_em->createQuery('SELECT a FROM Doctrine\Tests\Models\CMS\CmsArticle a'); + + // Paginator::count is right when the query has not yet been executed + $paginator = new Paginator($query); + $paginator->setUseOutputWalkers(false); + self::assertSame(1, $paginator->count()); + + // Execute the query + $result = $query->getResult(); + self::assertCount(1, $result); + + $paginator = new Paginator($query); + $paginator->setUseOutputWalkers(false); + self::assertSame(1, $paginator->count()); + } +} diff --git a/tests/Tests/ORM/Functional/Ticket/GH6740Test.php b/tests/Tests/ORM/Functional/Ticket/GH6740Test.php index 1eb82f85baf..6c0f07d69e6 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH6740Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH6740Test.php @@ -49,7 +49,7 @@ protected function setUp(): void public function testCollectionFilteringLteOperator(): void { $product = $this->_em->find(ECommerceProduct::class, $this->productId); - $criteria = Criteria::create()->where(Criteria::expr()->lte('id', $this->secondCategoryId)); + $criteria = Criteria::create(true)->where(Criteria::expr()->lte('id', $this->secondCategoryId)); self::assertCount(2, $product->getCategories()->matching($criteria)); } @@ -58,7 +58,7 @@ public function testCollectionFilteringLteOperator(): void public function testCollectionFilteringLtOperator(): void { $product = $this->_em->find(ECommerceProduct::class, $this->productId); - $criteria = Criteria::create()->where(Criteria::expr()->lt('id', $this->secondCategoryId)); + $criteria = Criteria::create(true)->where(Criteria::expr()->lt('id', $this->secondCategoryId)); self::assertCount(1, $product->getCategories()->matching($criteria)); } @@ -67,7 +67,7 @@ public function testCollectionFilteringLtOperator(): void public function testCollectionFilteringGteOperator(): void { $product = $this->_em->find(ECommerceProduct::class, $this->productId); - $criteria = Criteria::create()->where(Criteria::expr()->gte('id', $this->firstCategoryId)); + $criteria = Criteria::create(true)->where(Criteria::expr()->gte('id', $this->firstCategoryId)); self::assertCount(2, $product->getCategories()->matching($criteria)); } @@ -76,7 +76,7 @@ public function testCollectionFilteringGteOperator(): void public function testCollectionFilteringGtOperator(): void { $product = $this->_em->find(ECommerceProduct::class, $this->productId); - $criteria = Criteria::create()->where(Criteria::expr()->gt('id', $this->firstCategoryId)); + $criteria = Criteria::create(true)->where(Criteria::expr()->gt('id', $this->firstCategoryId)); self::assertCount(1, $product->getCategories()->matching($criteria)); } @@ -85,7 +85,7 @@ public function testCollectionFilteringGtOperator(): void public function testCollectionFilteringEqualsOperator(): void { $product = $this->_em->find(ECommerceProduct::class, $this->productId); - $criteria = Criteria::create()->where(Criteria::expr()->eq('id', $this->firstCategoryId)); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq('id', $this->firstCategoryId)); self::assertCount(1, $product->getCategories()->matching($criteria)); } diff --git a/tests/Tests/ORM/Functional/Ticket/GH7717Test.php b/tests/Tests/ORM/Functional/Ticket/GH7717Test.php index 81e5c1d6202..23fe357ff16 100755 --- a/tests/Tests/ORM/Functional/Ticket/GH7717Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH7717Test.php @@ -37,6 +37,8 @@ public function testManyToManyPersisterIsNullComparison(): void $parent = $this->_em->find(GH7717Parent::class, 1); - $this->assertCount(1, $parent->children->matching(new Criteria(Criteria::expr()->isNull('nullableProperty')))); + $this->assertCount(1, $parent->children->matching(Criteria::create(true)->where( + Criteria::expr()->isNull('nullableProperty'), + ))); } } diff --git a/tests/Tests/ORM/Functional/Ticket/GH7737Test.php b/tests/Tests/ORM/Functional/Ticket/GH7737Test.php index 774820eb5d6..d1d0bb238b8 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH7737Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH7737Test.php @@ -43,7 +43,7 @@ public function memberOfCriteriaShouldBeCompatibleWithQueryBuilder(): void $query = $this->_em->createQueryBuilder() ->select('person') ->from(GH7737Person::class, 'person') - ->addCriteria(Criteria::create()->where(Criteria::expr()->memberOf(':group', 'person.groups'))) + ->addCriteria(Criteria::create(true)->where(Criteria::expr()->memberOf(':group', 'person.groups'))) ->getQuery(); $group1 = $this->_em->find(GH7737Group::class, 1); diff --git a/tests/Tests/ORM/Functional/Ticket/GH7767Test.php b/tests/Tests/ORM/Functional/Ticket/GH7767Test.php index 2eb32dd7c92..50918d59941 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH7767Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH7767Test.php @@ -45,7 +45,7 @@ public function testMatchingRespectsCollectionOrdering(): void $parent = $this->_em->find(GH7767ParentEntity::class, 1); assert($parent instanceof GH7767ParentEntity); - $children = $parent->getChildren()->matching(Criteria::create()); + $children = $parent->getChildren()->matching(Criteria::create(true)); self::assertEquals(100, $children[0]->position); self::assertEquals(200, $children[1]->position); @@ -58,7 +58,7 @@ public function testMatchingOverrulesCollectionOrdering(): void assert($parent instanceof GH7767ParentEntity); $children = $parent->getChildren()->matching( - Criteria::create()->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']), + Criteria::create(true)->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']), ); self::assertEquals(300, $children[0]->position); diff --git a/tests/Tests/ORM/Functional/Ticket/GH7836Test.php b/tests/Tests/ORM/Functional/Ticket/GH7836Test.php index 1cdaea018a1..0ff02355d2f 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH7836Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH7836Test.php @@ -45,7 +45,7 @@ public function testMatchingRespectsCollectionOrdering(): void $parent = $this->_em->find(GH7836ParentEntity::class, 1); assert($parent instanceof GH7836ParentEntity); - $children = $parent->getChildren()->matching(Criteria::create()); + $children = $parent->getChildren()->matching(Criteria::create(true)); self::assertSame(100, $children[0]->position); self::assertSame('bar', $children[0]->name); @@ -61,7 +61,7 @@ public function testMatchingOverrulesCollectionOrdering(): void assert($parent instanceof GH7836ParentEntity); $children = $parent->getChildren()->matching( - Criteria::create()->orderBy( + Criteria::create(true)->orderBy( class_exists(Order::class) ? ['position' => Order::Descending, 'name' => Order::Ascending] : ['position' => 'DESC', 'name' => 'ASC'], @@ -82,7 +82,7 @@ public function testMatchingKeepsOrderOfCriteriaOrderingKeys(): void assert($parent instanceof GH7836ParentEntity); $children = $parent->getChildren()->matching( - Criteria::create()->orderBy( + Criteria::create(true)->orderBy( class_exists(Order::class) ? ['name' => Order::Ascending, 'position' => Order::Ascending] : ['name' => 'ASC', 'position' => 'ASC'], diff --git a/tests/Tests/ORM/Functional/Ticket/GH9109Test.php b/tests/Tests/ORM/Functional/Ticket/GH9109Test.php index cf32fbf0a58..bf494a78555 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH9109Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH9109Test.php @@ -67,7 +67,7 @@ public function testIssue(): void self::assertEquals($userLastName, $user->getLastName()); // assert NOT QUOTED will WORK with Criteria - $criteria = Criteria::create(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('lastName', $userLastName)); $user = $persistedProduct->getBuyers()->matching($criteria)->first(); self::assertInstanceOf(GH9109User::class, $user); @@ -79,7 +79,7 @@ public function testIssue(): void self::assertEquals($userFirstName, $user->getFirstName()); // assert QUOTED will WORK with Criteria - $criteria = Criteria::create(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('firstName', $userFirstName)); $user = $persistedProduct->getBuyers()->matching($criteria)->first(); self::assertInstanceOf(GH9109User::class, $user); diff --git a/tests/Tests/ORM/Functional/ValueConversionType/ManyToManyCriteriaMatchingTest.php b/tests/Tests/ORM/Functional/ValueConversionType/ManyToManyCriteriaMatchingTest.php new file mode 100644 index 00000000000..c11c6a7b0a7 --- /dev/null +++ b/tests/Tests/ORM/Functional/ValueConversionType/ManyToManyCriteriaMatchingTest.php @@ -0,0 +1,64 @@ +useModelSet('vct_manytomany'); + + parent::setUp(); + } + + #[DataProvider('provideMatchingExpressions')] + public function testCriteriaMatchingOnFieldInManyToManyTarget(Comparison $comparison): void + { + $associated = new InversedManyToManyEntity(); + $associated->id1 = 'associated'; + + $matching = new OwningManyToManyEntity(); + $matching->id2 = 'first'; + $matching->field = 'match this'; // stored as 'zngpu guvf' + $matching->associatedEntities->add($associated); + + $nonMatching = new OwningManyToManyEntity(); + $nonMatching->id2 = 'second'; + $nonMatching->field = 'this is no match'; + $nonMatching->associatedEntities->add($associated); + + $this->_em->persist($associated); + $this->_em->persist($matching); + $this->_em->persist($nonMatching); + $this->_em->flush(); + $this->_em->clear(); + + $this->getQueryLog()->reset()->enable(); + + $associated = $this->_em->find(InversedManyToManyEntity::class, 'associated'); + self::assertFalse($associated->associatedEntities->isInitialized(), 'Pre-condition: lazy collection'); + + $result = $associated->associatedEntities->matching(Criteria::create(true)->where($comparison)); + + $l = $this->getQueryLog(); + + self::assertCount(1, $result); + self::assertSame('first', $result[0]->id2); + } + + public static function provideMatchingExpressions(): Generator + { + yield [Criteria::expr()->eq('field', 'match this')]; // should convert to 'zngpu guvf' + yield [Criteria::expr()->in('field', ['match this'])]; // should convert to ['zngpu guvf'] + } +} diff --git a/tests/Tests/ORM/Functional/ValueConversionType/OneToManyCriteriaMatchingTest.php b/tests/Tests/ORM/Functional/ValueConversionType/OneToManyCriteriaMatchingTest.php new file mode 100644 index 00000000000..4a416abee02 --- /dev/null +++ b/tests/Tests/ORM/Functional/ValueConversionType/OneToManyCriteriaMatchingTest.php @@ -0,0 +1,64 @@ +useModelSet('vct_onetomany'); + + parent::setUp(); + } + + #[DataProvider('provideMatchingExpressions')] + public function testCriteriaMatchingOnFieldInOneToManyTarget(Comparison $comparison): void + { + $entityWithCollection = new InversedOneToManyEntity(); + $entityWithCollection->id1 = 'associated'; + + $matching = new OwningManyToOneEntity(); + $matching->id2 = 'first'; + $matching->field = 'match this'; // stored as 'zngpu guvf' + $matching->associatedEntity = $entityWithCollection; + + $notMatching = new OwningManyToOneEntity(); + $notMatching->id2 = 'second'; + $notMatching->field = 'this is no match'; + $notMatching->associatedEntity = $entityWithCollection; + + $this->_em->persist($entityWithCollection); + $this->_em->persist($matching); + $this->_em->persist($notMatching); + $this->_em->flush(); + $this->_em->clear(); + + $this->getQueryLog()->reset()->enable(); + + $entityWithCollection = $this->_em->find(InversedOneToManyEntity::class, 'associated'); + self::assertFalse($entityWithCollection->associatedEntities->isInitialized(), 'Pre-condition: lazy collection'); + + $result = $entityWithCollection->associatedEntities->matching(Criteria::create(true)->where($comparison)); + + $l = $this->getQueryLog(); + + self::assertCount(1, $result); + self::assertSame('first', $result[0]->id2); + } + + public static function provideMatchingExpressions(): Generator + { + yield [Criteria::expr()->eq('field', 'match this')]; // should convert to 'zngpu guvf' + yield [Criteria::expr()->in('field', ['match this'])]; // should convert to ['zngpu guvf'] + } +} diff --git a/tests/Tests/ORM/LazyCriteriaCollectionTest.php b/tests/Tests/ORM/LazyCriteriaCollectionTest.php index 82733dd643c..bd2786dc8cc 100644 --- a/tests/Tests/ORM/LazyCriteriaCollectionTest.php +++ b/tests/Tests/ORM/LazyCriteriaCollectionTest.php @@ -11,7 +11,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -use stdClass; #[CoversClass(LazyCriteriaCollection::class)] class LazyCriteriaCollectionTest extends TestCase @@ -23,7 +22,7 @@ class LazyCriteriaCollectionTest extends TestCase protected function setUp(): void { $this->persister = $this->createMock(EntityPersister::class); - $this->criteria = new Criteria(); + $this->criteria = Criteria::create(true); $this->lazyCriteriaCollection = new LazyCriteriaCollection($this->persister, $this->criteria); } @@ -64,9 +63,9 @@ public function testCountUsesWrappedCollectionWhenInitialized(): void public function testMatchingUsesThePersisterOnlyOnce(): void { - $foo = new stdClass(); - $bar = new stdClass(); - $baz = new stdClass(); + $foo = new LazyCriteriaCollectionTestObject(); + $bar = new LazyCriteriaCollectionTestObject(); + $baz = new LazyCriteriaCollectionTestObject(); $foo->val = 'foo'; $bar->val = 'bar'; @@ -79,7 +78,7 @@ public function testMatchingUsesThePersisterOnlyOnce(): void ->with($this->criteria) ->willReturn([$foo, $bar, $baz]); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->andWhere($criteria->expr()->eq('val', 'foo')); @@ -122,3 +121,9 @@ public function testIsEmptyUsesWrappedCollectionWhenInitialized(): void self::assertFalse($this->lazyCriteriaCollection->isEmpty()); } } + +class LazyCriteriaCollectionTestObject +{ + /** @var mixed */ + public $val; +} diff --git a/tests/Tests/ORM/Persisters/BasicEntityPersisterCompositeTypeParametersTest.php b/tests/Tests/ORM/Persisters/BasicEntityPersisterCompositeTypeParametersTest.php index 3f2d0abd8c2..8777375e0f9 100644 --- a/tests/Tests/ORM/Persisters/BasicEntityPersisterCompositeTypeParametersTest.php +++ b/tests/Tests/ORM/Persisters/BasicEntityPersisterCompositeTypeParametersTest.php @@ -46,7 +46,7 @@ public function testExpandCriteriaParametersWillExpandCompositeEntityKeys(): voi $country = new Country('IT', 'Italy'); $admin1 = new Admin1(10, 'Rome', $country); - $criteria = Criteria::create(); + $criteria = Criteria::create(true); $criteria->andWhere(Criteria::expr()->eq('admin1', $admin1)); [$values, $types] = $this->persister->expandCriteriaParameters($criteria); diff --git a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php index 4d337aacd4c..095b347099e 100644 --- a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php +++ b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php @@ -150,7 +150,7 @@ public function testSelectConditionStatementNeqNull(): void public function testSelectConditionStatementWithMultipleValuesContainingNull(): void { self::assertEquals( - '(t0.id IN (?) OR t0.id IS NULL)', + 't0.id IS NULL', $this->persister->getSelectConditionStatementSQL('id', [null]), ); @@ -163,6 +163,11 @@ public function testSelectConditionStatementWithMultipleValuesContainingNull(): '(t0.id IN (?) OR t0.id IS NULL)', $this->persister->getSelectConditionStatementSQL('id', [123, null]), ); + + self::assertEquals( + '(t0.id IN (?, ?) OR t0.id IS NULL)', + $this->persister->getSelectConditionStatementSQL('id', [123, null, 234]), + ); } public function testCountCondition(): void @@ -174,7 +179,7 @@ public function testCountCondition(): void self::assertEquals('SELECT COUNT(*) FROM "not-a-simple-entity" t0 WHERE t0."simple-entity-value" = ?', $statement); // Using a criteria object - $criteria = new Criteria(Criteria::expr()->eq('value', 'bar')); + $criteria = Criteria::create(true)->where(Criteria::expr()->eq('value', 'bar')); $statement = $persister->getCountSQL($criteria); self::assertEquals('SELECT COUNT(*) FROM "not-a-simple-entity" t0 WHERE t0."simple-entity-value" = ?', $statement); } diff --git a/tests/Tests/ORM/QueryBuilderTest.php b/tests/Tests/ORM/QueryBuilderTest.php index 9d989ced9db..f75279e5ab0 100644 --- a/tests/Tests/ORM/QueryBuilderTest.php +++ b/tests/Tests/ORM/QueryBuilderTest.php @@ -488,7 +488,7 @@ public function testAddCriteriaWhere(): void $qb->select('u') ->from(CmsUser::class, 'u'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('field', 'value')); $qb->addCriteria($criteria); @@ -502,7 +502,7 @@ public function testAddMultipleSameCriteriaWhere(): void $qb = $this->entityManager->createQueryBuilder(); $qb->select('alias1')->from(CmsUser::class, 'alias1'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->andX( $criteria->expr()->eq('field', 'value1'), $criteria->expr()->eq('field', 'value2'), @@ -521,7 +521,7 @@ public function testAddCriteriaWhereWithMultipleParametersWithSameField(): void $qb = $this->entityManager->createQueryBuilder(); $qb->select('alias1')->from(CmsUser::class, 'alias1'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('field', 'value1')); $criteria->andWhere($criteria->expr()->gt('field', 'value2')); @@ -538,7 +538,7 @@ public function testAddCriteriaWhereWithMultipleParametersWithDifferentFields(): $qb = $this->entityManager->createQueryBuilder(); $qb->select('alias1')->from(CmsUser::class, 'alias1'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('field1', 'value1')); $criteria->andWhere($criteria->expr()->gt('field2', 'value2')); @@ -555,7 +555,7 @@ public function testAddCriteriaWhereWithMultipleParametersWithSubpathsAndDiffere $qb = $this->entityManager->createQueryBuilder(); $qb->select('alias1')->from(CmsUser::class, 'alias1'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('field1', 'value1')); $criteria->andWhere($criteria->expr()->gt('field2', 'value2')); @@ -572,7 +572,7 @@ public function testAddCriteriaWhereWithMultipleParametersWithSubpathsAndSamePro $qb = $this->entityManager->createQueryBuilder(); $qb->select('alias1')->from(CmsUser::class, 'alias1'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('field1', 'value1')); $criteria->andWhere($criteria->expr()->gt('field1', 'value2')); @@ -589,7 +589,7 @@ public function testAddCriteriaOrder(): void $qb->select('u') ->from(CmsUser::class, 'u'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->orderBy(['field' => class_exists(Order::class) ? Order::Descending : Criteria::DESC]); $qb->addCriteria($criteria); @@ -606,7 +606,7 @@ public function testAddCriteriaOrderOnJoinAlias(): void ->from(CmsUser::class, 'u') ->join('u.article', 'a'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->orderBy(['a.field' => class_exists(Order::class) ? Order::Descending : Criteria::DESC]); $qb->addCriteria($criteria); @@ -621,7 +621,7 @@ public function testAddCriteriaLimit(): void $qb->select('u') ->from(CmsUser::class, 'u'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->setFirstResult(2); $criteria->setMaxResults(10); @@ -639,7 +639,7 @@ public function testAddCriteriaUndefinedLimit(): void ->setFirstResult(2) ->setMaxResults(10); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $qb->addCriteria($criteria); @@ -929,7 +929,7 @@ public function testAddCriteriaWhereWithJoinAlias(): void $qb->select('alias1')->from(CmsUser::class, 'alias1'); $qb->join('alias1.articles', 'alias2'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('field', 'value1')); $criteria->andWhere($criteria->expr()->gt('alias2.field', 'value2')); @@ -947,7 +947,7 @@ public function testAddCriteriaWhereWithDefaultAndJoinAlias(): void $qb->select('alias1')->from(CmsUser::class, 'alias1'); $qb->join('alias1.articles', 'alias2'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('alias1.field', 'value1')); $criteria->andWhere($criteria->expr()->gt('alias2.field', 'value2')); @@ -965,7 +965,7 @@ public function testAddCriteriaWhereOnJoinAliasWithDuplicateFields(): void $qb->select('alias1')->from(CmsUser::class, 'alias1'); $qb->join('alias1.articles', 'alias2'); - $criteria = new Criteria(); + $criteria = Criteria::create(true); $criteria->where($criteria->expr()->eq('alias1.field', 'value1')); $criteria->andWhere($criteria->expr()->gt('alias2.field', 'value2')); $criteria->andWhere($criteria->expr()->lt('alias2.field', 'value3'));