Compare commits

...

68 Commits
3.0.0 ... 3.0.3

Author SHA1 Message Date
Alexander M. Turek
c3cc0fdd8c Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Fix annotation
  Bump CI workflows (#11336)
  Fix SchemaTool::getSchemaFromMetadata() uniqueConstraint without a predefined name (#11314)
2024-03-03 17:13:16 +01:00
Alexander M. Turek
e3e96745cc Fix annotation 2024-03-03 16:49:00 +01:00
Alexander M. Turek
21221f73cc Bump CI workflows (#11336) 2024-03-03 16:46:12 +01:00
Rok Motaln
ab5e9e393b Fix SchemaTool::getSchemaFromMetadata() uniqueConstraint without a predefined name (#11314)
* Fix loading SchemaTool::getSchemaFromMetadata() uniqueConstraint without a name

Fixes a type miss-match exception when reading a UniqueConstraint defined on an Entity which doesn't have a predefined name.

* Fix deprecation on DBAL 3

---------

Co-authored-by: Alexander M. Turek <me@derrabus.de>
2024-03-03 16:02:48 +01:00
Alexander M. Turek
b8d0a85017 Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Psalm 5.22.2 (#11326)
2024-03-01 10:51:50 +01:00
Alexander M. Turek
52a6a21387 Psalm 5.22.2 (#11326) 2024-03-01 10:47:18 +01:00
Alexander M. Turek
bf49055a1f Use enum_exists() for enums 2024-03-01 08:56:07 +01:00
Alexander M. Turek
694413a888 Remove PHP 7 workarounds (#11324) 2024-03-01 08:51:21 +01:00
Alexander M. Turek
20a6efdff6 Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  PHPStan 1.10.59 (#11320)
2024-02-29 16:52:42 +01:00
Alexander M. Turek
4fc8629414 PHPStan 1.10.59 (#11320) 2024-02-29 16:47:35 +01:00
Grégoire Paris
95da667862 Merge remote-tracking branch 'origin/2.18.x' into 3.0.x 2024-02-28 22:57:35 +01:00
Alexander M. Turek
feb27f00c1 Address deprecations from Collection 2.2 (#11315) 2024-02-27 17:37:52 +01:00
Grégoire Paris
b187bc8588 Merge pull request #11308 from greg0ire/throw-instead-of-assert
Throw a full-fledged exception on invalid call
2024-02-26 20:38:49 +01:00
Grégoire Paris
719d007a81 Merge pull request #11298 from VincentLanglet/sqlWalkerPhpdoc
Fix sqlWalker::walkSimpleArithmeticExpression phpdoc
2024-02-26 08:21:47 +01:00
Grégoire Paris
3f7a3333ad Throw a full-fledged exception on invalid call
In 2.x, getAssociationMappedByTargetField() used to return null when
called with the owning side of an association.
That was undocumented and wrong because the phpdoc advertises a string
as a return type.

In 6ce0cf4a3d, I wrongly assumed that
nobody would be calling this method with the owning side of an
association.

Let us throw a full fledged exception and advertise the proper way of
avoiding this situation.

Closes #11250
2024-02-25 21:49:03 +01:00
Grégoire Paris
2a8802af12 Merge pull request #11305 from doctrine/typo
Remove extra word
2024-02-25 13:20:55 +01:00
Grégoire Paris
9cc11d2541 Remove extra word 2024-02-25 11:20:44 +01:00
Grégoire Paris
3907872046 Merge pull request #11302 from greg0ire/3.0.x
Merge 2.18.x up into 3.0.x
2024-02-24 21:03:34 +01:00
Grégoire Paris
54cd70002c Merge remote-tracking branch 'origin/2.18.x' into 3.0.x 2024-02-24 20:47:36 +01:00
Grégoire Paris
76c4539ffa Merge pull request #11293 from greg0ire/wrong-type
Remove wrong annotation about return type
2024-02-24 13:05:08 +01:00
Vincent Langlet
0f8d193512 Fix sql walker phpdoc 2024-02-23 15:11:15 +01:00
Grégoire Paris
cc314d0fb7 Remove wrong annotation about return type
Although this method is guaranteed to return either null or something
that can be used as a fully qualified class name, it never actually
checks that the class actually exists. Adding such a check breaks
several tests, including some that expect a exceptions at some later
points in the execution.
2024-02-22 23:14:52 +01:00
Alexander M. Turek
2a250b5814 Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Backport QueryParameterTest (#11288)
2024-02-22 13:23:53 +01:00
Alexander M. Turek
e6eef1a97d Backport QueryParameterTest (#11288) 2024-02-22 13:22:44 +01:00
Alexander M. Turek
44fa5d340a Merge pull request #11287 from derrabus/bugfix/parameter-types
Allow (Array)ParameterType in QueryBuilder
2024-02-22 09:31:34 +01:00
Alexander M. Turek
708146bbbc Test different ways of settings query parameters 2024-02-22 09:19:39 +01:00
Vincent Langlet
a5bf9bb96a Be less restrictive in DiscriminatorColumnMapping phpdoc (#11226)
* Be less restrictive in params

* Allow null options

* Simplify expression

* Fix ci

* Add support for null
2024-02-22 09:12:39 +01:00
Hanish Singla
3eace16e85 Allow (Array)ParameterType in QueryBuilder 2024-02-22 00:01:05 +01:00
Alexander M. Turek
bc5efd4bfe Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Fix Static Analysis folder reference (#11281)
  docs: recommend safer way to disable logging (#11269)
  Remove unused baseline entries
  Treat '0' as a legitimate trim char
2024-02-21 18:54:21 +01:00
Karoly Gossler
0efac09141 Fix Static Analysis folder reference (#11281) 2024-02-21 18:51:21 +01:00
Ondřej Mirtes
b6b4cbcb93 Remove broken assertion from DateAddFunction and DateSubFunction (#11243)
* Remove wrong asserts in DATE_ADD and DATE_SUB query AST function handlers

* Require DBAL 3.8.2
2024-02-20 20:29:56 +01:00
Grégoire Paris
efb6cebd41 Merge pull request #11270 from greg0ire/fix-trim-bug
Treat '0' as a legitimate trim char
2024-02-18 23:53:25 +01:00
Grégoire Paris
e4769d3191 docs: recommend safer way to disable logging (#11269)
* Remove trailing newlines

* Recommend safer way to disable logging

Resetting the middlewares on the configuration object will only work if
the connection object hasn't been built from that configuration object
yet. Instead, people should find the logger bound to the logging
middleware and disable it.
2024-02-18 15:51:05 +01:00
Grégoire Paris
cf408ad9ae Remove unused baseline entries 2024-02-18 12:26:18 +01:00
Grégoire Paris
7c29078051 Treat '0' as a legitimate trim char
Because of a loose comparison, it was not.
2024-02-18 11:34:10 +01:00
Grégoire Paris
d5ba106803 Merge pull request #11268 from greg0ire/3.0.x
Merge 2.18.x up into 3.0.x
2024-02-17 21:37:33 +01:00
Grégoire Paris
f9a4adc8ab Merge remote-tracking branch 'origin/2.18.x' into 3.0.x 2024-02-17 19:59:26 +01:00
Grégoire Paris
401a0c4fe9 Merge pull request #11266 from greg0ire/more-valid-docs
More valid docs
2024-02-17 19:57:34 +01:00
Grégoire Paris
dba9d72b2d Add type field mapper documentation to the sidebar 2024-02-17 15:10:28 +01:00
Grégoire Paris
fe0647053a Mark document as orphan
It is here for backward compatibilty reasons.
2024-02-17 15:06:46 +01:00
Grégoire Paris
7b3db4a037 Use correction sectionauthor syntax 2024-02-17 14:59:24 +01:00
Grégoire Paris
6672aaf165 Merge pull request #11265 from greg0ire/remove-unneeded-verify-depr
Remove unused trait
2024-02-17 14:55:10 +01:00
Grégoire Paris
aa3b331cae Remove unused trait 2024-02-17 11:14:47 +01:00
Thomas Landauer
3918dcfb42 [Documentation] Adding link to Postgres upgrade article (#11257)
* [Documentation] Adding link to Postgres upgrade article

* Update UPGRADE.md

* Update UPGRADE.md
2024-02-15 22:12:57 +01:00
Grégoire Paris
bfb033fe3c Merge pull request #11256 from greg0ire/3.0.x
Merge 2.18.x up into 3.0.x
2024-02-13 12:07:29 +01:00
Grégoire Paris
bf86155dc2 Merge remote-tracking branch 'origin/2.18.x' into 3.0.x 2024-02-13 11:29:26 +01:00
Grégoire Paris
1d218bae30 Make docs valid according to guides 0.3.3 (#11252) 2024-02-12 23:46:09 +01:00
Simon Podlipsky
9acc70d5b8 fix: support array-type arg in QB variadic calls (#11242) 2024-02-09 15:23:22 +01:00
Alexander M. Turek
5a40b99e11 Merge branch '2.18.x' into 3.0.x
* 2.18.x:
  Point link to correct upgrade guide (#11220)
  Ignore subclasses without discriminatorValue when generating discriminator column condition SQL (#11200)
  Update branches in README
2024-02-07 13:44:56 +01:00
Alexander M. Turek
94144e1227 Revert "Merge pull request #11229 from greg0ire/add-columns"
This reverts commit 599dd58fe1, reversing
changes made to 1854ce2d32.
2024-02-07 09:36:32 +01:00
Grégoire Paris
599dd58fe1 Merge pull request #11229 from greg0ire/add-columns
Add columns for 3.1.x and 4.0x
2024-02-07 07:56:20 +01:00
Grégoire Paris
aff543a4ff Add columns for 3.1.x and 4.0x 2024-02-06 23:12:13 +01:00
Andrey Bolonin
1854ce2d32 Update version ORM from 2 to 3 in docs (#11221) 2024-02-06 07:09:30 +01:00
Benjamin Morel
b00f0c258e Clean up outdated sentence (#11224)
The static create() method is gone in version 3
2024-02-05 22:58:19 +01:00
Grégoire Paris
13a79b068c Merge pull request #11222 from andreybolonin/patch-3
Update README.md
2024-02-05 08:36:56 +01:00
Andrey Bolonin
27c9e9cab3 Update README.md 2024-02-05 09:31:22 +03:00
Grégoire Paris
40fbbf4429 Point link to correct upgrade guide (#11220) 2024-02-04 17:41:45 +01:00
Alexander M. Turek
00ed2ca991 Bump dependencies in the "getting started" docs page (#11219) 2024-02-04 17:40:24 +01:00
Alexander M. Turek
54b7ad2073 DoctrineSetup was renamed to ORMSetup (#11218) 2024-02-04 17:40:04 +01:00
Michael Skvortsov
6f98147d09 Ignore subclasses without discriminatorValue when generating discriminator column condition SQL (#11200)
After commit 4e8e3ef30b when `\Doctrine\ORM\Query\SqlWalker` generates dicsriminator column condition SQL (method `\Doctrine\ORM\Query\SqlWalker::generateDiscriminatorColumnConditionSQL`) it adds an empty string to the list of possible values if the inheritance hierarchy contains a non-root abstract class. 

When the discriminator column is implemented with a custom type in PostgreSQL (equivalent of Enum) the query fails because the type cannot have a value of an empty string. It boils down to the fact that `\Doctrine\ORM\Mapping\ClassMetadataInfo::$subClasses` contains an abstract class and in its Metadata the value of `\Doctrine\ORM\Mapping\ClassMetadataInfo::$discriminatorValue` is `null`.

#### Previous behavior

In version 2.14.1 `\Doctrine\ORM\Mapping\ClassMetadataInfo::$subClasses` does not contain an abstract class.

Fixes #11199, fixes #11177, fixes #10846.
---------

Co-authored-by: Michael Skvortsov <michael.skvortsov@eleving.com>
Co-authored-by: Matthias Pigulla <mp@webfactory.de>
2024-02-04 00:11:40 +01:00
Alexander M. Turek
7527b788de Switch back to stable dependencies (#11210) 2024-02-03 21:24:40 +01:00
Grégoire Paris
cfadb5499d Merge pull request #11207 from derrabus/chore/readme
Update branches in README
2024-02-03 20:06:12 +01:00
Grégoire Paris
e52bc846f0 Merge pull request #11209 from greg0ire/update-branch-metdata
Update branch metadata
2024-02-03 20:00:29 +01:00
Alexander M. Turek
9ce9ae2818 Update branches in README 2024-02-03 19:43:49 +01:00
Grégoire Paris
f259754b7c Update branch metadata 2024-02-03 19:40:21 +01:00
Alexander M. Turek
3bc2cb6b15 Merge branch '2.19.x' into 3.0.x
* 2.19.x:
  Update branch metadata
2024-02-03 18:45:55 +01:00
Grégoire Paris
fdb9d44538 Merge pull request #11206 from greg0ire/update-branch-metdata
Update branch metadata
2024-02-03 18:37:27 +01:00
Grégoire Paris
a9fcaf1d18 Update branch metadata 2024-02-03 18:35:43 +01:00
58 changed files with 956 additions and 545 deletions

View File

@@ -5,23 +5,41 @@
"slug": "orm",
"docsSlug": "doctrine-orm",
"versions": [
{
"name": "4.0",
"branchName": "4.0.x",
"slug": "latest",
"upcoming": true
},
{
"name": "3.1",
"branchName": "3.1.x",
"slug": "3.1",
"upcoming": true
},
{
"name": "3.0",
"branchName": "3.0.x",
"slug": "latest",
"slug": "3.0",
"current": true
},
{
"name": "2.19",
"branchName": "2.19.x",
"slug": "2.19",
"upcoming": true
},
{
"name": "2.18",
"branchName": "2.18.x",
"slug": "2.18",
"upcoming": true
"maintained": true
},
{
"name": "2.17",
"branchName": "2.17.x",
"slug": "2.17",
"current": true
"maintained": false
},
{
"name": "2.16",

View File

@@ -91,9 +91,9 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-coverage"
path: "coverage*.xml"
@@ -164,9 +164,9 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
path: "coverage.xml"
@@ -230,7 +230,7 @@ jobs:
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
- name: "Upload coverage file"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage.xml"
@@ -311,7 +311,7 @@ jobs:
ENABLE_SECOND_LEVEL_CACHE: 1
- name: "Upload coverage files"
uses: "actions/upload-artifact@v3"
uses: "actions/upload-artifact@v4"
with:
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
path: "coverage*.xml"
@@ -332,7 +332,7 @@ jobs:
fetch-depth: 2
- name: "Download coverage files"
uses: "actions/download-artifact@v3"
uses: "actions/download-artifact@v4"
with:
path: "reports"

View File

@@ -27,7 +27,7 @@ jobs:
uses: "shivammathur/setup-php@v2"
with:
coverage: "none"
php-version: "8.2"
php-version: "8.3"
- name: "Remove existing composer file"
run: "rm composer.json"
@@ -40,9 +40,5 @@ jobs:
with:
dependency-versions: "highest"
- name: "Add dummy title to the sidebar"
run: |
printf '%s\n%s\n\n%s\n' "Dummy title" "===========" "$(cat docs/en/sidebar.rst)" > docs/en/sidebar.rst
- name: "Run guides-cli"
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'Unknown directive' | ( ! grep WARNING )"
run: "vendor/bin/guides -vvv --no-progress docs/en 2>&1 | grep -v 'No template found for rendering directive' | ( ! grep WARNING )"

View File

@@ -7,7 +7,7 @@ on:
jobs:
release:
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@3.0.0"
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@4.0.0"
secrets:
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}

View File

@@ -10,7 +10,7 @@ on:
- src/**
- phpstan*
- psalm*
- tests/Doctrine/StaticAnalysis/**
- tests/StaticAnalysis/**
push:
branches:
- "*.x"
@@ -20,7 +20,7 @@ on:
- src/**
- phpstan*
- psalm*
- tests/Doctrine/StaticAnalysis/**
- tests/StaticAnalysis/**
jobs:
static-analysis-phpstan:
@@ -32,7 +32,7 @@ jobs:
include:
- dbal-version: default
config: phpstan.neon
- dbal-version: 3.7
- dbal-version: 3.8.2
config: phpstan-dbal3.neon
steps:
@@ -43,7 +43,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "8.2"
php-version: "8.3"
tools: cs2pr
- name: Require specific DBAL version
@@ -65,7 +65,7 @@ jobs:
matrix:
dbal-version:
- default
- 3.7
- 3.8.2
steps:
- name: "Checkout code"
@@ -75,7 +75,7 @@ jobs:
uses: shivammathur/setup-php@v2
with:
coverage: none
php-version: "8.2"
php-version: "8.3"
tools: cs2pr
- name: Require specific DBAL version

View File

@@ -1,11 +1,11 @@
| [3.0.x][3.0] | [2.18.x][2.18] | [2.17.x][2.17] |
|:----------------:|:----------------:|:----------:|
| [![Build status][3.0 image]][3.0] | [![Build status][2.18 image]][2.18] | [![Build status][2.17 image]][2.17] |
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.18 coverage image]][2.18 coverage] | [![Coverage Status][2.17 coverage image]][2.17 coverage] |
| [4.0.x][4.0] | [3.1.x][3.1] | [3.0.x][3.0] | [2.19.x][2.19] | [2.18.x][2.18] |
|:------------------------------------------------------:|:------------------------------------------------------:|:-------------------------------------------------------:|:--------------------------------------------------------:|:---------------------------------------------------------:|
| [![Build status][4.0 image]][4.0] | [![Build status][3.1 image]][3.1] | [![Build status][3.0 image]][3.0] | [![Build status][2.19 image]][2.19] | [![Build status][2.18 image]][2.18] |
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.1 coverage image]][3.1 coverage] | [![Coverage Status][3.0 coverage image]][3.0 coverage] | [![Coverage Status][2.19 coverage image]][2.19 coverage] | [![Coverage Status][2.18 coverage image]][2.18 coverage] |
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
Doctrine ORM is an object-relational mapper for PHP 7.1+ that provides transparent persistence
Doctrine ORM is an object-relational mapper for PHP 8.1+ that provides transparent persistence
for PHP objects. It sits on top of a powerful database abstraction layer (DBAL). One of its key features
is the option to write database queries in a proprietary object oriented SQL dialect called Doctrine Query Language (DQL),
inspired by Hibernate's HQL. This provides developers with a powerful alternative to SQL that maintains flexibility
@@ -18,15 +18,23 @@ without requiring unnecessary code duplication.
* [Documentation](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/index.html)
[4.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=4.0.x
[4.0]: https://github.com/doctrine/orm/tree/4.0.x
[4.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/4.0.x/graph/badge.svg
[4.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/4.0.x
[3.1 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.1.x
[3.1]: https://github.com/doctrine/orm/tree/3.1.x
[3.1 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.1.x/graph/badge.svg
[3.1 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.1.x
[3.0 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.0.x
[3.0]: https://github.com/doctrine/orm/tree/3.0.x
[3.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.0.x/graph/badge.svg
[3.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.0.x
[2.19 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.19.x
[2.19]: https://github.com/doctrine/orm/tree/2.19.x
[2.19 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.19.x/graph/badge.svg
[2.19 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.19.x
[2.18 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.18.x
[2.18]: https://github.com/doctrine/orm/tree/2.18.x
[2.18 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.18.x/graph/badge.svg
[2.18 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.18.x
[2.17 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.17.x
[2.17]: https://github.com/doctrine/orm/tree/2.17.x
[2.17 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.17.x/graph/badge.svg
[2.17 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.17.x

View File

@@ -1,5 +1,16 @@
# Upgrade to 3.0
## BC BREAK: Calling `ClassMetadata::getAssociationMappedByTargetField()` with the owning side of an association now throws an exception
Previously, calling
`Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField()` with
the owning side of an association returned `null`, which was undocumented, and
wrong according to the phpdoc of the parent method.
If you do not know whether you are on the owning or inverse side of an association,
you can use `Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide()`
to find out.
## BC BREAK: `Doctrine\ORM\Proxy\Autoloader` no longer extends `Doctrine\Common\Proxy\Autoloader`
Make sure to use the former when writing a type declaration or an `instanceof` check.
@@ -13,9 +24,9 @@ so `$targetEntity` is a first argument now. This change affects only non-named a
When using the `AUTO` strategy to let Doctrine determine the identity generation mechanism for
an entity, and when using `doctrine/dbal` 4, PostgreSQL now uses `IDENTITY`
instead of `SEQUENCE`. When upgrading from ORM 2.x and preference is on keeping
the `SEQUENCE` based identity generation, then configure the ORM this way:
instead of `SEQUENCE` or `SERIAL`.
* If you want to upgrade your existing tables to identity columns, you will need to follow [migration to identity columns on PostgreSQL](https://www.doctrine-project.org/projects/doctrine-dbal/en/4.0/how-to/postgresql-identity-migration.html)
* If you want to keep using SQL sequences, you need to configure the ORM this way:
```php
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\ORM\Configuration;
@@ -495,8 +506,8 @@ The methods have been replaced by PSR-6 compatible counterparts
## BC BREAK: Remove `Doctrine\ORM\Configuration::newDefaultAnnotationDriver`
This functionality has been moved to the new `DoctrineSetup` class. Call
`Doctrine\ORM\Tools\DoctrineSetup::createDefaultAnnotationDriver()` to create
This functionality has been moved to the new `ORMSetup` class. Call
`Doctrine\ORM\ORMSetup::createDefaultAnnotationDriver()` to create
a new annotation driver.
## BC BREAK: Remove `Doctrine\ORM\Tools\Setup`
@@ -504,7 +515,7 @@ a new annotation driver.
In our effort to migrate from Doctrine Cache to PSR-6, the `Setup` class which
accepted a Doctrine Cache instance in each method has been removed.
The replacement is `Doctrine\ORM\Tools\DoctrineSetup` which accepts a PSR-6
The replacement is `Doctrine\ORM\ORMSetup` which accepts a PSR-6
cache instead.
## BC BREAK: Removed named queries

View File

@@ -24,7 +24,7 @@
"composer-runtime-api": "^2",
"ext-ctype": "*",
"doctrine/collections": "^2.1",
"doctrine/dbal": "^3.6 || ^4",
"doctrine/dbal": "^3.8.2 || ^4",
"doctrine/deprecations": "^0.5.3 || ^1",
"doctrine/event-manager": "^1.2 || ^2",
"doctrine/inflector": "^1.4 || ^2.0",
@@ -38,14 +38,13 @@
"require-dev": {
"doctrine/coding-standard": "^12.0",
"phpbench/phpbench": "^1.0",
"phpstan/phpstan": "1.10.35",
"phpstan/phpstan": "1.10.59",
"phpunit/phpunit": "^10.4.0",
"psr/log": "^1 || ^2 || ^3",
"squizlabs/php_codesniffer": "3.7.2",
"symfony/cache": "^5.4 || ^6.2 || ^7.0",
"vimeo/psalm": "5.16.0"
"vimeo/psalm": "5.22.2"
},
"minimum-stability": "RC",
"suggest": {
"ext-dom": "Provides support for XSD validation for XML mapping files",
"symfony/cache": "Provides cache support for Setup Tool with doctrine/cache 2.0"

View File

@@ -1,7 +1,7 @@
Implementing ArrayAccess for Domain Objects
===========================================
.. sectionauthor:: Roman Borschel (roman@code-factory.org)
.. sectionauthor:: Roman Borschel <roman@code-factory.org>
This recipe will show you how to implement ArrayAccess for your
domain objects in order to allow more uniform access, for example

View File

@@ -1,4 +1,4 @@
Welcome to Doctrine 2 ORM's documentation!
Welcome to Doctrine ORM's documentation!
==========================================
The Doctrine documentation is comprised of tutorials, a reference section and
@@ -93,7 +93,7 @@ Tutorials
Changelogs
----------
* `Upgrade <https://github.com/doctrine/doctrine2/blob/master/UPGRADE.md>`_
* `Upgrade <https://github.com/doctrine/orm/blob/HEAD/UPGRADE.md>`_
Cookbook
--------

View File

@@ -377,7 +377,7 @@ Here is the list of possible generation strategies:
a new entity is passed to ``EntityManager#persist``. NONE is the
same as leaving off the ``#[GeneratedValue]`` entirely.
- ``CUSTOM``: With this option, you can use the ``#[CustomIdGenerator]`` attribute.
It will allow you to pass a :ref:`class of your own to generate the identifiers.<attrref_customidgenerator>`
It will allow you to pass a :ref:`class of your own to generate the identifiers. <attrref_customidgenerator>`
Sequence Generator
^^^^^^^^^^^^^^^^^^

View File

@@ -18,14 +18,20 @@ especially what the strategies presented here provide help with.
.. note::
Having an SQL logger enabled when processing batches can have a serious impact on performance and resource usage.
To avoid that you should remove the corresponding middleware.
To remove all middlewares, you can use this line:
Having an SQL logger enabled when processing batches can have a
serious impact on performance and resource usage.
To avoid that, you should use a PSR logger implementation that can be
disabled at runtime.
For example, with Monolog, you can use ``Logger::pushHandler()``
to push a ``NullHandler`` to the logger instance, and then pop it
when you need to enable logging again.
With DBAL 2, you can disable the SQL logger like below:
.. code-block:: php
<?php
$em->getConnection()->getConfiguration()->setMiddlewares([]); // DBAL 3
$em->getConnection()->getConfiguration()->setSQLLogger(null); // DBAL 2
$em->getConnection()->getConfiguration()->setSQLLogger(null);
Bulk Inserts
------------
@@ -188,6 +194,3 @@ problems using the following approach:
Iterating results is not possible with queries that
fetch-join a collection-valued association. The nature of such SQL
result sets is not suitable for incremental hydration.

View File

@@ -131,47 +131,47 @@ There are two ways to set up an event handler:
* For *all events* you can create a Lifecycle Event Listener or Subscriber class and register
it by calling ``$eventManager->addEventListener()`` or ``eventManager->addEventSubscriber()``,
see
:ref:`Listening and subscribing to Lifecycle Events<listening-and-subscribing-to-lifecycle-events>`
:ref:`Listening and subscribing to Lifecycle Events <listening-and-subscribing-to-lifecycle-events>`
* For *some events* (see table below), you can create a *Lifecycle Callback* method in the
entity, see :ref:`Lifecycle Callbacks<lifecycle-callbacks>`.
entity, see :ref:`Lifecycle Callbacks <lifecycle-callbacks>`.
.. _reference-events-lifecycle-events:
Events Overview
---------------
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| Event | Dispatched by | Lifecycle | Passed |
| | | Callback | Argument |
+=================================================================+=======================+===========+=====================================+
| :ref:`preRemove<reference-events-pre-remove>` | ``$em->remove()`` | Yes | `PreRemoveEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postRemove<reference-events-post-update-remove-persist>` | ``$em->flush()`` | Yes | `PostRemoveEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`prePersist<reference-events-pre-persist>` | ``$em->persist()`` | Yes | `PrePersistEventArgs`_ |
| | on *initial* persist | | |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postPersist<reference-events-post-update-remove-persist>` | ``$em->flush()`` | Yes | `PostPersistEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`preUpdate<reference-events-pre-update>` | ``$em->flush()`` | Yes | `PreUpdateEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postUpdate<reference-events-post-update-remove-persist>` | ``$em->flush()`` | Yes | `PostUpdateEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postLoad<reference-events-post-load>` | Loading from database | Yes | `PostLoadEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`loadClassMetadata<reference-events-load-class-metadata>` | Loading of mapping | No | `LoadClassMetadataEventArgs`_ |
| | metadata | | |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| ``onClassMetadataNotFound`` | ``MappingException`` | No | `OnClassMetadataNotFoundEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`preFlush<reference-events-pre-flush>` | ``$em->flush()`` | Yes | `PreFlushEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`onFlush<reference-events-on-flush>` | ``$em->flush()`` | No | `OnFlushEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postFlush<reference-events-post-flush>` | ``$em->flush()`` | No | `PostFlushEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`onClear<reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| Event | Dispatched by | Lifecycle | Passed |
| | | Callback | Argument |
+==================================================================+=======================+===========+=====================================+
| :ref:`preRemove <reference-events-pre-remove>` | ``$em->remove()`` | Yes | `PreRemoveEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postRemove <reference-events-post-update-remove-persist>` | ``$em->flush()`` | Yes | `PostRemoveEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`prePersist <reference-events-pre-persist>` | ``$em->persist()`` | Yes | `PrePersistEventArgs`_ |
| | on *initial* persist | | |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postPersist <reference-events-post-update-remove-persist>` | ``$em->flush()`` | Yes | `PostPersistEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`preUpdate <reference-events-pre-update>` | ``$em->flush()`` | Yes | `PreUpdateEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postUpdate <reference-events-post-update-remove-persist>` | ``$em->flush()`` | Yes | `PostUpdateEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postLoad <reference-events-post-load>` | Loading from database | Yes | `PostLoadEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`loadClassMetadata <reference-events-load-class-metadata>` | Loading of mapping | No | `LoadClassMetadataEventArgs`_ |
| | metadata | | |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| ``onClassMetadataNotFound`` | ``MappingException`` | No | `OnClassMetadataNotFoundEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`preFlush <reference-events-pre-flush>` | ``$em->flush()`` | Yes | `PreFlushEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`onFlush <reference-events-on-flush>` | ``$em->flush()`` | No | `OnFlushEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`postFlush <reference-events-post-flush>` | ``$em->flush()`` | No | `PostFlushEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
| :ref:`onClear <reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
+------------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
.. warning::
@@ -313,7 +313,7 @@ behaviors across different entity classes.
Note that they require much more detailed knowledge about the inner
workings of the ``EntityManager`` and ``UnitOfWork`` classes. Please
read the :ref:`Implementing Event Listeners<reference-events-implementing-listeners>` section
read the :ref:`Implementing Event Listeners <reference-events-implementing-listeners>` section
carefully if you are trying to write your own listener.
For event subscribers, there are no surprises. They declare the
@@ -426,11 +426,11 @@ prePersist
There are two ways for the ``prePersist`` event to be triggered:
- One is when you call ``EntityManager::persist()``. The
event is also called for all :ref:`cascaded associations<transitive-persistence>`.
event is also called for all :ref:`cascaded associations <transitive-persistence>`.
- The other is inside the ``flush()`` method when changes to associations are computed and
this association is marked as :ref:`cascade: persist<transitive-persistence>`. Any new entity found
this association is marked as :ref:`cascade: persist <transitive-persistence>`. Any new entity found
during this operation is also persisted and ``prePersist`` called
on it. This is called :ref:`persistence by reachability<persistence-by-reachability>`.
on it. This is called :ref:`persistence by reachability <persistence-by-reachability>`.
In both cases you get passed a ``PrePersistEventArgs`` instance
which has access to the entity and the entity manager.
@@ -454,7 +454,7 @@ preRemove
The ``preRemove`` event is called on every entity immediately when it is passed
to the ``EntityManager::remove()`` method. It is cascaded for all
associations that are marked as :ref:`cascade: remove<transitive-persistence>`
associations that are marked as :ref:`cascade: remove <transitive-persistence>`
It is not called for a DQL ``DELETE`` statement.
@@ -502,7 +502,7 @@ entities and their associations have been computed. This means, the
- Collections scheduled for removal
To make use of the ``onFlush`` event you have to be familiar with the
internal :ref:`UnitOfWork<unit-of-work>` API, which grants you access to the previously
internal :ref:`UnitOfWork <unit-of-work>` API, which grants you access to the previously
mentioned sets. See this example:
.. code-block:: php

View File

@@ -101,7 +101,7 @@ The many-to-many association is only supporting foreign keys in the table defini
To work with many-to-many tables containing extra columns you have to use the
foreign keys as primary keys feature of Doctrine ORM.
See :doc:`the tutorial on composite primary keys for more information<../tutorials/composite-primary-keys>`.
See :doc:`the tutorial on composite primary keys for more information <../tutorials/composite-primary-keys>`.
How can i paginate fetch-joined collections?

View File

@@ -342,7 +342,7 @@ It is not supported to use overrides in entity inheritance scenarios.
.. note::
When using traits, make sure not to miss the warnings given in the
:doc:`Limitations and Known Issues</reference/limitations-and-known-issues>` chapter.
:doc:`Limitations and Known Issues </reference/limitations-and-known-issues>` chapter.
Association Override

View File

@@ -1,3 +1,5 @@
:orphan:
Installation
============

View File

@@ -1,3 +1,5 @@
:orphan:
.. toc::
.. tocheader:: Tutorials
@@ -31,6 +33,7 @@
reference/inheritance-mapping
reference/working-with-objects
reference/working-with-associations
reference/typedfieldmapper
reference/events
reference/unitofwork
reference/unitofwork-associations

View File

@@ -82,9 +82,9 @@ that directory with the following contents:
{
"require": {
"doctrine/orm": "^2.11.0",
"doctrine/dbal": "^3.2",
"symfony/cache": "^5.4"
"doctrine/orm": "^3",
"doctrine/dbal": "^4",
"symfony/cache": "^7"
},
"autoload": {
"psr-0": {"": "src/"}

View File

@@ -125,11 +125,6 @@ parameters:
count: 1
path: src/EntityRepository.php
-
message: "#^Method Doctrine\\\\ORM\\\\Mapping\\\\ClassMetadata\\:\\:fullyQualifiedClassName\\(\\) should return class\\-string\\|null but returns string\\|null\\.$#"
count: 1
path: src/Mapping/ClassMetadata.php
-
message: "#^If condition is always true\\.$#"
count: 1

View File

@@ -30,7 +30,7 @@ parameters:
message: '~^Result of method Doctrine\\DBAL\\Connection::commit\(\) \(void\) is used\.$~'
path: src/UnitOfWork.php
-
message: '~^Strict comparison using === between void and false will always evaluate to false\.$~'
message: '~^Strict comparison using === between null and false will always evaluate to false\.$~'
path: src/UnitOfWork.php
-
message: '~^Variable \$e on left side of \?\? always exists and is not nullable\.$~'

File diff suppressed because it is too large Load Diff

View File

@@ -170,6 +170,12 @@
<file name="src/Mapping/ClassMetadataFactory.php"/>
</errorLevel>
</ReferenceConstraintViolation>
<RiskyTruthyFalsyComparison>
<!-- TODO: Enable this new rule on higher branches. -->
<errorLevel type="suppress">
<directory name="src" />
</errorLevel>
</RiskyTruthyFalsyComparison>
<TooManyArguments>
<errorLevel type="suppress">
<!-- Symfony cache supports passing a key prefix to the clear method. -->

View File

@@ -17,6 +17,7 @@ use Doctrine\ORM\Cache\Region;
use Doctrine\ORM\Cache\TimestampCacheKey;
use Doctrine\ORM\Cache\TimestampRegion;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Internal\CriteriaOrderings;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ClassMetadataFactory;
@@ -33,6 +34,8 @@ use function sha1;
abstract class AbstractEntityPersister implements CachedEntityPersister
{
use CriteriaOrderings;
protected UnitOfWork $uow;
protected ClassMetadataFactory $metadataFactory;
@@ -426,7 +429,7 @@ abstract class AbstractEntityPersister implements CachedEntityPersister
*/
public function loadCriteria(Criteria $criteria): array
{
$orderBy = $criteria->getOrderings();
$orderBy = self::getCriteriaOrderings($criteria);
$limit = $criteria->getMaxResults();
$offset = $criteria->getFirstResult();
$query = $this->persister->getSelectSQL($criteria);

View File

@@ -36,8 +36,7 @@ use function method_exists;
* The EntityManager is the central access point to ORM functionality.
*
* It is a facade to all different ORM subsystems such as UnitOfWork,
* Query Language and Repository API. Instantiation is done through
* the static create() method. The quickest way to obtain a fully
* Query Language and Repository API. The quickest way to obtain a fully
* configured EntityManager is:
*
* use Doctrine\ORM\Tools\ORMSetup;

View File

@@ -0,0 +1,50 @@
<?php
declare(strict_types=1);
namespace Doctrine\ORM\Internal;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use function array_map;
use function enum_exists;
use function method_exists;
use function strtoupper;
trait CriteriaOrderings
{
/**
* @return array<string, string>
*
* @psalm-suppress DeprecatedMethod We need to call the deprecated API if the new one does not exist yet.
*/
private static function getCriteriaOrderings(Criteria $criteria): array
{
if (! method_exists(Criteria::class, 'orderings')) {
return $criteria->getOrderings();
}
return array_map(
static fn (Order $order): string => $order->value,
$criteria->orderings(),
);
}
/**
* @param array<string, string> $orderings
*
* @return array<string, string>|array<string, Order>
*/
private static function mapToOrderEnumIfAvailable(array $orderings): array
{
if (! enum_exists(Order::class)) {
return $orderings;
}
return array_map(
static fn (string $order): Order => Order::from(strtoupper($order)),
$orderings,
);
}
}

View File

@@ -41,6 +41,7 @@ use function is_subclass_of;
use function ltrim;
use function method_exists;
use function spl_object_id;
use function sprintf;
use function str_contains;
use function str_replace;
use function strtolower;
@@ -2108,13 +2109,12 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
* @param DiscriminatorColumnMapping|mixed[]|null $columnDef
* @psalm-param DiscriminatorColumnMapping|array{
* name: string|null,
* fieldName?: string,
* type?: string,
* length?: int,
* fieldName?: string|null,
* type?: string|null,
* length?: int|null,
* columnDefinition?: string|null,
* enumType?: class-string<BackedEnum>|null,
* options?:array<string,
* mixed>|null
* options?: array<string, mixed>|null
* }|null $columnDef
*
* @throws MappingException
@@ -2136,13 +2136,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
throw MappingException::duplicateColumnName($this->name, $columnDef['name']);
}
if (! isset($columnDef['fieldName'])) {
$columnDef['fieldName'] = $columnDef['name'];
}
if (! isset($columnDef['type'])) {
$columnDef['type'] = 'string';
}
$columnDef['fieldName'] ??= $columnDef['name'];
$columnDef['type'] ??= 'string';
$columnDef['options'] ??= [];
if (in_array($columnDef['type'], ['boolean', 'array', 'object', 'datetime', 'time', 'date'], true)) {
throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
@@ -2462,17 +2458,25 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
public function getAssociationMappedByTargetField(string $assocName): string
{
$assoc = $this->associationMappings[$assocName];
$assoc = $this->getAssociationMapping($assocName);
assert($assoc instanceof InverseSideMapping);
if (! $assoc instanceof InverseSideMapping) {
throw new LogicException(sprintf(
<<<'EXCEPTION'
Context: Calling %s() with "%s", which is the owning side of an association.
Problem: The owning side of an association has no "mappedBy" field.
Solution: Call %s::isAssociationInverseSide() to check first.
EXCEPTION,
__METHOD__,
$assocName,
self::class,
));
}
return $assoc->mappedBy;
}
/**
* @return string|null null if the input value is null
* @psalm-return class-string|null
*/
/** @return string|null null if the input value is null */
public function fullyQualifiedClassName(string|null $className): string|null
{
if (empty($className)) {

View File

@@ -642,7 +642,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
configuration.
We currently recommend "SEQUENCE" for "%s", when using DBAL 3,
and "IDENTITY" when using DBAL 4,
so you should use probably use the following configuration before upgrading to DBAL 4,
so you should probably use the following configuration before upgrading to DBAL 4,
and remove it after deploying that upgrade:
$configuration->setIdentityGenerationPreferences([

View File

@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping;
use BackedEnum;
use DateInterval;
use DateTime;
use DateTimeImmutable;
@@ -16,6 +17,7 @@ use ReflectionProperty;
use function array_merge;
use function assert;
use function enum_exists;
use function is_a;
/** @psalm-type ScalarName = 'array'|'bool'|'float'|'int'|'string' */
final class DefaultTypedFieldMapper implements TypedFieldMapper
@@ -52,18 +54,18 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
&& ($type instanceof ReflectionNamedType)
) {
if (! $type->isBuiltin() && enum_exists($type->getName())) {
$mapping['enumType'] = $type->getName();
$reflection = new ReflectionEnum($type->getName());
if (! $reflection->isBacked()) {
throw MappingException::backedEnumTypeRequired(
$field->class,
$mapping['fieldName'],
$mapping['enumType'],
$type->getName(),
);
}
$type = $reflection->getBackingType();
assert(is_a($type->getName(), BackedEnum::class, true));
$mapping['enumType'] = $type->getName();
$type = $reflection->getBackingType();
assert($type instanceof ReflectionNamedType);
}

View File

@@ -39,10 +39,10 @@ final class DiscriminatorColumnMapping implements ArrayAccess
* type: string,
* fieldName: string,
* name: string,
* length?: int,
* columnDefinition?: string,
* enumType?: class-string<BackedEnum>,
* options?: array<string, mixed>,
* length?: int|null,
* columnDefinition?: string|null,
* enumType?: class-string<BackedEnum>|null,
* options?: array<string, mixed>|null,
* } $mappingArray
*/
public static function fromMappingArray(array $mappingArray): self
@@ -58,7 +58,7 @@ final class DiscriminatorColumnMapping implements ArrayAccess
}
if (property_exists($mapping, $key)) {
$mapping->$key = $value;
$mapping->$key = $value ?? $mapping->$key;
} else {
throw new Exception('Unknown property ' . $key . ' on class ' . static::class);
}

View File

@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Doctrine\ORM\Mapping\Driver;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\MappingException;
@@ -20,6 +21,7 @@ use function assert;
use function constant;
use function count;
use function defined;
use function enum_exists;
use function explode;
use function extension_loaded;
use function file_get_contents;
@@ -403,9 +405,10 @@ class XmlDriver extends FileDriver
if (isset($oneToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
/** @psalm-suppress DeprecatedConstant */
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
}
$mapping['orderBy'] = $orderBy;
@@ -531,9 +534,10 @@ class XmlDriver extends FileDriver
if (isset($manyToManyElement->{'order-by'})) {
$orderBy = [];
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
/** @psalm-suppress DeprecatedConstant */
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
? (string) $orderByField['direction']
: Criteria::ASC;
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
}
$mapping['orderBy'] = $orderBy;

View File

@@ -9,6 +9,7 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Internal\CriteriaOrderings;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\ToManyAssociationMapping;
@@ -40,6 +41,8 @@ use function spl_object_id;
*/
final class PersistentCollection extends AbstractLazyCollection implements Selectable
{
use CriteriaOrderings;
/**
* A snapshot of the collection at the moment it was fetched from the database.
* This is used to create a diff of the collection at commit time.
@@ -585,7 +588,9 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
$criteria = clone $criteria;
$criteria->where($expression);
$criteria->orderBy($criteria->getOrderings() ?: $association->orderBy());
$criteria->orderBy(self::mapToOrderEnumIfAvailable(
self::getCriteriaOrderings($criteria) ?: $association->orderBy(),
));
$persister = $this->getUnitOfWork()->getEntityPersister($association->targetEntity);

View File

@@ -9,6 +9,7 @@ use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Expr\Comparison;
use Doctrine\DBAL\Exception as DBALException;
use Doctrine\DBAL\LockMode;
use Doctrine\ORM\Internal\CriteriaOrderings;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\InverseSideMapping;
@@ -32,6 +33,8 @@ use function sprintf;
*/
class ManyToManyPersister extends AbstractCollectionPersister
{
use CriteriaOrderings;
public function delete(PersistentCollection $collection): void
{
$mapping = $this->getMapping($collection);
@@ -732,7 +735,7 @@ class ManyToManyPersister extends AbstractCollectionPersister
private function getOrderingSql(Criteria $criteria, ClassMetadata $targetClass): string
{
$orderings = $criteria->getOrderings();
$orderings = self::getCriteriaOrderings($criteria);
if ($orderings) {
$orderBy = [];
foreach ($orderings as $name => $direction) {

View File

@@ -16,6 +16,7 @@ use Doctrine\DBAL\Result;
use Doctrine\DBAL\Types\Type;
use Doctrine\DBAL\Types\Types;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Internal\CriteriaOrderings;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\Mapping\ClassMetadata;
use Doctrine\ORM\Mapping\JoinColumnMapping;
@@ -97,6 +98,7 @@ use function trim;
*/
class BasicEntityPersister implements EntityPersister
{
use CriteriaOrderings;
use LockSqlHelper;
/** @var array<string,string> */
@@ -842,7 +844,7 @@ class BasicEntityPersister implements EntityPersister
*/
public function loadCriteria(Criteria $criteria): array
{
$orderBy = $criteria->getOrderings();
$orderBy = self::getCriteriaOrderings($criteria);
$limit = $criteria->getMaxResults();
$offset = $criteria->getFirstResult();
$query = $this->getSelectSQL($criteria, null, null, $limit, $offset, $orderBy);

View File

@@ -11,8 +11,6 @@ use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\SqlWalker;
use Doctrine\ORM\Query\TokenType;
use function assert;
use function is_numeric;
use function strtolower;
/**
@@ -63,17 +61,10 @@ class DateAddFunction extends FunctionNode
};
}
/**
* @return numeric-string
*
* @throws ASTException
*/
/** @throws ASTException */
private function dispatchIntervalExpression(SqlWalker $sqlWalker): string
{
$sql = $this->intervalExpression->dispatch($sqlWalker);
assert(is_numeric($sql));
return $sql;
return $this->intervalExpression->dispatch($sqlWalker);
}
public function parse(Parser $parser): void

View File

@@ -8,8 +8,6 @@ use Doctrine\ORM\Query\AST\ASTException;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\Query\SqlWalker;
use function assert;
use function is_numeric;
use function strtolower;
/**
@@ -56,16 +54,9 @@ class DateSubFunction extends DateAddFunction
};
}
/**
* @return numeric-string
*
* @throws ASTException
*/
/** @throws ASTException */
private function dispatchIntervalExpression(SqlWalker $sqlWalker): string
{
$sql = $this->intervalExpression->dispatch($sqlWalker);
assert(is_numeric($sql));
return $sql;
return $this->intervalExpression->dispatch($sqlWalker);
}
}

View File

@@ -59,7 +59,7 @@ class TrimFunction extends FunctionNode
$this->trimChar = $lexer->token->value;
}
if ($this->leading || $this->trailing || $this->both || $this->trimChar) {
if ($this->leading || $this->trailing || $this->both || ($this->trimChar !== false)) {
$parser->match(TokenType::T_FROM);
}

View File

@@ -7,10 +7,12 @@ namespace Doctrine\ORM\Query\Expr;
use InvalidArgumentException;
use Stringable;
use function array_key_exists;
use function count;
use function get_debug_type;
use function implode;
use function in_array;
use function is_array;
use function is_string;
use function sprintf;
@@ -33,6 +35,10 @@ abstract class Base implements Stringable
public function __construct(mixed $args = [])
{
if (is_array($args) && array_key_exists(0, $args) && is_array($args[0])) {
$args = $args[0];
}
$this->addMultiple($args);
}

View File

@@ -376,6 +376,10 @@ class SqlWalker
continue;
}
$sqlTableAlias = $this->useSqlTableAliases
? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'
: '';
$conn = $this->em->getConnection();
$values = [];
@@ -384,14 +388,22 @@ class SqlWalker
}
foreach ($class->subClasses as $subclassName) {
$values[] = $conn->quote((string) $this->em->getClassMetadata($subclassName)->discriminatorValue);
$subclassMetadata = $this->em->getClassMetadata($subclassName);
// Abstract entity classes show up in the list of subClasses, but may be omitted
// from the discriminator map. In that case, they have a null discriminator value.
if ($subclassMetadata->discriminatorValue === null) {
continue;
}
$values[] = $conn->quote((string) $subclassMetadata->discriminatorValue);
}
$sqlTableAlias = $this->useSqlTableAliases
? $this->getSQLTableAlias($class->getTableName(), $dqlAlias) . '.'
: '';
$sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')';
if ($values !== []) {
$sqlParts[] = $sqlTableAlias . $class->getDiscriminatorColumn()->name . ' IN (' . implode(', ', $values) . ')';
} else {
$sqlParts[] = '1=0'; // impossible condition
}
}
$sql = implode(' AND ', $sqlParts);

View File

@@ -6,6 +6,9 @@ namespace Doctrine\ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\ORM\Internal\CriteriaOrderings;
use Doctrine\ORM\Internal\QueryType;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\Query\Parameter;
@@ -38,6 +41,8 @@ use function substr;
*/
class QueryBuilder implements Stringable
{
use CriteriaOrderings;
/**
* The array of DQL parts collected.
*
@@ -428,12 +433,12 @@ class QueryBuilder implements Stringable
* ->setParameter('user_id', 1);
* </code>
*
* @param string|int $key The parameter position or name.
* @param string|int|null $type ParameterType::* or \Doctrine\DBAL\Types\Type::* constant
* @param string|int $key The parameter position or name.
* @param ParameterType|ArrayParameterType|string|int|null $type ParameterType::*, ArrayParameterType::* or \Doctrine\DBAL\Types\Type::* constant
*
* @return $this
*/
public function setParameter(string|int $key, mixed $value, string|int|null $type = null): static
public function setParameter(string|int $key, mixed $value, ParameterType|ArrayParameterType|string|int|null $type = null): static
{
$existingParameter = $this->getParameter($key);
@@ -1162,22 +1167,20 @@ class QueryBuilder implements Stringable
}
}
if ($criteria->getOrderings()) {
foreach ($criteria->getOrderings() as $sort => $order) {
$hasValidAlias = false;
foreach ($allAliases as $alias) {
if (str_starts_with($sort . '.', $alias . '.')) {
$hasValidAlias = true;
break;
}
foreach (self::getCriteriaOrderings($criteria) as $sort => $order) {
$hasValidAlias = false;
foreach ($allAliases as $alias) {
if (str_starts_with($sort . '.', $alias . '.')) {
$hasValidAlias = true;
break;
}
if (! $hasValidAlias) {
$sort = $allAliases[0] . '.' . $sort;
}
$this->addOrderBy($sort, $order);
}
if (! $hasValidAlias) {
$sort = $allAliases[0] . '.' . $sort;
}
$this->addOrderBy($sort, $order);
}
// Overwrite limits only if they was set in criteria

View File

@@ -337,7 +337,7 @@ class SchemaTool
if (isset($class->table['uniqueConstraints'])) {
foreach ($class->table['uniqueConstraints'] as $indexName => $indexData) {
$uniqIndex = new Index($indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []);
$uniqIndex = new Index('tmp__' . $indexName, $this->getIndexColumns($class, $indexData), true, false, [], $indexData['options'] ?? []);
foreach ($table->getIndexes() as $tableIndexName => $tableIndex) {
if ($tableIndex->isFulfilledBy($uniqIndex)) {

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Cache\CacheConfiguration;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityRepository;
@@ -26,8 +25,6 @@ use Psr\Cache\CacheItemPoolInterface;
*/
class ConfigurationTest extends TestCase
{
use VerifyDeprecations;
private Configuration $configuration;
protected function setUp(): void

View File

@@ -6,7 +6,6 @@ namespace Doctrine\Tests\ORM;
use Doctrine\Common\EventManager;
use Doctrine\DBAL\Connection;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Exception\EntityManagerClosed;
@@ -27,8 +26,6 @@ use TypeError;
class EntityManagerTest extends OrmTestCase
{
use VerifyDeprecations;
private EntityManagerMock $entityManager;
protected function setUp(): void

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Mapping\AssociationMapping;
use Doctrine\ORM\PersistentCollection;
use Doctrine\ORM\UnitOfWork;
@@ -16,6 +17,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
/**
* Basic many-to-many association tests.
@@ -435,7 +437,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
->orderBy(['name' => Criteria::ASC]);
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
['A', 'B', 'C', 'Developers_0'],
@@ -475,7 +477,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
$user = $this->_em->find($user::class, $user->id);
$criteria = Criteria::create()
->orderBy(['name' => Criteria::ASC]);
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
self::assertEquals(
['A', 'B', 'C'],

View File

@@ -8,7 +8,6 @@ use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Type as DBALType;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Internal\Hydration\HydrationException;
use Doctrine\ORM\Internal\SQLResultCasing;
use Doctrine\ORM\PersistentCollection;
@@ -33,7 +32,6 @@ use PHPUnit\Framework\Attributes\Group;
class NativeQueryTest extends OrmFunctionalTestCase
{
use SQLResultCasing;
use VerifyDeprecations;
private AbstractPlatform|null $platform = null;

View File

@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Functional;
use DateTimeImmutable;
use Doctrine\DBAL\Platforms\SQLitePlatform;
use Doctrine\ORM\AbstractQuery;
use Doctrine\Tests\Models\Company\CompanyEmployee;
use Doctrine\Tests\Models\Company\CompanyManager;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\DataProvider;
@@ -487,4 +488,34 @@ SQL;
$this->_em->flush();
$this->_em->clear();
}
#[Group('GH-11240')]
public function testDateAddWithColumnInterval(): void
{
$query = sprintf(
'SELECT DATE_ADD(CURRENT_TIMESTAMP(), m.salary, \'day\') AS add FROM %s m',
CompanyEmployee::class,
);
$result = $this->_em->createQuery($query)
->setMaxResults(1)
->getSingleResult(AbstractQuery::HYDRATE_ARRAY);
self::assertArrayHasKey('add', $result);
}
#[Group('GH-11240')]
public function testDateSubWithColumnInterval(): void
{
$query = sprintf(
'SELECT DATE_SUB(CURRENT_TIMESTAMP(), m.salary, \'day\') AS add FROM %s m',
CompanyEmployee::class,
);
$result = $this->_em->createQuery($query)
->setMaxResults(1)
->getSingleResult(AbstractQuery::HYDRATE_ARRAY);
self::assertArrayHasKey('add', $result);
}
}

View File

@@ -0,0 +1,124 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\DBAL\ArrayParameterType;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Types\Types;
use Doctrine\Tests\Models\CMS\CmsUser;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
#[Group('GH-11278')]
final class QueryParameterTest extends OrmFunctionalTestCase
{
private int $userId;
protected function setUp(): void
{
$this->useModelSet('cms');
parent::setUp();
$user = new CmsUser();
$user->name = 'John Doe';
$user->username = 'john';
$user2 = new CmsUser();
$user2->name = 'Jane Doe';
$user2->username = 'jane';
$user3 = new CmsUser();
$user3->name = 'Just Bill';
$user3->username = 'bill';
$this->_em->persist($user);
$this->_em->persist($user2);
$this->_em->persist($user3);
$this->_em->flush();
$this->userId = $user->id;
$this->_em->clear();
}
public function testParameterTypeInBuilder(): void
{
$result = $this->_em->createQueryBuilder()
->from(CmsUser::class, 'u')
->select('u.name')
->where('u.id = :id')
->setParameter('id', $this->userId, ParameterType::INTEGER)
->getQuery()
->getArrayResult();
self::assertSame([['name' => 'John Doe']], $result);
}
public function testParameterTypeInQuery(): void
{
$result = $this->_em->createQueryBuilder()
->from(CmsUser::class, 'u')
->select('u.name')
->where('u.id = :id')
->getQuery()
->setParameter('id', $this->userId, ParameterType::INTEGER)
->getArrayResult();
self::assertSame([['name' => 'John Doe']], $result);
}
public function testDbalTypeStringInBuilder(): void
{
$result = $this->_em->createQueryBuilder()
->from(CmsUser::class, 'u')
->select('u.name')
->where('u.id = :id')
->setParameter('id', $this->userId, Types::INTEGER)
->getQuery()
->getArrayResult();
self::assertSame([['name' => 'John Doe']], $result);
}
public function testDbalTypeStringInQuery(): void
{
$result = $this->_em->createQueryBuilder()
->from(CmsUser::class, 'u')
->select('u.name')
->where('u.id = :id')
->getQuery()
->setParameter('id', $this->userId, Types::INTEGER)
->getArrayResult();
self::assertSame([['name' => 'John Doe']], $result);
}
public function testArrayParameterTypeInBuilder(): void
{
$result = $this->_em->createQueryBuilder()
->from(CmsUser::class, 'u')
->select('u.name')
->where('u.username IN (:usernames)')
->orderBy('u.username')
->setParameter('usernames', ['john', 'jane'], ArrayParameterType::STRING)
->getQuery()
->getArrayResult();
self::assertSame([['name' => 'Jane Doe'], ['name' => 'John Doe']], $result);
}
public function testArrayParameterTypeInQuery(): void
{
$result = $this->_em->createQueryBuilder()
->from(CmsUser::class, 'u')
->select('u.name')
->where('u.username IN (:usernames)')
->orderBy('u.username')
->getQuery()
->setParameter('usernames', ['john', 'jane'], ArrayParameterType::STRING)
->getArrayResult();
self::assertSame([['name' => 'Jane Doe'], ['name' => 'John Doe']], $result);
}
}

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\UnitOfWork;
use Doctrine\Tests\Models\DDC117\DDC117ApproveChanges;
use Doctrine\Tests\Models\DDC117\DDC117Article;
@@ -23,8 +22,6 @@ use function count;
#[Group('DDC-117')]
class DDC117Test extends OrmFunctionalTestCase
{
use VerifyDeprecations;
private DDC117Article|null $article1;
private DDC117Article|null $article2;

View File

@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\ORM\Mapping as ORM;
use Doctrine\Tests\OrmFunctionalTestCase;
use Generator;
use PHPUnit\Framework\Attributes\DataProvider;
class GH11199Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
GH11199Root::class,
GH11199Parent::class,
GH11199Foo::class,
GH11199Baz::class,
GH11199AbstractLeaf::class,
]);
}
public static function dqlStatements(): Generator
{
yield ['SELECT e FROM ' . GH11199Root::class . ' e', "/WHERE g0_.asset_type IN \('root', 'foo', 'baz'\)$/"];
yield ['SELECT e FROM ' . GH11199Parent::class . ' e', "/WHERE g0_.asset_type IN \('foo'\)$/"];
yield ['SELECT e FROM ' . GH11199Foo::class . ' e', "/WHERE g0_.asset_type IN \('foo'\)$/"];
yield ['SELECT e FROM ' . GH11199Baz::class . ' e', "/WHERE g0_.asset_type IN \('baz'\)$/"];
yield ['SELECT e FROM ' . GH11199AbstractLeaf::class . ' e', '/WHERE 1=0/'];
}
#[DataProvider('dqlStatements')]
public function testGH11199(string $dql, string $expectedDiscriminatorValues): void
{
$query = $this->_em->createQuery($dql);
$sql = $query->getSQL();
self::assertMatchesRegularExpression($expectedDiscriminatorValues, $sql);
}
}
#[ORM\Entity]
#[ORM\Table(name: 'gh11199')]
#[ORM\InheritanceType('SINGLE_TABLE')]
#[ORM\DiscriminatorColumn(name: 'asset_type', type: 'string')]
#[ORM\DiscriminatorMap([
'root' => GH11199Root::class,
'foo' => GH11199Foo::class,
'baz' => GH11199Baz::class,
])]
class GH11199Root
{
#[ORM\Id]
#[ORM\GeneratedValue(strategy: 'IDENTITY')]
#[ORM\Column(type: 'integer')]
private int|null $id = null;
}
#[ORM\Entity]
abstract class GH11199Parent extends GH11199Root
{
}
#[ORM\Entity]
class GH11199Foo extends GH11199Parent
{
}
#[ORM\Entity]
class GH11199Baz extends GH11199Root
{
}
#[ORM\Entity]
abstract class GH11199AbstractLeaf extends GH11199Root
{
}

View File

@@ -6,6 +6,8 @@ namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
@@ -17,6 +19,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
#[Group('GH7767')]
class GH7767Test extends OrmFunctionalTestCase
@@ -54,7 +57,9 @@ class GH7767Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7767ParentEntity::class, 1);
assert($parent instanceof GH7767ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create()->orderBy(['position' => 'DESC']));
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(['position' => class_exists(Order::class) ? Order::Descending : 'DESC']),
);
self::assertEquals(300, $children[0]->position);
self::assertEquals(200, $children[1]->position);
@@ -70,7 +75,7 @@ class GH7767ParentEntity
#[GeneratedValue]
private int $id;
/** @psalm-var Collection<int, GH7767ChildEntity> */
/** @psalm-var Collection<int, GH7767ChildEntity>&Selectable<int, GH7767ChildEntity> */
#[OneToMany(targetEntity: GH7767ChildEntity::class, mappedBy: 'parent', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
#[OrderBy(['position' => 'ASC'])]
private $children;
@@ -80,7 +85,7 @@ class GH7767ParentEntity
$this->children[] = new GH7767ChildEntity($this, $position);
}
/** @psalm-return Collection<int, GH7767ChildEntity> */
/** @psalm-return Collection<int, GH7767ChildEntity>&Selectable<int, GH7767ChildEntity> */
public function getChildren(): Collection
{
return $this->children;

View File

@@ -6,6 +6,8 @@ namespace Doctrine\Tests\ORM\Functional\Ticket;
use Doctrine\Common\Collections\Collection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\Common\Collections\Selectable;
use Doctrine\ORM\Mapping\Column;
use Doctrine\ORM\Mapping\Entity;
use Doctrine\ORM\Mapping\GeneratedValue;
@@ -17,6 +19,7 @@ use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\Attributes\Group;
use function assert;
use function class_exists;
#[Group('GH7836')]
class GH7836Test extends OrmFunctionalTestCase
@@ -57,7 +60,13 @@ class GH7836Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7836ParentEntity::class, 1);
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create()->orderBy(['position' => 'DESC', 'name' => 'ASC']));
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(
class_exists(Order::class)
? ['position' => Order::Descending, 'name' => Order::Ascending]
: ['position' => 'DESC', 'name' => 'ASC'],
),
);
self::assertSame(200, $children[0]->position);
self::assertSame('baz', $children[0]->name);
@@ -72,7 +81,13 @@ class GH7836Test extends OrmFunctionalTestCase
$parent = $this->_em->find(GH7836ParentEntity::class, 1);
assert($parent instanceof GH7836ParentEntity);
$children = $parent->getChildren()->matching(Criteria::create()->orderBy(['name' => 'ASC', 'position' => 'ASC']));
$children = $parent->getChildren()->matching(
Criteria::create()->orderBy(
class_exists(Order::class)
? ['name' => Order::Ascending, 'position' => Order::Ascending]
: ['name' => 'ASC', 'position' => 'ASC'],
),
);
self::assertSame(100, $children[0]->position);
self::assertSame('bar', $children[0]->name);
@@ -91,7 +106,7 @@ class GH7836ParentEntity
#[GeneratedValue]
private int $id;
/** @var Collection<int, GH7836ChildEntity> */
/** @var Collection<int, GH7836ChildEntity>&Selectable<int, GH7836ChildEntity> */
#[OneToMany(targetEntity: GH7836ChildEntity::class, mappedBy: 'parent', fetch: 'EXTRA_LAZY', cascade: ['persist'])]
#[OrderBy(['position' => 'ASC', 'name' => 'ASC'])]
private $children;
@@ -101,7 +116,7 @@ class GH7836ParentEntity
$this->children[] = new GH7836ChildEntity($this, $position, $name);
}
/** @psalm-return Collection<int, GH7836ChildEntity> */
/** @psalm-return Collection<int, GH7836ChildEntity>&Selectable<int, GH7836ChildEntity> */
public function getChildren(): Collection
{
return $this->children;

View File

@@ -4,7 +4,6 @@ declare(strict_types=1);
namespace Doctrine\Tests\ORM\Mapping;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\EntityRepository;
use Doctrine\ORM\Id\SequenceGenerator as IdSequenceGenerator;
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -42,8 +41,6 @@ use function unserialize;
class BasicInheritanceMappingTest extends OrmTestCase
{
use VerifyDeprecations;
private ClassMetadataFactory $cmf;
protected function setUp(): void

View File

@@ -9,7 +9,6 @@ use Doctrine\DBAL\Connection;
use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Configuration;
use Doctrine\ORM\EntityManagerInterface;
use Doctrine\ORM\Event\OnClassMetadataNotFoundEventArgs;
@@ -55,8 +54,6 @@ use function sprintf;
class ClassMetadataFactoryTest extends OrmTestCase
{
use VerifyDeprecations;
public function testGetMetadataForSingleClass(): void
{
$platform = $this->createMock(AbstractPlatform::class);

View File

@@ -47,6 +47,7 @@ use Doctrine\Tests\Models\TypedProperties\UserTypedWithCustomTypedField;
use Doctrine\Tests\ORM\Mapping\TypedFieldMapper\CustomIntAsStringTypedFieldMapper;
use Doctrine\Tests\OrmTestCase;
use DoctrineGlobalArticle;
use LogicException;
use PHPUnit\Framework\Attributes\Group as TestGroup;
use ReflectionClass;
use stdClass;
@@ -1054,6 +1055,21 @@ class ClassMetadataTest extends OrmTestCase
$metadata->addLifecycleCallback('foo', 'bar');
}
public function testItThrowsOnInvalidCallToGetAssociationMappedByTargetField(): void
{
$metadata = new ClassMetadata(self::class);
$metadata->mapOneToOne(['fieldName' => 'foo', 'targetEntity' => 'bar']);
$this->expectException(LogicException::class);
$this->expectExceptionMessage(<<<'EXCEPTION'
Context: Calling Doctrine\ORM\Mapping\ClassMetadata::getAssociationMappedByTargetField() with "foo", which is the owning side of an association.
Problem: The owning side of an association has no "mappedBy" field.
Solution: Call Doctrine\ORM\Mapping\ClassMetadata::isAssociationInverseSide() to check first.
EXCEPTION);
$metadata->getAssociationMappedByTargetField('foo');
}
}
#[MappedSuperclass]

View File

@@ -162,6 +162,11 @@ class LanguageRecognitionTest extends OrmTestCase
$this->assertValidDQL("SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE TRIM(u.name) = 'someone'");
}
public function testTrimFalsyString(): void
{
$this->assertValidDQL("SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE TRIM('0' FROM u.name) = 'someone'");
}
public function testArithmeticExpressionsSupportedInWherePart(): void
{
$this->assertValidDQL('SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE ((u.id + 5000) * u.id + 3) < 10000000');

View File

@@ -15,7 +15,6 @@ use Doctrine\DBAL\Driver\Result;
use Doctrine\DBAL\ParameterType;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\DBAL\Types\Types;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\Query\Parameter;
use Doctrine\ORM\Query\QueryException;
use Doctrine\ORM\UnitOfWork;
@@ -40,8 +39,6 @@ use function array_map;
class QueryTest extends OrmTestCase
{
use VerifyDeprecations;
/** @var EntityManagerMock */
protected $entityManager;

View File

@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Criteria;
use Doctrine\Common\Collections\Order;
use Doctrine\ORM\Cache;
use Doctrine\ORM\Query;
use Doctrine\ORM\Query\Expr\Join;
@@ -22,6 +23,7 @@ use InvalidArgumentException;
use PHPUnit\Framework\Attributes\Group;
use function array_filter;
use function class_exists;
/**
* Test case for the QueryBuilder class used to build DQL query string in a
@@ -80,6 +82,15 @@ class QueryBuilderTest extends OrmTestCase
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
}
public function testSimpleSelectArray(): void
{
$qb = $this->entityManager->createQueryBuilder()
->from(CmsUser::class, 'u')
->select(['u.id', 'u.username']);
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
}
public function testSimpleDelete(): void
{
$qb = $this->entityManager->createQueryBuilder()
@@ -563,7 +574,7 @@ class QueryBuilderTest extends OrmTestCase
->from(CmsUser::class, 'u');
$criteria = new Criteria();
$criteria->orderBy(['field' => Criteria::DESC]);
$criteria->orderBy(['field' => class_exists(Order::class) ? Order::Descending : Criteria::DESC]);
$qb->addCriteria($criteria);
@@ -580,7 +591,7 @@ class QueryBuilderTest extends OrmTestCase
->join('u.article', 'a');
$criteria = new Criteria();
$criteria->orderBy(['a.field' => Criteria::DESC]);
$criteria->orderBy(['a.field' => class_exists(Order::class) ? Order::Descending : Criteria::DESC]);
$qb->addCriteria($criteria);

View File

@@ -370,6 +370,27 @@ class SchemaToolTest extends OrmTestCase
self::assertTrue($schema->hasTable('first_entity'), 'Table first_entity should exist.');
self::assertFalse($schema->hasTable('second_entity'), 'Table second_entity should not exist.');
}
#[Group('GH-11314')]
public function testLoadUniqueConstraintWithoutName(): void
{
$em = $this->getTestEntityManager();
$entity = $em->getClassMetadata(GH11314Entity::class);
$schemaTool = new SchemaTool($em);
$schema = $schemaTool->getSchemaFromMetadata([$entity]);
self::assertTrue($schema->hasTable('GH11314Entity'));
$tableEntity = $schema->getTable('GH11314Entity');
self::assertTrue($tableEntity->hasIndex('uniq_2d81a3ed5bf54558875f7fd5'));
$tableIndex = $tableEntity->getIndex('uniq_2d81a3ed5bf54558875f7fd5');
self::assertTrue($tableIndex->isUnique());
self::assertSame(['field', 'anotherField'], $tableIndex->getColumns());
}
}
#[Table(options: ['foo' => 'bar', 'baz' => ['key' => 'val']])]
@@ -500,6 +521,21 @@ class IndexByFieldEntity
public $fieldName;
}
#[Entity]
#[UniqueConstraint(columns: ['field', 'anotherField'])]
class GH11314Entity
{
#[Column]
#[Id]
private int $id;
#[Column(name: 'field')]
private string $field;
#[Column(name: 'anotherField')]
private string $anotherField;
}
class IncorrectIndexByFieldEntity
{
/** @var int */

View File

@@ -11,7 +11,6 @@ use Doctrine\DBAL\Driver;
use Doctrine\DBAL\Driver\Statement;
use Doctrine\DBAL\Exception;
use Doctrine\DBAL\Platforms\AbstractPlatform;
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
use Doctrine\ORM\EntityNotFoundException;
use Doctrine\ORM\Exception\EntityIdentityCollisionException;
use Doctrine\ORM\Mapping\ClassMetadata;
@@ -46,8 +45,6 @@ use function uniqid;
*/
class UnitOfWorkTest extends OrmTestCase
{
use VerifyDeprecations;
/**
* SUT
*/