mirror of
https://github.com/doctrine/orm.git
synced 2026-04-02 20:32:19 +02:00
Compare commits
54 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e88cd591f0 | ||
|
|
88a8f75f62 | ||
|
|
9fe8ce4bf7 | ||
|
|
a46ff16339 | ||
|
|
94e60e4318 | ||
|
|
f59cd4019a | ||
|
|
81558a8b2a | ||
|
|
e431ee113d | ||
|
|
de4ec208fd | ||
|
|
df014a74c9 | ||
|
|
2f46d95028 | ||
|
|
3fda5629f6 | ||
|
|
6b273234d6 | ||
|
|
580a95ce3f | ||
|
|
8b91e248eb | ||
|
|
d2266c7d0c | ||
|
|
eb0485869a | ||
|
|
8372d600c6 | ||
|
|
dde1d71b34 | ||
|
|
0d03255061 | ||
|
|
75a18090d9 | ||
|
|
77ffd2ab68 | ||
|
|
401cc06d71 | ||
|
|
5029b193ee | ||
|
|
27c33cf88d | ||
|
|
6068b61a0d | ||
|
|
00024f7d88 | ||
|
|
331f8b52cb | ||
|
|
b2faba62b7 | ||
|
|
da426a0036 | ||
|
|
1891a76f13 | ||
|
|
4262eb495b | ||
|
|
d3b47d2cbb | ||
|
|
026f5bfe1b | ||
|
|
0b0f2f4d86 | ||
|
|
0bd839a720 | ||
|
|
b65004fc26 | ||
|
|
d2418ab074 | ||
|
|
39a05e31c9 | ||
|
|
ab156a551c | ||
|
|
2148940290 | ||
|
|
d3538095fd | ||
|
|
0c1bf14729 | ||
|
|
3b8c23c51d | ||
|
|
60d4ea694a | ||
|
|
e923bbc932 | ||
|
|
8cbd34c666 | ||
|
|
8bdefef6d1 | ||
|
|
0f8730a6e5 | ||
|
|
62477b5d42 | ||
|
|
12116aa3c2 | ||
|
|
0aeddd0592 | ||
|
|
2491c4b20d | ||
|
|
08d6167243 |
@@ -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
|
||||
|
||||
4
.github/dependabot.yml
vendored
4
.github/dependabot.yml
vendored
@@ -7,3 +7,7 @@ updates:
|
||||
labels:
|
||||
- "CI"
|
||||
target-branch: "2.20.x"
|
||||
groups:
|
||||
doctrine:
|
||||
patterns:
|
||||
- "doctrine/*"
|
||||
|
||||
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@13.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@14.0.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@13.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/composer-lint.yml@14.0.0"
|
||||
|
||||
20
.github/workflows/continuous-integration.yml
vendored
20
.github/workflows/continuous-integration.yml
vendored
@@ -116,7 +116,7 @@ jobs:
|
||||
if: "${{ matrix.native_lazy == '0' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
uses: "ramsey/composer-install@v4"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
dependency-versions: "${{ matrix.deps }}"
|
||||
@@ -150,7 +150,7 @@ jobs:
|
||||
ENABLE_NATIVE_LAZY_OBJECTS: ${{ matrix.native_lazy }}
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -257,7 +257,7 @@ jobs:
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
uses: "ramsey/composer-install@v4"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
@@ -265,7 +265,7 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/pdo_pgsql.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.postgres-version }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.extension }}-coverage"
|
||||
path: "coverage.xml"
|
||||
@@ -331,7 +331,7 @@ jobs:
|
||||
extensions: "${{ matrix.extension }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
uses: "ramsey/composer-install@v4"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
@@ -339,7 +339,7 @@ jobs:
|
||||
run: "vendor/bin/phpunit -c ci/github/phpunit/${{ matrix.extension }}.xml --coverage-clover=coverage.xml"
|
||||
|
||||
- name: "Upload coverage file"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mariadb-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage.xml"
|
||||
@@ -413,7 +413,7 @@ jobs:
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
uses: "ramsey/composer-install@v4"
|
||||
with:
|
||||
composer-options: "--ignore-platform-req=php+"
|
||||
|
||||
@@ -442,7 +442,7 @@ jobs:
|
||||
ENABLE_SECOND_LEVEL_CACHE: 1
|
||||
|
||||
- name: "Upload coverage files"
|
||||
uses: "actions/upload-artifact@v6"
|
||||
uses: "actions/upload-artifact@v7"
|
||||
with:
|
||||
name: "${{ github.job }}-${{ matrix.mysql-version }}-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
@@ -465,12 +465,12 @@ jobs:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Download coverage files"
|
||||
uses: "actions/download-artifact@v7"
|
||||
uses: "actions/download-artifact@v8"
|
||||
with:
|
||||
path: "reports"
|
||||
|
||||
- name: "Upload to Codecov"
|
||||
uses: "codecov/codecov-action@v5"
|
||||
uses: "codecov/codecov-action@v6"
|
||||
with:
|
||||
directory: reports
|
||||
env:
|
||||
|
||||
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@13.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@14.0.0"
|
||||
|
||||
2
.github/workflows/phpbench.yml
vendored
2
.github/workflows/phpbench.yml
vendored
@@ -48,7 +48,7 @@ jobs:
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
|
||||
- name: "Install dependencies with Composer"
|
||||
uses: "ramsey/composer-install@v3"
|
||||
uses: "ramsey/composer-install@v4"
|
||||
|
||||
- name: "Run PHPBench"
|
||||
run: "vendor/bin/phpbench run --report=default"
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@13.1.0"
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@14.0.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
@@ -50,7 +50,7 @@ jobs:
|
||||
|
||||
|
||||
- name: Install dependencies with Composer
|
||||
uses: ramsey/composer-install@v2
|
||||
uses: ramsey/composer-install@v4
|
||||
|
||||
- name: Run static analysis with phpstan/phpstan
|
||||
run: "vendor/bin/phpstan analyse -c ${{ matrix.config }} --error-format=checkstyle | cs2pr"
|
||||
|
||||
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
|
||||
|
||||
@@ -2301,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
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -168,7 +168,7 @@ Here is a complete list of ``Column``s attributes (all optional):
|
||||
- ``insertable`` (default: ``true``): Whether the column should be inserted.
|
||||
- ``updatable`` (default: ``true``): Whether the column should be updated.
|
||||
- ``generated`` (default: ``null``): Whether the generated strategy should be ``'NEVER'``, ``'INSERT'`` and ``ALWAYS``.
|
||||
- ``enumType`` (requires PHP 8.1 and ``doctrine/orm`` 2.11): The PHP enum class name to convert the database value into.
|
||||
- ``enumType`` (requires PHP 8.1 and ``doctrine/orm`` 2.11): The PHP enum class name to convert the database value into. See :ref:`reference-enum-mapping`.
|
||||
- ``precision`` (default: 0): The precision for a decimal (exact numeric) column
|
||||
(applies only for decimal column),
|
||||
which is the maximum number of digits that are stored for the values.
|
||||
@@ -321,6 +321,160 @@ that value and raw value are different, you have to use the raw value representa
|
||||
$messageRepository = $entityManager->getRepository(Message::class);
|
||||
$deMessages = $messageRepository->findBy(['language' => 'de']); // Use lower case here for raw value representation
|
||||
|
||||
.. _reference-enum-mapping:
|
||||
|
||||
Mapping PHP Enums
|
||||
-----------------
|
||||
|
||||
.. versionadded:: 2.11
|
||||
|
||||
Doctrine natively supports mapping PHP backed enums to database columns.
|
||||
A backed enum is a PHP enum that the same scalar type (``string`` or ``int``)
|
||||
assigned to each case. Doctrine stores the scalar value in the database and
|
||||
converts it back to the enum instance when hydrating the entity.
|
||||
|
||||
Using ``enumType`` provides three main benefits:
|
||||
|
||||
- **Automatic conversion**: Doctrine handles the conversion in both directions
|
||||
transparently. When loading an entity, scalar values from the database are
|
||||
converted into enum instances. When persisting, enum instances are reduced
|
||||
to their scalar ``->value`` before being sent to the database.
|
||||
- **Type-safety**: Entity properties contain enum instances directly. Your
|
||||
getters return ``Suit`` instead of ``string``, removing the need to call
|
||||
``Suit::from()`` manually.
|
||||
- **Validation**: When a database value does not match any enum case, Doctrine
|
||||
throws a ``MappingException`` during hydration instead of silently returning
|
||||
an invalid value.
|
||||
|
||||
This feature works with all database platforms supported by Doctrine (MySQL,
|
||||
PostgreSQL, SQLite, etc.) as it relies on standard column types (``string``,
|
||||
``integer``, ``json``, ``simple_array``) rather than any vendor-specific enum
|
||||
type.
|
||||
|
||||
.. note::
|
||||
|
||||
This is unrelated to the MySQL-specific ``ENUM`` column type covered in
|
||||
:doc:`the MySQL Enums cookbook entry </cookbook/mysql-enums>`.
|
||||
|
||||
Defining an Enum
|
||||
~~~~~~~~~~~~~~~~
|
||||
|
||||
.. literalinclude:: basic-mapping/Suit.php
|
||||
:language: php
|
||||
|
||||
Only backed enums (``string`` or ``int``) are supported. Unit enums (without
|
||||
a scalar value) cannot be mapped.
|
||||
|
||||
Single-Value Columns
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Use the ``enumType`` option on ``#[Column]`` to map a property to a backed enum.
|
||||
The underlying database column stores the enum's scalar value (``string`` or ``int``).
|
||||
|
||||
.. literalinclude:: basic-mapping/EnumMapping.php
|
||||
:language: php
|
||||
|
||||
When the PHP property is typed with the enum class, Doctrine automatically
|
||||
infers the appropriate column type (``string`` for string-backed enums,
|
||||
``integer`` for int-backed enums) and sets ``enumType``. You can also specify
|
||||
the column ``type`` explicitly.
|
||||
|
||||
Storing Collections of Enums
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
You can store multiple enum values in a single column by combining ``enumType``
|
||||
with a collection column type: ``json`` or ``simple_array``.
|
||||
|
||||
.. note::
|
||||
|
||||
Automatic type inference does not apply to collection columns. When the
|
||||
PHP property is typed as ``array``, Doctrine cannot detect the enum class.
|
||||
You must specify both ``type`` and ``enumType`` explicitly.
|
||||
|
||||
.. literalinclude:: basic-mapping/EnumCollectionMapping.php
|
||||
:language: php
|
||||
|
||||
With ``json``, the values are stored as a JSON array (e.g. ``["hearts","spades"]``).
|
||||
With ``simple_array``, the values are stored as a comma-separated string
|
||||
(e.g. ``hearts,spades``).
|
||||
|
||||
In both cases, Doctrine converts each element to and from the enum
|
||||
automatically during hydration and persistence.
|
||||
|
||||
.. tip::
|
||||
|
||||
Use ``json`` when enum values may contain commas, when you need to store
|
||||
int-backed enums (as it preserves value types), when the column also
|
||||
stores complex/nested data structures, or when you want to query individual
|
||||
values using database-native JSON operators (e.g. PostgreSQL ``jsonb``).
|
||||
Prefer ``simple_array`` for a compact, human-readable storage of
|
||||
string-backed enums whose values do not contain commas.
|
||||
|
||||
+-------------------+-----------------------------+-------------------------------+
|
||||
| Column type | Database storage | PHP type |
|
||||
+===================+=============================+===============================+
|
||||
| ``string`` | ``hearts`` | ``Suit`` |
|
||||
+-------------------+-----------------------------+-------------------------------+
|
||||
| ``integer`` | ``1`` | ``Priority`` |
|
||||
+-------------------+-----------------------------+-------------------------------+
|
||||
| ``json`` | ``["hearts","spades"]`` | ``array<Suit>`` |
|
||||
+-------------------+-----------------------------+-------------------------------+
|
||||
| ``simple_array`` | ``hearts,spades`` | ``array<Suit>`` |
|
||||
+-------------------+-----------------------------+-------------------------------+
|
||||
|
||||
Nullable Enums
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
Enum columns can be nullable. When the database value is ``NULL``, Doctrine
|
||||
preserves it as ``null`` without triggering any validation error.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[ORM\Column(type: 'string', nullable: true, enumType: Suit::class)]
|
||||
private Suit|null $suit = null;
|
||||
|
||||
Default Values
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
You can specify a database-level default using an enum case directly in the
|
||||
column options:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
#[ORM\Column(options: ['default' => Suit::Hearts])]
|
||||
public Suit $suit;
|
||||
|
||||
Using Enums in Queries
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Enum instances can be used directly as parameters in DQL, QueryBuilder, and
|
||||
repository methods. Doctrine converts them to their scalar value automatically.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
// QueryBuilder
|
||||
$qb = $em->createQueryBuilder();
|
||||
$qb->select('c')
|
||||
->from(Card::class, 'c')
|
||||
->where('c.suit = :suit')
|
||||
->setParameter('suit', Suit::Clubs);
|
||||
|
||||
// Repository
|
||||
$cards = $em->getRepository(Card::class)->findBy(['suit' => Suit::Clubs]);
|
||||
|
||||
XML Mapping
|
||||
~~~~~~~~~~~
|
||||
|
||||
When using XML mapping, the ``enum-type`` attribute is used on ``<field>``
|
||||
elements:
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<field name="suit" type="string" enum-type="App\Entity\Suit" />
|
||||
|
||||
.. _reference-mapping-types:
|
||||
|
||||
Doctrine Mapping Types
|
||||
|
||||
24
docs/en/reference/basic-mapping/EnumCollectionMapping.php
Normal file
24
docs/en/reference/basic-mapping/EnumCollectionMapping.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Player
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private int $id;
|
||||
|
||||
/** @var list<Suit> */
|
||||
#[ORM\Column(type: 'json', enumType: Suit::class)]
|
||||
private array $favouriteSuits = [];
|
||||
|
||||
/** @var list<Suit> */
|
||||
#[ORM\Column(type: 'simple_array', enumType: Suit::class)]
|
||||
private array $allowedSuits = [];
|
||||
}
|
||||
19
docs/en/reference/basic-mapping/EnumMapping.php
Normal file
19
docs/en/reference/basic-mapping/EnumMapping.php
Normal file
@@ -0,0 +1,19 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Card
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Column(enumType: Suit::class)]
|
||||
private Suit $suit;
|
||||
}
|
||||
13
docs/en/reference/basic-mapping/Suit.php
Normal file
13
docs/en/reference/basic-mapping/Suit.php
Normal file
@@ -0,0 +1,13 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Entity;
|
||||
|
||||
enum Suit: string
|
||||
{
|
||||
case Hearts = 'hearts';
|
||||
case Diamonds = 'diamonds';
|
||||
case Clubs = 'clubs';
|
||||
case Spades = 'spades';
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -2580,30 +2580,12 @@ parameters:
|
||||
count: 1
|
||||
path: src/Query/Exec/SingleTableDeleteUpdateExecutor.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Andx\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
|
||||
identifier: property.phpDocType
|
||||
count: 1
|
||||
path: src/Query/Expr/Andx.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Query\\Expr\\Func\:\:getArguments\(\) should return list\<mixed\> but returns array\<mixed\>\.$#'
|
||||
identifier: return.type
|
||||
count: 1
|
||||
path: src/Query/Expr/Func.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Orx\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
|
||||
identifier: property.phpDocType
|
||||
count: 1
|
||||
path: src/Query/Expr/Orx.php
|
||||
|
||||
-
|
||||
message: '#^PHPDoc type array\<string\> of property Doctrine\\ORM\\Query\\Expr\\Select\:\:\$allowedClasses is not covariant with PHPDoc type list\<class\-string\> of overridden property Doctrine\\ORM\\Query\\Expr\\Base\:\:\$allowedClasses\.$#'
|
||||
identifier: property.phpDocType
|
||||
count: 1
|
||||
path: src/Query/Expr/Select.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns int so it can be removed from the return type\.$#'
|
||||
identifier: return.unusedType
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -142,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));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -40,13 +40,21 @@ abstract class AbstractEntityInheritancePersister extends BasicEntityPersister
|
||||
{
|
||||
$tableAlias = $alias === 'r' ? '' : $alias;
|
||||
$fieldMapping = $class->fieldMappings[$field];
|
||||
$columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
|
||||
$sql = sprintf(
|
||||
'%s.%s',
|
||||
$this->getSQLTableAlias($class->name, $tableAlias),
|
||||
$this->quoteStrategy->getColumnName($field, $class, $this->platform),
|
||||
);
|
||||
|
||||
$columnAlias = null;
|
||||
if ($this->currentPersisterContext->rsm->hasColumnAliasByField($alias, $field)) {
|
||||
$columnAlias = $this->currentPersisterContext->rsm->getColumnAliasByField($alias, $field);
|
||||
}
|
||||
|
||||
if ($columnAlias === null) {
|
||||
$columnAlias = $this->getSQLColumnAlias($fieldMapping->columnName);
|
||||
}
|
||||
|
||||
$this->currentPersisterContext->rsm->addFieldResult($alias, $columnAlias, $field, $class->name);
|
||||
|
||||
$type = Type::getType($fieldMapping->type);
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Expr;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Expression class for building DQL and parts.
|
||||
*
|
||||
@@ -13,7 +15,7 @@ class Andx extends Composite
|
||||
{
|
||||
protected string $separator = ' AND ';
|
||||
|
||||
/** @var string[] */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [
|
||||
Comparison::class,
|
||||
Func::class,
|
||||
|
||||
@@ -13,6 +13,7 @@ use function get_debug_type;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function is_array;
|
||||
use function is_object;
|
||||
use function is_string;
|
||||
use function sprintf;
|
||||
|
||||
@@ -27,7 +28,7 @@ abstract class Base implements Stringable
|
||||
protected string $separator = ', ';
|
||||
protected string $postSeparator = ')';
|
||||
|
||||
/** @var list<class-string> */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [];
|
||||
|
||||
/** @var list<string|Stringable> */
|
||||
@@ -58,6 +59,8 @@ abstract class Base implements Stringable
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string|Stringable|null $arg
|
||||
*
|
||||
* @return $this
|
||||
*
|
||||
* @throws InvalidArgumentException
|
||||
@@ -66,7 +69,8 @@ abstract class Base implements Stringable
|
||||
{
|
||||
if ($arg !== null && (! $arg instanceof self || $arg->count() > 0)) {
|
||||
// If we decide to keep Expr\Base instances, we can use this check
|
||||
if (! is_string($arg) && ! in_array($arg::class, $this->allowedClasses, true)) {
|
||||
// @phpstan-ignore function.alreadyNarrowedType (input validation)
|
||||
if (! is_string($arg) && ! (is_object($arg) && in_array($arg::class, $this->allowedClasses, true))) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
"Expression of type '%s' not allowed in this context.",
|
||||
get_debug_type($arg),
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Expr;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Expression class for building DQL OR clauses.
|
||||
*
|
||||
@@ -13,7 +15,7 @@ class Orx extends Composite
|
||||
{
|
||||
protected string $separator = ' OR ';
|
||||
|
||||
/** @var string[] */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [
|
||||
Comparison::class,
|
||||
Func::class,
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Expr;
|
||||
|
||||
use Stringable;
|
||||
|
||||
/**
|
||||
* Expression class for building DQL select statements.
|
||||
*
|
||||
@@ -14,7 +16,7 @@ class Select extends Base
|
||||
protected string $preSeparator = '';
|
||||
protected string $postSeparator = '';
|
||||
|
||||
/** @var string[] */
|
||||
/** @var list<class-string<Stringable>> */
|
||||
protected array $allowedClasses = [Func::class];
|
||||
|
||||
/** @phpstan-var list<string|Func> */
|
||||
|
||||
@@ -363,6 +363,10 @@ class ResultSetMapping
|
||||
|
||||
public function hasColumnAliasByField(string $alias, string $fieldName): bool
|
||||
{
|
||||
if (! isset($this->aliasMap[$alias])) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$declaringClass = $this->aliasMap[$alias];
|
||||
|
||||
return isset($this->columnAliasMappings[$declaringClass][$alias][$fieldName]);
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
|
||||
|
||||
use DateTimeImmutable;
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\DiscriminatorColumn;
|
||||
use Doctrine\ORM\Mapping\DiscriminatorMap;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
use Doctrine\ORM\Mapping\Index;
|
||||
use Doctrine\ORM\Mapping\InheritanceType;
|
||||
use Doctrine\ORM\Mapping\ManyToOne;
|
||||
use Doctrine\ORM\Mapping\OneToMany;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
|
||||
#[Entity]
|
||||
#[Table(name: 'gh_12225_directory')]
|
||||
#[Index(columns: ['dir_key'])]
|
||||
#[InheritanceType('SINGLE_TABLE')]
|
||||
#[DiscriminatorColumn(name: 'type', type: 'string')]
|
||||
#[DiscriminatorMap(['main' => ConcreteDirectory::class])]
|
||||
class AbstractDirectory
|
||||
{
|
||||
#[Id]
|
||||
#[GeneratedValue]
|
||||
#[Column(name: 'id', type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
#[Column(name: 'dir_key', type: 'string')]
|
||||
private string $dirKey;
|
||||
|
||||
#[Column(name: 'deleted_at', type: 'datetime_immutable', nullable: true)]
|
||||
private DateTimeImmutable|null $deletedAt = null;
|
||||
|
||||
#[ManyToOne(targetEntity: self::class, fetch: 'LAZY', inversedBy: 'directories')]
|
||||
private AbstractDirectory|null $parent = null;
|
||||
|
||||
/** @var Collection<string, self> */
|
||||
#[OneToMany(mappedBy: 'parent', targetEntity: self::class, fetch: 'EXTRA_LAZY', indexBy: 'dirKey')]
|
||||
private Collection $children;
|
||||
|
||||
public function __construct(string $dirKey)
|
||||
{
|
||||
$this->dirKey = $dirKey;
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function getId(): int
|
||||
{
|
||||
return $this->id;
|
||||
}
|
||||
|
||||
public function getDirKey(): string
|
||||
{
|
||||
return $this->dirKey;
|
||||
}
|
||||
|
||||
public function getDeletedAt(): DateTimeImmutable|null
|
||||
{
|
||||
return $this->deletedAt;
|
||||
}
|
||||
|
||||
public function setDeletedAt(DateTimeImmutable|null $deletedAt): AbstractDirectory
|
||||
{
|
||||
$this->deletedAt = $deletedAt;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
public function getParent(): AbstractDirectory|null
|
||||
{
|
||||
return $this->parent;
|
||||
}
|
||||
|
||||
public function setParent(AbstractDirectory|null $parent): AbstractDirectory
|
||||
{
|
||||
$this->parent = $parent;
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/** @return Collection<string, self> */
|
||||
public function getChildren(): Collection
|
||||
{
|
||||
return $this->children;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
|
||||
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
|
||||
#[Entity]
|
||||
class ConcreteDirectory extends AbstractDirectory
|
||||
{
|
||||
}
|
||||
49
tests/Tests/ORM/Functional/Ticket/GH12225/GH12225Test.php
Normal file
49
tests/Tests/ORM/Functional/Ticket/GH12225/GH12225Test.php
Normal file
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
|
||||
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH12225Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
AbstractDirectory::class,
|
||||
ConcreteDirectory::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testHydrateWithIndexByFilterAndInheritanceMapping(): void
|
||||
{
|
||||
// Enable the filter
|
||||
$this->_em->getConfiguration()->addFilter('my_filter', MyFilter::class);
|
||||
$this->_em->getFilters()->enable('my_filter');
|
||||
|
||||
// Load entities into database
|
||||
$parent = new ConcreteDirectory('parent');
|
||||
$child = (new ConcreteDirectory('child'))->setParent($parent);
|
||||
$this->_em->persist($parent);
|
||||
$this->_em->persist($child);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$repository = $this->_em->getRepository(AbstractDirectory::class);
|
||||
|
||||
// Fetch entities from database while changing filters
|
||||
$this->_em->getFilters()->suspend('my_filter');
|
||||
$directories = $repository->findBy(['parent' => null]);
|
||||
$this->_em->getFilters()->restore('my_filter');
|
||||
|
||||
// Ensure we got the parent directory
|
||||
self::assertCount(1, $directories);
|
||||
self::assertEquals('parent', $directories[0]->getDirKey());
|
||||
|
||||
// Try to hydrate all children of the parent directory (toArray is important here to initialize the collection)
|
||||
self::assertCount(1, $directories[0]->getChildren()->toArray());
|
||||
}
|
||||
}
|
||||
16
tests/Tests/ORM/Functional/Ticket/GH12225/MyFilter.php
Normal file
16
tests/Tests/ORM/Functional/Ticket/GH12225/MyFilter.php
Normal file
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH12225;
|
||||
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Query\Filter\SQLFilter;
|
||||
|
||||
class MyFilter extends SQLFilter
|
||||
{
|
||||
public function addFilterConstraint(ClassMetadata $targetEntity, $targetTableAlias): string
|
||||
{
|
||||
return $targetTableAlias . '.deleted_at IS 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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -374,11 +374,27 @@ class ExprTest extends OrmTestCase
|
||||
|
||||
public function testAddThrowsException(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$orExpr = $this->expr->orX();
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$orExpr->add($this->expr->quot(5, 2));
|
||||
}
|
||||
|
||||
#[DataProvider('provideInvalidTypesForAdd')]
|
||||
public function testAddThrowsExceptionOnInvalidType(mixed $arg): void
|
||||
{
|
||||
$orExpr = $this->expr->orX();
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
$orExpr->add($arg);
|
||||
}
|
||||
|
||||
/** @return Generator<string, array{mixed}> */
|
||||
public static function provideInvalidTypesForAdd(): Generator
|
||||
{
|
||||
yield 'integer 1' => [1];
|
||||
yield 'object' => [(object) ['foo' => 'bar']];
|
||||
yield 'array' => [['foo' => 'bar']];
|
||||
}
|
||||
|
||||
#[Group('DDC-1683')]
|
||||
public function testBooleanLiteral(): void
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user