Merge branch '3.5.x' into 3.6.x

* 3.5.x:
  Add a CI job that fails on deprecations (#12188)
  use the empty string instead of null as an array offset (#12181)
  do not call setAccessible() on PHP >= 8.1 (#12182)
  Fix docs on final entities (#12176)
  Remove Database and Model First chapters that said little of value.
  Switch to IgnoreDeprecations
  docs: consistent PostgreSQL's name case
  docs: generation strategies differences between DBAL 3 and 4
  Check extra condition to decide if a test was skipped
  Use PHPUnit 11 when possible
  Migrate away from annotations in tests
  Migrate away from assertStringNotMatchesFormat()
  Migrate to willReturn()
  Migrate away from getMockForAbstractClass()
  Fix `IN`/`NOT IN` expression handling and support enums when matching on to-many-collections
This commit is contained in:
Alexander M. Turek
2025-10-07 16:07:07 +02:00
47 changed files with 550 additions and 305 deletions

View File

@@ -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

View File

@@ -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"

View File

@@ -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**)
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -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 <reference-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

View File

@@ -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

View File

@@ -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

View File

@@ -1,26 +0,0 @@
Getting Started: Database First
===============================
.. note:: *Development Workflows*
When you :doc:`Code First <getting-started>`, you
start with developing Objects and then map them onto your database. When
you :doc:`Model First <getting-started-models>`, 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.

View File

@@ -1,24 +0,0 @@
Getting Started: Model First
============================
.. note:: *Development Workflows*
When you :doc:`Code First <getting-started>`, 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 <getting-started-database>`, 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.

View File

@@ -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 <reference-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.

View File

@@ -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\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\} but returns array\{array\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\}\.$#'
identifier: return.type
count: 1
path: src/Persisters/Entity/BasicEntityPersister.php
-
message: '#^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:expandParameters\(\) should return array\{list\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\} but returns array\{array\<mixed\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\>\}\.$#'
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\<mixed\> but returns array\<mixed\>\.$#'
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\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\> but returns list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|int\>\.$#'
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\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|string\> but returns list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|int\>\.$#'
identifier: return.type
count: 1
path: src/Utility/PersisterHelper.php

View File

@@ -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~'

View File

@@ -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

View File

@@ -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)];
}
}

View File

@@ -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<ParameterType|ArrayParameterType|int|string>
* @phpstan-return list<ParameterType::*|ArrayParameterType::*|string>
*
* @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<mixed>
*/
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);

View File

@@ -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);

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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<ParameterType|int|string>
* @phpstan-return list<ParameterType::*|ArrayParameterType::*|string>
*
* @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<mixed>
*/
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<mixed> */
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)];
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
#[Entity]
class BookCategory
{
#[Id]
#[Column]
#[GeneratedValue]
public int $id;
#[ManyToMany(targetEntity: BookWithGenre::class, mappedBy: 'categories')]
public Collection $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
}

View File

@@ -0,0 +1,11 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
enum BookGenre: string
{
case FICTION = 'fiction';
case NON_FICTION = 'non fiction';
}

View File

@@ -0,0 +1,38 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\ManyToMany;
use Doctrine\ORM\Mapping\ManyToOne;
#[Entity]
class BookWithGenre
{
#[Id]
#[GeneratedValue]
#[Column]
public int $id;
#[ManyToOne(targetEntity: Library::class, inversedBy: 'books')]
public Library $library;
#[Column(enumType: BookGenre::class)]
public BookGenre $genre;
#[ManyToMany(targetEntity: BookCategory::class, inversedBy: 'books')]
public Collection $categories;
public function __construct(BookGenre $genre)
{
$this->genre = $genre;
$this->categories = new ArrayCollection();
}
}

View File

@@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\Enums;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
use Doctrine\ORM\Mapping\Id;
use Doctrine\ORM\Mapping\OneToMany;
#[Entity]
class Library
{
#[Id]
#[GeneratedValue]
#[Column]
public int $id;
#[OneToMany(targetEntity: BookWithGenre::class, mappedBy: 'library')]
public Collection $books;
public function __construct()
{
$this->books = new ArrayCollection();
}
}

View File

@@ -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');

View File

@@ -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]);

View File

@@ -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])];
}
}

View File

@@ -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();

View File

@@ -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);
}

View File

@@ -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')

View File

@@ -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;

View File

@@ -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.',
);
}
}

View File

@@ -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);

View File

@@ -54,8 +54,9 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
$this->hydrator = $this
->getMockBuilder(AbstractHydrator::class)
->onlyMethods(['hydrateAllData'])
->setConstructorArgs([$mockEntityManagerInterface])
->getMockForAbstractClass();
->getMock();
}
/**

View File

@@ -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');

View File

@@ -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',

View File

@@ -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');

View File

@@ -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');

View File

@@ -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);

View File

@@ -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);

View File

@@ -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);

View File

@@ -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());
}

View File

@@ -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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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);

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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')) {