mirror of
https://github.com/doctrine/orm.git
synced 2026-03-24 15:02:22 +01:00
Compare commits
92 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4262eb495b | ||
|
|
d3b47d2cbb | ||
|
|
026f5bfe1b | ||
|
|
0b0f2f4d86 | ||
|
|
0bd839a720 | ||
|
|
b65004fc26 | ||
|
|
d2418ab074 | ||
|
|
39a05e31c9 | ||
|
|
ab156a551c | ||
|
|
2148940290 | ||
|
|
d3538095fd | ||
|
|
0c1bf14729 | ||
|
|
3b8c23c51d | ||
|
|
60d4ea694a | ||
|
|
e923bbc932 | ||
|
|
8cbd34c666 | ||
|
|
8bdefef6d1 | ||
|
|
0f8730a6e5 | ||
|
|
62477b5d42 | ||
|
|
12116aa3c2 | ||
|
|
0aeddd0592 | ||
|
|
2491c4b20d | ||
|
|
08d6167243 | ||
|
|
d4e9276e79 | ||
|
|
cee74faa97 | ||
|
|
9ae2181185 | ||
|
|
3e25efd72b | ||
|
|
47496ed882 | ||
|
|
492745d710 | ||
|
|
67419cf951 | ||
|
|
1237f5c909 | ||
|
|
609e616f2d | ||
|
|
4016d6ba4b | ||
|
|
dcdd46251e | ||
|
|
3d98b43561 | ||
|
|
9f3f70944a | ||
|
|
05e07c0ae0 | ||
|
|
fea42ab984 | ||
|
|
7c347b85c1 | ||
|
|
458b040d93 | ||
|
|
396636a2c2 | ||
|
|
78dd074266 | ||
|
|
ff22a00fcf | ||
|
|
02e8ff9663 | ||
|
|
01fd55e9ea | ||
|
|
2e75a7f1c1 | ||
|
|
152b0e3d65 | ||
|
|
9d11fdd3da | ||
|
|
87f1ba74e0 | ||
|
|
f357a33d23 | ||
|
|
ee70178314 | ||
|
|
ab148d3d9d | ||
|
|
3924c38fab | ||
|
|
9814078a2c | ||
|
|
6de5684fd9 | ||
|
|
c142503a52 | ||
|
|
01c178b297 | ||
|
|
ffa50a777f | ||
|
|
649048f745 | ||
|
|
15537bc218 | ||
|
|
bc95c7c08d | ||
|
|
6982c8ab9d | ||
|
|
3df11d518c | ||
|
|
c1becd54e6 | ||
|
|
e4d7df29c2 | ||
|
|
608705427e | ||
|
|
f0562f4120 | ||
|
|
9f19310f27 | ||
|
|
e38278bfca | ||
|
|
62f2cff218 | ||
|
|
cdd774906b | ||
|
|
96776e091d | ||
|
|
f7470d8a3f | ||
|
|
2c41cc7f1c | ||
|
|
f18de9d569 | ||
|
|
37f76a8381 | ||
|
|
a6c1e63a60 | ||
|
|
b62292256a | ||
|
|
b138395194 | ||
|
|
6881cdff4c | ||
|
|
5bff0919a7 | ||
|
|
9ef0f5301b | ||
|
|
4989ca6f15 | ||
|
|
32d1e97ce7 | ||
|
|
ca8147b148 | ||
|
|
c8ebea77f0 | ||
|
|
23f22860f1 | ||
|
|
b24586b1b5 | ||
|
|
9e5442a892 | ||
|
|
01774c035c | ||
|
|
6f83166266 | ||
|
|
cb8a76ba3a |
@@ -12,42 +12,17 @@
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.6",
|
||||
"branchName": "3.6.x",
|
||||
"slug": "3.6",
|
||||
"name": "3.7",
|
||||
"branchName": "3.7.x",
|
||||
"slug": "3.7",
|
||||
"upcoming": true
|
||||
},
|
||||
{
|
||||
"name": "3.5",
|
||||
"branchName": "3.5.x",
|
||||
"slug": "3.5",
|
||||
"name": "3.6",
|
||||
"branchName": "3.6.x",
|
||||
"slug": "3.6",
|
||||
"current": true
|
||||
},
|
||||
{
|
||||
"name": "3.4",
|
||||
"slug": "3.4",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.3",
|
||||
"slug": "3.3",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.2",
|
||||
"slug": "3.2",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.1",
|
||||
"slug": "3.1",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "3.0",
|
||||
"slug": "3.0",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.21",
|
||||
"branchName": "2.21.x",
|
||||
@@ -89,26 +64,6 @@
|
||||
"name": "2.14",
|
||||
"slug": "2.14",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.13",
|
||||
"slug": "2.13",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.12",
|
||||
"slug": "2.12",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.11",
|
||||
"slug": "2.11",
|
||||
"maintained": false
|
||||
},
|
||||
{
|
||||
"name": "2.10",
|
||||
"slug": "2.10",
|
||||
"maintained": false
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -15,6 +15,6 @@ phpcs.xml.dist export-ignore
|
||||
phpbench.json export-ignore
|
||||
phpstan.neon export-ignore
|
||||
phpstan-baseline.neon export-ignore
|
||||
phpstan-dbal2.neon export-ignore
|
||||
phpstan-dbal3.neon export-ignore
|
||||
phpstan-params.neon export-ignore
|
||||
phpstan-persistence2.neon export-ignore
|
||||
|
||||
2
.github/workflows/coding-standards.yml
vendored
2
.github/workflows/coding-standards.yml
vendored
@@ -24,4 +24,4 @@ on:
|
||||
|
||||
jobs:
|
||||
coding-standards:
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@12.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@13.1.0"
|
||||
|
||||
2
.github/workflows/composer-lint.yml
vendored
2
.github/workflows/composer-lint.yml
vendored
@@ -17,4 +17,4 @@ on:
|
||||
jobs:
|
||||
composer-lint:
|
||||
name: "Composer Lint"
|
||||
uses: "doctrine/.github/.github/workflows/composer-lint.yml@12.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/composer-lint.yml@13.1.0"
|
||||
|
||||
59
.github/workflows/continuous-integration.yml
vendored
59
.github/workflows/continuous-integration.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: "CI"
|
||||
name: "CI: PHPUnit"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
@@ -25,7 +25,14 @@ env:
|
||||
|
||||
jobs:
|
||||
phpunit-smoke-check:
|
||||
name: "PHPUnit with SQLite"
|
||||
name: >
|
||||
SQLite -
|
||||
${{ format('PHP {0} - DBAL {1} - ext. {2} - proxy {3}',
|
||||
matrix.php-version || 'Ø',
|
||||
matrix.dbal-version || 'Ø',
|
||||
matrix.extension || 'Ø',
|
||||
matrix.proxy || 'Ø'
|
||||
) }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
|
||||
strategy:
|
||||
@@ -80,7 +87,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -104,6 +111,10 @@ jobs:
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Downgrade VarExporter"
|
||||
run: 'composer require --no-update "symfony/var-exporter:^6.4 || ^7.4"'
|
||||
if: "${{ matrix.native_lazy == '0' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
with:
|
||||
@@ -139,7 +150,7 @@ jobs:
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v5"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
with:
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -180,7 +191,13 @@ jobs:
|
||||
|
||||
|
||||
phpunit-postgres:
|
||||
name: "PHPUnit with PostgreSQL"
|
||||
name: >
|
||||
${{ format('PostgreSQL {0} - PHP {1} - DBAL {2} - ext. {3}',
|
||||
matrix.postgres-version || 'Ø',
|
||||
matrix.php-version || 'Ø',
|
||||
matrix.dbal-version || 'Ø',
|
||||
matrix.extension || 'Ø'
|
||||
) }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
needs: "phpunit-smoke-check"
|
||||
|
||||
@@ -223,7 +240,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -248,14 +265,20 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v5"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
|
||||
path: "coverage.xml"
|
||||
|
||||
|
||||
phpunit-mariadb:
|
||||
name: "PHPUnit with MariaDB"
|
||||
name: >
|
||||
${{ format('MariaDB {0} - PHP {1} - DBAL {2} - ext. {3}',
|
||||
matrix.mariadb-version || 'Ø',
|
||||
matrix.php-version || 'Ø',
|
||||
matrix.dbal-version || 'Ø',
|
||||
matrix.extension || 'Ø'
|
||||
) }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
needs: "phpunit-smoke-check"
|
||||
|
||||
@@ -291,7 +314,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -316,14 +339,20 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v5"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage.xml"
|
||||
|
||||
|
||||
phpunit-mysql:
|
||||
name: "PHPUnit with MySQL"
|
||||
name: >
|
||||
${{ format('MySQL {0} - PHP {1} - DBAL {2} - ext. {3}',
|
||||
matrix.mysql-version || 'Ø',
|
||||
matrix.php-version || 'Ø',
|
||||
matrix.dbal-version || 'Ø',
|
||||
matrix.extension || 'Ø'
|
||||
) }}
|
||||
runs-on: "ubuntu-22.04"
|
||||
needs: "phpunit-smoke-check"
|
||||
|
||||
@@ -367,7 +396,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
@@ -413,7 +442,7 @@ jobs:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
|
||||
- name: "Upload coverage files"
|
||||
uses: "actions/upload-artifact@v5"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -431,12 +460,12 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Download coverage files"
|
||||
uses: "actions/download-artifact@v6"
|
||||
uses: "actions/download-artifact@v7"
|
||||
with:
|
||||
path: "reports"
|
||||
|
||||
|
||||
2
.github/workflows/documentation.yml
vendored
2
.github/workflows/documentation.yml
vendored
@@ -17,4 +17,4 @@ on:
|
||||
jobs:
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@12.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@13.1.0"
|
||||
|
||||
2
.github/workflows/phpbench.yml
vendored
2
.github/workflows/phpbench.yml
vendored
@@ -36,7 +36,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
with:
|
||||
fetch-depth: 2
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@12.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@13.1.0"
|
||||
secrets:
|
||||
GIT_AUTHOR_EMAIL: ${{ secrets.GIT_AUTHOR_EMAIL }}
|
||||
GIT_AUTHOR_NAME: ${{ secrets.GIT_AUTHOR_NAME }}
|
||||
|
||||
2
.github/workflows/static-analysis.yml
vendored
2
.github/workflows/static-analysis.yml
vendored
@@ -35,7 +35,7 @@ jobs:
|
||||
|
||||
steps:
|
||||
- name: "Checkout code"
|
||||
uses: "actions/checkout@v5"
|
||||
uses: "actions/checkout@v6"
|
||||
|
||||
- name: Install PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
|
||||
16
README.md
16
README.md
@@ -1,7 +1,7 @@
|
||||
| [4.0.x][4.0] | [3.6.x][3.6] | [3.5.x][3.5] | [2.21.x][2.21] | [2.20.x][2.20] |
|
||||
| [4.0.x][4.0] | [3.7.x][3.7] | [3.6.x][3.6] | [2.21.x][2.21] | [2.20.x][2.20] |
|
||||
|:------------------------------------------------------:|:------------------------------------------------------:|:------------------------------------------------------:|:--------------------------------------------------------:|:--------------------------------------------------------:|
|
||||
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][3.5 image]][3.5 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][3.5 coverage image]][3.5 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
|
||||
| [![Build status][4.0 image]][4.0 workflow] | [![Build status][3.7 image]][3.7 workflow] | [![Build status][3.6 image]][3.6 workflow] | [![Build status][2.21 image]][2.21 workflow] | [![Build status][2.20 image]][2.20 workflow] |
|
||||
| [![Coverage Status][4.0 coverage image]][4.0 coverage] | [![Coverage Status][3.7 coverage image]][3.7 coverage] | [![Coverage Status][3.6 coverage image]][3.6 coverage] | [![Coverage Status][2.21 coverage image]][2.21 coverage] | [![Coverage Status][2.20 coverage image]][2.20 coverage] |
|
||||
|
||||
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
|
||||
@@ -21,16 +21,16 @@ without requiring unnecessary code duplication.
|
||||
[4.0 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A4.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.7 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.7.x
|
||||
[3.7]: https://github.com/doctrine/orm/tree/3.7.x
|
||||
[3.7 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.7.x
|
||||
[3.7 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.7.x/graph/badge.svg
|
||||
[3.7 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.7.x
|
||||
[3.6 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.6.x
|
||||
[3.6]: https://github.com/doctrine/orm/tree/3.6.x
|
||||
[3.6 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.6.x
|
||||
[3.6 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.6.x/graph/badge.svg
|
||||
[3.6 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.6.x
|
||||
[3.5 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=3.5.x
|
||||
[3.5]: https://github.com/doctrine/orm/tree/3.5.x
|
||||
[3.5 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A3.5.x
|
||||
[3.5 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.5.x/graph/badge.svg
|
||||
[3.5 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.5.x
|
||||
[2.21 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.21.x
|
||||
[2.21]: https://github.com/doctrine/orm/tree/2.21.x
|
||||
[2.21 workflow]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml?query=branch%3A2.21.x
|
||||
|
||||
96
UPGRADE.md
96
UPGRADE.md
@@ -29,6 +29,100 @@ and directly start using native lazy objects.
|
||||
|
||||
# Upgrade to 3.6
|
||||
|
||||
## Deprecate using string expression for default values in mappings
|
||||
|
||||
Using a string expression for default values in field mappings is deprecated.
|
||||
Use `Doctrine\DBAL\Schema\DefaultExpression` instances instead.
|
||||
|
||||
Here is how to address this deprecation when mapping entities using PHP attributes:
|
||||
|
||||
```diff
|
||||
use DateTime;
|
||||
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentDate;
|
||||
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentTime;
|
||||
+use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
final class TimeEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
public int $id;
|
||||
|
||||
- #[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'], insertable: false, updatable: false)]
|
||||
+ #[ORM\Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
|
||||
public DateTime $createdAt;
|
||||
|
||||
- #[ORM\Column(options: ['default' => 'CURRENT_TIME'], insertable: false, updatable: false)]
|
||||
+ #[ORM\Column(options: ['default' => new CurrentTime()], insertable: false, updatable: false)]
|
||||
public DateTime $createdTime;
|
||||
|
||||
- #[ORM\Column(options: ['default' => 'CURRENT_DATE'], insertable: false, updatable: false)]
|
||||
+ #[ORM\Column(options: ['default' => new CurrentDate()], insertable: false, updatable: false)]
|
||||
public DateTime $createdDate;
|
||||
}
|
||||
```
|
||||
|
||||
Here is how to do the same when mapping entities using XML:
|
||||
|
||||
```diff
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Doctrine\Tests\ORM\Functional\XmlTimeEntity">
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="AUTO"/>
|
||||
</id>
|
||||
|
||||
<field name="createdAt" type="datetime" insertable="false" updatable="false">
|
||||
<options>
|
||||
- <option name="default">CURRENT_TIMESTAMP</option>
|
||||
+ <option name="default">
|
||||
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
|
||||
+ </option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
|
||||
<options>
|
||||
- <option name="default">CURRENT_TIMESTAMP</option>
|
||||
+ <option name="default">
|
||||
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
|
||||
+ </option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<field name="createdTime" type="time" insertable="false" updatable="false">
|
||||
<options>
|
||||
- <option name="default">CURRENT_TIME</option>
|
||||
+ <option name="default">
|
||||
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTime"/>
|
||||
+ </option>
|
||||
</options>
|
||||
</field>
|
||||
<field name="createdDate" type="date" insertable="false" updatable="false">
|
||||
<options>
|
||||
- <option name="default">CURRENT_DATE</option>
|
||||
+ <option name="default">
|
||||
+ <object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentDate"/>
|
||||
+ </option>
|
||||
</options>
|
||||
</field>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
```
|
||||
|
||||
|
||||
## Deprecate `FieldMapping::$default`
|
||||
|
||||
The `default` property of `Doctrine\ORM\Mapping\FieldMapping` is deprecated and
|
||||
will be removed in 4.0. Instead, use `FieldMapping::$options['default']`.
|
||||
|
||||
## Deprecate specifying `nullable` on columns that end up being used in a primary key
|
||||
|
||||
Specifying `nullable` on join columns that are part of a primary key is
|
||||
@@ -2207,7 +2301,7 @@ from 2.0 have to configure the annotation driver if they don't use `Configuratio
|
||||
|
||||
## Scalar mappings can now be omitted from DQL result
|
||||
|
||||
You are now allowed to mark scalar SELECT expressions as HIDDEN an they are not hydrated anymore.
|
||||
You are now allowed to mark scalar SELECT expressions as HIDDEN and they are not hydrated anymore.
|
||||
Example:
|
||||
|
||||
SELECT u, SUM(a.id) AS HIDDEN numArticles FROM User u LEFT JOIN u.Articles a ORDER BY numArticles DESC HAVING numArticles > 10
|
||||
|
||||
@@ -49,9 +49,8 @@
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^14.0",
|
||||
"phpbench/phpbench": "^1.0",
|
||||
"phpdocumentor/guides-cli": "^1.4",
|
||||
"phpstan/extension-installer": "^1.4",
|
||||
"phpstan/phpstan": "2.1.22",
|
||||
"phpstan/phpstan": "2.1.23",
|
||||
"phpstan/phpstan-deprecation-rules": "^2",
|
||||
"phpunit/phpunit": "^10.5.0 || ^11.5",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
|
||||
@@ -40,7 +40,8 @@ Now this is all awfully technical, so let me come to some use-cases
|
||||
fast to keep you motivated. Using walker implementation you can for
|
||||
example:
|
||||
|
||||
|
||||
- Modify the Output walker to get the raw SQL via ``Query->getSQL()``
|
||||
with interpolated parameters.
|
||||
- Modify the AST to generate a Count Query to be used with a
|
||||
paginator for any given DQL query.
|
||||
- Modify the Output Walker to generate vendor-specific SQL
|
||||
@@ -50,7 +51,7 @@ example:
|
||||
- Modify the Output walker to pretty print the SQL for debugging
|
||||
purposes.
|
||||
|
||||
In this cookbook-entry I will show examples of the first two
|
||||
In this cookbook-entry I will show examples of the first three
|
||||
points. There are probably much more use-cases.
|
||||
|
||||
Generic count query for pagination
|
||||
@@ -223,3 +224,39 @@ huge benefits with using vendor specific features. This would still
|
||||
allow you write DQL queries instead of NativeQueries to make use of
|
||||
vendor specific features.
|
||||
|
||||
Modifying the Output Walker to get the raw SQL with interpolated parameters
|
||||
---------------------------------------------------------------------------
|
||||
|
||||
Sometimes we may want to log or trace the raw SQL being generated from its DQL
|
||||
for profiling slow queries afterwards or audit queries that changed many rows
|
||||
``$query->getSQL()`` will give us the prepared statement being passed to database
|
||||
with all values of SQL parameters being replaced by positional ``?`` or named ``:name``
|
||||
as parameters are interpolated into prepared statements by the database while executing the SQL.
|
||||
``$query->getParameters()`` will give us details about SQL parameters that we've provided.
|
||||
So we can create an output walker to interpolate all SQL parameters that will be
|
||||
passed into prepared statement in PHP before database handle them internally:
|
||||
|
||||
.. literalinclude:: dql-custom-walkers/InterpolateParametersSQLOutputWalker.php
|
||||
:language: php
|
||||
|
||||
Then you may get the raw SQL with this output walker:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query
|
||||
->where('t.int IN (:ints)')->setParameter(':ints', [1, 2])
|
||||
->orWhere('t.string IN (?0)')->setParameter(0, ['3', '4'])
|
||||
->orWhere("t.bool = ?1")->setParameter('?1', true)
|
||||
->orWhere("t.string = :string")->setParameter(':string', 'ABC')
|
||||
->setHint(\Doctrine\ORM\Query::HINT_CUSTOM_OUTPUT_WALKER, InterpolateParametersSQLOutputWalker::class)
|
||||
->getSQL();
|
||||
|
||||
The where clause of the returned SQL should be like:
|
||||
|
||||
.. code-block:: sql
|
||||
|
||||
WHERE t0_.int IN (1, 2)
|
||||
OR t0_.string IN ('3', '4')
|
||||
OR t0_.bool = 1
|
||||
OR t0_.string = 'ABC'
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
<?php
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\DBAL\Types\BooleanType;
|
||||
use Doctrine\DBAL\Types\Exception\ValueNotConvertible;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\ORM\Query\AST;
|
||||
use Doctrine\ORM\Query\SqlOutputWalker;
|
||||
|
||||
class InterpolateParametersSQLOutputWalker extends SqlOutputWalker
|
||||
{
|
||||
/** {@inheritdoc} */
|
||||
public function walkInputParameter(AST\InputParameter $inputParam): string
|
||||
{
|
||||
$parameter = $this->getQuery()->getParameter($inputParam->name);
|
||||
if ($parameter === null) {
|
||||
return '?';
|
||||
}
|
||||
|
||||
$value = $parameter->getValue();
|
||||
/** @var ParameterType|ArrayParameterType|int|string $typeName */
|
||||
/** @see \Doctrine\ORM\Query\ParameterTypeInferer::inferType() */
|
||||
$typeName = $parameter->getType();
|
||||
$platform = $this->getConnection()->getDatabasePlatform();
|
||||
$processParameterType = static fn(ParameterType $type) => static fn($value): string =>
|
||||
(match ($type) { /** @see Type::getBindingType() */
|
||||
ParameterType::NULL => 'NULL',
|
||||
ParameterType::INTEGER => $value,
|
||||
ParameterType::BOOLEAN => (new BooleanType())->convertToDatabaseValue($value, $platform),
|
||||
ParameterType::STRING, ParameterType::ASCII => $platform->quoteStringLiteral($value),
|
||||
default => throw new ValueNotConvertible($value, $type->name)
|
||||
});
|
||||
|
||||
if (is_string($typeName) && Type::hasType($typeName)) {
|
||||
return Type::getType($typeName)->convertToDatabaseValue($value, $platform);
|
||||
}
|
||||
if ($typeName instanceof ParameterType) {
|
||||
return $processParameterType($typeName)($value);
|
||||
}
|
||||
if ($typeName instanceof ArrayParameterType && is_array($value)) {
|
||||
$type = ArrayParameterType::toElementParameterType($typeName);
|
||||
return implode(', ', array_map($processParameterType($type), $value));
|
||||
}
|
||||
|
||||
throw new ValueNotConvertible($value, $typeName);
|
||||
}
|
||||
}
|
||||
@@ -668,11 +668,6 @@ and in the Context of a :ref:`#[ManyToMany] <attrref_manytomany>`. If this attri
|
||||
are missing they will be computed considering the field's name and the current
|
||||
:doc:`naming strategy <namingstrategy>`.
|
||||
|
||||
The ``#[InverseJoinColumn]`` is the same as ``#[JoinColumn]`` and is used in the context
|
||||
of a ``#[ManyToMany]`` attribute declaration to specifiy the details of the join table's
|
||||
column information used for the join to the inverse entity. This is only required
|
||||
on PHP 8.0, where nested attributes are not yet supported.
|
||||
|
||||
Optional parameters:
|
||||
|
||||
- **name**: Column name that holds the foreign key identifier for
|
||||
|
||||
@@ -182,6 +182,37 @@ Here is a complete list of ``Column``s attributes (all optional):
|
||||
- ``options``: Key-value pairs of options that get passed
|
||||
to the underlying database platform when generating DDL statements.
|
||||
|
||||
Specifying default values
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
While it is possible to specify default values for properties in your
|
||||
PHP class, Doctrine also allows you to specify default values for
|
||||
database columns using the ``default`` key in the ``options`` array of
|
||||
the ``Column`` attribute.
|
||||
|
||||
When using XML, you can specify object instances using the ``<object>``
|
||||
element:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="createdAt" type="datetime" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">
|
||||
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
|
||||
</option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
The ``<object>`` element requires a ``class`` attribute specifying the
|
||||
fully qualified class name to instantiate.
|
||||
|
||||
.. configuration-block::
|
||||
.. literalinclude:: basic-mapping/DefaultValues.php
|
||||
:language: attribute
|
||||
|
||||
.. literalinclude:: basic-mapping/default-values.xml
|
||||
:language: xml
|
||||
|
||||
.. _reference-php-mapping-types:
|
||||
|
||||
PHP Types Mapping
|
||||
|
||||
20
docs/en/reference/basic-mapping/DefaultValues.php
Normal file
20
docs/en/reference/basic-mapping/DefaultValues.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use DateTime;
|
||||
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
|
||||
#[Entity]
|
||||
class Message
|
||||
{
|
||||
#[Column(options: ['default' => 'Hello World!'])]
|
||||
private string $text;
|
||||
|
||||
#[Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
|
||||
private DateTime $createdAt;
|
||||
}
|
||||
16
docs/en/reference/basic-mapping/default-values.xml
Normal file
16
docs/en/reference/basic-mapping/default-values.xml
Normal file
@@ -0,0 +1,16 @@
|
||||
<doctrine-mapping>
|
||||
<entity name="Message">
|
||||
<field name="text">
|
||||
<options>
|
||||
<option name="default">Hello World!</option>
|
||||
</options>
|
||||
</field>
|
||||
<field name="createdAt" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">
|
||||
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
|
||||
</option>
|
||||
</options>
|
||||
</field>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -1411,8 +1411,7 @@ Result Cache API:
|
||||
|
||||
$query->setResultCacheDriver(new ApcCache());
|
||||
|
||||
$query->useResultCache(true)
|
||||
->setResultCacheLifeTime(3600);
|
||||
$query->enableResultCache(3600);
|
||||
|
||||
$result = $query->getResult(); // cache miss
|
||||
|
||||
@@ -1422,8 +1421,8 @@ Result Cache API:
|
||||
$query->setResultCacheId('my_query_result');
|
||||
$result = $query->getResult(); // saved in given result cache id.
|
||||
|
||||
// or call useResultCache() with all parameters:
|
||||
$query->useResultCache(true, 3600, 'my_query_result');
|
||||
// or call enableResultCache() with all parameters:
|
||||
$query->enableResultCache(3600, 'my_query_result');
|
||||
$result = $query->getResult(); // cache hit!
|
||||
|
||||
// Introspection
|
||||
|
||||
@@ -18,30 +18,6 @@ In your mapping configuration, the column definition (for example, the
|
||||
the ``charset`` and ``collation``. The default values are ``utf8`` and
|
||||
``utf8_unicode_ci``, respectively.
|
||||
|
||||
Entity Classes
|
||||
--------------
|
||||
|
||||
How can I add default values to a column?
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Doctrine does not support to set the default values in columns through the "DEFAULT" keyword in SQL.
|
||||
This is not necessary however, you can just use your class properties as default values. These are then used
|
||||
upon insert:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
class User
|
||||
{
|
||||
private const STATUS_DISABLED = 0;
|
||||
private const STATUS_ENABLED = 1;
|
||||
|
||||
private string $algorithm = "sha1";
|
||||
/** @var self::STATUS_* */
|
||||
private int $status = self::STATUS_DISABLED;
|
||||
}
|
||||
|
||||
.
|
||||
|
||||
Mapping
|
||||
-------
|
||||
|
||||
|
||||
@@ -208,6 +208,22 @@ Example:
|
||||
// ...
|
||||
}
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="MyProject\Model\Person" inheritance-type="SINGLE_TABLE">
|
||||
<discriminator-column name="discr" type="string" />
|
||||
<discriminator-map>
|
||||
<discriminator-mapping value="person" class="MyProject\Model\Person"/>
|
||||
<discriminator-mapping value="employee" class="MyProject\Model\Employee"/>
|
||||
</discriminator-map>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
<doctrine-mapping>
|
||||
<entity name="MyProject\Model\Employee">
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
|
||||
In this example, the ``#[DiscriminatorMap]`` specifies that in the
|
||||
discriminator column, a value of "person" identifies a row as being of type
|
||||
|
||||
@@ -344,10 +344,10 @@ the Query object which can be retrieved from ``EntityManager#createQuery()``.
|
||||
Executing a Query
|
||||
^^^^^^^^^^^^^^^^^
|
||||
|
||||
The QueryBuilder is a builder object only - it has no means of actually
|
||||
executing the Query. Additionally a set of parameters such as query hints
|
||||
cannot be set on the QueryBuilder itself. This is why you always have to convert
|
||||
a querybuilder instance into a Query object:
|
||||
The QueryBuilder is only a builder object - it has no means of actually
|
||||
executing the Query. Additional functionality, such as enabling the result cache,
|
||||
cannot be set on the QueryBuilder itself. This is why you must always convert
|
||||
a QueryBuilder instance into a Query object:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
@@ -355,9 +355,8 @@ a querybuilder instance into a Query object:
|
||||
// $qb instanceof QueryBuilder
|
||||
$query = $qb->getQuery();
|
||||
|
||||
// Set additional Query options
|
||||
$query->setQueryHint('foo', 'bar');
|
||||
$query->useResultCache('my_cache_id');
|
||||
// Enable the result cache
|
||||
$query->enableResultCache(3600, 'my_custom_id');
|
||||
|
||||
// Execute Query
|
||||
$result = $query->getResult();
|
||||
@@ -555,6 +554,24 @@ using ``addCriteria``:
|
||||
$qb->addCriteria($criteria);
|
||||
// then execute your query like normal
|
||||
|
||||
Adding hints to a Query
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can also set query hints to a QueryBuilder by using ``setHint``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// ...
|
||||
|
||||
// $qb instanceof QueryBuilder
|
||||
$qb->setHint('hintName', 'hintValue');
|
||||
// then execute your query like normal
|
||||
|
||||
The query hint can hold anything the usual query hints can hold
|
||||
except null. Those hints will be applied to the query when the
|
||||
query is created.
|
||||
|
||||
Low Level API
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ Caching mode
|
||||
* Read Write cache employs locks before update/delete.
|
||||
* Use if data needs to be updated.
|
||||
* Slowest strategy.
|
||||
* To use it a the cache region implementation must support locking.
|
||||
* To use it the cache region implementation must support locking.
|
||||
|
||||
|
||||
Built-in cached persisters
|
||||
|
||||
@@ -22,7 +22,7 @@ have to register them yourself.
|
||||
All the commands of the Doctrine Console require access to the
|
||||
``EntityManager``. You have to inject it into the console application.
|
||||
|
||||
Here is an example of a the project-specific ``bin/doctrine`` binary.
|
||||
Here is an example of a project-specific ``bin/doctrine`` binary.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
@@ -96,6 +96,10 @@ The following Commands are currently available:
|
||||
- ``orm:schema-tool:update`` Processes the schema and either
|
||||
update the database schema of EntityManager Storage Connection or
|
||||
generate the SQL output.
|
||||
- ``orm:debug:event-manager`` Lists event listeners for an entity
|
||||
manager, optionally filtered by event name.
|
||||
- ``orm:debug:entity-listeners`` Lists entity listeners for a given
|
||||
entity, optionally filtered by event name.
|
||||
|
||||
The following alias is defined:
|
||||
|
||||
|
||||
@@ -155,10 +155,20 @@
|
||||
</xs:restriction>
|
||||
</xs:simpleType>
|
||||
|
||||
<xs:complexType name="object">
|
||||
<xs:attribute name="class" type="xs:string" use="required"/>
|
||||
<xs:anyAttribute namespace="##other"/>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="option" mixed="true">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="option" type="orm:option"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="object" type="orm:object"/>
|
||||
<xs:sequence>
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="option" type="orm:option"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
</xs:sequence>
|
||||
</xs:choice>
|
||||
<xs:attribute name="name" type="xs:NMTOKEN" use="required"/>
|
||||
<xs:anyAttribute namespace="##other"/>
|
||||
|
||||
@@ -619,7 +619,7 @@ parameters:
|
||||
path: src/EntityRepository.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\>&Doctrine\\Common\\Collections\\Selectable\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
|
||||
message: '#^Method Doctrine\\ORM\\EntityRepository\:\:matching\(\) should return Doctrine\\Common\\Collections\\AbstractLazyCollection\<int, T of object\> but returns Doctrine\\ORM\\LazyCriteriaCollection\<\(int\|string\), object\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/EntityRepository.php
|
||||
@@ -696,12 +696,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Internal/Hydration/AbstractHydrator.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:gatherRowData\(\) should return array\{data\: array\<array\>, newObjects\?\: array\<array\{class\: ReflectionClass, args\: array, obj\: object\}\>, scalars\?\: array\} but returns array\{data\: array, newObjects\: array\<array\{class\: ReflectionClass\<object\>, args\: array, obj\?\: object\}\>, scalars\?\: non\-empty\-array\}\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Internal/Hydration/AbstractHydrator.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Internal\\Hydration\\AbstractHydrator\:\:getClassMetadata\(\) return type with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -822,6 +816,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Internal/HydrationCompleteHandler.php
|
||||
|
||||
-
|
||||
message: '#^Offset int\|null might not exist on array\<int, object\>\.$#'
|
||||
identifier: offsetAccess.notFound
|
||||
count: 1
|
||||
path: src/Internal/StronglyConnectedComponents.php
|
||||
|
||||
-
|
||||
message: '#^Property Doctrine\\ORM\\Internal\\StronglyConnectedComponents\:\:\$representingNodes \(array\<int, object\>\) does not accept array\<int\|string, object\>\.$#'
|
||||
identifier: assign.propertyType
|
||||
@@ -1410,12 +1410,6 @@ parameters:
|
||||
count: 2
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$asset of static method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getAssetName\(\) expects Doctrine\\DBAL\\Schema\\AbstractAsset, Doctrine\\DBAL\\Schema\\Column\|false given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$columnName of method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getFieldNameForColumn\(\) expects string, string\|false given\.$#'
|
||||
identifier: argument.type
|
||||
@@ -1447,7 +1441,7 @@ parameters:
|
||||
path: src/Mapping/Driver/XmlDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$columnDef of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:setDiscriminatorColumn\(\) expects array\{name\: string\|null, fieldName\?\: string\|null, type\?\: string\|null, length\?\: int\|null, columnDefinition\?\: string\|null, enumType\?\: class\-string\<BackedEnum\>\|null, options\?\: array\<string, mixed\>\|null\}\|Doctrine\\ORM\\Mapping\\DiscriminatorColumnMapping\|null, array\{name\: string\|null, type\: string, length\: int, columnDefinition\: string\|null, enumType\: string\|null, options\?\: array\<int\|string, array\<int\|string, mixed\>\|bool\|string\>\} given\.$#'
|
||||
message: '#^Parameter \#1 \$columnDef of method Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:setDiscriminatorColumn\(\) expects array\{name\: string\|null, fieldName\?\: string\|null, type\?\: string\|null, length\?\: int\|null, columnDefinition\?\: string\|null, enumType\?\: class\-string\<BackedEnum\>\|null, options\?\: array\<string, mixed\>\|null\}\|Doctrine\\ORM\\Mapping\\DiscriminatorColumnMapping\|null, array\{name\: string\|null, type\: string, length\: int, columnDefinition\: string\|null, enumType\: string\|null, options\?\: array\<int\|string, array\<int\|string, mixed\>\|bool\|object\|string\>\} given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Mapping/Driver/XmlDriver.php
|
||||
@@ -1477,7 +1471,7 @@ parameters:
|
||||
path: src/Mapping/Driver/XmlDriver.php
|
||||
|
||||
-
|
||||
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:\$table \(array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\?\: array\<string, mixed\>, quoted\?\: bool\}\) does not accept array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\: array\<int\|string, array\<int\|string, mixed\>\|bool\|string\>, quoted\?\: bool\}\.$#'
|
||||
message: '#^Property Doctrine\\ORM\\Mapping\\ClassMetadata\<T of object\>\:\:\$table \(array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\?\: array\<string, mixed\>, quoted\?\: bool\}\) does not accept array\{name\: string, schema\?\: string, indexes\?\: array, uniqueConstraints\?\: array, options\: array\<int\|string, array\<int\|string, mixed\>\|bool\|object\|string\>, quoted\?\: bool\}\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 1
|
||||
path: src/Mapping/Driver/XmlDriver.php
|
||||
@@ -1536,6 +1530,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Mapping/LegacyReflectionFields.php
|
||||
|
||||
-
|
||||
message: '#^Strict comparison using \!\=\= between array\<string, string\> and null will always evaluate to true\.$#'
|
||||
identifier: notIdentical.alwaysTrue
|
||||
count: 1
|
||||
path: src/Mapping/ManyToManyOwningSideMapping.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Mapping\\MappedSuperclass\:\:__construct\(\) has parameter \$repositoryClass with generic class Doctrine\\ORM\\EntityRepository but does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -2874,12 +2874,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Tools/Console/Command/GenerateProxiesCommand.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#2 \$proxyDir of method Doctrine\\ORM\\Proxy\\ProxyFactory\:\:generateProxyClasses\(\) expects string\|null, string\|false given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Tools/Console/Command/GenerateProxiesCommand.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Tools\\Console\\Command\\MappingDescribeCommand\:\:getClassMetadata\(\) return type with generic class Doctrine\\ORM\\Mapping\\ClassMetadata does not specify its types\: T$#'
|
||||
identifier: missingType.generics
|
||||
@@ -3345,6 +3339,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\AssociationMapping\:\:\$joinColumns\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$inversedBy\.$#'
|
||||
identifier: property.notFound
|
||||
@@ -3360,7 +3360,7 @@ parameters:
|
||||
-
|
||||
message: '#^Access to an undefined property Doctrine\\ORM\\Mapping\\ManyToManyInverseSideMapping\|Doctrine\\ORM\\Mapping\\ManyToManyOwningSideMapping\|Doctrine\\ORM\\Mapping\\ManyToOneAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToManyAssociationMapping\|Doctrine\\ORM\\Mapping\\OneToOneInverseSideMapping\|Doctrine\\ORM\\Mapping\\OneToOneOwningSideMapping\:\:\$mappedBy\.$#'
|
||||
identifier: property.notFound
|
||||
count: 1
|
||||
count: 3
|
||||
path: src/UnitOfWork.php
|
||||
|
||||
-
|
||||
|
||||
@@ -98,6 +98,17 @@ parameters:
|
||||
identifier: argument.unresolvableType
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#1 \$asset of static method Doctrine\\ORM\\Mapping\\Driver\\DatabaseDriver\:\:getAssetName\(\) expects Doctrine\\DBAL\\Schema\\AbstractAsset, Doctrine\\DBAL\\Schema\\Column\|false given\.$#'
|
||||
identifier: argument.type
|
||||
path: src/Mapping/Driver/DatabaseDriver.php
|
||||
|
||||
-
|
||||
message: '#^Instantiated class Doctrine\\DBAL\\Schema\\DefaultExpression\\\w+ not found\.$#'
|
||||
identifier: class.notFound
|
||||
path: src/Tools/SchemaTool.php
|
||||
|
||||
|
||||
# To be removed in 4.0
|
||||
-
|
||||
message: '#Negated boolean expression is always false\.#'
|
||||
@@ -149,8 +160,3 @@ parameters:
|
||||
-
|
||||
message: '~inferType.*never returns~'
|
||||
path: src/Query/ParameterTypeInferer.php
|
||||
|
||||
# Compatibility with Symfony 8
|
||||
-
|
||||
message: '#^Call to function method_exists\(\) with ''Symfony\\\\Component\\\\VarExporter\\\\ProxyHelper'' and ''generateLazyGhost'' will always evaluate to true\.$#'
|
||||
path: src/Proxy/ProxyFactory.php
|
||||
|
||||
@@ -54,8 +54,3 @@ parameters:
|
||||
-
|
||||
message: '#Expression on left side of \?\? is not nullable.#'
|
||||
path: src/Mapping/Driver/AttributeDriver.php
|
||||
|
||||
# Compatibility with Symfony 8
|
||||
-
|
||||
message: '#^Call to function method_exists\(\) with ''Symfony\\\\Component\\\\VarExporter\\\\ProxyHelper'' and ''generateLazyGhost'' will always evaluate to true\.$#'
|
||||
path: src/Proxy/ProxyFactory.php
|
||||
|
||||
@@ -1065,7 +1065,7 @@ abstract class AbstractQuery
|
||||
}
|
||||
|
||||
/**
|
||||
* Executes the query and returns a the resulting Statement object.
|
||||
* Executes the query and returns the resulting Statement object.
|
||||
*
|
||||
* @return Result|int The executed database statement that holds
|
||||
* the results, or an integer indicating how
|
||||
|
||||
@@ -17,6 +17,7 @@ use Doctrine\ORM\UnitOfWork;
|
||||
use Generator;
|
||||
use LogicException;
|
||||
use ReflectionClass;
|
||||
use ReflectionEnum;
|
||||
|
||||
use function array_key_exists;
|
||||
use function array_keys;
|
||||
@@ -597,13 +598,18 @@ abstract class AbstractHydrator
|
||||
*/
|
||||
final protected function buildEnum(mixed $value, string $enumType): BackedEnum|array
|
||||
{
|
||||
$reflection = new ReflectionEnum($enumType);
|
||||
$isIntBacked = $reflection->isBacked() && $reflection->getBackingType()->getName() === 'int';
|
||||
|
||||
if (is_array($value)) {
|
||||
return array_map(
|
||||
static fn ($value) => $enumType::from($value),
|
||||
static fn ($value) => $enumType::from($isIntBacked ? (int) $value : $value),
|
||||
$value,
|
||||
);
|
||||
}
|
||||
|
||||
$value = $isIntBacked ? (int) $value : $value;
|
||||
|
||||
return $enumType::from($value);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2204,6 +2204,20 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
throw MappingException::invalidDiscriminatorColumnType($this->name, $columnDef['type']);
|
||||
}
|
||||
|
||||
if (isset($columnDef['enumType'])) {
|
||||
if (! enum_exists($columnDef['enumType'])) {
|
||||
throw MappingException::nonEnumTypeMapped($this->name, $columnDef['fieldName'], $columnDef['enumType']);
|
||||
}
|
||||
|
||||
if (
|
||||
defined('Doctrine\DBAL\Types\Types::ENUM')
|
||||
&& $columnDef['type'] === Types::ENUM
|
||||
&& ! isset($columnDef['options']['values'])
|
||||
) {
|
||||
$columnDef['options']['values'] = array_column($columnDef['enumType']::cases(), 'value');
|
||||
}
|
||||
}
|
||||
|
||||
$this->discriminatorColumn = DiscriminatorColumnMapping::fromMappingArray($columnDef);
|
||||
}
|
||||
}
|
||||
@@ -2222,6 +2236,8 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
* Used for JOINED and SINGLE_TABLE inheritance mapping strategies.
|
||||
*
|
||||
* @param array<int|string, string> $map
|
||||
*
|
||||
* @throws MappingException
|
||||
*/
|
||||
public function setDiscriminatorMap(array $map): void
|
||||
{
|
||||
@@ -2241,6 +2257,16 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
);
|
||||
}
|
||||
|
||||
$values = $this->discriminatorColumn->options['values'] ?? null;
|
||||
|
||||
if ($values !== null) {
|
||||
$diff = array_diff(array_keys($map), $values);
|
||||
|
||||
if ($diff !== []) {
|
||||
throw MappingException::invalidEntriesInDiscriminatorMap(array_values($diff), $this->name, $this->discriminatorColumn->enumType);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($map as $value => $className) {
|
||||
$this->addDiscriminatorMapClass($value, $className);
|
||||
}
|
||||
@@ -2454,9 +2480,9 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
|
||||
if (! isset($mapping['default'])) {
|
||||
if (in_array($mapping['type'], ['integer', 'bigint', 'smallint'], true)) {
|
||||
$mapping['default'] = 1;
|
||||
$mapping['options']['default'] = 1;
|
||||
} elseif ($mapping['type'] === 'datetime') {
|
||||
$mapping['default'] = 'CURRENT_TIMESTAMP';
|
||||
$mapping['options']['default'] = 'CURRENT_TIMESTAMP';
|
||||
} else {
|
||||
throw MappingException::unsupportedOptimisticLockingType($this->name, $mapping['fieldName'], $mapping['type']);
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use LogicException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function constant;
|
||||
use function count;
|
||||
use function defined;
|
||||
@@ -656,15 +657,30 @@ class XmlDriver extends FileDriver
|
||||
* Parses (nested) option elements.
|
||||
*
|
||||
* @return mixed[] The options array.
|
||||
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string>
|
||||
* @phpstan-return array<int|string, array<int|string, mixed|string>|bool|string|object>
|
||||
*/
|
||||
private function parseOptions(SimpleXMLElement|null $options): array
|
||||
{
|
||||
$array = [];
|
||||
|
||||
foreach ($options ?? [] as $option) {
|
||||
$value = null;
|
||||
if ($option->count()) {
|
||||
$value = $this->parseOptions($option->children());
|
||||
// Check if this option contains an <object> element
|
||||
$children = $option->children();
|
||||
$hasObjectElement = false;
|
||||
|
||||
foreach ($children as $child) {
|
||||
if ($child->getName() === 'object') {
|
||||
$value = $this->parseObjectElement($child);
|
||||
$hasObjectElement = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (! $hasObjectElement) {
|
||||
$value = $this->parseOptions($children);
|
||||
}
|
||||
} else {
|
||||
$value = (string) $option;
|
||||
}
|
||||
@@ -684,6 +700,33 @@ class XmlDriver extends FileDriver
|
||||
return $array;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses an <object> element and returns the instantiated object.
|
||||
*
|
||||
* @param SimpleXMLElement $objectElement The XML element.
|
||||
*
|
||||
* @return object The instantiated object.
|
||||
*
|
||||
* @throws MappingException If the object specification is invalid.
|
||||
* @throws InvalidArgumentException If the class does not exist.
|
||||
*/
|
||||
private function parseObjectElement(SimpleXMLElement $objectElement): object
|
||||
{
|
||||
$attributes = $objectElement->attributes();
|
||||
|
||||
if (! isset($attributes->class)) {
|
||||
throw MappingException::missingRequiredOption('object', 'class');
|
||||
}
|
||||
|
||||
$className = (string) $attributes->class;
|
||||
|
||||
if (! class_exists($className)) {
|
||||
throw new InvalidArgumentException(sprintf('Class "%s" does not exist', $className));
|
||||
}
|
||||
|
||||
return new $className();
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructs a joinColumn mapping array based on the information
|
||||
* found in the given SimpleXMLElement.
|
||||
|
||||
@@ -71,7 +71,9 @@ final class FieldMapping implements ArrayAccess
|
||||
public string|null $declaredField = null;
|
||||
public array|null $options = null;
|
||||
public bool|null $version = null;
|
||||
public string|int|null $default = null;
|
||||
|
||||
/** @deprecated Use options with 'default' key instead */
|
||||
public string|int|null $default = null;
|
||||
|
||||
/**
|
||||
* @param string $type The type name of the mapped field. Can be one of
|
||||
@@ -140,7 +142,7 @@ final class FieldMapping implements ArrayAccess
|
||||
{
|
||||
$serialized = ['type', 'fieldName', 'columnName'];
|
||||
|
||||
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted'] as $boolKey) {
|
||||
foreach (['nullable', 'notInsertable', 'notUpdatable', 'id', 'unique', 'version', 'quoted', 'index'] as $boolKey) {
|
||||
if ($this->$boolKey) {
|
||||
$serialized[] = $boolKey;
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ class MappingException extends PersistenceMappingException implements ORMExcepti
|
||||
|
||||
public static function joinTableRequired(string $fieldName): self
|
||||
{
|
||||
return new self(sprintf("The mapping of field '%s' requires an the 'joinTable' attribute.", $fieldName));
|
||||
return new self(sprintf("The mapping of field '%s' requires the 'joinTable' attribute.", $fieldName));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -329,6 +329,24 @@ class MappingException extends PersistenceMappingException implements ORMExcepti
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an exception that indicates that discriminator entries used in a discriminator map
|
||||
* does not exist in the backed enum provided by enumType option.
|
||||
*
|
||||
* @param array<int,int|string> $entries The discriminator entries that could not be found.
|
||||
* @param string $owningClass The class that declares the discriminator map.
|
||||
* @param string $enumType The enum that entries were checked against.
|
||||
*/
|
||||
public static function invalidEntriesInDiscriminatorMap(array $entries, string $owningClass, string $enumType): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
"The entries %s in the discriminator map of class '%s' do not correspond to enum cases of '%s'.",
|
||||
implode(', ', array_map(static fn ($entry): string => sprintf("'%s'", $entry), $entries)),
|
||||
$owningClass,
|
||||
$enumType,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an exception that indicates that a class used in a discriminator map does not exist.
|
||||
* An example would be an outdated (maybe renamed) classname.
|
||||
|
||||
@@ -10,6 +10,8 @@ use ReflectionProperty;
|
||||
|
||||
use function sprintf;
|
||||
|
||||
use const PHP_VERSION_ID;
|
||||
|
||||
/** @internal */
|
||||
class ReadonlyAccessor implements PropertyAccessor
|
||||
{
|
||||
@@ -26,7 +28,12 @@ class ReadonlyAccessor implements PropertyAccessor
|
||||
|
||||
public function setValue(object $object, mixed $value): void
|
||||
{
|
||||
if (! $this->reflectionProperty->isInitialized($object)) {
|
||||
/* For lazy properties, skip the isInitialized() check
|
||||
because it would trigger the initialization of the whole object. */
|
||||
if (
|
||||
PHP_VERSION_ID >= 80400 && $this->reflectionProperty->isLazy($object)
|
||||
|| ! $this->reflectionProperty->isInitialized($object)
|
||||
) {
|
||||
$this->parent->setValue($object, $value);
|
||||
|
||||
return;
|
||||
|
||||
@@ -1668,6 +1668,11 @@ class BasicEntityPersister implements EntityPersister
|
||||
$value = [$value];
|
||||
}
|
||||
|
||||
if ($value === []) {
|
||||
$selectedColumns[] = '1=0';
|
||||
continue;
|
||||
}
|
||||
|
||||
$nullKeys = array_keys($value, null, true);
|
||||
$nonNullValues = array_diff_key($value, array_flip($nullKeys));
|
||||
|
||||
|
||||
@@ -162,6 +162,7 @@ EOPHP;
|
||||
);
|
||||
}
|
||||
|
||||
// @phpstan-ignore function.impossibleType (This method has been removed in Symfony 8)
|
||||
if (! method_exists(ProxyHelper::class, 'generateLazyGhost')) {
|
||||
throw ORMInvalidArgumentException::lazyGhostUnavailable();
|
||||
}
|
||||
@@ -469,7 +470,7 @@ EOPHP;
|
||||
|
||||
private function generateUseLazyGhostTrait(ClassMetadata $class): string
|
||||
{
|
||||
// @phpstan-ignore staticMethod.deprecated (Because we support Symfony < 7.3)
|
||||
// @phpstan-ignore staticMethod.notFound (This method has been removed in Symfony 8)
|
||||
$code = ProxyHelper::generateLazyGhost($class->getReflectionClass());
|
||||
$code = substr($code, 7 + (int) strpos($code, "\n{"));
|
||||
$code = substr($code, 0, (int) strpos($code, "\n}"));
|
||||
|
||||
@@ -117,6 +117,13 @@ class QueryBuilder implements Stringable
|
||||
*/
|
||||
private int $boundCounter = 0;
|
||||
|
||||
/**
|
||||
* The hints to set on the query.
|
||||
*
|
||||
* @var array<string, string|int|bool|iterable<mixed>|object>
|
||||
*/
|
||||
private array $hints = [];
|
||||
|
||||
/**
|
||||
* Initializes a new <tt>QueryBuilder</tt> that uses the given <tt>EntityManager</tt>.
|
||||
*
|
||||
@@ -208,6 +215,39 @@ class QueryBuilder implements Stringable
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return array<string, string|int|bool|iterable<mixed>|object> */
|
||||
public function getHints(): array
|
||||
{
|
||||
return $this->hints;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of a query hint. If the hint name is not recognized, FALSE is returned.
|
||||
*
|
||||
* @return mixed The value of the hint or FALSE, if the hint name is not recognized.
|
||||
*/
|
||||
public function getHint(string $name): mixed
|
||||
{
|
||||
return $this->hints[$name] ?? false;
|
||||
}
|
||||
|
||||
public function hasHint(string $name): bool
|
||||
{
|
||||
return isset($this->hints[$name]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds hints for the query.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setHint(string $name, mixed $value): static
|
||||
{
|
||||
$this->hints[$name] = $value;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @phpstan-return Cache::MODE_*|null */
|
||||
public function getCacheMode(): int|null
|
||||
{
|
||||
@@ -288,6 +328,10 @@ class QueryBuilder implements Stringable
|
||||
$query->setCacheRegion($this->cacheRegion);
|
||||
}
|
||||
|
||||
foreach ($this->hints as $name => $value) {
|
||||
$query->setHint($name, $value);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
|
||||
@@ -18,11 +18,12 @@ trait ApplicationCompatibility
|
||||
{
|
||||
private static function addCommandToApplication(Application $application, Command $command): Command|null
|
||||
{
|
||||
// @phpstan-ignore function.alreadyNarrowedType (This method did not exist before Symfony 7.4)
|
||||
if (method_exists(Application::class, 'addCommand')) {
|
||||
// @phpstan-ignore method.notFound (This method will be added in Symfony 7.4)
|
||||
return $application->addCommand($command);
|
||||
}
|
||||
|
||||
// @phpstan-ignore method.notFound
|
||||
return $application->add($command);
|
||||
}
|
||||
}
|
||||
|
||||
34
src/Tools/Console/Command/Debug/AbstractCommand.php
Normal file
34
src/Tools/Console/Command/Debug/AbstractCommand.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\Console\Command\Debug;
|
||||
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
|
||||
use function assert;
|
||||
|
||||
/** @internal */
|
||||
abstract class AbstractCommand extends Command
|
||||
{
|
||||
public function __construct(private readonly ManagerRegistry $managerRegistry)
|
||||
{
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
final protected function getEntityManager(string $name): EntityManagerInterface
|
||||
{
|
||||
$manager = $this->getManagerRegistry()->getManager($name);
|
||||
|
||||
assert($manager instanceof EntityManagerInterface);
|
||||
|
||||
return $manager;
|
||||
}
|
||||
|
||||
final protected function getManagerRegistry(): ManagerRegistry
|
||||
{
|
||||
return $this->managerRegistry;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\Console\Command\Debug;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_keys;
|
||||
use function array_merge;
|
||||
use function array_unique;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function ksort;
|
||||
use function ltrim;
|
||||
use function sort;
|
||||
use function sprintf;
|
||||
|
||||
final class DebugEntityListenersDoctrineCommand extends AbstractCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('orm:debug:entity-listeners')
|
||||
->setDescription('Lists entity listeners for a given entity')
|
||||
->addArgument('entity', InputArgument::OPTIONAL, 'The fully-qualified entity class name')
|
||||
->addArgument('event', InputArgument::OPTIONAL, 'The event name to filter by (e.g. postPersist)')
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command lists all entity listeners for a given entity:
|
||||
|
||||
<info>php %command.full_name% 'App\Entity\User'</info>
|
||||
|
||||
To show only listeners for a specific event, pass the event name:
|
||||
|
||||
<info>php %command.full_name% 'App\Entity\User' postPersist</info>
|
||||
EOT);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
/** @var class-string|null $entityName */
|
||||
$entityName = $input->getArgument('entity');
|
||||
|
||||
if ($entityName === null) {
|
||||
$choices = $this->listAllEntities();
|
||||
|
||||
if ($choices === []) {
|
||||
$io->error('No entities are configured.');
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
/** @var class-string $entityName */
|
||||
$entityName = $io->choice('Which entity do you want to list listeners for?', $choices);
|
||||
}
|
||||
|
||||
$entityName = ltrim($entityName, '\\');
|
||||
$entityManager = $this->getManagerRegistry()->getManagerForClass($entityName);
|
||||
|
||||
if ($entityManager === null) {
|
||||
$io->error(sprintf('No entity manager found for class "%s".', $entityName));
|
||||
|
||||
return self::FAILURE;
|
||||
}
|
||||
|
||||
$classMetadata = $entityManager->getClassMetadata($entityName);
|
||||
assert($classMetadata instanceof ClassMetadata);
|
||||
|
||||
$eventName = $input->getArgument('event');
|
||||
|
||||
if ($eventName === null) {
|
||||
$allListeners = $classMetadata->entityListeners;
|
||||
if (! $allListeners) {
|
||||
$io->info(sprintf('No listeners are configured for the "%s" entity.', $entityName));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
ksort($allListeners);
|
||||
} else {
|
||||
if (! isset($classMetadata->entityListeners[$eventName])) {
|
||||
$io->info(sprintf('No listeners are configured for the "%s" event.', $eventName));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$allListeners = [$eventName => $classMetadata->entityListeners[$eventName]];
|
||||
}
|
||||
|
||||
$io->title(sprintf('Entity listeners for <info>%s</info>', $entityName));
|
||||
|
||||
$rows = [];
|
||||
foreach ($allListeners as $event => $listeners) {
|
||||
if ($rows) {
|
||||
$rows[] = new TableSeparator();
|
||||
}
|
||||
|
||||
foreach ($listeners as $order => $listener) {
|
||||
$rows[] = [$order === 0 ? $event : '', sprintf('#%d', ++$order), sprintf('%s::%s()', $listener['class'], $listener['method'])];
|
||||
}
|
||||
}
|
||||
|
||||
$io->table(['Event', 'Order', 'Listener'], $rows);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('entity')) {
|
||||
$suggestions->suggestValues($this->listAllEntities());
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestArgumentValuesFor('event')) {
|
||||
$entityName = ltrim($input->getArgument('entity'), '\\');
|
||||
|
||||
if (! class_exists($entityName)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$entityManager = $this->getManagerRegistry()->getManagerForClass($entityName);
|
||||
|
||||
if ($entityManager === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$classMetadata = $entityManager->getClassMetadata($entityName);
|
||||
assert($classMetadata instanceof ClassMetadata);
|
||||
|
||||
$suggestions->suggestValues(array_keys($classMetadata->entityListeners));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/** @return list<class-string> */
|
||||
private function listAllEntities(): array
|
||||
{
|
||||
$entities = [];
|
||||
foreach (array_keys($this->getManagerRegistry()->getManagerNames()) as $managerName) {
|
||||
$entities[] = $this->getEntityManager($managerName)->getConfiguration()->getMetadataDriverImpl()->getAllClassNames();
|
||||
}
|
||||
|
||||
$entities = array_values(array_unique(array_merge(...$entities)));
|
||||
|
||||
sort($entities);
|
||||
|
||||
return $entities;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\Console\Command\Debug;
|
||||
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Helper\TableSeparator;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
use function array_keys;
|
||||
use function array_values;
|
||||
use function ksort;
|
||||
use function method_exists;
|
||||
use function sprintf;
|
||||
|
||||
final class DebugEventManagerDoctrineCommand extends AbstractCommand
|
||||
{
|
||||
protected function configure(): void
|
||||
{
|
||||
$this
|
||||
->setName('orm:debug:event-manager')
|
||||
->setDescription('Lists event listeners for an entity manager')
|
||||
->addArgument('event', InputArgument::OPTIONAL, 'The event name to filter by (e.g. postPersist)')
|
||||
->addOption('em', null, InputOption::VALUE_REQUIRED, 'The entity manager to use for this command')
|
||||
->setHelp(<<<'EOT'
|
||||
The <info>%command.name%</info> command lists all event listeners for the default entity manager:
|
||||
|
||||
<info>php %command.full_name%</info>
|
||||
|
||||
You can also specify an entity manager:
|
||||
|
||||
<info>php %command.full_name% --em=default</info>
|
||||
|
||||
To show only listeners for a specific event, pass the event name as an argument:
|
||||
|
||||
<info>php %command.full_name% postPersist</info>
|
||||
EOT);
|
||||
}
|
||||
|
||||
protected function execute(InputInterface $input, OutputInterface $output): int
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$entityManagerName = $input->getOption('em') ?: $this->getManagerRegistry()->getDefaultManagerName();
|
||||
$eventManager = $this->getEntityManager($entityManagerName)->getEventManager();
|
||||
|
||||
$eventName = $input->getArgument('event');
|
||||
|
||||
if ($eventName === null) {
|
||||
$allListeners = $eventManager->getAllListeners();
|
||||
if (! $allListeners) {
|
||||
$io->info(sprintf('No listeners are configured for the "%s" entity manager.', $entityManagerName));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
ksort($allListeners);
|
||||
} else {
|
||||
$listeners = $eventManager->hasListeners($eventName) ? $eventManager->getListeners($eventName) : [];
|
||||
if (! $listeners) {
|
||||
$io->info(sprintf('No listeners are configured for the "%s" event.', $eventName));
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
$allListeners = [$eventName => $listeners];
|
||||
}
|
||||
|
||||
$io->title(sprintf('Event listeners for <info>%s</info> entity manager', $entityManagerName));
|
||||
|
||||
$rows = [];
|
||||
foreach ($allListeners as $event => $listeners) {
|
||||
if ($rows) {
|
||||
$rows[] = new TableSeparator();
|
||||
}
|
||||
|
||||
foreach (array_values($listeners) as $order => $listener) {
|
||||
$method = method_exists($listener, '__invoke') ? '__invoke' : $event;
|
||||
$rows[] = [$order === 0 ? $event : '', sprintf('#%d', ++$order), sprintf('%s::%s()', $listener::class, $method)];
|
||||
}
|
||||
}
|
||||
|
||||
$io->table(['Event', 'Order', 'Listener'], $rows);
|
||||
|
||||
return self::SUCCESS;
|
||||
}
|
||||
|
||||
public function complete(CompletionInput $input, CompletionSuggestions $suggestions): void
|
||||
{
|
||||
if ($input->mustSuggestArgumentValuesFor('event')) {
|
||||
$entityManagerName = $input->getOption('em') ?: $this->getManagerRegistry()->getDefaultManagerName();
|
||||
$eventManager = $this->getEntityManager($entityManagerName)->getEventManager();
|
||||
|
||||
$suggestions->suggestValues(array_keys($eventManager->getAllListeners()));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('em')) {
|
||||
$suggestions->suggestValues(array_keys($this->getManagerRegistry()->getManagerNames()));
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,6 +23,7 @@ use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
|
||||
use Doctrine\DBAL\Schema\Schema;
|
||||
use Doctrine\DBAL\Schema\Table;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\AssociationMapping;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
@@ -484,7 +485,9 @@ class SchemaTool
|
||||
$options['scale'] = $mapping->scale;
|
||||
}
|
||||
|
||||
/** @phpstan-ignore property.deprecated */
|
||||
if (isset($mapping->default)) {
|
||||
/** @phpstan-ignore property.deprecated */
|
||||
$options['default'] = $mapping->default;
|
||||
}
|
||||
|
||||
@@ -505,7 +508,16 @@ class SchemaTool
|
||||
], true)
|
||||
&& $options['default'] === $this->platform->getCurrentTimestampSQL()
|
||||
) {
|
||||
/** @phpstan-ignore class.notFound (if DefaultExpression exists, CurrentTimestamp exists as well) */
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/issues/12252',
|
||||
<<<'DEPRECATION'
|
||||
Using "%s" as a default value for datetime fields is deprecated and
|
||||
will not be supported in Doctrine ORM 4.0.
|
||||
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp` instance instead.
|
||||
DEPRECATION,
|
||||
$this->platform->getCurrentTimestampSQL(),
|
||||
);
|
||||
$options['default'] = new CurrentTimestamp();
|
||||
}
|
||||
|
||||
@@ -513,7 +525,16 @@ class SchemaTool
|
||||
in_array($mapping->type, [Types::TIME_MUTABLE, Types::TIME_IMMUTABLE], true)
|
||||
&& $options['default'] === $this->platform->getCurrentTimeSQL()
|
||||
) {
|
||||
/** @phpstan-ignore class.notFound (if DefaultExpression exists, CurrentTime exists as well) */
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/issues/12252',
|
||||
<<<'DEPRECATION'
|
||||
Using "%s" as a default value for time fields is deprecated and
|
||||
will not be supported in Doctrine ORM 4.0.
|
||||
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentTime` instance instead.
|
||||
DEPRECATION,
|
||||
$this->platform->getCurrentTimeSQL(),
|
||||
);
|
||||
$options['default'] = new CurrentTime();
|
||||
}
|
||||
|
||||
@@ -521,7 +542,16 @@ class SchemaTool
|
||||
in_array($mapping->type, [Types::DATE_MUTABLE, Types::DATE_IMMUTABLE], true)
|
||||
&& $options['default'] === $this->platform->getCurrentDateSQL()
|
||||
) {
|
||||
/** @phpstan-ignore class.notFound (if DefaultExpression exists, CurrentDate exists as well) */
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/issues/12252',
|
||||
<<<'DEPRECATION'
|
||||
Using "%s" as a default value for date fields is deprecated and
|
||||
will not be supported in Doctrine ORM 4.0.
|
||||
Pass a `Doctrine\DBAL\Schema\DefaultExpression\CurrentDate` instance instead.
|
||||
DEPRECATION,
|
||||
$this->platform->getCurrentDateSQL(),
|
||||
);
|
||||
$options['default'] = new CurrentDate();
|
||||
}
|
||||
}
|
||||
@@ -1046,7 +1076,7 @@ class SchemaTool
|
||||
{
|
||||
return $asset instanceof NamedObject
|
||||
? $asset->getObjectName()->toString()
|
||||
// DBAL < 4.4
|
||||
// @phpstan-ignore method.deprecated (DBAL < 4.4)
|
||||
: $asset->getName();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,7 @@ use function array_map;
|
||||
use function array_sum;
|
||||
use function array_values;
|
||||
use function assert;
|
||||
use function count;
|
||||
use function current;
|
||||
use function get_debug_type;
|
||||
use function implode;
|
||||
@@ -2594,8 +2595,14 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$reflField->setValue($entity, $pColl);
|
||||
|
||||
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
|
||||
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
|
||||
if (! $isIteration && $assoc->isOneToMany() && ! $targetClass->isIdentifierComposite && ! $assoc->isIndexed()) {
|
||||
if (
|
||||
$assoc->isOneToMany()
|
||||
// is iteration
|
||||
&& ! (isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION])
|
||||
// is foreign key composite
|
||||
&& ! ($targetClass->hasAssociation($assoc->mappedBy) && count($targetClass->getAssociationMapping($assoc->mappedBy)->joinColumns) > 1)
|
||||
&& ! $assoc->isIndexed()
|
||||
) {
|
||||
$this->scheduleCollectionForBatchLoading($pColl, $class);
|
||||
} else {
|
||||
$this->loadCollection($pColl);
|
||||
|
||||
@@ -24,11 +24,16 @@ class RootEntity
|
||||
#[ORM\OneToMany(mappedBy: 'root', targetEntity: SecondLevel::class, fetch: 'EAGER')]
|
||||
private Collection $secondLevel;
|
||||
|
||||
/** @var Collection<int, SecondLevelWithoutCompositePrimaryKey> */
|
||||
#[ORM\OneToMany(mappedBy: 'root', targetEntity: SecondLevelWithoutCompositePrimaryKey::class, fetch: 'EAGER')]
|
||||
private Collection $anotherSecondLevel;
|
||||
|
||||
public function __construct(int $id, string $other)
|
||||
{
|
||||
$this->otherKey = $other;
|
||||
$this->secondLevel = new ArrayCollection();
|
||||
$this->id = $id;
|
||||
$this->otherKey = $other;
|
||||
$this->secondLevel = new ArrayCollection();
|
||||
$this->anotherSecondLevel = new ArrayCollection();
|
||||
$this->id = $id;
|
||||
}
|
||||
|
||||
public function getId(): int|null
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\EagerFetchedCompositeOneToMany;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class SecondLevelWithoutCompositePrimaryKey
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer', nullable: false)]
|
||||
private int|null $id;
|
||||
|
||||
#[ORM\ManyToOne(targetEntity: RootEntity::class, inversedBy: 'anotherSecondLevel')]
|
||||
#[ORM\JoinColumn(name: 'root_id', referencedColumnName: 'id')]
|
||||
#[ORM\JoinColumn(name: 'root_other_key', referencedColumnName: 'other_key')]
|
||||
private RootEntity $root;
|
||||
|
||||
public function __construct(RootEntity $upper)
|
||||
{
|
||||
$this->root = $upper;
|
||||
}
|
||||
|
||||
public function getId(): int|null
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
}
|
||||
@@ -6,38 +6,187 @@ namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\DefaultExpression;
|
||||
use Doctrine\DBAL\Schema\DefaultExpression\CurrentDate;
|
||||
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTime;
|
||||
use Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
use PHPUnit\Framework\Attributes\RequiresMethod;
|
||||
|
||||
use function interface_exists;
|
||||
|
||||
class DefaultTimeExpressionTest extends OrmFunctionalTestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
public function testUsingTimeRelatedDefaultExpressionCausesNoDbalDeprecation(): void
|
||||
#[IgnoreDeprecations]
|
||||
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
|
||||
public function testUsingTimeRelatedDefaultExpressionCausesAnOrmDeprecationAndNoDbalDeprecation(): void
|
||||
{
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
|
||||
if (
|
||||
$platform->getCurrentTimestampSQL() !== 'CURRENT_TIMESTAMP'
|
||||
|| $platform->getCurrentTimeSQL() !== 'CURRENT_TIME'
|
||||
|| $platform->getCurrentDateSQL() !== 'CURRENT_DATE'
|
||||
) {
|
||||
$this->markTestSkipped(
|
||||
'This test requires platforms to support exactly CURRENT_TIMESTAMP, CURRENT_TIME and CURRENT_DATE.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
$this->markTestSkipped(
|
||||
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
|
||||
|
||||
$this->createSchemaForModels(LegacyTimeEntity::class);
|
||||
$this->_em->persist($entity = new LegacyTimeEntity());
|
||||
$this->_em->flush();
|
||||
$this->_em->find(LegacyTimeEntity::class, $entity->id);
|
||||
}
|
||||
|
||||
public function testNoDeprecationsAreTrownWhenTheyCannotBeAddressed(): void
|
||||
{
|
||||
if (interface_exists(DefaultExpression::class)) {
|
||||
$this->markTestSkipped(
|
||||
'This test requires Doctrine DBAL 4.3 or lower.',
|
||||
);
|
||||
}
|
||||
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
|
||||
if (
|
||||
$platform->getCurrentTimestampSQL() !== 'CURRENT_TIMESTAMP'
|
||||
|| $platform->getCurrentTimeSQL() !== 'CURRENT_TIME'
|
||||
|| $platform->getCurrentDateSQL() !== 'CURRENT_DATE'
|
||||
) {
|
||||
$this->markTestSkipped(
|
||||
'This test requires platforms to support exactly CURRENT_TIMESTAMP, CURRENT_TIME and CURRENT_DATE.',
|
||||
);
|
||||
}
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
$this->markTestSkipped(
|
||||
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
|
||||
|
||||
$this->createSchemaForModels(LegacyTimeEntity::class);
|
||||
$this->_em->persist($entity = new LegacyTimeEntity());
|
||||
$this->_em->flush();
|
||||
$this->_em->find(LegacyTimeEntity::class, $entity->id);
|
||||
}
|
||||
|
||||
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
|
||||
public function testUsingDefaultExpressionInstancesCausesNoDeprecation(): void
|
||||
{
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
$this->markTestSkipped('MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.');
|
||||
}
|
||||
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
|
||||
|
||||
$this->createSchemaForModels(TimeEntity::class);
|
||||
$this->_em->persist($entity = new TimeEntity());
|
||||
$this->_em->flush();
|
||||
$this->_em->find(TimeEntity::class, $entity->id);
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class LegacyTimeEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
public int $id;
|
||||
|
||||
#[ORM\Column(
|
||||
type: Types::DATETIME_MUTABLE,
|
||||
options: ['default' => 'CURRENT_TIMESTAMP'],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTime $createdAt;
|
||||
|
||||
#[ORM\Column(
|
||||
type: Types::DATETIME_IMMUTABLE,
|
||||
options: ['default' => 'CURRENT_TIMESTAMP'],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTimeImmutable $createdAtImmutable;
|
||||
|
||||
#[ORM\Column(
|
||||
type: Types::TIME_MUTABLE,
|
||||
options: ['default' => 'CURRENT_TIME'],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTime $createdTime;
|
||||
|
||||
#[ORM\Column(
|
||||
type: Types::DATE_MUTABLE,
|
||||
options: ['default' => 'CURRENT_DATE'],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTime $createdDate;
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class TimeEntity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column]
|
||||
#[ORM\GeneratedValue]
|
||||
public int $id;
|
||||
|
||||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
#[ORM\Column(
|
||||
type: Types::DATETIME_MUTABLE,
|
||||
options: ['default' => new CurrentTimestamp()],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTime $createdAt;
|
||||
|
||||
#[ORM\Column(options: ['default' => 'CURRENT_TIMESTAMP'])]
|
||||
#[ORM\Column(
|
||||
type: Types::DATETIME_IMMUTABLE,
|
||||
options: ['default' => new CurrentTimestamp()],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTimeImmutable $createdAtImmutable;
|
||||
|
||||
#[ORM\Column(options: ['default' => 'CURRENT_TIME'])]
|
||||
#[ORM\Column(
|
||||
type: Types::TIME_MUTABLE,
|
||||
options: ['default' => new CurrentTime()],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTime $createdTime;
|
||||
|
||||
#[ORM\Column(options: ['default' => 'CURRENT_DATE'])]
|
||||
#[ORM\Column(
|
||||
type: Types::DATE_MUTABLE,
|
||||
options: ['default' => new CurrentDate()],
|
||||
insertable: false,
|
||||
updatable: false,
|
||||
)]
|
||||
public DateTime $createdDate;
|
||||
}
|
||||
|
||||
101
tests/Tests/ORM/Functional/DefaultTimeExpressionXmlTest.php
Normal file
101
tests/Tests/ORM/Functional/DefaultTimeExpressionXmlTest.php
Normal file
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use DateTime;
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\DBAL\Platforms\AbstractMySQLPlatform;
|
||||
use Doctrine\DBAL\Schema\DefaultExpression;
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Mapping\Driver\XmlDriver;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
use PHPUnit\Framework\Attributes\RequiresMethod;
|
||||
|
||||
class DefaultTimeExpressionXmlTest extends OrmFunctionalTestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
|
||||
public function testUsingTimeRelatedDefaultExpressionCausesAnOrmDeprecationAndNoDbalDeprecation(): void
|
||||
{
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
|
||||
if (
|
||||
$platform->getCurrentTimestampSQL() !== 'CURRENT_TIMESTAMP'
|
||||
|| $platform->getCurrentTimeSQL() !== 'CURRENT_TIME'
|
||||
|| $platform->getCurrentDateSQL() !== 'CURRENT_DATE'
|
||||
) {
|
||||
$this->markTestSkipped('Platform does not use standard SQL for current time expressions.');
|
||||
}
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
$this->markTestSkipped(
|
||||
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_em = $this->getEntityManager(
|
||||
mappingDriver: new XmlDriver(__DIR__ . '/../Mapping/xml/'),
|
||||
);
|
||||
$this->_schemaTool = new SchemaTool($this->_em);
|
||||
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
|
||||
|
||||
$this->createSchemaForModels(XmlLegacyTimeEntity::class);
|
||||
$this->_em->persist($entity = new XmlLegacyTimeEntity());
|
||||
$this->_em->flush();
|
||||
$this->_em->find(XmlLegacyTimeEntity::class, $entity->id);
|
||||
}
|
||||
|
||||
#[RequiresMethod(DefaultExpression::class, 'toSQL')]
|
||||
public function testUsingDefaultExpressionInstancesCausesNoDeprecationXmlDriver(): void
|
||||
{
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
|
||||
if ($platform instanceof AbstractMySQLPlatform) {
|
||||
$this->markTestSkipped(
|
||||
'MySQL platform does not support CURRENT_TIME or CURRENT_DATE as default expression.',
|
||||
);
|
||||
}
|
||||
|
||||
$this->_em = $this->getEntityManager(
|
||||
mappingDriver: new XmlDriver(__DIR__ . '/../Mapping/xml/'),
|
||||
);
|
||||
$this->_schemaTool = new SchemaTool($this->_em);
|
||||
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12252');
|
||||
$this->expectNoDeprecationWithIdentifier('https://github.com/doctrine/dbal/pull/7195');
|
||||
|
||||
$this->createSchemaForModels(XmlTimeEntity::class);
|
||||
$this->_em->persist($entity = new XmlTimeEntity());
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
$this->_em->find(XmlTimeEntity::class, $entity->id);
|
||||
}
|
||||
}
|
||||
|
||||
class XmlLegacyTimeEntity
|
||||
{
|
||||
public int $id;
|
||||
|
||||
public DateTime $createdAt;
|
||||
public DateTimeImmutable $createdAtImmutable;
|
||||
public DateTime $createdTime;
|
||||
public DateTime $createdDate;
|
||||
}
|
||||
|
||||
class XmlTimeEntity
|
||||
{
|
||||
public int $id;
|
||||
|
||||
public DateTime $createdAt;
|
||||
public DateTimeImmutable $createdAtImmutable;
|
||||
public DateTime $createdTime;
|
||||
public DateTime $createdDate;
|
||||
}
|
||||
@@ -6,6 +6,7 @@ namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity;
|
||||
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel;
|
||||
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevelWithoutCompositePrimaryKey;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
@@ -14,7 +15,7 @@ final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCas
|
||||
#[Group('GH11154')]
|
||||
public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void
|
||||
{
|
||||
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class]);
|
||||
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class, SecondLevelWithoutCompositePrimaryKey::class]);
|
||||
|
||||
$a1 = new RootEntity(1, 'A');
|
||||
|
||||
|
||||
@@ -678,7 +678,7 @@ SQL,
|
||||
|
||||
public function testDifferentResultLengthsDoNotRequireExtraQueryCacheEntries(): void
|
||||
{
|
||||
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id';
|
||||
$dql = 'SELECT u FROM Doctrine\Tests\Models\CMS\CmsUser u WHERE u.id >= :id ORDER BY u.id';
|
||||
$query = $this->_em->createQuery($dql);
|
||||
$query->setMaxResults(10);
|
||||
|
||||
|
||||
@@ -17,7 +17,6 @@ use Generator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
use ReflectionMethod;
|
||||
use Symfony\Component\VarExporter\Instantiator;
|
||||
use Symfony\Component\VarExporter\VarExporter;
|
||||
|
||||
use function file_get_contents;
|
||||
@@ -81,11 +80,6 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
},
|
||||
];
|
||||
|
||||
$instantiatorMethod = new ReflectionMethod(Instantiator::class, 'instantiate');
|
||||
if ($instantiatorMethod->getReturnType() === null) {
|
||||
self::markTestSkipped('symfony/var-exporter 5.4+ is required.');
|
||||
}
|
||||
|
||||
yield 'symfony/var-exporter' => [
|
||||
static function (ParserResult $parserResult): ParserResult {
|
||||
return eval('return ' . VarExporter::export($parserResult) . ';');
|
||||
|
||||
@@ -474,7 +474,9 @@ class QueryTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$query = $this->_em->createQuery('select a, u, a.topic, a.text from ' . CmsArticle::class . ' a, ' . CmsUser::class . ' u WHERE a.user = u ');
|
||||
$query = $this->_em->createQuery(
|
||||
'select a, u, a.topic, a.text from ' . CmsArticle::class . ' a, ' . CmsUser::class . ' u WHERE a.user = u order by a.id asc',
|
||||
);
|
||||
$result = $query->toIterable();
|
||||
|
||||
$it = iterator_to_array($result);
|
||||
@@ -517,7 +519,9 @@ class QueryTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$query = $this->_em->createQuery('select a.topic, a.text from ' . CmsArticle::class . ' a ');
|
||||
$query = $this->_em->createQuery(
|
||||
'select a.topic, a.text from ' . CmsArticle::class . ' a order by a.id asc',
|
||||
);
|
||||
$result = $query->toIterable();
|
||||
|
||||
$it = iterator_to_array($result);
|
||||
@@ -545,7 +549,9 @@ class QueryTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$query = $this->_em->createQuery('select a from Doctrine\Tests\Models\CMS\CmsArticle a');
|
||||
$query = $this->_em->createQuery(
|
||||
'select a from Doctrine\Tests\Models\CMS\CmsArticle a order by a.id asc',
|
||||
);
|
||||
|
||||
$articles = $query->toIterable();
|
||||
$iteratedCount = 0;
|
||||
|
||||
@@ -142,6 +142,8 @@ class MySqlSchemaToolTest extends OrmFunctionalTestCase
|
||||
self::equalTo('CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT(1) NOT NULL, PRIMARY KEY(id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'),
|
||||
// DBAL 4.3 (see https://github.com/doctrine/dbal/pull/6864)
|
||||
self::equalTo('CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT(1) NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'),
|
||||
// DBAL 4.4 (see https://github.com/doctrine/dbal/pull/7221)
|
||||
self::equalTo('CREATE TABLE boolean_model (id INT AUTO_INCREMENT NOT NULL, booleanField TINYINT NOT NULL, PRIMARY KEY (id)) DEFAULT CHARACTER SET utf8mb4 COLLATE `utf8mb4_unicode_ci` ENGINE = InnoDB'),
|
||||
));
|
||||
}
|
||||
|
||||
|
||||
@@ -50,7 +50,7 @@ class DDC1452Test extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba';
|
||||
$dql = 'SELECT a, b, ba FROM ' . __NAMESPACE__ . '\DDC1452EntityA AS a LEFT JOIN a.entitiesB AS b LEFT JOIN b.entityATo AS ba ORDER BY a.id';
|
||||
$results = $this->_em->createQuery($dql)->setMaxResults(1)->getResult();
|
||||
|
||||
self::assertSame($results[0], $results[0]->entitiesB[0]->entityAFrom);
|
||||
|
||||
@@ -49,7 +49,7 @@ class DDC2359Test extends TestCase
|
||||
$entityManager->expects(self::any())->method('getConnection')->willReturn($connection);
|
||||
$entityManager
|
||||
->method('getEventManager')
|
||||
->willReturn($this->createMock(EventManager::class));
|
||||
->willReturn(new EventManager());
|
||||
|
||||
$metadataFactory->method('newClassMetadataInstance')->willReturn($mockMetadata);
|
||||
$metadataFactory->expects(self::once())->method('wakeupReflection');
|
||||
|
||||
@@ -34,7 +34,7 @@ class GH10387Test extends OrmTestCase
|
||||
{
|
||||
yield 'hierarchy with Entity classes only' => [[GH10387EntitiesOnlyRoot::class, GH10387EntitiesOnlyMiddle::class, GH10387EntitiesOnlyLeaf::class]];
|
||||
yield 'MappedSuperclass in the middle of the hierarchy' => [[GH10387MappedSuperclassRoot::class, GH10387MappedSuperclassMiddle::class, GH10387MappedSuperclassLeaf::class]];
|
||||
yield 'abstract entity the the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
|
||||
yield 'abstract entity at the root and in the middle of the hierarchy' => [[GH10387AbstractEntitiesRoot::class, GH10387AbstractEntitiesMiddle::class, GH10387AbstractEntitiesLeaf::class]];
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
32
tests/Tests/ORM/Functional/Ticket/GH12166/GH12166Test.php
Normal file
32
tests/Tests/ORM/Functional/Ticket/GH12166/GH12166Test.php
Normal file
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12166;
|
||||
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH12166Test extends OrmFunctionalTestCase
|
||||
{
|
||||
public function testProxyWithReadonlyIdIsNotInitializedImmediately(): void
|
||||
{
|
||||
$this->createSchemaForModels(LazyEntityWithReadonlyId::class);
|
||||
$this->_em->persist(new LazyEntityWithReadonlyId(123, 'Test Name'));
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
$proxy = $this->_em->getReference(LazyEntityWithReadonlyId::class, 123);
|
||||
|
||||
$reflClass = $this->_em->getClassMetadata(LazyEntityWithReadonlyId::class)->reflClass;
|
||||
self::assertTrue(
|
||||
$this->isUninitializedObject($proxy),
|
||||
'Proxy should remain uninitialized after creation',
|
||||
);
|
||||
|
||||
$id = $proxy->getId();
|
||||
self::assertSame(123, $id);
|
||||
self::assertTrue(
|
||||
$this->isUninitializedObject($proxy),
|
||||
'Proxy should remain uninitialized after accessing ID',
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12166;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'gh12166_lazy_entity')]
|
||||
class LazyEntityWithReadonlyId
|
||||
{
|
||||
#[Column(type: 'integer')]
|
||||
#[Id]
|
||||
private readonly int $id;
|
||||
|
||||
#[Column(type: 'string')]
|
||||
private string $name;
|
||||
|
||||
public function __construct(int $id, string $name)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->name = $name;
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getName(): string
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
public function setName(string $name): void
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
54
tests/Tests/ORM/Functional/Ticket/GH12254Test.php
Normal file
54
tests/Tests/ORM/Functional/Ticket/GH12254Test.php
Normal file
@@ -0,0 +1,54 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH12254Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH12254EntityA::class,
|
||||
]);
|
||||
|
||||
$this->_em->persist(new GH12254EntityA());
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
}
|
||||
|
||||
public function testFindByEmptyArrayShouldReturnEmptyArray(): void
|
||||
{
|
||||
// pretend we are starting afresh
|
||||
$this->_em = $this->getEntityManager();
|
||||
$result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['id' => []]);
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
|
||||
public function testFindByInNullableField(): void
|
||||
{
|
||||
$this->_em = $this->getEntityManager();
|
||||
$result = $this->_em->getRepository(GH12254EntityA::class)->findBy(['name' => []]);
|
||||
$this->assertEmpty($result);
|
||||
}
|
||||
}
|
||||
|
||||
#[Entity]
|
||||
class GH12254EntityA
|
||||
{
|
||||
#[Column(type: 'integer')]
|
||||
#[Id]
|
||||
#[GeneratedValue(strategy: 'AUTO')]
|
||||
public int $id;
|
||||
|
||||
#[Column(type: 'string', nullable: true)]
|
||||
public string|null $name = null;
|
||||
}
|
||||
@@ -23,7 +23,7 @@ class GH6394Test extends OrmFunctionalTestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the the version of an entity can be fetched, when the id field and
|
||||
* Test the version of an entity can be fetched, when the id field and
|
||||
* the id column are different.
|
||||
*/
|
||||
#[Group('6393')]
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Hydration;
|
||||
|
||||
use BackedEnum;
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
@@ -12,6 +13,8 @@ use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Tests\Models\Enums\AccessLevel;
|
||||
use Doctrine\Tests\Models\Enums\UserStatus;
|
||||
use Doctrine\Tests\Models\Hydration\SimpleEntity;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use LogicException;
|
||||
@@ -24,7 +27,7 @@ use function iterator_to_array;
|
||||
#[CoversClass(AbstractHydrator::class)]
|
||||
class AbstractHydratorTest extends OrmFunctionalTestCase
|
||||
{
|
||||
private EventManager&MockObject $mockEventManager;
|
||||
private EventManager $eventManager;
|
||||
private Result&MockObject $mockResult;
|
||||
private ResultSetMapping&MockObject $mockResultMapping;
|
||||
private DummyHydrator $hydrator;
|
||||
@@ -35,7 +38,7 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
|
||||
|
||||
$mockConnection = $this->createMock(Connection::class);
|
||||
$mockEntityManagerInterface = $this->createMock(EntityManagerInterface::class);
|
||||
$this->mockEventManager = $this->createMock(EventManager::class);
|
||||
$this->eventManager = new EventManager();
|
||||
$this->mockResult = $this->createMock(Result::class);
|
||||
$this->mockResultMapping = $this->createMock(ResultSetMapping::class);
|
||||
|
||||
@@ -44,7 +47,7 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
|
||||
->willReturn($this->createMock(AbstractPlatform::class));
|
||||
$mockEntityManagerInterface
|
||||
->method('getEventManager')
|
||||
->willReturn($this->mockEventManager);
|
||||
->willReturn($this->eventManager);
|
||||
$mockEntityManagerInterface
|
||||
->method('getConnection')
|
||||
->willReturn($mockConnection);
|
||||
@@ -63,85 +66,47 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
|
||||
#[Group('#1515')]
|
||||
public function testOnClearEventListenerIsDetachedOnCleanup(): void
|
||||
{
|
||||
$eventListenerHasBeenRegistered = false;
|
||||
|
||||
$this
|
||||
->mockEventManager
|
||||
->expects(self::once())
|
||||
->method('addEventListener')
|
||||
->with([Events::onClear], $this->hydrator)
|
||||
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
|
||||
$this->assertFalse($eventListenerHasBeenRegistered);
|
||||
$eventListenerHasBeenRegistered = true;
|
||||
});
|
||||
|
||||
$this
|
||||
->mockEventManager
|
||||
->expects(self::once())
|
||||
->method('removeEventListener')
|
||||
->with([Events::onClear], $this->hydrator)
|
||||
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
|
||||
$this->assertTrue($eventListenerHasBeenRegistered);
|
||||
});
|
||||
|
||||
iterator_to_array($this->hydrator->toIterable($this->mockResult, $this->mockResultMapping));
|
||||
$iterator = $this->hydrator->toIterable($this->mockResult, $this->mockResultMapping);
|
||||
iterator_to_array($iterator);
|
||||
self::assertTrue($this->hydrator->hasListener);
|
||||
self::assertFalse($this->eventManager->hasListeners(Events::onClear));
|
||||
}
|
||||
|
||||
#[Group('#6623')]
|
||||
public function testHydrateAllRegistersAndClearsAllAttachedListeners(): void
|
||||
{
|
||||
$eventListenerHasBeenRegistered = false;
|
||||
|
||||
$this
|
||||
->mockEventManager
|
||||
->expects(self::once())
|
||||
->method('addEventListener')
|
||||
->with([Events::onClear], $this->hydrator)
|
||||
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
|
||||
$this->assertFalse($eventListenerHasBeenRegistered);
|
||||
$eventListenerHasBeenRegistered = true;
|
||||
});
|
||||
|
||||
$this
|
||||
->mockEventManager
|
||||
->expects(self::once())
|
||||
->method('removeEventListener')
|
||||
->with([Events::onClear], $this->hydrator)
|
||||
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
|
||||
$this->assertTrue($eventListenerHasBeenRegistered);
|
||||
});
|
||||
|
||||
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
|
||||
self::assertTrue($this->hydrator->hasListener);
|
||||
self::assertFalse($this->eventManager->hasListeners(Events::onClear));
|
||||
}
|
||||
|
||||
#[Group('#8482')]
|
||||
public function testHydrateAllClearsAllAttachedListenersEvenOnError(): void
|
||||
{
|
||||
$eventListenerHasBeenRegistered = false;
|
||||
|
||||
$this
|
||||
->mockEventManager
|
||||
->expects(self::once())
|
||||
->method('addEventListener')
|
||||
->with([Events::onClear], $this->hydrator)
|
||||
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
|
||||
$this->assertFalse($eventListenerHasBeenRegistered);
|
||||
$eventListenerHasBeenRegistered = true;
|
||||
});
|
||||
|
||||
$this
|
||||
->mockEventManager
|
||||
->expects(self::once())
|
||||
->method('removeEventListener')
|
||||
->with([Events::onClear], $this->hydrator)
|
||||
->willReturnCallback(function () use (&$eventListenerHasBeenRegistered): void {
|
||||
$this->assertTrue($eventListenerHasBeenRegistered);
|
||||
});
|
||||
|
||||
$this->hydrator->throwException = true;
|
||||
|
||||
$this->expectException(LogicException::class);
|
||||
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
|
||||
self::assertTrue($this->hydrator->hasListener);
|
||||
self::assertFalse($this->eventManager->hasListeners(Events::onClear));
|
||||
}
|
||||
|
||||
public function testEnumCastsIntegerBackedEnumValues(): void
|
||||
{
|
||||
$accessLevel = $this->hydrator->buildEnumForTesting('2', AccessLevel::class);
|
||||
$userStatus = $this->hydrator->buildEnumForTesting('active', UserStatus::class);
|
||||
|
||||
self::assertSame(AccessLevel::User, $accessLevel);
|
||||
self::assertSame(UserStatus::Active, $userStatus);
|
||||
}
|
||||
|
||||
public function testEnumCastsIntegerBackedEnumArrayValues(): void
|
||||
{
|
||||
$accessLevels = $this->hydrator->buildEnumForTesting(['1', '2'], AccessLevel::class);
|
||||
$userStatus = $this->hydrator->buildEnumForTesting(['active', 'inactive'], UserStatus::class);
|
||||
|
||||
self::assertSame([AccessLevel::Admin, AccessLevel::User], $accessLevels);
|
||||
self::assertSame([UserStatus::Active, UserStatus::Inactive], $userStatus);
|
||||
}
|
||||
|
||||
public function testToIterableIfYieldAndBreakBeforeFinishAlwaysCleansUp(): void
|
||||
@@ -177,6 +142,12 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
|
||||
class DummyHydrator extends AbstractHydrator
|
||||
{
|
||||
public bool $throwException = false;
|
||||
public bool $hasListener = false;
|
||||
|
||||
public function buildEnumForTesting(mixed $value, string $enumType): BackedEnum|array
|
||||
{
|
||||
return $this->buildEnum($value, $enumType);
|
||||
}
|
||||
|
||||
/** @return array{} */
|
||||
protected function hydrateAllData(): array
|
||||
@@ -187,4 +158,9 @@ class DummyHydrator extends AbstractHydrator
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
public function prepare(): void
|
||||
{
|
||||
$this->hasListener = $this->em->getEventManager()->hasListeners(Events::onClear);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ class ClassMetadataBuilderTest extends OrmTestCase
|
||||
FieldMapping::fromMappingArray([
|
||||
'columnDefinition' => 'foobar',
|
||||
'columnName' => 'username',
|
||||
'default' => 1,
|
||||
'options' => ['default' => 1],
|
||||
'fieldName' => 'name',
|
||||
'length' => 124,
|
||||
'type' => 'integer',
|
||||
|
||||
@@ -33,6 +33,7 @@ final class FieldMappingTest extends TestCase
|
||||
$mapping->precision = 10;
|
||||
$mapping->scale = 2;
|
||||
$mapping->unique = true;
|
||||
$mapping->index = true;
|
||||
$mapping->inherited = self::class;
|
||||
$mapping->originalClass = self::class;
|
||||
$mapping->originalField = 'id';
|
||||
@@ -57,6 +58,7 @@ final class FieldMappingTest extends TestCase
|
||||
self::assertSame(10, $resurrectedMapping->precision);
|
||||
self::assertSame(2, $resurrectedMapping->scale);
|
||||
self::assertTrue($resurrectedMapping->unique);
|
||||
self::assertTrue($resurrectedMapping->index);
|
||||
self::assertSame(self::class, $resurrectedMapping->inherited);
|
||||
self::assertSame(self::class, $resurrectedMapping->originalClass);
|
||||
self::assertSame('id', $resurrectedMapping->originalField);
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Doctrine\Tests\ORM\Functional\XmlLegacyTimeEntity">
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="AUTO"/>
|
||||
</id>
|
||||
|
||||
<field name="createdAt" type="datetime" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">CURRENT_TIMESTAMP</option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">CURRENT_TIMESTAMP</option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<field name="createdTime" type="time" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">CURRENT_TIME</option>
|
||||
</options>
|
||||
</field>
|
||||
<field name="createdDate" type="date" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">CURRENT_DATE</option>
|
||||
</options>
|
||||
</field>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -0,0 +1,44 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
|
||||
<doctrine-mapping xmlns="http://doctrine-project.org/schemas/orm/doctrine-mapping"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://doctrine-project.org/schemas/orm/doctrine-mapping
|
||||
https://www.doctrine-project.org/schemas/orm/doctrine-mapping.xsd">
|
||||
|
||||
<entity name="Doctrine\Tests\ORM\Functional\XmlTimeEntity">
|
||||
<id name="id" type="integer" column="id">
|
||||
<generator strategy="AUTO"/>
|
||||
</id>
|
||||
|
||||
<field name="createdAt" type="datetime" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">
|
||||
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
|
||||
</option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<field name="createdAtImmutable" type="datetime_immutable" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">
|
||||
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTimestamp"/>
|
||||
</option>
|
||||
</options>
|
||||
</field>
|
||||
|
||||
<field name="createdTime" type="time" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">
|
||||
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentTime"/>
|
||||
</option>
|
||||
</options>
|
||||
</field>
|
||||
<field name="createdDate" type="date" insertable="false" updatable="false">
|
||||
<options>
|
||||
<option name="default">
|
||||
<object class="Doctrine\DBAL\Schema\DefaultExpression\CurrentDate"/>
|
||||
</option>
|
||||
</options>
|
||||
</field>
|
||||
</entity>
|
||||
</doctrine-mapping>
|
||||
@@ -19,6 +19,7 @@ use ReflectionProperty;
|
||||
use Symfony\Component\Cache\Adapter\AbstractAdapter;
|
||||
use Symfony\Component\Cache\Adapter\ApcuAdapter;
|
||||
use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
use Symfony\Component\Cache\Adapter\NullAdapter;
|
||||
|
||||
use function sys_get_temp_dir;
|
||||
|
||||
@@ -68,7 +69,7 @@ class ORMSetupTest extends TestCase
|
||||
{
|
||||
$this->expectNotToPerformAssertions();
|
||||
|
||||
ORMSetup::createXMLMetadataConfig(paths: [], isXsdValidationEnabled: false);
|
||||
ORMSetup::createXMLMetadataConfig(paths: [], cache: new NullAdapter(), isXsdValidationEnabled: false);
|
||||
}
|
||||
|
||||
#[RequiresPhpExtension('apcu')]
|
||||
|
||||
@@ -147,6 +147,7 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
|
||||
}
|
||||
|
||||
#[Group('DDC-3056')]
|
||||
#[Group('GH12254')]
|
||||
public function testSelectConditionStatementWithMultipleValuesContainingNull(): void
|
||||
{
|
||||
self::assertEquals(
|
||||
@@ -168,6 +169,11 @@ class BasicEntityPersisterTypeValueSqlTest extends OrmTestCase
|
||||
'(t0.id IN (?, ?) OR t0.id IS NULL)',
|
||||
$this->persister->getSelectConditionStatementSQL('id', [123, null, 234]),
|
||||
);
|
||||
|
||||
self::assertEquals(
|
||||
'1=0',
|
||||
$this->persister->getSelectConditionStatementSQL('id', []),
|
||||
);
|
||||
}
|
||||
|
||||
public function testCountCondition(): void
|
||||
|
||||
@@ -24,6 +24,7 @@ use Doctrine\Tests\Models\CMS\CmsGroup;
|
||||
use Doctrine\Tests\Models\CMS\CmsUser;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use InvalidArgumentException;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
@@ -1375,4 +1376,99 @@ class QueryBuilderTest extends OrmTestCase
|
||||
|
||||
$qb->test();
|
||||
}
|
||||
|
||||
#[DataProvider('provideHint')]
|
||||
public function testSingleHint(mixed $expected): void
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->delete(CmsUser::class, 'u')
|
||||
->select('u.id', 'u.username')
|
||||
->setHint('foo', $expected);
|
||||
|
||||
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
self::assertTrue($query->hasHint('foo'));
|
||||
self::assertEquals($expected, $query->getHint('foo'));
|
||||
}
|
||||
|
||||
public static function provideHint(): array
|
||||
{
|
||||
return [
|
||||
['bar'],
|
||||
[new CmsUser()],
|
||||
[['a','b','c']],
|
||||
[1],
|
||||
[true],
|
||||
];
|
||||
}
|
||||
|
||||
public function testMultipleHints(): void
|
||||
{
|
||||
$object = new CmsUser();
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->delete(CmsUser::class, 'u')
|
||||
->select('u.id', 'u.username')
|
||||
->setHint('string', 'bar')
|
||||
->setHint('object', $object)
|
||||
->setHint('array', ['a', 'b', 'c'])
|
||||
->setHint('int', 5)
|
||||
->setHint('bool', true);
|
||||
|
||||
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
|
||||
$query = $qb->getQuery();
|
||||
self::assertTrue($query->hasHint('string'));
|
||||
self::assertTrue($query->hasHint('object'));
|
||||
self::assertTrue($query->hasHint('array'));
|
||||
self::assertTrue($query->hasHint('int'));
|
||||
self::assertTrue($query->hasHint('bool'));
|
||||
|
||||
self::assertEquals('bar', $query->getHint('string'));
|
||||
self::assertInstanceOf(CmsUser::class, $query->getHint('object'));
|
||||
self::assertEquals(['a', 'b', 'c'], $query->getHint('array'));
|
||||
self::assertEquals(5, $query->getHint('int'));
|
||||
self::assertTrue($query->getHint('bool'));
|
||||
}
|
||||
|
||||
public function testHasHint(): void
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->delete(CmsUser::class, 'u')
|
||||
->select('u.id', 'u.username')
|
||||
->setHint('foo', 'bar');
|
||||
|
||||
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
|
||||
self::assertTrue($qb->hasHint('foo'));
|
||||
}
|
||||
|
||||
public function testGetHint(): void
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->delete(CmsUser::class, 'u')
|
||||
->select('u.id', 'u.username')
|
||||
->setHint('foo', 'bar');
|
||||
|
||||
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
|
||||
self::assertEquals('bar', $qb->getHint('foo'));
|
||||
}
|
||||
|
||||
public function testGetHints(): void
|
||||
{
|
||||
$object = new CmsUser();
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->delete(CmsUser::class, 'u')
|
||||
->select('u.id', 'u.username')
|
||||
->setHint('string', 'bar')
|
||||
->setHint('object', $object)
|
||||
->setHint('array', ['a', 'b', 'c'])
|
||||
->setHint('int', 5)
|
||||
->setHint('bool', true);
|
||||
|
||||
$this->assertValidQueryBuilder($qb, 'SELECT u.id, u.username FROM Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
|
||||
self::assertCount(5, $qb->getHints('foo'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug;
|
||||
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
|
||||
use Doctrine\ORM\Tools\Console\Command\Debug\DebugEntityListenersDoctrineCommand;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener;
|
||||
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener;
|
||||
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function array_map;
|
||||
|
||||
class DebugEntityListenersDoctrineCommandTest extends TestCase
|
||||
{
|
||||
use ApplicationCompatibility;
|
||||
|
||||
private DebugEntityListenersDoctrineCommand $command;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$application = new Application();
|
||||
$this->command = new DebugEntityListenersDoctrineCommand($this->getMockManagerRegistry());
|
||||
|
||||
self::addCommandToApplication($application, $this->command);
|
||||
}
|
||||
|
||||
public function testExecute(): void
|
||||
{
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute(
|
||||
['command' => $this->command->getName(), 'entity' => self::class],
|
||||
);
|
||||
|
||||
self::assertSame(<<<'TXT'
|
||||
|
||||
Entity listeners for Doctrine\Tests\ORM\Tools\Console\Command\Debug\DebugEntityListenersDoctrineCommandTest
|
||||
===========================================================================================================
|
||||
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
Event Order Listener
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
preUpdate #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener::preUpdate()
|
||||
#2 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener::__invoke()
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
TXT
|
||||
, $commandTester->getDisplay(true));
|
||||
}
|
||||
|
||||
public function testExecuteWithEvent(): void
|
||||
{
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute(
|
||||
['command' => $this->command->getName(), 'entity' => self::class, 'event' => 'postPersist'],
|
||||
);
|
||||
|
||||
self::assertSame(<<<'TXT'
|
||||
|
||||
Entity listeners for Doctrine\Tests\ORM\Tools\Console\Command\Debug\DebugEntityListenersDoctrineCommandTest
|
||||
===========================================================================================================
|
||||
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
Event Order Listener
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
TXT
|
||||
, $commandTester->getDisplay(true));
|
||||
}
|
||||
|
||||
public function testExecuteWithMissingEvent(): void
|
||||
{
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute(
|
||||
['command' => $this->command->getName(), 'entity' => self::class, 'event' => 'preRemove'],
|
||||
);
|
||||
|
||||
self::assertSame(<<<'TXT'
|
||||
|
||||
[INFO] No listeners are configured for the "preRemove" event.
|
||||
|
||||
|
||||
TXT
|
||||
, $commandTester->getDisplay(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $args
|
||||
* @param list<string> $expectedSuggestions
|
||||
*/
|
||||
#[TestWith([['console'], 1, [self::class]])]
|
||||
#[TestWith([['console', self::class], 2, ['preUpdate', 'postPersist']])]
|
||||
#[TestWith([['console', 'NonExistentEntity'], 2, []])]
|
||||
public function testComplete(array $args, int $currentIndex, array $expectedSuggestions): void
|
||||
{
|
||||
$input = CompletionInput::fromTokens($args, $currentIndex);
|
||||
$input->bind($this->command->getDefinition());
|
||||
$suggestions = new CompletionSuggestions();
|
||||
|
||||
$this->command->complete($input, $suggestions);
|
||||
|
||||
self::assertSame($expectedSuggestions, array_map(static fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
|
||||
}
|
||||
|
||||
/** @return MockObject&ManagerRegistry */
|
||||
private function getMockManagerRegistry(): ManagerRegistry
|
||||
{
|
||||
$mappingDriverMock = $this->createMock(MappingDriver::class);
|
||||
$mappingDriverMock->method('getAllClassNames')->willReturn([self::class]);
|
||||
|
||||
$config = new Configuration();
|
||||
$config->setMetadataDriverImpl($mappingDriverMock);
|
||||
|
||||
$classMetadata = new ClassMetadata(self::class);
|
||||
$classMetadata->addEntityListener('preUpdate', FooListener::class, 'preUpdate');
|
||||
$classMetadata->addEntityListener('preUpdate', BarListener::class, '__invoke');
|
||||
$classMetadata->addEntityListener('postPersist', BazListener::class, 'postPersist');
|
||||
|
||||
$emMock = $this->createMock(EntityManagerInterface::class);
|
||||
$emMock->method('getConfiguration')->willReturn($config);
|
||||
$emMock->method('getClassMetadata')->willReturn($classMetadata);
|
||||
|
||||
$doctrineMock = $this->createMock(ManagerRegistry::class);
|
||||
$doctrineMock->method('getManagerNames')->willReturn(['default' => 'entity_manager.default']);
|
||||
$doctrineMock->method('getManager')->willReturn($emMock);
|
||||
$doctrineMock->method('getManagerForClass')->willReturn($emMock);
|
||||
|
||||
return $doctrineMock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug;
|
||||
|
||||
use Doctrine\Common\EventManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Tools\Console\ApplicationCompatibility;
|
||||
use Doctrine\ORM\Tools\Console\Command\Debug\DebugEventManagerDoctrineCommand;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener;
|
||||
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener;
|
||||
use Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener;
|
||||
use PHPUnit\Framework\Attributes\TestWith;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Completion\Suggestion;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function array_map;
|
||||
|
||||
class DebugEventManagerDoctrineCommandTest extends TestCase
|
||||
{
|
||||
use ApplicationCompatibility;
|
||||
|
||||
private DebugEventManagerDoctrineCommand $command;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$application = new Application();
|
||||
$this->command = new DebugEventManagerDoctrineCommand($this->getMockManagerRegistry());
|
||||
|
||||
self::addCommandToApplication($application, $this->command);
|
||||
}
|
||||
|
||||
public function testExecute(): void
|
||||
{
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute(
|
||||
['command' => $this->command->getName()],
|
||||
);
|
||||
|
||||
self::assertSame(<<<'TXT'
|
||||
|
||||
Event listeners for default entity manager
|
||||
==========================================
|
||||
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
Event Order Listener
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
preUpdate #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\FooListener::preUpdate()
|
||||
#2 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BarListener::__invoke()
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
TXT
|
||||
, $commandTester->getDisplay(true));
|
||||
}
|
||||
|
||||
public function testExecuteWithEvent(): void
|
||||
{
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute(
|
||||
['command' => $this->command->getName(), 'event' => 'postPersist'],
|
||||
);
|
||||
|
||||
self::assertSame(<<<'TXT'
|
||||
|
||||
Event listeners for default entity manager
|
||||
==========================================
|
||||
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
Event Order Listener
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
postPersist #1 Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures\BazListener::postPersist()
|
||||
------------- ------- ------------------------------------------------------------------------------------
|
||||
|
||||
|
||||
TXT
|
||||
, $commandTester->getDisplay(true));
|
||||
}
|
||||
|
||||
public function testExecuteWithMissingEvent(): void
|
||||
{
|
||||
$commandTester = new CommandTester($this->command);
|
||||
$commandTester->execute(
|
||||
['command' => $this->command->getName(), 'event' => 'preRemove'],
|
||||
);
|
||||
|
||||
self::assertSame(<<<'TXT'
|
||||
|
||||
[INFO] No listeners are configured for the "preRemove" event.
|
||||
|
||||
|
||||
TXT
|
||||
, $commandTester->getDisplay(true));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $args
|
||||
* @param list<string> $expectedSuggestions
|
||||
*/
|
||||
#[TestWith([['console'], 1, ['preUpdate', 'postPersist']])]
|
||||
#[TestWith([['console', '--em'], 1, ['default']])]
|
||||
public function testComplete(array $args, int $currentIndex, array $expectedSuggestions): void
|
||||
{
|
||||
$input = CompletionInput::fromTokens($args, $currentIndex);
|
||||
$input->bind($this->command->getDefinition());
|
||||
$suggestions = new CompletionSuggestions();
|
||||
|
||||
$this->command->complete($input, $suggestions);
|
||||
|
||||
self::assertSame($expectedSuggestions, array_map(static fn (Suggestion $suggestion) => $suggestion->getValue(), $suggestions->getValueSuggestions()));
|
||||
}
|
||||
|
||||
/** @return MockObject&ManagerRegistry */
|
||||
private function getMockManagerRegistry(): ManagerRegistry
|
||||
{
|
||||
$eventManager = new EventManager();
|
||||
$eventManager->addEventListener('preUpdate', new FooListener());
|
||||
$eventManager->addEventListener('preUpdate', new BarListener());
|
||||
$eventManager->addEventListener('postPersist', new BazListener());
|
||||
|
||||
$emMock = $this->createMock(EntityManagerInterface::class);
|
||||
$emMock->method('getEventManager')->willReturn($eventManager);
|
||||
|
||||
$doctrineMock = $this->createMock(ManagerRegistry::class);
|
||||
$doctrineMock->method('getDefaultManagerName')->willReturn('default');
|
||||
$doctrineMock->method('getManager')->willReturn($emMock);
|
||||
$doctrineMock->method('getManagerNames')->willReturn(['default' => 'entity_manager.default']);
|
||||
|
||||
return $doctrineMock;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures;
|
||||
|
||||
class BarListener
|
||||
{
|
||||
public function __invoke(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures;
|
||||
|
||||
class BazListener
|
||||
{
|
||||
public function postPersist(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Tools\Console\Command\Debug\Fixtures;
|
||||
|
||||
class FooListener
|
||||
{
|
||||
public function preUpdate(): void
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,8 @@ use Doctrine\DBAL\Schema\Name\UnqualifiedName;
|
||||
use Doctrine\DBAL\Schema\PrimaryKeyConstraint;
|
||||
use Doctrine\DBAL\Schema\PrimaryKeyConstraintEditor;
|
||||
use Doctrine\DBAL\Schema\Table as DbalTable;
|
||||
use Doctrine\DBAL\Types\EnumType;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
@@ -46,6 +48,7 @@ use Doctrine\Tests\Models\Enums\Card;
|
||||
use Doctrine\Tests\Models\Enums\Suit;
|
||||
use Doctrine\Tests\Models\Forum\ForumAvatar;
|
||||
use Doctrine\Tests\Models\Forum\ForumUser;
|
||||
use Doctrine\Tests\Models\GH10288\GH10288People;
|
||||
use Doctrine\Tests\Models\NullDefault\NullDefaultColumn;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
@@ -272,6 +275,38 @@ class SchemaToolTest extends OrmTestCase
|
||||
self::assertEquals(255, $column->getLength());
|
||||
}
|
||||
|
||||
public function testSetDiscriminatorColumnWithEnumType(): void
|
||||
{
|
||||
if (! class_exists(EnumType::class)) {
|
||||
self::markTestSkipped('Test valid for doctrine/dbal versions with EnumType only.');
|
||||
}
|
||||
|
||||
$em = $this->getTestEntityManager();
|
||||
$schemaTool = new SchemaTool($em);
|
||||
$metadata = $em->getClassMetadata(FirstEntity::class);
|
||||
|
||||
$metadata->setInheritanceType(ClassMetadata::INHERITANCE_TYPE_SINGLE_TABLE);
|
||||
$metadata->setDiscriminatorColumn([
|
||||
'name' => 'discriminator',
|
||||
'type' => Types::ENUM,
|
||||
'enumType' => GH10288People::class,
|
||||
]);
|
||||
|
||||
$schema = $schemaTool->getSchemaFromMetadata([$metadata]);
|
||||
|
||||
self::assertTrue($schema->hasTable('first_entity'));
|
||||
$table = $schema->getTable('first_entity');
|
||||
|
||||
self::assertTrue($table->hasColumn('discriminator'));
|
||||
$column = $table->getColumn('discriminator');
|
||||
self::assertEquals(GH10288People::class, $column->getPlatformOption('enumType'));
|
||||
self::assertEquals([0 => 'boss', 1 => 'employee'], $column->getValues());
|
||||
|
||||
$this->expectException(MappingException::class);
|
||||
$this->expectExceptionMessage("The entries 'user' in the discriminator map of class '" . FirstEntity::class . "' do not correspond to enum cases of '" . GH10288People::class . "'.");
|
||||
$metadata->setDiscriminatorMap(['user' => CmsUser::class, 'employee' => CmsEmployee::class]);
|
||||
}
|
||||
|
||||
public function testDerivedCompositeKey(): void
|
||||
{
|
||||
$em = $this->getTestEntityManager();
|
||||
|
||||
Reference in New Issue
Block a user