diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1cbf5e981d..fdba707e0d 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -115,8 +115,24 @@ jobs: ENABLE_SECOND_LEVEL_CACHE: 0 ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }} - - name: "Run PHPUnit with Second Level Cache" - run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-cache.xml" + - name: "Run PHPUnit with Second Level Cache and PHPUnit 10" + run: | + vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \ + --exclude-group=performance,non-cacheable,locking_functional \ + --coverage-clover=coverage-cache.xml + if: "${{ matrix.php-version == '8.1' }}" + env: + ENABLE_SECOND_LEVEL_CACHE: 1 + ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }} + + - name: "Run PHPUnit with Second Level Cache and PHPUnit 11+" + run: | + vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \ + --exclude-group=performance \ + --exclude-group=non-cacheable \ + --exclude-group=locking_functional \ + --coverage-clover=coverage-cache.xml + if: "${{ matrix.php-version != '8.1' }}" env: ENABLE_SECOND_LEVEL_CACHE: 1 ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }} @@ -128,6 +144,40 @@ jobs: path: "coverage*.xml" + phpunit-deprecations: + name: "PHPUnit (fail on deprecations)" + runs-on: "ubuntu-24.04" + + steps: + - name: "Checkout" + uses: "actions/checkout@v5" + with: + fetch-depth: 2 + + - name: "Install PHP" + uses: "shivammathur/setup-php@v2" + with: + php-version: "8.5" + extensions: "apcu, pdo, sqlite3" + coverage: "pcov" + ini-values: "zend.assertions=1, apc.enable_cli=1" + + - name: "Allow dev dependencies" + run: composer config minimum-stability dev + + - name: "Install dependencies with Composer" + uses: "ramsey/composer-install@v3" + with: + composer-options: "--ignore-platform-req=php+" + dependency-versions: "highest" + + - name: "Run PHPUnit" + run: "vendor/bin/phpunit -c ci/github/phpunit/sqlite3.xml --fail-on-deprecation" + env: + ENABLE_SECOND_LEVEL_CACHE: 0 + ENABLE_NATIVE_LAZY_OBJECTS: 1 + + phpunit-postgres: name: "PHPUnit with PostgreSQL" runs-on: "ubuntu-22.04" @@ -339,8 +389,22 @@ jobs: env: ENABLE_SECOND_LEVEL_CACHE: 0 - - name: "Run PHPUnit with Second Level Cache" - run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --exclude-group performance,non-cacheable,locking_functional --coverage-clover=coverage-no-cache.xml" + - name: "Run PHPUnit with Second Level Cache and PHPUnit 10" + run: | + vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \ + --exclude-group=performance,non-cacheable,locking_functional \ + --coverage-clover=coverage-no-cache.xml" + if: "${{ matrix.php-version == '8.1' }}" + env: + ENABLE_SECOND_LEVEL_CACHE: 1 + - name: "Run PHPUnit with Second Level Cache and PHPUnit 11+" + run: | + vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml \ + --exclude-group=performance \ + --exclude-group=non-cacheable \ + --exclude-group=locking_functional \ + --coverage-clover=coverage-no-cache.xml + if: "${{ matrix.php-version != '8.1' }}" env: ENABLE_SECOND_LEVEL_CACHE: 1 diff --git a/composer.json b/composer.json index 74292b0332..154fc2bfb5 100644 --- a/composer.json +++ b/composer.json @@ -43,7 +43,7 @@ "phpstan/extension-installer": "^1.4", "phpstan/phpstan": "2.1.22", "phpstan/phpstan-deprecation-rules": "^2", - "phpunit/phpunit": "^10.4.0", + "phpunit/phpunit": "^10.5.0 || ^11.5", "psr/log": "^1 || ^2 || ^3", "squizlabs/php_codesniffer": "3.13.2", "symfony/cache": "^5.4 || ^6.2 || ^7.0 || ^8.0" diff --git a/docs/en/reference/advanced-configuration.rst b/docs/en/reference/advanced-configuration.rst index 02ac470dd3..d0b662e26d 100644 --- a/docs/en/reference/advanced-configuration.rst +++ b/docs/en/reference/advanced-configuration.rst @@ -76,6 +76,8 @@ Configuration Options The following sections describe all the configuration options available on a ``Doctrine\ORM\Configuration`` instance. +.. _reference-native-lazy-objects: + Native Lazy Objects (**OPTIONAL**) ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/docs/en/reference/architecture.rst b/docs/en/reference/architecture.rst index 5dabd25997..e4896bcb48 100644 --- a/docs/en/reference/architecture.rst +++ b/docs/en/reference/architecture.rst @@ -79,8 +79,9 @@ Entities An entity is a lightweight, persistent domain object. An entity can be any regular PHP class observing the following restrictions: -- An entity class must not be final nor read-only but - it may contain final methods or read-only properties. +- An entity class can be final or read-only when + you use :ref:`native lazy objects `. + It may contain final methods or read-only properties too. - Any two entity classes in a class hierarchy that inherit directly or indirectly from one another must not have a mapped property with the same name. That is, if B inherits from A then B diff --git a/docs/en/reference/basic-mapping.rst b/docs/en/reference/basic-mapping.rst index 2164932dab..976ef91688 100644 --- a/docs/en/reference/basic-mapping.rst +++ b/docs/en/reference/basic-mapping.rst @@ -389,17 +389,19 @@ Here is the list of possible generation strategies: - ``AUTO`` (default): Tells Doctrine to pick the strategy that is preferred by the used database platform. The preferred strategies - are ``IDENTITY`` for MySQL, SQLite, MsSQL and SQL Anywhere and, for - historical reasons, ``SEQUENCE`` for Oracle and PostgreSQL. This - strategy provides full portability. + are ``IDENTITY`` for MySQL, SQLite, MsSQL, SQL Anywhere and + PostgreSQL (on DBAL 4) and, for historical reasons, ``SEQUENCE`` + for Oracle and PostgreSQL (on DBAL 3). This strategy provides + full portability. - ``IDENTITY``: Tells Doctrine to use special identity columns in the database that generate a value on insertion of a row. This strategy does currently not provide full portability and is supported by the following platforms: MySQL/SQLite/SQL Anywhere - (``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL``). + (``AUTO_INCREMENT``), MSSQL (``IDENTITY``) and PostgreSQL (``SERIAL`` + on DBAL 3, ``GENERATED BY DEFAULT AS IDENTITY`` on DBAL 4). - ``SEQUENCE``: Tells Doctrine to use a database sequence for ID generation. This strategy does currently not provide full - portability. Sequences are supported by Oracle, PostgreSql and + portability. Sequences are supported by Oracle, PostgreSQL and SQL Anywhere. - ``NONE``: Tells Doctrine that the identifiers are assigned (and thus generated) by your code. The assignment must take place before diff --git a/docs/en/sidebar.rst b/docs/en/sidebar.rst index 344effb499..a382bc3743 100644 --- a/docs/en/sidebar.rst +++ b/docs/en/sidebar.rst @@ -5,8 +5,6 @@ :depth: 3 tutorials/getting-started - tutorials/getting-started-database - tutorials/getting-started-models tutorials/working-with-indexed-associations tutorials/extra-lazy-associations tutorials/composite-primary-keys diff --git a/docs/en/tutorials/getting-started-database.rst b/docs/en/tutorials/getting-started-database.rst deleted file mode 100644 index 578b27921a..0000000000 --- a/docs/en/tutorials/getting-started-database.rst +++ /dev/null @@ -1,26 +0,0 @@ -Getting Started: Database First -=============================== - -.. note:: *Development Workflows* - - When you :doc:`Code First `, you - start with developing Objects and then map them onto your database. When - you :doc:`Model First `, you are modelling your application using tools (for - example UML) and generate database schema and PHP code from this model. - When you have a Database First, you already have a database schema - and generate the corresponding PHP code from it. - -.. note:: - - This getting started guide is in development. - -Development of new applications often starts with an existing database schema. -When the database schema is the starting point for your application, then -development is said to use the *Database First* approach to Doctrine. - -In this workflow you would modify the database schema first and then -regenerate the PHP code to use with this schema. You need a flexible -code-generator for this task. - -We spun off a subproject, Doctrine CodeGenerator, that will fill this gap and -allow you to do *Database First* development. diff --git a/docs/en/tutorials/getting-started-models.rst b/docs/en/tutorials/getting-started-models.rst deleted file mode 100644 index 01b7187629..0000000000 --- a/docs/en/tutorials/getting-started-models.rst +++ /dev/null @@ -1,24 +0,0 @@ -Getting Started: Model First -============================ - -.. note:: *Development Workflows* - - When you :doc:`Code First `, you - start with developing Objects and then map them onto your database. When - you Model First, you are modelling your application using tools (for - example UML) and generate database schema and PHP code from this model. - When you have a :doc:`Database First `, then you already have a database schema - and generate the corresponding PHP code from it. - -.. note:: - - This getting started guide is in development. - -There are applications when you start with a high-level description of the -model using modelling tools such as UML. Modelling tools could also be Excel, -XML or CSV files that describe the model in some structured way. If your -application is using a modelling tool, then the development workflow is said to -be a *Model First* approach to Doctrine2. - -In this workflow you always change the model description and then regenerate -both PHP code and database schema from this model. diff --git a/docs/en/tutorials/getting-started.rst b/docs/en/tutorials/getting-started.rst index c3b60f956b..a73a7788ee 100644 --- a/docs/en/tutorials/getting-started.rst +++ b/docs/en/tutorials/getting-started.rst @@ -49,8 +49,9 @@ An entity contains persistable properties. A persistable property is an instance variable of the entity that is saved into and retrieved from the database by Doctrine's data mapping capabilities. -An entity class must not be final nor read-only, although -it can contain final methods or read-only properties. +An entity class can be final or read-only when you use +:ref:`native lazy objects `. +It may contain final methods or read-only properties too. An Example Model: Bug Tracker ----------------------------- @@ -534,7 +535,7 @@ the ``id`` tag. It has a ``generator`` tag nested inside, which specifies that the primary key generation mechanism should automatically use the database platform's native id generation strategy (for example, AUTO INCREMENT in the case of MySql, or Sequences in the -case of PostgreSql and Oracle). +case of PostgreSQL and Oracle). Now that we have defined our first entity and its metadata, let's update the database schema: @@ -1287,7 +1288,7 @@ The console output of this script is then: result set to retrieve entities from the database. DQL boils down to a Native SQL statement and a ``ResultSetMapping`` instance itself. Using Native SQL you could even use stored procedures for data retrieval, or - make use of advanced non-portable database queries like PostgreSql's + make use of advanced non-portable database queries like PostgreSQL's recursive queries. diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 4a53051bfc..94bb2a8cc1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -2023,14 +2023,8 @@ parameters: path: src/Persisters/Entity/BasicEntityPersister.php - - message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$relationToTargetKeyColumns\.$#' - identifier: property.notFound - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - - - message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$sourceToTargetKeyColumns\.$#' - identifier: property.notFound + message: '#^Access to property \$value on an unknown class Doctrine\\ORM\\Persisters\\Entity\\BackedEnum\.$#' + identifier: class.notFound count: 1 path: src/Persisters/Entity/BasicEntityPersister.php @@ -2040,24 +2034,18 @@ parameters: count: 1 path: src/Persisters/Entity/BasicEntityPersister.php + - + message: '#^Class Doctrine\\ORM\\Persisters\\Entity\\BackedEnum not found\.$#' + identifier: class.notFound + count: 1 + path: src/Persisters/Entity/BasicEntityPersister.php + - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:__construct\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' identifier: missingType.generics count: 1 path: src/Persisters/Entity/BasicEntityPersister.php - - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandCriteriaParameters\(\) should return array\{list\, list\\} but returns array\{array\, list\\}\.$#' - identifier: return.type - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandParameters\(\) should return array\{list\, list\\} but returns array\{array\, list\\}\.$#' - identifier: return.type - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandToManyParameters\(\) return type has no value type specified in iterable type array\.$#' identifier: missingType.iterableValue @@ -2094,12 +2082,6 @@ parameters: count: 1 path: src/Persisters/Entity/BasicEntityPersister.php - - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getIndividualValue\(\) should return list\ but returns array\\.$#' - identifier: return.type - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getSelectColumnAssociationSQL\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' identifier: missingType.generics @@ -2112,18 +2094,6 @@ parameters: count: 1 path: src/Persisters/Entity/BasicEntityPersister.php - - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getTypes\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' - identifier: missingType.generics - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getTypes\(\) should return list\ but returns list\\.$#' - identifier: return.type - count: 1 - path: src/Persisters/Entity/BasicEntityPersister.php - - message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:loadCollectionFromStatement\(\) has parameter \$coll with generic class Doctrine\\ORM\\PersistentCollection but does not specify its types\: TKey, T$#' identifier: missingType.generics @@ -3603,6 +3573,18 @@ parameters: count: 1 path: src/Utility/PersisterHelper.php + - + message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$relationToTargetKeyColumns\.$#' + identifier: property.notFound + count: 1 + path: src/Utility/PersisterHelper.php + + - + message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$sourceToTargetKeyColumns\.$#' + identifier: property.notFound + count: 1 + path: src/Utility/PersisterHelper.php + - message: '#^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:getTypeOfColumn\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' identifier: missingType.generics @@ -3614,3 +3596,15 @@ parameters: identifier: missingType.generics count: 1 path: src/Utility/PersisterHelper.php + + - + message: '#^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:inferParameterTypes\(\) has parameter \$class with generic class Doctrine\\ORM\\Mapping\\ClassMetadata but does not specify its types\: T$#' + identifier: missingType.generics + count: 1 + path: src/Utility/PersisterHelper.php + + - + message: '#^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:inferParameterTypes\(\) should return list\ but returns list\\.$#' + identifier: return.type + count: 1 + path: src/Utility/PersisterHelper.php diff --git a/phpstan-dbal3.neon b/phpstan-dbal3.neon index 76d404b3ec..600bffed47 100644 --- a/phpstan-dbal3.neon +++ b/phpstan-dbal3.neon @@ -11,7 +11,7 @@ parameters: # We can be certain that those values are not matched. - message: '~^Match expression does not handle remaining values:~' - path: src/Persisters/Entity/BasicEntityPersister.php + path: src/Utility/PersisterHelper.php # DBAL 4 compatibility - @@ -109,12 +109,12 @@ parameters: path: src/Mapping/Driver/AttributeDriver.php - - message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~' - path: src/Persisters/Entity/BasicEntityPersister.php + message: '~^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~' + path: src/Utility/PersisterHelper.php - - message: '~getTypes.*should return~' - path: src/Persisters/Entity/BasicEntityPersister.php + message: '~inferParameterTypes.*should return~' + path: src/Utility/PersisterHelper.php - message: '~.*appendLockHint.*expects.*LockMode given~' diff --git a/phpstan.neon b/phpstan.neon index e0aecc9b1b..40f6f50c81 100644 --- a/phpstan.neon +++ b/phpstan.neon @@ -10,15 +10,15 @@ parameters: # We can be certain that those values are not matched. - message: '~^Match expression does not handle remaining values:~' - path: src/Persisters/Entity/BasicEntityPersister.php + path: src/Utility/PersisterHelper.php # DBAL 4 compatibility - message: '~^Method Doctrine\\ORM\\Query\\AST\\Functions\\TrimFunction::getTrimMode\(\) never returns .* so it can be removed from the return type\.$~' path: src/Query/AST/Functions/TrimFunction.php - - message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~' - path: src/Persisters/Entity/BasicEntityPersister.php + message: '~^Method Doctrine\\ORM\\Utility\\PersisterHelper\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~' + path: src/Utility/PersisterHelper.php # Compatibility with DBAL 3 # See https://github.com/doctrine/dbal/pull/3480 diff --git a/src/Persisters/Collection/ManyToManyPersister.php b/src/Persisters/Collection/ManyToManyPersister.php index 893e0644e6..1f2c7629de 100644 --- a/src/Persisters/Collection/ManyToManyPersister.php +++ b/src/Persisters/Collection/ManyToManyPersister.php @@ -248,9 +248,16 @@ class ManyToManyPersister extends AbstractCollectionPersister if ($value === null && ($operator === Comparison::EQ || $operator === Comparison::NEQ)) { $whereClauses[] = sprintf('te.%s %s NULL', $field, $operator === Comparison::EQ ? 'IS' : 'IS NOT'); } else { - $whereClauses[] = sprintf('te.%s %s ?', $field, $operator); - $params[] = $value; - $paramTypes[] = PersisterHelper::getTypeOfField($name, $targetClass, $this->em)[0]; + 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); + } + + $params = [...$params, ...PersisterHelper::convertToParameterValue($value, $this->em)]; + $paramTypes = [...$paramTypes, ...PersisterHelper::inferParameterTypes($name, $value, $targetClass, $this->em)]; } } diff --git a/src/Persisters/Entity/BasicEntityPersister.php b/src/Persisters/Entity/BasicEntityPersister.php index be48d263df..4670a62a9e 100644 --- a/src/Persisters/Entity/BasicEntityPersister.php +++ b/src/Persisters/Entity/BasicEntityPersister.php @@ -4,7 +4,6 @@ declare(strict_types=1); namespace Doctrine\ORM\Persisters\Entity; -use BackedEnum; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Expr\Comparison; use Doctrine\Common\Collections\Order; @@ -31,9 +30,7 @@ use Doctrine\ORM\Persisters\Exception\InvalidOrientation; use Doctrine\ORM\Persisters\Exception\UnrecognizedField; use Doctrine\ORM\Persisters\SqlExpressionVisitor; use Doctrine\ORM\Persisters\SqlValueVisitor; -use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; use Doctrine\ORM\Query; -use Doctrine\ORM\Query\QueryException; use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\ORM\Repository\Exception\InvalidFindByCall; use Doctrine\ORM\UnitOfWork; @@ -45,7 +42,6 @@ use LengthException; use function array_combine; use function array_keys; use function array_map; -use function array_merge; use function array_search; use function array_unique; use function array_values; @@ -53,7 +49,6 @@ use function assert; use function count; use function implode; use function is_array; -use function is_object; use function reset; use function spl_object_id; use function sprintf; @@ -353,7 +348,7 @@ class BasicEntityPersister implements EntityPersister $types = []; foreach ($id as $field => $value) { - $types = [...$types, ...$this->getTypes($field, $value, $versionedClass)]; + $types = [...$types, ...PersisterHelper::inferParameterTypes($field, $value, $versionedClass, $this->em)]; } return $types; @@ -919,8 +914,8 @@ class BasicEntityPersister implements EntityPersister continue; } - $sqlParams = [...$sqlParams, ...$this->getValues($value)]; - $sqlTypes = [...$sqlTypes, ...$this->getTypes($field, $value, $this->class)]; + $sqlParams = [...$sqlParams, ...PersisterHelper::convertToParameterValue($value, $this->em)]; + $sqlTypes = [...$sqlTypes, ...PersisterHelper::inferParameterTypes($field, $value, $this->class, $this->em)]; } return [$sqlParams, $sqlTypes]; @@ -1858,8 +1853,8 @@ class BasicEntityPersister implements EntityPersister continue; // skip null values. } - $types = [...$types, ...$this->getTypes($field, $value, $this->class)]; - $params = array_merge($params, $this->getValues($value)); + $types = [...$types, ...PersisterHelper::inferParameterTypes($field, $value, $this->class, $this->em)]; + $params = [...$params, ...PersisterHelper::convertToParameterValue($value, $this->em)]; } return [$params, $types]; @@ -1887,130 +1882,13 @@ class BasicEntityPersister implements EntityPersister continue; // skip null values. } - $types = [...$types, ...$this->getTypes($criterion['field'], $criterion['value'], $criterion['class'])]; - $params = array_merge($params, $this->getValues($criterion['value'])); + $types = [...$types, ...PersisterHelper::inferParameterTypes($criterion['field'], $criterion['value'], $criterion['class'], $this->em)]; + $params = [...$params, ...PersisterHelper::convertToParameterValue($criterion['value'], $this->em)]; } return [$params, $types]; } - /** - * Infers field types to be used by parameter type casting. - * - * @return list - * @phpstan-return list - * - * @throws QueryException - */ - private function getTypes(string $field, mixed $value, ClassMetadata $class): array - { - $types = []; - - switch (true) { - case isset($class->fieldMappings[$field]): - $types = array_merge($types, [$class->fieldMappings[$field]->type]); - break; - - case isset($class->associationMappings[$field]): - $assoc = $this->em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]); - $class = $this->em->getClassMetadata($assoc->targetEntity); - - if ($assoc->isManyToManyOwningSide()) { - $columns = $assoc->relationToTargetKeyColumns; - } else { - assert($assoc->isToOneOwningSide()); - $columns = $assoc->sourceToTargetKeyColumns; - } - - foreach ($columns as $column) { - $types[] = PersisterHelper::getTypeOfColumn($column, $class, $this->em); - } - - break; - - default: - $types[] = ParameterType::STRING; - break; - } - - if (is_array($value)) { - return array_map($this->getArrayBindingType(...), $types); - } - - return $types; - } - - /** @phpstan-return ArrayParameterType::* */ - private function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int - { - if (! $type instanceof ParameterType) { - $type = Type::getType((string) $type)->getBindingType(); - } - - return match ($type) { - ParameterType::STRING => ArrayParameterType::STRING, - ParameterType::INTEGER => ArrayParameterType::INTEGER, - ParameterType::ASCII => ArrayParameterType::ASCII, - ParameterType::BINARY => ArrayParameterType::BINARY, - }; - } - - /** - * Retrieves the parameters that identifies a value. - * - * @return mixed[] - */ - private function getValues(mixed $value): array - { - if (is_array($value)) { - $newValue = []; - - foreach ($value as $itemValue) { - $newValue = array_merge($newValue, $this->getValues($itemValue)); - } - - return [$newValue]; - } - - return $this->getIndividualValue($value); - } - - /** - * Retrieves an individual parameter value. - * - * @phpstan-return list - */ - private function getIndividualValue(mixed $value): array - { - if (! is_object($value)) { - return [$value]; - } - - if ($value instanceof BackedEnum) { - return [$value->value]; - } - - $valueClass = DefaultProxyClassNameResolver::getClass($value); - - if ($this->em->getMetadataFactory()->isTransient($valueClass)) { - return [$value]; - } - - $class = $this->em->getClassMetadata($valueClass); - - if ($class->isIdentifierComposite) { - $newValue = []; - - foreach ($class->getIdentifierValues($value) as $innerValue) { - $newValue = array_merge($newValue, $this->getValues($innerValue)); - } - - return $newValue; - } - - return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)]; - } - public function exists(object $entity, Criteria|null $extraConditions = null): bool { $criteria = $this->class->getIdentifierValues($entity); diff --git a/src/Persisters/Entity/JoinedSubclassPersister.php b/src/Persisters/Entity/JoinedSubclassPersister.php index 08ab72c0f4..f7e6cf8ec7 100644 --- a/src/Persisters/Entity/JoinedSubclassPersister.php +++ b/src/Persisters/Entity/JoinedSubclassPersister.php @@ -61,7 +61,7 @@ class JoinedSubclassPersister extends AbstractEntityInheritancePersister */ private function getVersionedClassMetadata(): ClassMetadata { - if (isset($this->class->fieldMappings[$this->class->versionField]->inherited)) { + if ($this->class->versionField !== null && isset($this->class->fieldMappings[$this->class->versionField]->inherited)) { $definingClassName = $this->class->fieldMappings[$this->class->versionField]->inherited; return $this->em->getClassMetadata($definingClassName); diff --git a/src/Query/SqlWalker.php b/src/Query/SqlWalker.php index a943deb050..ce0400368f 100644 --- a/src/Query/SqlWalker.php +++ b/src/Query/SqlWalker.php @@ -1412,7 +1412,9 @@ class SqlWalker $sqlParts[] = $col . ' AS ' . $columnAlias; - $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + if ($resultAlias !== null) { + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + } $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $class->name); @@ -1445,7 +1447,9 @@ class SqlWalker $sqlParts[] = $col . ' AS ' . $columnAlias; - $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + if ($resultAlias !== null) { + $this->scalarResultAliasMap[$resultAlias][] = $columnAlias; + } $this->rsm->addFieldResult($dqlAlias, $columnAlias, $fieldName, $subClassName); } diff --git a/src/Tools/Pagination/LimitSubqueryOutputWalker.php b/src/Tools/Pagination/LimitSubqueryOutputWalker.php index a7450257d3..ff1d138e1e 100644 --- a/src/Tools/Pagination/LimitSubqueryOutputWalker.php +++ b/src/Tools/Pagination/LimitSubqueryOutputWalker.php @@ -132,7 +132,9 @@ class LimitSubqueryOutputWalker extends SqlOutputWalker $selectAliasToExpressionMap = []; // Get any aliases that are available for select expressions. foreach ($AST->selectClause->selectExpressions as $selectExpression) { - $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression; + if ($selectExpression->fieldIdentificationVariable !== null) { + $selectAliasToExpressionMap[$selectExpression->fieldIdentificationVariable] = $selectExpression->expression; + } } // Rebuild string orderby expressions to use the select expression they're referencing diff --git a/src/Utility/PersisterHelper.php b/src/Utility/PersisterHelper.php index 76e9242782..248f4b28c5 100644 --- a/src/Utility/PersisterHelper.php +++ b/src/Utility/PersisterHelper.php @@ -4,11 +4,21 @@ declare(strict_types=1); namespace Doctrine\ORM\Utility; +use BackedEnum; +use Doctrine\DBAL\ArrayParameterType; +use Doctrine\DBAL\ParameterType; +use Doctrine\DBAL\Types\Type; use Doctrine\ORM\EntityManagerInterface; use Doctrine\ORM\Mapping\ClassMetadata; +use Doctrine\ORM\Proxy\DefaultProxyClassNameResolver; use Doctrine\ORM\Query\QueryException; use RuntimeException; +use function array_map; +use function array_merge; +use function assert; +use function is_array; +use function is_object; use function sprintf; /** @@ -105,4 +115,121 @@ class PersisterHelper $class->getName(), )); } + + /** + * Infers field types to be used by parameter type casting. + * + * @return list + * @phpstan-return list + * + * @throws QueryException + */ + public static function inferParameterTypes( + string $field, + mixed $value, + ClassMetadata $class, + EntityManagerInterface $em, + ): array { + $types = []; + + switch (true) { + case isset($class->fieldMappings[$field]): + $types = array_merge($types, [$class->fieldMappings[$field]->type]); + break; + + case isset($class->associationMappings[$field]): + $assoc = $em->getMetadataFactory()->getOwningSide($class->associationMappings[$field]); + $class = $em->getClassMetadata($assoc->targetEntity); + + if ($assoc->isManyToManyOwningSide()) { + $columns = $assoc->relationToTargetKeyColumns; + } else { + assert($assoc->isToOneOwningSide()); + $columns = $assoc->sourceToTargetKeyColumns; + } + + foreach ($columns as $column) { + $types[] = self::getTypeOfColumn($column, $class, $em); + } + + break; + + default: + $types[] = ParameterType::STRING; + break; + } + + if (is_array($value)) { + return array_map(self::getArrayBindingType(...), $types); + } + + return $types; + } + + /** @phpstan-return ArrayParameterType::* */ + private static function getArrayBindingType(ParameterType|int|string $type): ArrayParameterType|int + { + if (! $type instanceof ParameterType) { + $type = Type::getType((string) $type)->getBindingType(); + } + + return match ($type) { + ParameterType::STRING => ArrayParameterType::STRING, + ParameterType::INTEGER => ArrayParameterType::INTEGER, + ParameterType::ASCII => ArrayParameterType::ASCII, + ParameterType::BINARY => ArrayParameterType::BINARY, + }; + } + + /** + * Converts a value to the type and value required to bind it as a parameter. + * + * @return list + */ + public static function convertToParameterValue(mixed $value, EntityManagerInterface $em): array + { + if (is_array($value)) { + $newValue = []; + + foreach ($value as $itemValue) { + $newValue = array_merge($newValue, self::convertToParameterValue($itemValue, $em)); + } + + return [$newValue]; + } + + return self::convertIndividualValue($value, $em); + } + + /** @phpstan-return list */ + private static function convertIndividualValue(mixed $value, EntityManagerInterface $em): array + { + if (! is_object($value)) { + return [$value]; + } + + if ($value instanceof BackedEnum) { + return [$value->value]; + } + + $valueClass = DefaultProxyClassNameResolver::getClass($value); + + if ($em->getMetadataFactory()->isTransient($valueClass)) { + return [$value]; + } + + $class = $em->getClassMetadata($valueClass); + + if ($class->isIdentifierComposite) { + $newValue = []; + + foreach ($class->getIdentifierValues($value) as $innerValue) { + $newValue = array_merge($newValue, self::convertToParameterValue($innerValue, $em)); + } + + return $newValue; + } + + return [$em->getUnitOfWork()->getSingleIdentifierValue($value)]; + } } diff --git a/tests/Tests/Models/Enums/BookCategory.php b/tests/Tests/Models/Enums/BookCategory.php new file mode 100644 index 0000000000..90a08bd9b0 --- /dev/null +++ b/tests/Tests/Models/Enums/BookCategory.php @@ -0,0 +1,30 @@ +books = new ArrayCollection(); + } +} diff --git a/tests/Tests/Models/Enums/BookGenre.php b/tests/Tests/Models/Enums/BookGenre.php new file mode 100644 index 0000000000..f585d86e26 --- /dev/null +++ b/tests/Tests/Models/Enums/BookGenre.php @@ -0,0 +1,11 @@ +genre = $genre; + $this->categories = new ArrayCollection(); + } +} diff --git a/tests/Tests/Models/Enums/Library.php b/tests/Tests/Models/Enums/Library.php new file mode 100644 index 0000000000..c1f44b13eb --- /dev/null +++ b/tests/Tests/Models/Enums/Library.php @@ -0,0 +1,30 @@ +books = new ArrayCollection(); + } +} diff --git a/tests/Tests/ORM/ConfigurationTest.php b/tests/Tests/ORM/ConfigurationTest.php index 59dcf3e7f4..9063900001 100644 --- a/tests/Tests/ORM/ConfigurationTest.php +++ b/tests/Tests/ORM/ConfigurationTest.php @@ -18,8 +18,8 @@ use Doctrine\ORM\Proxy\ProxyFactory; use Doctrine\Persistence\Mapping\Driver\MappingDriver; use Doctrine\Tests\Models\DDC753\DDC753CustomRepository; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RequiresPhp; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; use PHPUnit\Framework\TestCase; use Psr\Cache\CacheItemPoolInterface; @@ -39,7 +39,7 @@ class ConfigurationTest extends TestCase $this->configuration = new Configuration(); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testSetGetProxyDir(): void { self::assertNull($this->configuration->getProxyDir()); // defaults @@ -48,7 +48,7 @@ class ConfigurationTest extends TestCase self::assertSame(__DIR__, $this->configuration->getProxyDir()); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testSetGetAutoGenerateProxyClasses(): void { self::assertSame(ProxyFactory::AUTOGENERATE_ALWAYS, $this->configuration->getAutoGenerateProxyClasses()); // defaults @@ -63,7 +63,7 @@ class ConfigurationTest extends TestCase self::assertSame(ProxyFactory::AUTOGENERATE_FILE_NOT_EXISTS, $this->configuration->getAutoGenerateProxyClasses()); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testSetGetProxyNamespace(): void { self::assertNull($this->configuration->getProxyNamespace()); // defaults @@ -222,7 +222,7 @@ class ConfigurationTest extends TestCase } #[RequiresPhp('8.4')] - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testDisablingNativeLazyObjectsIsDeprecated(): void { $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12005'); diff --git a/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php index 82b9d0b8ac..47b3a098de 100644 --- a/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php +++ b/tests/Tests/ORM/Functional/EagerFetchOneToManyWithCompositeKeyTest.php @@ -7,10 +7,11 @@ namespace Doctrine\Tests\ORM\Functional; use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity; use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel; use Doctrine\Tests\OrmFunctionalTestCase; +use PHPUnit\Framework\Attributes\Group; final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCase { - /** @ticket 11154 */ + #[Group('GH11154')] public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void { $this->setUpEntitySchema([RootEntity::class, SecondLevel::class]); diff --git a/tests/Tests/ORM/Functional/EnumTest.php b/tests/Tests/ORM/Functional/EnumTest.php index ad97693ace..a374a769f6 100644 --- a/tests/Tests/ORM/Functional/EnumTest.php +++ b/tests/Tests/ORM/Functional/EnumTest.php @@ -4,6 +4,8 @@ declare(strict_types=1); namespace Doctrine\Tests\ORM\Functional; +use Doctrine\Common\Collections\Criteria; +use Doctrine\Common\Collections\Expr\Comparison; use Doctrine\DBAL\Types\EnumType; use Doctrine\ORM\AbstractQuery; use Doctrine\ORM\Mapping\Column; @@ -13,10 +15,14 @@ use Doctrine\ORM\Tools\SchemaTool; use Doctrine\Tests\Mocks\AttributeDriverFactory; use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums; use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum; +use Doctrine\Tests\Models\Enums\BookCategory; +use Doctrine\Tests\Models\Enums\BookGenre; +use Doctrine\Tests\Models\Enums\BookWithGenre; use Doctrine\Tests\Models\Enums\Card; use Doctrine\Tests\Models\Enums\CardNativeEnum; use Doctrine\Tests\Models\Enums\CardWithDefault; use Doctrine\Tests\Models\Enums\CardWithNullable; +use Doctrine\Tests\Models\Enums\Library; use Doctrine\Tests\Models\Enums\Product; use Doctrine\Tests\Models\Enums\Quantity; use Doctrine\Tests\Models\Enums\Scale; @@ -25,6 +31,7 @@ use Doctrine\Tests\Models\Enums\TypedCard; use Doctrine\Tests\Models\Enums\TypedCardNativeEnum; use Doctrine\Tests\Models\Enums\Unit; use Doctrine\Tests\OrmFunctionalTestCase; +use Generator; use PHPUnit\Framework\Attributes\DataProvider; use function class_exists; @@ -528,4 +535,68 @@ EXCEPTION self::assertSame(Suit::Hearts, $card->suit); } + + #[DataProvider('provideGenreMatchingExpressions')] + public function testEnumCollectionMatchingOnOneToMany(Comparison $comparison): void + { + $this->setUpEntitySchema([BookWithGenre::class, Library::class, BookCategory::class]); + + $library = new Library(); + + $fictionBook = new BookWithGenre(BookGenre::FICTION); + $fictionBook->library = $library; + + $nonfictionBook = new BookWithGenre(BookGenre::NON_FICTION); + $nonfictionBook->library = $library; + + $this->_em->persist($library); + $this->_em->persist($nonfictionBook); + $this->_em->persist($fictionBook); + + $this->_em->flush(); + $this->_em->clear(); + + $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)); + + self::assertCount(1, $result); + self::assertSame($nonfictionBook->id, $result[0]->id); + } + + #[DataProvider('provideGenreMatchingExpressions')] + public function testEnumCollectionMatchingOnManyToMany(Comparison $comparison): void + { + $this->setUpEntitySchema([Library::class, BookWithGenre::class, BookCategory::class]); + + $category = new BookCategory(); + + $fictionBook = new BookWithGenre(BookGenre::FICTION); + $fictionBook->categories->add($category); + + $nonfictionBook = new BookWithGenre(BookGenre::NON_FICTION); + $nonfictionBook->categories->add($category); + + $this->_em->persist($category); + $this->_em->persist($nonfictionBook); + $this->_em->persist($fictionBook); + + $this->_em->flush(); + $this->_em->clear(); + + $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)); + + self::assertCount(1, $result); + self::assertSame($nonfictionBook->id, $result[0]->id); + } + + public static function provideGenreMatchingExpressions(): Generator + { + yield [Criteria::expr()->eq('genre', BookGenre::NON_FICTION)]; + yield [Criteria::expr()->in('genre', [BookGenre::NON_FICTION])]; + } } diff --git a/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php b/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php index 352bffd3b2..fdb97e38f0 100644 --- a/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php +++ b/tests/Tests/ORM/Functional/ManyToManyBasicAssociationTest.php @@ -18,6 +18,7 @@ use PHPUnit\Framework\Attributes\Group; use function assert; use function class_exists; +use function get_class; /** * Basic many-to-many association tests. @@ -573,6 +574,44 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection'); } + public function testMatchingWithInCondition(): void + { + $user = $this->addCmsUserGblancoWithGroups(2); + $this->_em->clear(); + + $user = $this->_em->find(get_class($user), $user->id); + + $groups = $user->groups; + self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); + + $criteria = Criteria::create()->where(Criteria::expr()->in('name', ['Developers_1'])); + $result = $groups->matching($criteria); + + self::assertCount(1, $result); + self::assertEquals('Developers_1', $result[0]->name); + + self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection'); + } + + public function testMatchingWithNotInCondition(): void + { + $user = $this->addCmsUserGblancoWithGroups(2); + $this->_em->clear(); + + $user = $this->_em->find(get_class($user), $user->id); + + $groups = $user->groups; + self::assertFalse($user->groups->isInitialized(), 'Pre-condition: lazy collection'); + + $criteria = Criteria::create()->where(Criteria::expr()->notIn('name', ['Developers_0'])); + $result = $groups->matching($criteria); + + self::assertCount(1, $result); + self::assertEquals('Developers_1', $result[0]->name); + + self::assertFalse($user->groups->isInitialized(), 'Post-condition: matching does not initialize collection'); + } + private function removeTransactionCommandsFromQueryLog(): void { $log = $this->getQueryLog(); diff --git a/tests/Tests/ORM/Functional/ParserResultSerializationTest.php b/tests/Tests/ORM/Functional/ParserResultSerializationTest.php index af247a35b8..33be260c29 100644 --- a/tests/Tests/ORM/Functional/ParserResultSerializationTest.php +++ b/tests/Tests/ORM/Functional/ParserResultSerializationTest.php @@ -14,7 +14,7 @@ use Doctrine\ORM\Query\ResultSetMapping; use Doctrine\Tests\OrmFunctionalTestCase; use Generator; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use ReflectionMethod; use Symfony\Component\VarExporter\Instantiator; use Symfony\Component\VarExporter\VarExporter; @@ -35,7 +35,7 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase /** @param Closure(ParserResult): ParserResult $toSerializedAndBack */ #[DataProvider('provideToSerializedAndBack')] - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testSerializeParserResultForQueryWithSqlWalker(Closure $toSerializedAndBack): void { $query = $this->_em @@ -131,7 +131,6 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase private static function parseQuery(Query $query): ParserResult { $r = new ReflectionMethod($query, 'parse'); - $r->setAccessible(true); return $r->invoke($query); } diff --git a/tests/Tests/ORM/Functional/QueryCacheTest.php b/tests/Tests/ORM/Functional/QueryCacheTest.php index 891a3ba18c..b2a1703fdf 100644 --- a/tests/Tests/ORM/Functional/QueryCacheTest.php +++ b/tests/Tests/ORM/Functional/QueryCacheTest.php @@ -122,8 +122,7 @@ class QueryCacheTest extends OrmFunctionalTestCase $query = $this->_em->createQuery('select ux from Doctrine\Tests\Models\CMS\CmsUser ux'); - $sqlExecMock = $this->getMockBuilder(AbstractSqlExecutor::class) - ->getMockForAbstractClass(); + $sqlExecMock = $this->createMock(AbstractSqlExecutor::class); $sqlExecMock->expects(self::once()) ->method('execute') diff --git a/tests/Tests/ORM/Functional/SecondLevelCacheCountQueriesTest.php b/tests/Tests/ORM/Functional/SecondLevelCacheCountQueriesTest.php index e7402066d2..bc72f23446 100644 --- a/tests/Tests/ORM/Functional/SecondLevelCacheCountQueriesTest.php +++ b/tests/Tests/ORM/Functional/SecondLevelCacheCountQueriesTest.php @@ -48,7 +48,6 @@ class SecondLevelCacheCountQueriesTest extends SecondLevelCacheFunctionalTestCas if ($cacheUsage === 0) { $metadataCacheReflection = new ReflectionProperty(ClassMetadata::class, 'cache'); - $metadataCacheReflection->setAccessible(true); $metadataCacheReflection->setValue($metadata, null); return; diff --git a/tests/Tests/ORM/Functional/Ticket/DDC3042Test.php b/tests/Tests/ORM/Functional/Ticket/DDC3042Test.php index c20799d56b..afd89902b8 100644 --- a/tests/Tests/ORM/Functional/Ticket/DDC3042Test.php +++ b/tests/Tests/ORM/Functional/Ticket/DDC3042Test.php @@ -11,6 +11,8 @@ use Doctrine\ORM\Mapping\Id; use Doctrine\Tests\OrmFunctionalTestCase; use PHPUnit\Framework\Attributes\Group; +use function substr_count; + #[Group('DDC-3042')] class DDC3042Test extends OrmFunctionalTestCase { @@ -23,14 +25,18 @@ class DDC3042Test extends OrmFunctionalTestCase public function testSQLGenerationDoesNotProvokeAliasCollisions(): void { - self::assertStringNotMatchesFormat( - '%sfield11%sfield11%s', - $this + self::assertSame( + 1, + substr_count( + $this ->_em ->createQuery( 'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b WITH 1 = 1', ) ->getSQL(), + 'field_11', + ), + 'The alias "field11" should only appear once in the SQL query.', ); } } diff --git a/tests/Tests/ORM/Functional/Ticket/GH10049/GH10049Test.php b/tests/Tests/ORM/Functional/Ticket/GH10049/GH10049Test.php index 7822a71c80..a85ccdfa8c 100644 --- a/tests/Tests/ORM/Functional/Ticket/GH10049/GH10049Test.php +++ b/tests/Tests/ORM/Functional/Ticket/GH10049/GH10049Test.php @@ -5,6 +5,7 @@ declare(strict_types=1); namespace Doctrine\Tests\ORM\Functional\Ticket\GH10049; use Doctrine\Tests\OrmFunctionalTestCase; +use PHPUnit\Framework\Attributes\DoesNotPerformAssertions; class GH10049Test extends OrmFunctionalTestCase { @@ -18,7 +19,7 @@ class GH10049Test extends OrmFunctionalTestCase ); } - /** @doesNotPerformAssertions */ + #[DoesNotPerformAssertions] public function testInheritedReadOnlyPropertyValueCanBeSet(): void { $child = new ReadOnlyPropertyInheritor(10049); diff --git a/tests/Tests/ORM/Hydration/AbstractHydratorTest.php b/tests/Tests/ORM/Hydration/AbstractHydratorTest.php index d83ee1a864..33911800b7 100644 --- a/tests/Tests/ORM/Hydration/AbstractHydratorTest.php +++ b/tests/Tests/ORM/Hydration/AbstractHydratorTest.php @@ -54,8 +54,9 @@ class AbstractHydratorTest extends OrmFunctionalTestCase $this->hydrator = $this ->getMockBuilder(AbstractHydrator::class) + ->onlyMethods(['hydrateAllData']) ->setConstructorArgs([$mockEntityManagerInterface]) - ->getMockForAbstractClass(); + ->getMock(); } /** diff --git a/tests/Tests/ORM/Mapping/ClassMetadataTest.php b/tests/Tests/ORM/Mapping/ClassMetadataTest.php index e6de96fbe3..6afa073791 100644 --- a/tests/Tests/ORM/Mapping/ClassMetadataTest.php +++ b/tests/Tests/ORM/Mapping/ClassMetadataTest.php @@ -54,7 +54,7 @@ use DoctrineGlobalArticle; use LogicException; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group as TestGroup; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use ReflectionClass; use stdClass; @@ -1126,7 +1126,7 @@ class ClassMetadataTest extends OrmTestCase $metadata->addLifecycleCallback('foo', 'bar'); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testGettingAnFQCNForNullIsDeprecated(): void { $metadata = new ClassMetadata(self::class); @@ -1165,7 +1165,7 @@ class ClassMetadataTest extends OrmTestCase ); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testDiscriminatorMapWithSameClassMultipleTimesDeprecated(): void { $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/3519'); diff --git a/tests/Tests/ORM/Mapping/DefaultQuoteStrategyTest.php b/tests/Tests/ORM/Mapping/DefaultQuoteStrategyTest.php index 013e0f4486..27b5defab4 100644 --- a/tests/Tests/ORM/Mapping/DefaultQuoteStrategyTest.php +++ b/tests/Tests/ORM/Mapping/DefaultQuoteStrategyTest.php @@ -5,7 +5,7 @@ declare(strict_types=1); namespace Doctrine\Tests\ORM\Mapping; use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Schema\Name\UnquotedIdentifierFolding; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Mapping\DefaultQuoteStrategy; use Doctrine\Tests\Models\NonPublicSchemaJoins\User as NonPublicSchemaUser; @@ -13,8 +13,6 @@ use Doctrine\Tests\OrmTestCase; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\Group; -use function assert; -use function enum_exists; use function sprintf; /** @@ -29,8 +27,7 @@ class DefaultQuoteStrategyTest extends OrmTestCase $em = $this->getTestEntityManager(); $metadata = $em->getClassMetadata(NonPublicSchemaUser::class); $strategy = new DefaultQuoteStrategy(); - $platform = $this->getMockForAbstractClass(AbstractPlatform::class, enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : []); - assert($platform instanceof AbstractPlatform); + $platform = new SQLitePlatform(); self::assertSame( 'readers.author_reader', diff --git a/tests/Tests/ORM/Mapping/TableMappingTest.php b/tests/Tests/ORM/Mapping/TableMappingTest.php index 50ceaa5fce..74d45d1b06 100644 --- a/tests/Tests/ORM/Mapping/TableMappingTest.php +++ b/tests/Tests/ORM/Mapping/TableMappingTest.php @@ -6,14 +6,14 @@ namespace Doctrine\Tests\ORM\Mapping; use Doctrine\Deprecations\PHPUnit\VerifyDeprecations; use Doctrine\ORM\Mapping\Table; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; final class TableMappingTest extends TestCase { use VerifyDeprecations; - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testDeprecationOnIndexesPropertyIsTriggered(): void { $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11357'); @@ -21,7 +21,7 @@ final class TableMappingTest extends TestCase new Table(indexes: []); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testDeprecationOnUniqueConstraintsPropertyIsTriggered(): void { $this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11357'); diff --git a/tests/Tests/ORM/ORMSetupTest.php b/tests/Tests/ORM/ORMSetupTest.php index a45f4471b1..67377d2eb1 100644 --- a/tests/Tests/ORM/ORMSetupTest.php +++ b/tests/Tests/ORM/ORMSetupTest.php @@ -11,9 +11,9 @@ use Doctrine\ORM\Mapping\Driver\AttributeDriver; use Doctrine\ORM\Mapping\Driver\XmlDriver; use Doctrine\ORM\ORMSetup; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RequiresPhpExtension; use PHPUnit\Framework\Attributes\RequiresSetting; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; use PHPUnit\Framework\TestCase; use ReflectionProperty; use Symfony\Component\Cache\Adapter\AbstractAdapter; @@ -28,7 +28,7 @@ class ORMSetupTest extends TestCase { use VerifyDeprecations; - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testAttributeConfiguration(): void { if (PHP_VERSION_ID >= 80400) { @@ -51,7 +51,7 @@ class ORMSetupTest extends TestCase self::assertInstanceOf(AttributeDriver::class, $config->getMetadataDriverImpl()); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testXMLConfiguration(): void { if (PHP_VERSION_ID >= 80400) { @@ -104,7 +104,7 @@ class ORMSetupTest extends TestCase } #[Group('DDC-1350')] - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testConfigureProxyDir(): void { $config = ORMSetup::createAttributeMetadataConfiguration([], true, '/foo'); diff --git a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php index f0ab8a0ac6..4d337aacd4 100644 --- a/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php +++ b/tests/Tests/ORM/Persisters/BasicEntityPersisterTypeValueSqlTest.php @@ -68,8 +68,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase $platform = $this->getMockBuilder(AbstractPlatform::class) ->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : []) - ->onlyMethods(['supportsIdentityColumns']) - ->getMockForAbstractClass(); + ->getMock(); $platform->method('supportsIdentityColumns') ->willReturn(true); diff --git a/tests/Tests/ORM/Persisters/ManyToManyPersisterTest.php b/tests/Tests/ORM/Persisters/ManyToManyPersisterTest.php index e3a78dba89..7457699f8f 100644 --- a/tests/Tests/ORM/Persisters/ManyToManyPersisterTest.php +++ b/tests/Tests/ORM/Persisters/ManyToManyPersisterTest.php @@ -6,8 +6,7 @@ namespace Doctrine\Tests\ORM\Persisters; use Doctrine\DBAL\Connection; use Doctrine\DBAL\Driver; -use Doctrine\DBAL\Platforms\AbstractPlatform; -use Doctrine\DBAL\Schema\Name\UnquotedIdentifierFolding; +use Doctrine\DBAL\Platforms\SQLitePlatform; use Doctrine\ORM\Persisters\Collection\ManyToManyPersister; use Doctrine\Tests\Models\ManyToManyPersister\ChildClass; use Doctrine\Tests\Models\ManyToManyPersister\OtherParentClass; @@ -16,8 +15,6 @@ use Doctrine\Tests\OrmTestCase; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\Group; -use function enum_exists; - #[CoversClass(ManyToManyPersister::class)] final class ManyToManyPersisterTest extends OrmTestCase { @@ -34,7 +31,7 @@ final class ManyToManyPersisterTest extends OrmTestCase ->onlyMethods(['executeStatement', 'getDatabasePlatform']) ->getMock(); $connection->method('getDatabasePlatform') - ->willReturn($this->getMockForAbstractClass(AbstractPlatform::class, enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : [])); + ->willReturn(new SQLitePlatform()); $parent = new ParentClass(1); $otherParent = new OtherParentClass(42); diff --git a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php index 4283fa6e1a..2295fd274c 100644 --- a/tests/Tests/ORM/Proxy/ProxyFactoryTest.php +++ b/tests/Tests/ORM/Proxy/ProxyFactoryTest.php @@ -22,9 +22,9 @@ use Doctrine\Tests\Models\Company\CompanyPerson; use Doctrine\Tests\Models\ECommerce\ECommerceFeature; use Doctrine\Tests\OrmTestCase; use PHPUnit\Framework\Attributes\Group; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\Attributes\RequiresMethod; use PHPUnit\Framework\Attributes\RequiresPhp; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; use ReflectionClass; use ReflectionProperty; use stdClass; @@ -251,7 +251,7 @@ class ProxyFactoryTest extends OrmTestCase #[RequiresPhp('8.4')] #[RequiresMethod(ProxyHelper::class, 'generateLazyGhost')] - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testProxyFactoryTriggersDeprecationWhenNativeLazyObjectsAreDisabled(): void { $this->emMock->getConfiguration()->enableNativeLazyObjects(false); diff --git a/tests/Tests/ORM/Query/ParserResultTest.php b/tests/Tests/ORM/Query/ParserResultTest.php index af9831bf77..b18d083057 100644 --- a/tests/Tests/ORM/Query/ParserResultTest.php +++ b/tests/Tests/ORM/Query/ParserResultTest.php @@ -37,7 +37,7 @@ class ParserResultTest extends TestCase public function testSetGetSqlExecutor(): void { - $executor = $this->getMockForAbstractClass(AbstractSqlExecutor::class); + $executor = $this->createMock(AbstractSqlExecutor::class); $this->parserResult->setSqlExecutor($executor); self::assertSame($executor, $this->parserResult->getSqlExecutor()); } diff --git a/tests/Tests/ORM/Query/QueryTest.php b/tests/Tests/ORM/Query/QueryTest.php index a2bfed19a6..172856c9f5 100644 --- a/tests/Tests/ORM/Query/QueryTest.php +++ b/tests/Tests/ORM/Query/QueryTest.php @@ -596,12 +596,11 @@ class QueryTest extends OrmTestCase { $driverConnection = $this->createMock(Driver\Connection::class); $driverConnection->method('query') - ->will($this->onConsecutiveCalls(...$results)); + ->willReturnOnConsecutiveCalls(...$results); $platform = $this->getMockBuilder(AbstractPlatform::class) ->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : []) - ->onlyMethods(['supportsIdentityColumns']) - ->getMockForAbstractClass(); + ->getMock(); $platform->method('supportsIdentityColumns') ->willReturn(true); diff --git a/tests/Tests/ORM/Repository/DefaultRepositoryFactoryTest.php b/tests/Tests/ORM/Repository/DefaultRepositoryFactoryTest.php index 83d39881f5..0bcffbd36b 100644 --- a/tests/Tests/ORM/Repository/DefaultRepositoryFactoryTest.php +++ b/tests/Tests/ORM/Repository/DefaultRepositoryFactoryTest.php @@ -111,7 +111,7 @@ class DefaultRepositoryFactoryTest extends TestCase private function buildClassMetadata(string $className): ClassMetadata&MockObject { $metadata = $this->createMock(ClassMetadata::class); - $metadata->method('getName')->will(self::returnValue($className)); + $metadata->method('getName')->willReturn($className); $metadata->name = $className; $metadata->customRepositoryClassName = null; diff --git a/tests/Tests/ORM/Tools/Pagination/PaginatorTest.php b/tests/Tests/ORM/Tools/Pagination/PaginatorTest.php index 16f0c28e12..361b98bcef 100644 --- a/tests/Tests/ORM/Tools/Pagination/PaginatorTest.php +++ b/tests/Tests/ORM/Tools/Pagination/PaginatorTest.php @@ -30,8 +30,7 @@ class PaginatorTest extends OrmTestCase { $platform = $this->getMockBuilder(AbstractPlatform::class) ->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : []) - ->onlyMethods(['supportsIdentityColumns']) - ->getMockForAbstractClass(); + ->getMock(); $platform->method('supportsIdentityColumns') ->willReturn(true); diff --git a/tests/Tests/ORM/UnitOfWorkTest.php b/tests/Tests/ORM/UnitOfWorkTest.php index 0dc82357ab..3cbc94a21c 100644 --- a/tests/Tests/ORM/UnitOfWorkTest.php +++ b/tests/Tests/ORM/UnitOfWorkTest.php @@ -570,8 +570,7 @@ class UnitOfWorkTest extends OrmTestCase { $platform = $this->getMockBuilder(AbstractPlatform::class) ->setConstructorArgs(enum_exists(UnquotedIdentifierFolding::class) ? [UnquotedIdentifierFolding::UPPER] : []) - ->onlyMethods(['supportsIdentityColumns']) - ->getMockForAbstractClass(); + ->getMock(); $platform->method('supportsIdentityColumns') ->willReturn(true); diff --git a/tests/Tests/OrmFunctionalTestCase.php b/tests/Tests/OrmFunctionalTestCase.php index ecf6102695..d437bce68c 100644 --- a/tests/Tests/OrmFunctionalTestCase.php +++ b/tests/Tests/OrmFunctionalTestCase.php @@ -539,7 +539,7 @@ abstract class OrmFunctionalTestCase extends OrmTestCase $conn = static::$sharedConn; // In case test is skipped, tearDown is called, but no setup may have run - if (! $conn) { + if (! $conn || ! isset($this->_em)) { return; } diff --git a/tests/Tests/OrmTestCase.php b/tests/Tests/OrmTestCase.php index dba73f5be9..a6ae8d2d62 100644 --- a/tests/Tests/OrmTestCase.php +++ b/tests/Tests/OrmTestCase.php @@ -145,7 +145,7 @@ abstract class OrmTestCase extends TestCase $connection = $this->getMockBuilder(Connection::class) ->setConstructorArgs([[], $this->createDriverMock($platform)]) ->onlyMethods(['quote']) - ->getMockForAbstractClass(); + ->getMock(); $connection->method('quote')->willReturnCallback(static fn (string $input) => sprintf("'%s'", $input)); return $connection; diff --git a/tests/Tests/Proxy/AutoloaderTest.php b/tests/Tests/Proxy/AutoloaderTest.php index 3604037292..8c30c4f37b 100644 --- a/tests/Tests/Proxy/AutoloaderTest.php +++ b/tests/Tests/Proxy/AutoloaderTest.php @@ -6,7 +6,7 @@ namespace Doctrine\Tests\Proxy; use Doctrine\ORM\Proxy\Autoloader; use PHPUnit\Framework\Attributes\DataProvider; -use PHPUnit\Framework\Attributes\WithoutErrorHandler; +use PHPUnit\Framework\Attributes\IgnoreDeprecations; use PHPUnit\Framework\TestCase; use function class_exists; @@ -32,7 +32,7 @@ class AutoloaderTest extends TestCase /** @param class-string $className */ #[DataProvider('dataResolveFile')] - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testResolveFile( string $proxyDir, string $proxyNamespace, @@ -43,7 +43,7 @@ class AutoloaderTest extends TestCase self::assertEquals($expectedProxyFile, $actualProxyFile); } - #[WithoutErrorHandler] + #[IgnoreDeprecations] public function testAutoload(): void { if (file_exists(sys_get_temp_dir() . '/AutoloaderTestClass.php')) {