mirror of
https://github.com/doctrine/orm.git
synced 2026-04-23 14:38:03 +02:00
Compare commits
33 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d4e9276e79 | |||
| cee74faa97 | |||
| 9ae2181185 | |||
| 3e25efd72b | |||
| 47496ed882 | |||
| 492745d710 | |||
| 67419cf951 | |||
| 1237f5c909 | |||
| 609e616f2d | |||
| 4016d6ba4b | |||
| dcdd46251e | |||
| 3d98b43561 | |||
| 9f3f70944a | |||
| 05e07c0ae0 | |||
| fea42ab984 | |||
| 7c347b85c1 | |||
| 458b040d93 | |||
| 396636a2c2 | |||
| 01fd55e9ea | |||
| f357a33d23 | |||
| 6982c8ab9d | |||
| f0562f4120 | |||
| 62f2cff218 | |||
| cdd774906b | |||
| 96776e091d | |||
| f7470d8a3f | |||
| 2c41cc7f1c | |||
| a6c1e63a60 | |||
| 6881cdff4c | |||
| 9e5442a892 | |||
| 01774c035c | |||
| 6f83166266 | |||
| cb8a76ba3a |
@@ -24,4 +24,4 @@ on:
|
||||
|
||||
jobs:
|
||||
coding-standards:
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@13.0.0"
|
||||
uses: "doctrine/.github/.github/workflows/coding-standards.yml@13.1.0"
|
||||
|
||||
@@ -17,4 +17,4 @@ on:
|
||||
jobs:
|
||||
composer-lint:
|
||||
name: "Composer Lint"
|
||||
uses: "doctrine/.github/.github/workflows/composer-lint.yml@13.0.0"
|
||||
uses: "doctrine/.github/.github/workflows/composer-lint.yml@13.1.0"
|
||||
|
||||
@@ -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:
|
||||
@@ -44,26 +51,38 @@ jobs:
|
||||
- "pdo_sqlite"
|
||||
deps:
|
||||
- "highest"
|
||||
stability:
|
||||
- "stable"
|
||||
native_lazy:
|
||||
- "0"
|
||||
include:
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
extension: "pdo_sqlite"
|
||||
stability: "stable"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.2"
|
||||
dbal-version: "4@dev"
|
||||
extension: "sqlite3"
|
||||
stability: "stable"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.1"
|
||||
dbal-version: "default"
|
||||
deps: "lowest"
|
||||
extension: "pdo_sqlite"
|
||||
stability: "stable"
|
||||
native_lazy: "0"
|
||||
- php-version: "8.4"
|
||||
dbal-version: "default"
|
||||
deps: "highest"
|
||||
extension: "pdo_sqlite"
|
||||
stability: "stable"
|
||||
native_lazy: "1"
|
||||
- php-version: "8.4"
|
||||
dbal-version: "default"
|
||||
deps: "highest"
|
||||
extension: "sqlite3"
|
||||
stability: "dev"
|
||||
native_lazy: "1"
|
||||
|
||||
steps:
|
||||
@@ -80,6 +99,14 @@ jobs:
|
||||
coverage: "pcov"
|
||||
ini-values: "zend.assertions=1, apc.enable_cli=1"
|
||||
|
||||
- name: "Allow dev dependencies"
|
||||
run: |
|
||||
composer config minimum-stability dev
|
||||
composer remove --no-update --dev phpbench/phpbench phpdocumentor/guides-cli
|
||||
composer require --no-update symfony/console:^8 symfony/var-exporter:^8 doctrine/dbal:^4.4
|
||||
composer require --dev --no-update symfony/cache:^8
|
||||
if: "${{ matrix.stability == 'dev' }}"
|
||||
|
||||
- name: "Require specific DBAL version"
|
||||
run: "composer require doctrine/dbal ^${{ matrix.dbal-version }} --no-update"
|
||||
if: "${{ matrix.dbal-version != 'default' }}"
|
||||
@@ -123,9 +150,9 @@ 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.native_lazy }}-coverage"
|
||||
name: "phpunit-${{ matrix.extension }}-${{ matrix.php-version }}-${{ matrix.dbal-version }}-${{ matrix.deps }}-${{ matrix.stability }}-${{ matrix.native_lazy }}-coverage"
|
||||
path: "coverage*.xml"
|
||||
|
||||
|
||||
@@ -164,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"
|
||||
|
||||
@@ -232,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"
|
||||
|
||||
@@ -300,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"
|
||||
|
||||
@@ -397,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"
|
||||
@@ -420,7 +465,7 @@ jobs:
|
||||
fetch-depth: 2
|
||||
|
||||
- name: "Download coverage files"
|
||||
uses: "actions/download-artifact@v6"
|
||||
uses: "actions/download-artifact@v7"
|
||||
with:
|
||||
path: "reports"
|
||||
|
||||
|
||||
@@ -17,4 +17,4 @@ on:
|
||||
jobs:
|
||||
documentation:
|
||||
name: "Documentation"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@13.0.0"
|
||||
uses: "doctrine/.github/.github/workflows/documentation.yml@13.1.0"
|
||||
|
||||
@@ -7,7 +7,7 @@ on:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
uses: "doctrine/.github/.github/workflows/release-on-milestone-closed.yml@13.0.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 }}
|
||||
|
||||
+142
@@ -27,6 +27,148 @@ At this point, we recommend upgrading to PHP 8.4 first and then directly from
|
||||
ORM 2.19 to 3.5 and up so that you can skip the lazy ghost proxy generation
|
||||
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
|
||||
deprecated and will be an error in 4.0.
|
||||
|
||||
This can happen when using a join column mapping together with an id mapping,
|
||||
or when using a join column mapping or an inverse join column mapping on a
|
||||
many-to-many relationship.
|
||||
|
||||
```diff
|
||||
class User
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\ManyToOne(targetEntity: Family::class, inversedBy: 'users')]
|
||||
- #[ORM\JoinColumn(name: 'family_id', referencedColumnName: 'id', nullable: true)]
|
||||
+ #[ORM\JoinColumn(name: 'family_id', referencedColumnName: 'id')]
|
||||
private ?Family $family;
|
||||
|
||||
#[ORM\ManyToMany(targetEntity: Group::class)]
|
||||
#[ORM\JoinTable(name: 'user_group')]
|
||||
- #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: true)]
|
||||
- #[ORM\InverseJoinColumn(name: 'group_id', referencedColumnName: 'id', nullable: true)]
|
||||
+ #[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||
+ #[ORM\InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
|
||||
private Collection $groups;
|
||||
}
|
||||
```
|
||||
|
||||
## Deprecate `Doctrine\ORM\QueryBuilder::add('join', ...)` with a list of join parts
|
||||
|
||||
Using `Doctrine\ORM\QueryBuilder::add('join', ...)` with a list of join parts
|
||||
is deprecated in favor of using an associative array of join parts with the
|
||||
root alias as key.
|
||||
|
||||
## Deprecate using the `WITH` keyword for arbitrary DQL joins
|
||||
|
||||
Using the `WITH` keyword to specify the condition for an arbitrary DQL join is
|
||||
deprecated in favor of using the `ON` keyword (similar to the SQL syntax for
|
||||
joins).
|
||||
The `WITH` keyword is now meant to be used only for filtering conditions in
|
||||
association joins.
|
||||
|
||||
# Upgrade to 3.5
|
||||
|
||||
See the General notes to upgrading to 3.x versions above.
|
||||
|
||||
@@ -29,7 +29,7 @@ steps of configuration.
|
||||
|
||||
$config = new Configuration;
|
||||
$config->setMetadataCache($metadataCache);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
$config->setQueryCache($queryCache);
|
||||
|
||||
@@ -156,15 +156,59 @@ The attribute driver can be injected in the ``Doctrine\ORM\Configuration``:
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities'], true);
|
||||
$driverImpl = new AttributeDriver(['/path/to/lib/MyProject/Entities']);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
|
||||
The path information to the entities is required for the attribute
|
||||
driver, because otherwise mass-operations on all entities through
|
||||
the console could not work correctly. All of metadata drivers
|
||||
accept either a single directory as a string or an array of
|
||||
directories. With this feature a single driver can support multiple
|
||||
directories of Entities.
|
||||
the console could not work correctly. Metadata drivers can accept either
|
||||
a single directory as a string or an array of directories.
|
||||
|
||||
AttributeDriver also accepts ``Doctrine\Persistence\Mapping\Driver\ClassLocator``,
|
||||
allowing one to customize file discovery logic. You may choose to use Symfony Finder, or
|
||||
utilize directory scan with ``FileClassLocator::createFromDirectories()``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
|
||||
|
||||
$paths = ['/path/to/lib/MyProject/Entities'];
|
||||
$classLocator = FileClassLocator::createFromDirectories($paths);
|
||||
|
||||
$driverImpl = new AttributeDriver($classLocator);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
|
||||
With this feature, you're empowered to provide a fine-grained iterator of only necessary
|
||||
files to the Driver. For example, if you are using Vertical Slice architecture, you can
|
||||
exclude ``*Test.php``, ``*Controller.php``, ``*Service.php``, etc.:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Symfony\Component\Finder\Finder;
|
||||
|
||||
$finder = new Finder()->files()->in($paths)
|
||||
->name('*.php')
|
||||
->notName(['*Test.php', '*Controller.php', '*Service.php']);
|
||||
|
||||
$classLocator = new FileClassLocator($finder);
|
||||
|
||||
If you know the list of class names you want to track, use
|
||||
``Doctrine\Persistence\Mapping\Driver\ClassNames``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
use Doctrine\Persistence\Mapping\Driver\ClassNames;
|
||||
use App\Entity\{Article, Book};
|
||||
|
||||
$entityClasses = [Article::class, Book::class];
|
||||
$classLocator = new ClassNames($entityClasses);
|
||||
|
||||
$driverImpl = new AttributeDriver($classLocator);
|
||||
$config->setMetadataDriverImpl($driverImpl);
|
||||
|
||||
Metadata Cache (**RECOMMENDED**)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -190,6 +190,22 @@ 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
|
||||
|
||||
@@ -4,6 +4,8 @@ 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;
|
||||
|
||||
@@ -12,4 +14,7 @@ class Message
|
||||
{
|
||||
#[Column(options: ['default' => 'Hello World!'])]
|
||||
private string $text;
|
||||
|
||||
#[Column(options: ['default' => new CurrentTimestamp()], insertable: false, updatable: false)]
|
||||
private DateTime $createdAt;
|
||||
}
|
||||
|
||||
@@ -5,5 +5,12 @@
|
||||
<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>
|
||||
|
||||
@@ -490,7 +490,7 @@ where you can generate an arbitrary join with the following syntax:
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM User u JOIN Banlist b WITH u.email = b.email');
|
||||
$query = $em->createQuery('SELECT u FROM User u JOIN Banlist b ON u.email = b.email');
|
||||
|
||||
With an arbitrary join the result differs from the joins using a mapped property.
|
||||
The result of an arbitrary join is an one dimensional array with a mix of the entity from the ``SELECT``
|
||||
@@ -513,13 +513,15 @@ it loads all the related ``Banlist`` objects corresponding to this ``User``. Thi
|
||||
when the DQL is switched to an arbitrary join.
|
||||
|
||||
.. note::
|
||||
The differences between WHERE, WITH and HAVING clauses may be
|
||||
The differences between WHERE, WITH, ON and HAVING clauses may be
|
||||
confusing.
|
||||
|
||||
- WHERE is applied to the results of an entire query
|
||||
- WITH is applied to a join as an additional condition. For
|
||||
arbitrary joins (SELECT f, b FROM Foo f, Bar b WITH f.id = b.id)
|
||||
the WITH is required, even if it is 1 = 1
|
||||
- ON is applied to arbitrary joins as the join condition. For
|
||||
arbitrary joins (SELECT f, b FROM Foo f, Bar b ON f.id = b.id)
|
||||
the ON is required, even if it is 1 = 1. WITH is also
|
||||
supported as alternative keyword for that case for BC reasons.
|
||||
- WITH is applied to an association join as an additional condition.
|
||||
- HAVING is applied to the results of a query after
|
||||
aggregation (GROUP BY)
|
||||
|
||||
@@ -1699,9 +1701,14 @@ From, Join and Index by
|
||||
SubselectIdentificationVariableDeclaration ::= IdentificationVariableDeclaration
|
||||
RangeVariableDeclaration ::= AbstractSchemaName ["AS"] AliasIdentificationVariable
|
||||
JoinAssociationDeclaration ::= JoinAssociationPathExpression ["AS"] AliasIdentificationVariable [IndexBy]
|
||||
Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration | RangeVariableDeclaration) ["WITH" ConditionalExpression]
|
||||
Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN" (JoinAssociationDeclaration ["WITH" ConditionalExpression] | RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression])
|
||||
IndexBy ::= "INDEX" "BY" SingleValuedPathExpression
|
||||
|
||||
.. note::
|
||||
Using the ``WITH`` keyword for the ``ConditionalExpression`` of a
|
||||
``RangeVariableDeclaration`` is deprecated and will be removed in
|
||||
ORM 4.0. Use the ``ON`` keyword instead.
|
||||
|
||||
Select Expressions
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -555,6 +555,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
|
||||
^^^^^^^^^^^^^
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
|
||||
+13
-3
@@ -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"/>
|
||||
|
||||
@@ -1441,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
|
||||
@@ -1471,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
|
||||
@@ -2557,7 +2557,7 @@ parameters:
|
||||
path: src/Query/Exec/MultiTableUpdateExecutor.php
|
||||
|
||||
-
|
||||
message: '#^Parameter \#3 \$types of method Doctrine\\DBAL\\Connection\:\:executeStatement\(\) expects array\<int\<0, max\>\|string, Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|string\>, list\<Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|int\|string\> given\.$#'
|
||||
message: '#^Parameter \#3 \$types of method Doctrine\\DBAL\\Connection\:\:executeStatement\(\) expects array\<int\<0, max\>\|string, Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|Doctrine\\DBAL\\Types\\Type\|string\>, list\<Doctrine\\DBAL\\ArrayParameterType\:\:ASCII\|Doctrine\\DBAL\\ArrayParameterType\:\:BINARY\|Doctrine\\DBAL\\ArrayParameterType\:\:INTEGER\|Doctrine\\DBAL\\ArrayParameterType\:\:STRING\|Doctrine\\DBAL\\ParameterType\:\:ASCII\|Doctrine\\DBAL\\ParameterType\:\:BINARY\|Doctrine\\DBAL\\ParameterType\:\:BOOLEAN\|Doctrine\\DBAL\\ParameterType\:\:INTEGER\|Doctrine\\DBAL\\ParameterType\:\:LARGE_OBJECT\|Doctrine\\DBAL\\ParameterType\:\:NULL\|Doctrine\\DBAL\\ParameterType\:\:STRING\|Doctrine\\DBAL\\Types\\Type\|int\|string\> given\.$#'
|
||||
identifier: argument.type
|
||||
count: 1
|
||||
path: src/Query/Exec/MultiTableUpdateExecutor.php
|
||||
@@ -2604,12 +2604,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/Query/Expr/Select.php
|
||||
|
||||
-
|
||||
message: '#^Property Doctrine\\ORM\\Query\\Filter\\SQLFilter\:\:\$parameters \(array\<string, array\{type\: string, value\: mixed, is_list\: bool\}\>\) does not accept non\-empty\-array\<string, array\{value\: mixed, type\: Doctrine\\DBAL\\ArrayParameterType\|Doctrine\\DBAL\\ParameterType\|int\|string, is_list\: bool\}\>\.$#'
|
||||
identifier: assign.propertyType
|
||||
count: 2
|
||||
path: src/Query/Filter/SQLFilter.php
|
||||
|
||||
-
|
||||
message: '#^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns int so it can be removed from the return type\.$#'
|
||||
identifier: return.unusedType
|
||||
|
||||
@@ -12,6 +12,10 @@ parameters:
|
||||
message: '~^Match expression does not handle remaining values:~'
|
||||
path: src/Utility/PersisterHelper.php
|
||||
|
||||
# The return type is already narrow enough.
|
||||
- '~^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns ''[a-z_]+'' so it can be removed from the return type\.$~'
|
||||
- '~^Method Doctrine\\ORM\\Query\\ParameterTypeInferer\:\:inferType\(\) never returns Doctrine\\DBAL\\(?:Array)?ParameterType\:\:[A-Z_]+ so it can be removed from the return type\.$~'
|
||||
|
||||
# DBAL 4 compatibility
|
||||
-
|
||||
message: '~^Method Doctrine\\ORM\\Query\\AST\\Functions\\TrimFunction::getTrimMode\(\) never returns .* so it can be removed from the return type\.$~'
|
||||
|
||||
@@ -7,7 +7,6 @@ namespace Doctrine\ORM\Internal\Hydration;
|
||||
use Doctrine\DBAL\Driver\Exception;
|
||||
use Doctrine\ORM\Exception\MultipleSelectorsFoundException;
|
||||
|
||||
use function array_column;
|
||||
use function count;
|
||||
|
||||
/**
|
||||
@@ -27,8 +26,6 @@ final class ScalarColumnHydrator extends AbstractHydrator
|
||||
throw MultipleSelectorsFoundException::create($this->resultSetMapping()->fieldMappings);
|
||||
}
|
||||
|
||||
$result = $this->statement()->fetchAllNumeric();
|
||||
|
||||
return array_column($result, 0);
|
||||
return $this->statement()->fetchFirstColumn();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -113,6 +113,10 @@ class AssociationBuilder
|
||||
string|null $onDelete = null,
|
||||
string|null $columnDef = null,
|
||||
): static {
|
||||
if ($this->mapping['id'] ?? false) {
|
||||
$nullable = null;
|
||||
}
|
||||
|
||||
$this->joinColumns[] = [
|
||||
'name' => $columnName,
|
||||
'referencedColumnName' => $referencedColumnName,
|
||||
@@ -133,6 +137,9 @@ class AssociationBuilder
|
||||
public function makePrimaryKey(): static
|
||||
{
|
||||
$this->mapping['id'] = true;
|
||||
foreach ($this->joinColumns ?? [] as $i => $joinColumn) {
|
||||
$this->joinColumns[$i]['nullable'] = null;
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
@@ -24,6 +24,30 @@ class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add Join Columns.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function addJoinColumn(
|
||||
string $columnName,
|
||||
string $referencedColumnName,
|
||||
bool $nullable = true,
|
||||
bool $unique = false,
|
||||
string|null $onDelete = null,
|
||||
string|null $columnDef = null,
|
||||
): static {
|
||||
$this->joinColumns[] = [
|
||||
'name' => $columnName,
|
||||
'referencedColumnName' => $referencedColumnName,
|
||||
'unique' => $unique,
|
||||
'onDelete' => $onDelete,
|
||||
'columnDefinition' => $columnDef,
|
||||
];
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds Inverse Join Columns.
|
||||
*
|
||||
@@ -40,7 +64,6 @@ class ManyToManyAssociationBuilder extends OneToManyAssociationBuilder
|
||||
$this->inverseJoinColumns[] = [
|
||||
'name' => $columnName,
|
||||
'referencedColumnName' => $referencedColumnName,
|
||||
'nullable' => $nullable,
|
||||
'unique' => $unique,
|
||||
'onDelete' => $onDelete,
|
||||
'columnDefinition' => $columnDef,
|
||||
|
||||
@@ -546,7 +546,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
*/
|
||||
public LegacyReflectionFields|array $reflFields = [];
|
||||
|
||||
/** @var array<string, PropertyAccessors\PropertyAccessor> */
|
||||
/** @var array<string, PropertyAccessor> */
|
||||
public array $propertyAccessors = [];
|
||||
|
||||
private InstantiatorInterface|null $instantiator = null;
|
||||
@@ -584,7 +584,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
/**
|
||||
* Gets the ReflectionProperties of the mapped class.
|
||||
*
|
||||
* @return PropertyAccessor[] An array of PropertyAccessor instances.
|
||||
* @return array<string, PropertyAccessor> An array of PropertyAccessor instances by name.
|
||||
*/
|
||||
public function getPropertyAccessors(): array
|
||||
{
|
||||
@@ -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']);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\Persistence\Mapping\ClassMetadata as PersistenceClassMetadata;
|
||||
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
|
||||
use Doctrine\Persistence\Mapping\Driver\ColocatedMappingDriver;
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
use InvalidArgumentException;
|
||||
@@ -35,10 +36,10 @@ class AttributeDriver implements MappingDriver
|
||||
private readonly AttributeReader $reader;
|
||||
|
||||
/**
|
||||
* @param array<string> $paths
|
||||
* @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0
|
||||
* @param string[]|ClassLocator $paths a ClassLocator, or an array of directories.
|
||||
* @param true $reportFieldsWhereDeclared no-op, to be removed in 4.0
|
||||
*/
|
||||
public function __construct(array $paths, bool $reportFieldsWhereDeclared = true)
|
||||
public function __construct(array|ClassLocator $paths, bool $reportFieldsWhereDeclared = true)
|
||||
{
|
||||
if (! $reportFieldsWhereDeclared) {
|
||||
throw new InvalidArgumentException(sprintf(
|
||||
@@ -48,7 +49,12 @@ class AttributeDriver implements MappingDriver
|
||||
}
|
||||
|
||||
$this->reader = new AttributeReader();
|
||||
$this->addPaths($paths);
|
||||
|
||||
if ($paths instanceof ClassLocator) {
|
||||
$this->classLocator = $paths;
|
||||
} else {
|
||||
$this->addPaths($paths);
|
||||
}
|
||||
}
|
||||
|
||||
public function isTransient(string $className): bool
|
||||
|
||||
@@ -4,8 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping\Driver;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
@@ -18,10 +16,10 @@ use LogicException;
|
||||
use SimpleXMLElement;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function constant;
|
||||
use function count;
|
||||
use function defined;
|
||||
use function enum_exists;
|
||||
use function explode;
|
||||
use function extension_loaded;
|
||||
use function file_get_contents;
|
||||
@@ -409,10 +407,7 @@ class XmlDriver extends FileDriver
|
||||
if (isset($oneToManyElement->{'order-by'})) {
|
||||
$orderBy = [];
|
||||
foreach ($oneToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
// @phpstan-ignore classConstant.deprecated
|
||||
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
|
||||
$orderBy[(string) $orderByField['name']] = (string) ($orderByField['direction'] ?? 'ASC');
|
||||
}
|
||||
|
||||
$mapping['orderBy'] = $orderBy;
|
||||
@@ -538,10 +533,7 @@ class XmlDriver extends FileDriver
|
||||
if (isset($manyToManyElement->{'order-by'})) {
|
||||
$orderBy = [];
|
||||
foreach ($manyToManyElement->{'order-by'}->{'order-by-field'} ?? [] as $orderByField) {
|
||||
$orderBy[(string) $orderByField['name']] = isset($orderByField['direction'])
|
||||
? (string) $orderByField['direction']
|
||||
// @phpstan-ignore classConstant.deprecated
|
||||
: (enum_exists(Order::class) ? Order::Ascending->value : Criteria::ASC);
|
||||
$orderBy[(string) $orderByField['name']] = (string) ($orderByField['direction'] ?? 'ASC');
|
||||
}
|
||||
|
||||
$mapping['orderBy'] = $orderBy;
|
||||
@@ -665,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;
|
||||
}
|
||||
@@ -693,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
|
||||
|
||||
@@ -84,9 +84,14 @@ final class JoinTableMapping implements ArrayAccess
|
||||
/** @return mixed[] */
|
||||
public function toArray(): array
|
||||
{
|
||||
$array = (array) $this;
|
||||
$array = (array) $this;
|
||||
$toArray = static function (JoinColumnMapping $column) {
|
||||
$array = (array) $column;
|
||||
|
||||
$toArray = static fn (JoinColumnMapping $column): array => (array) $column;
|
||||
unset($array['nullable']);
|
||||
|
||||
return $array;
|
||||
};
|
||||
$array['joinColumns'] = array_map($toArray, $array['joinColumns']);
|
||||
$array['inverseJoinColumns'] = array_map($toArray, $array['inverseJoinColumns']);
|
||||
|
||||
|
||||
@@ -4,6 +4,8 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
|
||||
use function strtolower;
|
||||
use function trim;
|
||||
|
||||
@@ -127,6 +129,20 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
$mapping->joinTableColumns = [];
|
||||
|
||||
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
|
||||
if ($joinColumn->nullable !== null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
<<<'DEPRECATION'
|
||||
Specifying the "nullable" attribute for join columns in many-to-many associations (here, %s::$%s) is a no-op.
|
||||
The ORM will always set it to false.
|
||||
Doing so is deprecated and will be an error in 4.0.
|
||||
DEPRECATION,
|
||||
$mapping->sourceEntity,
|
||||
$mapping->fieldName,
|
||||
);
|
||||
}
|
||||
|
||||
$joinColumn->nullable = false;
|
||||
|
||||
if (empty($joinColumn->referencedColumnName)) {
|
||||
@@ -152,6 +168,20 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
}
|
||||
|
||||
foreach ($mapping->joinTable->inverseJoinColumns as $inverseJoinColumn) {
|
||||
if ($inverseJoinColumn->nullable !== null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
<<<'DEPRECATION'
|
||||
Specifying the "nullable" attribute for join columns in many-to-many associations (here, %s::$%s) is a no-op.
|
||||
The ORM will always set it to false.
|
||||
Doing so is deprecated and will be an error in 4.0.
|
||||
DEPRECATION,
|
||||
$mapping->targetEntity,
|
||||
$mapping->fieldName,
|
||||
);
|
||||
}
|
||||
|
||||
$inverseJoinColumn->nullable = false;
|
||||
|
||||
if (empty($inverseJoinColumn->referencedColumnName)) {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Mapping;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_flip;
|
||||
@@ -131,6 +132,20 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
|
||||
foreach ($mapping->joinColumns as $joinColumn) {
|
||||
if ($mapping->id) {
|
||||
if ($joinColumn->nullable !== null) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
<<<'DEPRECATION'
|
||||
Specifying the "nullable" attribute for join columns in to-one associations (here, %s::$%s) that are part of the identifier is a no-op.
|
||||
The ORM will always set it to false.
|
||||
Doing so is deprecated and will be an error in 4.0.
|
||||
DEPRECATION,
|
||||
$mapping->sourceEntity,
|
||||
$mapping->fieldName,
|
||||
);
|
||||
}
|
||||
|
||||
$joinColumn->nullable = false;
|
||||
} elseif ($joinColumn->nullable === null) {
|
||||
$joinColumn->nullable = true;
|
||||
@@ -200,7 +215,12 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
|
||||
$joinColumns = [];
|
||||
foreach ($array['joinColumns'] as $column) {
|
||||
$joinColumns[] = (array) $column;
|
||||
$columnArray = (array) $column;
|
||||
if ($this->id) {
|
||||
unset($columnArray['nullable']);
|
||||
}
|
||||
|
||||
$joinColumns[] = $columnArray;
|
||||
}
|
||||
|
||||
$array['joinColumns'] = $joinColumns;
|
||||
|
||||
+5
-4
@@ -7,6 +7,7 @@ namespace Doctrine\ORM;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Mapping\Driver\XmlDriver;
|
||||
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
use Redis;
|
||||
use RuntimeException;
|
||||
@@ -28,10 +29,10 @@ final class ORMSetup
|
||||
/**
|
||||
* Creates a configuration with an attribute metadata driver.
|
||||
*
|
||||
* @param string[] $paths
|
||||
* @param string[]|ClassLocator $paths
|
||||
*/
|
||||
public static function createAttributeMetadataConfiguration(
|
||||
array $paths,
|
||||
array|ClassLocator $paths,
|
||||
bool $isDevMode = false,
|
||||
string|null $proxyDir = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
@@ -55,10 +56,10 @@ final class ORMSetup
|
||||
/**
|
||||
* Creates a configuration with an attribute metadata driver.
|
||||
*
|
||||
* @param string[] $paths
|
||||
* @param string[]|ClassLocator $paths
|
||||
*/
|
||||
public static function createAttributeMetadataConfig(
|
||||
array $paths,
|
||||
array|ClassLocator $paths,
|
||||
bool $isDevMode = false,
|
||||
string|null $cacheNamespaceSeed = null,
|
||||
CacheItemPoolInterface|null $cache = null,
|
||||
|
||||
@@ -6,18 +6,28 @@ namespace Doctrine\ORM\Query\Exec;
|
||||
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\Result;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Query\AST\SelectStatement;
|
||||
use Doctrine\ORM\Query\SqlWalker;
|
||||
|
||||
/**
|
||||
* Executor that executes the SQL statement for simple DQL SELECT statements.
|
||||
*
|
||||
* @deprecated This class is no longer needed by the ORM and will be removed in 4.0.
|
||||
*
|
||||
* @link www.doctrine-project.org
|
||||
*/
|
||||
class SingleSelectExecutor extends AbstractSqlExecutor
|
||||
{
|
||||
public function __construct(SelectStatement $AST, SqlWalker $sqlWalker)
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188/',
|
||||
'The %s is no longer needed by the ORM and will be removed in 4.0',
|
||||
self::class,
|
||||
);
|
||||
|
||||
$this->sqlStatements = $sqlWalker->walkSelectStatement($AST);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query\Filter;
|
||||
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
|
||||
/** @internal */
|
||||
final class Parameter
|
||||
{
|
||||
/** @param ParameterType::*|ArrayParameterType::*|string $type */
|
||||
public function __construct(
|
||||
public readonly mixed $value,
|
||||
public readonly ParameterType|ArrayParameterType|int|string $type,
|
||||
public readonly bool $isList,
|
||||
) {
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ abstract class SQLFilter implements Stringable
|
||||
/**
|
||||
* Parameters for the filter.
|
||||
*
|
||||
* @phpstan-var array<string,array{type: string, value: mixed, is_list: bool}>
|
||||
* @phpstan-var array<string, Parameter>
|
||||
*/
|
||||
private array $parameters = [];
|
||||
|
||||
@@ -49,7 +49,7 @@ abstract class SQLFilter implements Stringable
|
||||
*/
|
||||
final public function setParameterList(string $name, array $values, string $type = Types::STRING): static
|
||||
{
|
||||
$this->parameters[$name] = ['value' => $values, 'type' => $type, 'is_list' => true];
|
||||
$this->parameters[$name] = new Parameter(value: $values, type: $type, isList: true);
|
||||
|
||||
// Keep the parameters sorted for the hash
|
||||
ksort($this->parameters);
|
||||
@@ -71,11 +71,11 @@ abstract class SQLFilter implements Stringable
|
||||
*/
|
||||
final public function setParameter(string $name, mixed $value, string|null $type = null): static
|
||||
{
|
||||
if ($type === null) {
|
||||
$type = ParameterTypeInferer::inferType($value);
|
||||
}
|
||||
|
||||
$this->parameters[$name] = ['value' => $value, 'type' => $type, 'is_list' => false];
|
||||
$this->parameters[$name] = new Parameter(
|
||||
value: $value,
|
||||
type: $type ?? ParameterTypeInferer::inferType($value),
|
||||
isList: false,
|
||||
);
|
||||
|
||||
// Keep the parameters sorted for the hash
|
||||
ksort($this->parameters);
|
||||
@@ -102,11 +102,11 @@ abstract class SQLFilter implements Stringable
|
||||
throw new InvalidArgumentException("Parameter '" . $name . "' does not exist.");
|
||||
}
|
||||
|
||||
if ($this->parameters[$name]['is_list']) {
|
||||
if ($this->parameters[$name]->isList) {
|
||||
throw FilterException::cannotConvertListParameterIntoSingleValue($name);
|
||||
}
|
||||
|
||||
return $this->em->getConnection()->quote((string) $this->parameters[$name]['value']);
|
||||
return $this->em->getConnection()->quote((string) $this->parameters[$name]->value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -124,7 +124,7 @@ abstract class SQLFilter implements Stringable
|
||||
throw new InvalidArgumentException("Parameter '" . $name . "' does not exist.");
|
||||
}
|
||||
|
||||
if ($this->parameters[$name]['is_list'] === false) {
|
||||
if (! $this->parameters[$name]->isList) {
|
||||
throw FilterException::cannotConvertSingleParameterIntoListValue($name);
|
||||
}
|
||||
|
||||
@@ -133,7 +133,7 @@ abstract class SQLFilter implements Stringable
|
||||
|
||||
$quoted = array_map(
|
||||
static fn (mixed $value): string => $connection->quote((string) $value),
|
||||
$param['value'],
|
||||
$param->value,
|
||||
);
|
||||
|
||||
return implode(',', $quoted);
|
||||
@@ -152,7 +152,14 @@ abstract class SQLFilter implements Stringable
|
||||
*/
|
||||
final public function __toString(): string
|
||||
{
|
||||
return serialize($this->parameters);
|
||||
return serialize(array_map(
|
||||
static fn (Parameter $value): array => [
|
||||
'value' => $value->value,
|
||||
'type' => $value->type,
|
||||
'is_list' => $value->isList,
|
||||
],
|
||||
$this->parameters,
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -25,9 +25,9 @@ use function is_int;
|
||||
final class ParameterTypeInferer
|
||||
{
|
||||
/**
|
||||
* Infers type of a given value, returning a compatible constant:
|
||||
* - Type (\Doctrine\DBAL\Types\Type::*)
|
||||
* - Connection (\Doctrine\DBAL\Connection::PARAM_*)
|
||||
* Infers the type of a given value
|
||||
*
|
||||
* @return ParameterType::*|ArrayParameterType::*|Types::*
|
||||
*/
|
||||
public static function inferType(mixed $value): ParameterType|ArrayParameterType|int|string
|
||||
{
|
||||
|
||||
+22
-13
@@ -1609,8 +1609,7 @@ final class Parser
|
||||
|
||||
/**
|
||||
* Join ::= ["LEFT" ["OUTER"] | "INNER"] "JOIN"
|
||||
* (JoinAssociationDeclaration | RangeVariableDeclaration)
|
||||
* ["WITH" ConditionalExpression]
|
||||
* (JoinAssociationDeclaration ["WITH" ConditionalExpression] | RangeVariableDeclaration [("ON" | "WITH") ConditionalExpression])
|
||||
*/
|
||||
public function Join(): AST\Join
|
||||
{
|
||||
@@ -1644,21 +1643,31 @@ final class Parser
|
||||
|
||||
$next = $this->lexer->glimpse();
|
||||
assert($next !== null);
|
||||
$joinDeclaration = $next->type === TokenType::T_DOT ? $this->JoinAssociationDeclaration() : $this->RangeVariableDeclaration();
|
||||
$adhocConditions = $this->lexer->isNextToken(TokenType::T_WITH);
|
||||
$join = new AST\Join($joinType, $joinDeclaration);
|
||||
$conditionalExpression = null;
|
||||
|
||||
// Describe non-root join declaration
|
||||
if ($joinDeclaration instanceof AST\RangeVariableDeclaration) {
|
||||
if ($next->type === TokenType::T_DOT) {
|
||||
$joinDeclaration = $this->JoinAssociationDeclaration();
|
||||
|
||||
if ($this->lexer->isNextToken(TokenType::T_WITH)) {
|
||||
$this->match(TokenType::T_WITH);
|
||||
$conditionalExpression = $this->ConditionalExpression();
|
||||
}
|
||||
} else {
|
||||
$joinDeclaration = $this->RangeVariableDeclaration();
|
||||
$joinDeclaration->isRoot = false;
|
||||
|
||||
if ($this->lexer->isNextToken(TokenType::T_ON)) {
|
||||
$this->match(TokenType::T_ON);
|
||||
$conditionalExpression = $this->ConditionalExpression();
|
||||
} elseif ($this->lexer->isNextToken(TokenType::T_WITH)) {
|
||||
$this->match(TokenType::T_WITH);
|
||||
$conditionalExpression = $this->ConditionalExpression();
|
||||
Deprecation::trigger('doctrine/orm', 'https://github.com/doctrine/orm/issues/12192', 'Using WITH for the join condition of arbitrary joins is deprecated. Use ON instead.');
|
||||
}
|
||||
}
|
||||
|
||||
// Check for ad-hoc Join conditions
|
||||
if ($adhocConditions) {
|
||||
$this->match(TokenType::T_WITH);
|
||||
|
||||
$join->conditionalExpression = $this->ConditionalExpression();
|
||||
}
|
||||
$join = new AST\Join($joinType, $joinDeclaration);
|
||||
$join->conditionalExpression = $conditionalExpression;
|
||||
|
||||
return $join;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Query;
|
||||
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
@@ -71,20 +72,36 @@ class ParserResult
|
||||
/**
|
||||
* Sets the SQL executor that should be used for this ParserResult.
|
||||
*
|
||||
* @deprecated
|
||||
* @deprecated The SqlExecutor will be removed from ParserResult in 4.0. Provide a SqlFinalizer instead that can create the executor.
|
||||
*/
|
||||
public function setSqlExecutor(AbstractSqlExecutor $executor): void
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188',
|
||||
'The SqlExecutor will be removed from %s in 4.0. Provide a %s instead that can create the executor.',
|
||||
self::class,
|
||||
SqlFinalizer::class,
|
||||
);
|
||||
|
||||
$this->sqlExecutor = $executor;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the SQL executor used by this ParserResult.
|
||||
*
|
||||
* @deprecated
|
||||
* @deprecated The SqlExecutor will be removed from ParserResult in 4.0. Provide a SqlFinalizer instead that can create the executor.
|
||||
*/
|
||||
public function getSqlExecutor(): AbstractSqlExecutor
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188',
|
||||
'The SqlExecutor will be removed from %s in 4.0. Provide a %s instead that can create the executor.',
|
||||
self::class,
|
||||
SqlFinalizer::class,
|
||||
);
|
||||
|
||||
if ($this->sqlExecutor === null) {
|
||||
throw new LogicException(sprintf(
|
||||
'Executor not set yet. Call %s::setSqlExecutor() first.',
|
||||
|
||||
@@ -9,6 +9,7 @@ use Doctrine\DBAL\Connection;
|
||||
use Doctrine\DBAL\LockMode;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\QuoteStrategy;
|
||||
@@ -230,6 +231,14 @@ class SqlWalker
|
||||
*/
|
||||
public function getExecutor(AST\SelectStatement|AST\UpdateStatement|AST\DeleteStatement $statement): Exec\AbstractSqlExecutor
|
||||
{
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/11188/',
|
||||
'Output walkers should implement %s. That way, the %s method is no longer needed and will be removed in 4.0',
|
||||
OutputWalker::class,
|
||||
__METHOD__,
|
||||
);
|
||||
|
||||
return match (true) {
|
||||
$statement instanceof AST\UpdateStatement => $this->createUpdateStatementExecutor($statement),
|
||||
$statement instanceof AST\DeleteStatement => $this->createDeleteStatementExecutor($statement),
|
||||
|
||||
@@ -90,4 +90,5 @@ enum TokenType: int
|
||||
case T_WHERE = 255;
|
||||
case T_WITH = 256;
|
||||
case T_NAMED = 257;
|
||||
case T_ON = 258;
|
||||
}
|
||||
|
||||
+71
-6
@@ -8,6 +8,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\ArrayParameterType;
|
||||
use Doctrine\DBAL\ParameterType;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Internal\NoUnknownNamedArguments;
|
||||
use Doctrine\ORM\Query\Expr;
|
||||
use Doctrine\ORM\Query\Parameter;
|
||||
@@ -116,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>.
|
||||
*
|
||||
@@ -207,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
|
||||
{
|
||||
@@ -287,6 +328,10 @@ class QueryBuilder implements Stringable
|
||||
$query->setCacheRegion($this->cacheRegion);
|
||||
}
|
||||
|
||||
foreach ($this->hints as $name => $value) {
|
||||
$query->setHint($name, $value);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
|
||||
@@ -305,8 +350,13 @@ class QueryBuilder implements Stringable
|
||||
} else {
|
||||
// Should never happen with correct joining order. Might be
|
||||
// thoughtful to throw exception instead.
|
||||
// @phpstan-ignore method.deprecated
|
||||
$rootAlias = $this->getRootAlias();
|
||||
$aliases = $this->getRootAliases();
|
||||
|
||||
if (! isset($aliases[0])) {
|
||||
throw new RuntimeException('No alias was set before invoking getRootAlias().');
|
||||
}
|
||||
|
||||
$rootAlias = $aliases[0];
|
||||
}
|
||||
|
||||
$this->joinRootAliases[$alias] = $rootAlias;
|
||||
@@ -541,6 +591,10 @@ class QueryBuilder implements Stringable
|
||||
*/
|
||||
public function setMaxResults(int|null $maxResults): static
|
||||
{
|
||||
if ($this->type === QueryType::Delete || $this->type === QueryType::Update) {
|
||||
throw new RuntimeException('Setting a limit is not supported for delete or update queries.');
|
||||
}
|
||||
|
||||
$this->maxResults = $maxResults;
|
||||
|
||||
return $this;
|
||||
@@ -582,14 +636,25 @@ class QueryBuilder implements Stringable
|
||||
$dqlPart = reset($dqlPart);
|
||||
}
|
||||
|
||||
// This is introduced for backwards compatibility reasons.
|
||||
// TODO: Remove for 3.0
|
||||
if ($dqlPartName === 'join') {
|
||||
$newDqlPart = [];
|
||||
|
||||
foreach ($dqlPart as $k => $v) {
|
||||
// @phpstan-ignore method.deprecated
|
||||
$k = is_numeric($k) ? $this->getRootAlias() : $k;
|
||||
if (is_numeric($k)) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/12051',
|
||||
'Using numeric keys in %s for join parts is deprecated and will not be supported in 4.0. Use an associative array with the root alias as key instead.',
|
||||
__METHOD__,
|
||||
);
|
||||
$aliases = $this->getRootAliases();
|
||||
|
||||
if (! isset($aliases[0])) {
|
||||
throw new RuntimeException('No alias was set before invoking add().');
|
||||
}
|
||||
|
||||
$k = $aliases[0];
|
||||
}
|
||||
|
||||
$newDqlPart[$k] = $v;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,6 +10,7 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\FieldMapping;
|
||||
use Doctrine\Persistence\Mapping\MappingException;
|
||||
use InvalidArgumentException;
|
||||
use JsonException;
|
||||
use Symfony\Component\Console\Completion\CompletionInput;
|
||||
use Symfony\Component\Console\Completion\CompletionSuggestions;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
@@ -52,9 +53,17 @@ final class MappingDescribeCommand extends AbstractEntityManagerCommand
|
||||
protected function configure(): void
|
||||
{
|
||||
$this->setName('orm:mapping:describe')
|
||||
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
|
||||
->setDescription('Display information about mapped objects')
|
||||
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
|
||||
->addArgument('entityName', InputArgument::REQUIRED, 'Full or partial name of entity')
|
||||
->setDescription('Display information about mapped objects')
|
||||
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
|
||||
->addOption(
|
||||
'format',
|
||||
null,
|
||||
InputOption::VALUE_REQUIRED,
|
||||
'Output format (text, json)',
|
||||
MappingDescribeCommandFormat::TEXT->value,
|
||||
array_map(static fn (MappingDescribeCommandFormat $format) => $format->value, MappingDescribeCommandFormat::cases()),
|
||||
)
|
||||
->setHelp(<<<'EOT'
|
||||
The %command.full_name% command describes the metadata for the given full or partial entity class name.
|
||||
|
||||
@@ -63,6 +72,13 @@ The %command.full_name% command describes the metadata for the given full or par
|
||||
Or:
|
||||
|
||||
<info>%command.full_name%</info> MyEntity
|
||||
|
||||
To output the metadata in JSON format, use the <info>--format</info> option:
|
||||
<info>%command.full_name% My\Namespace\Entity\MyEntity --format=json</info>
|
||||
|
||||
To use a specific entity manager (e.g., for multi-DB projects), use the <info>--em</info> option:
|
||||
<info>%command.full_name% My\Namespace\Entity\MyEntity --em=my_custom_entity_manager</info>
|
||||
|
||||
EOT);
|
||||
}
|
||||
|
||||
@@ -70,9 +86,11 @@ EOT);
|
||||
{
|
||||
$ui = new SymfonyStyle($input, $output);
|
||||
|
||||
$format = MappingDescribeCommandFormat::from($input->getOption('format'));
|
||||
|
||||
$entityManager = $this->getEntityManager($input);
|
||||
|
||||
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui);
|
||||
$this->displayEntity($input->getArgument('entityName'), $entityManager, $ui, $format);
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -89,6 +107,10 @@ EOT);
|
||||
|
||||
$suggestions->suggestValues(array_values($entities));
|
||||
}
|
||||
|
||||
if ($input->mustSuggestOptionValuesFor('format')) {
|
||||
$suggestions->suggestValues(array_map(static fn (MappingDescribeCommandFormat $format) => $format->value, MappingDescribeCommandFormat::cases()));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -100,9 +122,47 @@ EOT);
|
||||
string $entityName,
|
||||
EntityManagerInterface $entityManager,
|
||||
SymfonyStyle $ui,
|
||||
MappingDescribeCommandFormat $format,
|
||||
): void {
|
||||
$metadata = $this->getClassMetadata($entityName, $entityManager);
|
||||
|
||||
if ($format === MappingDescribeCommandFormat::JSON) {
|
||||
$ui->text(json_encode(
|
||||
[
|
||||
'name' => $metadata->name,
|
||||
'rootEntityName' => $metadata->rootEntityName,
|
||||
'customGeneratorDefinition' => $this->formatValueAsJson($metadata->customGeneratorDefinition),
|
||||
'customRepositoryClassName' => $metadata->customRepositoryClassName,
|
||||
'isMappedSuperclass' => $metadata->isMappedSuperclass,
|
||||
'isEmbeddedClass' => $metadata->isEmbeddedClass,
|
||||
'parentClasses' => $metadata->parentClasses,
|
||||
'subClasses' => $metadata->subClasses,
|
||||
'embeddedClasses' => $metadata->embeddedClasses,
|
||||
'identifier' => $metadata->identifier,
|
||||
'inheritanceType' => $metadata->inheritanceType,
|
||||
'discriminatorColumn' => $this->formatValueAsJson($metadata->discriminatorColumn),
|
||||
'discriminatorValue' => $metadata->discriminatorValue,
|
||||
'discriminatorMap' => $metadata->discriminatorMap,
|
||||
'generatorType' => $metadata->generatorType,
|
||||
'table' => $this->formatValueAsJson($metadata->table),
|
||||
'isIdentifierComposite' => $metadata->isIdentifierComposite,
|
||||
'containsForeignIdentifier' => $metadata->containsForeignIdentifier,
|
||||
'containsEnumIdentifier' => $metadata->containsEnumIdentifier,
|
||||
'sequenceGeneratorDefinition' => $this->formatValueAsJson($metadata->sequenceGeneratorDefinition),
|
||||
'changeTrackingPolicy' => $metadata->changeTrackingPolicy,
|
||||
'isVersioned' => $metadata->isVersioned,
|
||||
'versionField' => $metadata->versionField,
|
||||
'isReadOnly' => $metadata->isReadOnly,
|
||||
'entityListeners' => $metadata->entityListeners,
|
||||
'associationMappings' => $this->formatMappingsAsJson($metadata->associationMappings),
|
||||
'fieldMappings' => $this->formatMappingsAsJson($metadata->fieldMappings),
|
||||
],
|
||||
JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR,
|
||||
));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
$ui->table(
|
||||
['Field', 'Value'],
|
||||
array_merge(
|
||||
@@ -240,6 +300,22 @@ EOT);
|
||||
throw new InvalidArgumentException(sprintf('Do not know how to format value "%s"', print_r($value, true)));
|
||||
}
|
||||
|
||||
/** @throws JsonException */
|
||||
private function formatValueAsJson(mixed $value): mixed
|
||||
{
|
||||
if (is_object($value)) {
|
||||
$value = (array) $value;
|
||||
}
|
||||
|
||||
if (is_array($value)) {
|
||||
foreach ($value as $k => $v) {
|
||||
$value[$k] = $this->formatValueAsJson($v);
|
||||
}
|
||||
}
|
||||
|
||||
return $value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Add the given label and value to the two column table output
|
||||
*
|
||||
@@ -281,6 +357,22 @@ EOT);
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, FieldMapping|AssociationMapping> $propertyMappings
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function formatMappingsAsJson(array $propertyMappings): array
|
||||
{
|
||||
$output = [];
|
||||
|
||||
foreach ($propertyMappings as $propertyName => $mapping) {
|
||||
$output[$propertyName] = $this->formatValueAsJson((array) $mapping);
|
||||
}
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
/**
|
||||
* Format the entity listeners
|
||||
*
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Tools\Console\Command;
|
||||
|
||||
enum MappingDescribeCommandFormat: string
|
||||
{
|
||||
case TEXT = 'text';
|
||||
case JSON = 'json';
|
||||
}
|
||||
@@ -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,6 +508,16 @@ class SchemaTool
|
||||
], true)
|
||||
&& $options['default'] === $this->platform->getCurrentTimestampSQL()
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -512,6 +525,16 @@ class SchemaTool
|
||||
in_array($mapping->type, [Types::TIME_MUTABLE, Types::TIME_IMMUTABLE], true)
|
||||
&& $options['default'] === $this->platform->getCurrentTimeSQL()
|
||||
) {
|
||||
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();
|
||||
}
|
||||
|
||||
@@ -519,6 +542,16 @@ class SchemaTool
|
||||
in_array($mapping->type, [Types::DATE_MUTABLE, Types::DATE_IMMUTABLE], true)
|
||||
&& $options['default'] === $this->platform->getCurrentDateSQL()
|
||||
) {
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,14 +13,13 @@ use Doctrine\DBAL\Result;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Proxy\ProxyFactory;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Tests\Mocks\ArrayResultFactory;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\TestUtil;
|
||||
|
||||
use function array_map;
|
||||
use function realpath;
|
||||
|
||||
final class EntityManagerFactory
|
||||
{
|
||||
@@ -30,9 +29,9 @@ final class EntityManagerFactory
|
||||
|
||||
TestUtil::configureProxies($config);
|
||||
$config->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_EVAL);
|
||||
$config->setMetadataDriverImpl(new AttributeDriver([
|
||||
realpath(__DIR__ . '/Models/Cache'),
|
||||
realpath(__DIR__ . '/Models/GeoNames'),
|
||||
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver([
|
||||
__DIR__ . '/../Tests/Models/Cache',
|
||||
__DIR__ . '/../Tests/Models/GeoNames',
|
||||
]));
|
||||
|
||||
$entityManager = new EntityManager(
|
||||
@@ -55,10 +54,10 @@ final class EntityManagerFactory
|
||||
|
||||
TestUtil::configureProxies($config);
|
||||
$config->setAutoGenerateProxyClasses(ProxyFactory::AUTOGENERATE_EVAL);
|
||||
$config->setMetadataDriverImpl(new AttributeDriver([
|
||||
realpath(__DIR__ . '/Models/Cache'),
|
||||
realpath(__DIR__ . '/Models/Generic'),
|
||||
realpath(__DIR__ . '/Models/GeoNames'),
|
||||
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver([
|
||||
__DIR__ . '/../Tests/Models/Cache',
|
||||
__DIR__ . '/../Tests/Models/Generic',
|
||||
__DIR__ . '/../Tests/Models/GeoNames',
|
||||
]));
|
||||
|
||||
// A connection that doesn't really do anything
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Mocks;
|
||||
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
|
||||
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
|
||||
|
||||
use function interface_exists;
|
||||
|
||||
final class AttributeDriverFactory
|
||||
{
|
||||
/** @param list<string> $paths */
|
||||
public static function createAttributeDriver(array $paths = []): AttributeDriver
|
||||
{
|
||||
if (! self::isClassLocatorSupported()) {
|
||||
// Persistence < 4.1
|
||||
return new AttributeDriver($paths);
|
||||
}
|
||||
|
||||
// Persistence >= 4.1
|
||||
$classLocator = FileClassLocator::createFromDirectories($paths);
|
||||
|
||||
return new AttributeDriver($classLocator);
|
||||
}
|
||||
|
||||
/** Supported since doctrine/persistence >= 4.1 */
|
||||
public static function isClassLocatorSupported(): bool
|
||||
{
|
||||
return interface_exists(ClassLocator::class);
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ use Doctrine\Common\EventManager;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Proxy\ProxyFactory;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Tests\TestUtil;
|
||||
@@ -26,7 +25,7 @@ class EntityManagerMock extends EntityManager
|
||||
if ($config === null) {
|
||||
$config = new Configuration();
|
||||
TestUtil::configureProxies($config);
|
||||
$config->setMetadataDriverImpl(new AttributeDriver([]));
|
||||
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver());
|
||||
}
|
||||
|
||||
parent::__construct($conn, $config, $eventManager);
|
||||
|
||||
@@ -6,13 +6,14 @@ namespace Doctrine\Tests\Models\DDC3579;
|
||||
|
||||
use Doctrine\ORM\Mapping\AssociationOverride;
|
||||
use Doctrine\ORM\Mapping\AssociationOverrides;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
|
||||
#[Entity]
|
||||
#[AssociationOverrides([new AssociationOverride(name: 'groups', inversedBy: 'admins')])]
|
||||
class DDC3579Admin extends DDC3579User
|
||||
{
|
||||
public static function loadMetadata($metadata): void
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->setAssociationOverride('groups', ['inversedBy' => 'admins']);
|
||||
}
|
||||
|
||||
@@ -58,7 +58,7 @@ class DDC3579User
|
||||
return $this->groups;
|
||||
}
|
||||
|
||||
public static function loadMetadata($metadata): void
|
||||
public static function loadMetadata(ClassMetadata $metadata): void
|
||||
{
|
||||
$metadata->isMappedSuperclass = true;
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ class GH10334Foo
|
||||
{
|
||||
#[Id]
|
||||
#[ManyToOne(targetEntity: GH10334FooCollection::class, inversedBy: 'foos')]
|
||||
#[JoinColumn(name: 'foo_collection_id', referencedColumnName: 'id', nullable: false)]
|
||||
#[JoinColumn(name: 'foo_collection_id', referencedColumnName: 'id')]
|
||||
#[GeneratedValue]
|
||||
protected GH10334FooCollection $collection;
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ class InverseSide
|
||||
/** Associative id (owning identifier) */
|
||||
#[Id]
|
||||
#[OneToOne(targetEntity: InverseSideIdTarget::class, inversedBy: 'inverseSide')]
|
||||
#[JoinColumn(nullable: false, name: 'associativeId')]
|
||||
#[JoinColumn(name: 'associativeId')]
|
||||
public InverseSideIdTarget $associativeId;
|
||||
|
||||
#[OneToOne(targetEntity: OwningSide::class, mappedBy: 'inverse')]
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -9,10 +9,10 @@ use Doctrine\Common\Collections\Expr\Comparison;
|
||||
use Doctrine\DBAL\Types\EnumType;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\ORM\Query\Expr\Func;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\Models\DataTransferObjects\DtoWithArrayOfEnums;
|
||||
use Doctrine\Tests\Models\DataTransferObjects\DtoWithEnum;
|
||||
use Doctrine\Tests\Models\Enums\BookCategory;
|
||||
@@ -35,7 +35,6 @@ use Generator;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
|
||||
use function class_exists;
|
||||
use function dirname;
|
||||
use function sprintf;
|
||||
use function uniqid;
|
||||
|
||||
@@ -45,7 +44,9 @@ class EnumTest extends OrmFunctionalTestCase
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->_em = $this->getEntityManager(null, new AttributeDriver([dirname(__DIR__, 2) . '/Models/Enums'], true));
|
||||
$mappingDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/../../Models/Enums']);
|
||||
|
||||
$this->_em = $this->getEntityManager(null, $mappingDriver);
|
||||
$this->_schemaTool = new SchemaTool($this->_em);
|
||||
|
||||
if ($this->isSecondLevelCacheEnabled) {
|
||||
|
||||
@@ -9,6 +9,7 @@ use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\ORM\Functional\Locking\Doctrine\ORM\Query;
|
||||
use Doctrine\Tests\TestUtil;
|
||||
use GearmanWorker;
|
||||
@@ -116,8 +117,8 @@ class LockAgentWorker
|
||||
TestUtil::configureProxies($config);
|
||||
$config->setAutoGenerateProxyClasses(true);
|
||||
|
||||
$annotDriver = new AttributeDriver([__DIR__ . '/../../../Models/']);
|
||||
$config->setMetadataDriverImpl($annotDriver);
|
||||
$attributeDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/../../../Models']);
|
||||
$config->setMetadataDriverImpl($attributeDriver);
|
||||
$config->setMetadataCache(new ArrayAdapter());
|
||||
|
||||
$config->setQueryCache(new ArrayAdapter());
|
||||
|
||||
@@ -17,7 +17,6 @@ use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
|
||||
use function assert;
|
||||
use function class_exists;
|
||||
use function get_class;
|
||||
|
||||
/**
|
||||
@@ -438,7 +437,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$user = $this->_em->find($user::class, $user->id);
|
||||
|
||||
$criteria = Criteria::create(true)
|
||||
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
|
||||
->orderBy(['name' => Order::Ascending]);
|
||||
|
||||
self::assertEquals(
|
||||
['A', 'B', 'C', 'Developers_0'],
|
||||
@@ -478,7 +477,7 @@ class ManyToManyBasicAssociationTest extends OrmFunctionalTestCase
|
||||
$user = $this->_em->find($user::class, $user->id);
|
||||
|
||||
$criteria = Criteria::create(true)
|
||||
->orderBy(['name' => class_exists(Order::class) ? Order::Ascending : Criteria::ASC]);
|
||||
->orderBy(['name' => Order::Ascending]);
|
||||
|
||||
self::assertEquals(
|
||||
['A', 'B', 'C'],
|
||||
|
||||
@@ -5,6 +5,7 @@ declare(strict_types=1);
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Closure;
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Exec\FinalizedSelectExecutor;
|
||||
use Doctrine\ORM\Query\Exec\PreparedExecutorFinalizer;
|
||||
@@ -25,6 +26,8 @@ use function unserialize;
|
||||
|
||||
class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->useModelSet('company');
|
||||
@@ -92,6 +95,8 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
$this->assertInstanceOf(ParserResult::class, $unserialized);
|
||||
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
|
||||
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
|
||||
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11188');
|
||||
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
|
||||
$this->assertIsString($unserialized->getSqlExecutor()->getSqlStatements());
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ namespace Doctrine\Tests\ORM\Functional;
|
||||
use Doctrine\DBAL\Connection;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\Exec\SqlFinalizer;
|
||||
use Doctrine\ORM\Query\ParserResult;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\Depends;
|
||||
@@ -130,8 +131,11 @@ class QueryCacheTest extends OrmFunctionalTestCase
|
||||
}
|
||||
};
|
||||
|
||||
$sqlFinalizerMock = $this->createMock(SqlFinalizer::class);
|
||||
$sqlFinalizerMock->method('createExecutor')->with($query)->willReturn($sqlExecutorStub);
|
||||
|
||||
$parserResultMock = new ParserResult();
|
||||
$parserResultMock->setSqlExecutor($sqlExecutorStub);
|
||||
$parserResultMock->setSqlFinalizer($sqlFinalizerMock);
|
||||
|
||||
$cache = $this->createMock(CacheItemPoolInterface::class);
|
||||
|
||||
|
||||
@@ -390,7 +390,7 @@ class QueryTest extends OrmFunctionalTestCase
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$query = $this->_em->createQuery('select a, u from ' . CmsArticle::class . ' a JOIN ' . CmsUser::class . ' u WITH a.user = u');
|
||||
$query = $this->_em->createQuery('select a, u from ' . CmsArticle::class . ' a JOIN ' . CmsUser::class . ' u ON a.user = u');
|
||||
|
||||
$result = iterator_to_array($query->toIterable());
|
||||
|
||||
@@ -1067,7 +1067,7 @@ class QueryTest extends OrmFunctionalTestCase
|
||||
$query = $this->_em->createQuery('
|
||||
SELECT u, p
|
||||
FROM Doctrine\Tests\Models\CMS\CmsUser u
|
||||
INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user
|
||||
INNER JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user
|
||||
');
|
||||
$users = $query->execute();
|
||||
|
||||
@@ -1100,7 +1100,7 @@ class QueryTest extends OrmFunctionalTestCase
|
||||
$query = $this->_em->createQuery('
|
||||
SELECT u, p
|
||||
FROM Doctrine\Tests\Models\CMS\CmsUser u
|
||||
LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user
|
||||
LEFT JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user
|
||||
');
|
||||
$users = $query->execute();
|
||||
|
||||
|
||||
@@ -4,16 +4,14 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional;
|
||||
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\Models\ReadonlyProperties\Author;
|
||||
use Doctrine\Tests\Models\ReadonlyProperties\Book;
|
||||
use Doctrine\Tests\Models\ReadonlyProperties\SimpleBook;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use Doctrine\Tests\TestUtil;
|
||||
|
||||
use function dirname;
|
||||
|
||||
class ReadonlyPropertiesTest extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
@@ -22,10 +20,9 @@ class ReadonlyPropertiesTest extends OrmFunctionalTestCase
|
||||
static::$sharedConn = TestUtil::getConnection();
|
||||
}
|
||||
|
||||
$this->_em = $this->getEntityManager(null, new AttributeDriver(
|
||||
[dirname(__DIR__, 2) . '/Models/ReadonlyProperties'],
|
||||
true,
|
||||
));
|
||||
$attributeDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/../../Models/ReadonlyProperties']);
|
||||
|
||||
$this->_em = $this->getEntityManager(null, $attributeDriver);
|
||||
$this->_schemaTool = new SchemaTool($this->_em);
|
||||
|
||||
parent::setUp();
|
||||
|
||||
@@ -102,7 +102,7 @@ class DDC1209Two
|
||||
public function __construct(
|
||||
#[Id]
|
||||
#[ManyToOne(targetEntity: 'DDC1209One')]
|
||||
#[JoinColumn(referencedColumnName: 'id', nullable: false)]
|
||||
#[JoinColumn(referencedColumnName: 'id')]
|
||||
private DDC1209One $future1,
|
||||
) {
|
||||
$this->startingDatetime = new DateTime2();
|
||||
|
||||
@@ -50,7 +50,7 @@ class DDC1225TestEntity1
|
||||
{
|
||||
#[Id]
|
||||
#[ManyToOne(targetEntity: 'Doctrine\Tests\ORM\Functional\Ticket\DDC1225TestEntity2')]
|
||||
#[JoinColumn(name: 'test_entity2_id', referencedColumnName: 'id', nullable: false)]
|
||||
#[JoinColumn(name: 'test_entity2_id', referencedColumnName: 'id')]
|
||||
private DDC1225TestEntity2|null $testEntity2 = null;
|
||||
|
||||
public function setTestEntity2(DDC1225TestEntity2 $testEntity2): void
|
||||
|
||||
@@ -80,7 +80,7 @@ class MyEntity1
|
||||
public function __construct(
|
||||
#[Id]
|
||||
#[OneToOne(targetEntity: 'MyEntity2')]
|
||||
#[JoinColumn(name: 'entity2_id', referencedColumnName: 'id', nullable: false)]
|
||||
#[JoinColumn(name: 'entity2_id', referencedColumnName: 'id')]
|
||||
private MyEntity2 $entity2,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -119,10 +119,10 @@ class DDC2575A
|
||||
public function __construct(
|
||||
#[Id]
|
||||
#[OneToOne(targetEntity: 'DDC2575Root', inversedBy: 'aRelation')]
|
||||
#[JoinColumn(name: 'root_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[JoinColumn(name: 'root_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
public DDC2575Root $rootRelation,
|
||||
#[ManyToOne(targetEntity: 'DDC2575B')]
|
||||
#[JoinColumn(name: 'b_id', referencedColumnName: 'id', nullable: false, onDelete: 'CASCADE')]
|
||||
#[JoinColumn(name: 'b_id', referencedColumnName: 'id', onDelete: 'CASCADE')]
|
||||
public DDC2575B $bRelation,
|
||||
) {
|
||||
}
|
||||
|
||||
@@ -31,7 +31,7 @@ class DDC3042Test extends OrmFunctionalTestCase
|
||||
$this
|
||||
->_em
|
||||
->createQuery(
|
||||
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b WITH 1 = 1',
|
||||
'SELECT f, b FROM ' . __NAMESPACE__ . '\DDC3042Foo f JOIN ' . __NAMESPACE__ . '\DDC3042Bar b ON 1 = 1',
|
||||
)
|
||||
->getSQL(),
|
||||
'field_11',
|
||||
|
||||
@@ -39,7 +39,7 @@ final class GH6362Test extends OrmFunctionalTestCase
|
||||
* SELECT a as base, b, c, d
|
||||
* FROM Start a
|
||||
* LEFT JOIN a.bases b
|
||||
* LEFT JOIN Child c WITH b.id = c.id
|
||||
* LEFT JOIN Child c ON b.id = c.id
|
||||
* LEFT JOIN c.joins d
|
||||
*/
|
||||
#[Group('GH-6362')]
|
||||
|
||||
@@ -39,7 +39,7 @@ class GH6464Test extends OrmFunctionalTestCase
|
||||
$query = $this->_em->createQueryBuilder()
|
||||
->select('p')
|
||||
->from(GH6464Post::class, 'p')
|
||||
->innerJoin(GH6464Author::class, 'a', 'WITH', 'p.authorId = a.id')
|
||||
->innerJoin(GH6464Author::class, 'a', 'ON', 'p.authorId = a.id')
|
||||
->getQuery();
|
||||
|
||||
self::assertDoesNotMatchRegularExpression(
|
||||
|
||||
@@ -40,7 +40,7 @@ final class GH7496WithToIterableTest extends OrmFunctionalTestCase
|
||||
public function testNonUniqueObjectHydrationDuringIteration(): void
|
||||
{
|
||||
$q = $this->_em->createQuery(
|
||||
'SELECT b FROM ' . GH7496EntityAinB::class . ' aib JOIN ' . GH7496EntityB::class . ' b WITH aib.eB = b',
|
||||
'SELECT b FROM ' . GH7496EntityAinB::class . ' aib JOIN ' . GH7496EntityB::class . ' b ON aib.eB = b',
|
||||
);
|
||||
|
||||
$bs = IterableTester::iterableToArray(
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\DBAL\Platforms\AbstractPlatform;
|
||||
use Doctrine\DBAL\Types\StringType;
|
||||
use Doctrine\DBAL\Types\Type;
|
||||
@@ -146,7 +145,7 @@ class GH7820Test extends OrmFunctionalTestCase
|
||||
{
|
||||
$query = $this->_em->getRepository(GH7820Line::class)
|
||||
->createQueryBuilder('l')
|
||||
->orderBy('l.lineNumber', Criteria::ASC)
|
||||
->orderBy('l.lineNumber', 'ASC')
|
||||
->setMaxResults(100);
|
||||
|
||||
return array_map(static fn (GH7820Line $line): string => $line->toString(), iterator_to_array(new Paginator($query)));
|
||||
|
||||
@@ -10,7 +10,10 @@ use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Mapping\JoinColumnMapping;
|
||||
use Doctrine\ORM\Mapping\MappingAttribute;
|
||||
use Doctrine\Persistence\Mapping\Driver\ClassNames;
|
||||
use Doctrine\Persistence\Mapping\Driver\MappingDriver;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\Models\Cache\City;
|
||||
use Doctrine\Tests\ORM\Mapping\Fixtures\AttributeEntityWithNestedJoinColumns;
|
||||
use InvalidArgumentException;
|
||||
use stdClass;
|
||||
@@ -19,9 +22,21 @@ class AttributeDriverTest extends MappingDriverTestCase
|
||||
{
|
||||
protected function loadDriver(): MappingDriver
|
||||
{
|
||||
$paths = [];
|
||||
return AttributeDriverFactory::createAttributeDriver();
|
||||
}
|
||||
|
||||
return new AttributeDriver($paths, true);
|
||||
public function testDriverCanAcceptClassLocator(): void
|
||||
{
|
||||
if (! AttributeDriverFactory::isClassLocatorSupported()) {
|
||||
self::markTestSkipped('This test is only relevant for versions of doctrine/persistence >= 4.1');
|
||||
}
|
||||
|
||||
$classLocator = new ClassNames([City::class]);
|
||||
|
||||
$driver = new AttributeDriver($classLocator);
|
||||
|
||||
self::assertSame([], $driver->getPaths(), 'Directory paths must be empty, since file paths are used');
|
||||
self::assertSame([City::class], $driver->getAllClassNames());
|
||||
}
|
||||
|
||||
public function testOriginallyNestedAttributesDeclaredWithoutOriginalParent(): void
|
||||
|
||||
@@ -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',
|
||||
@@ -539,7 +539,6 @@ class ClassMetadataBuilderTest extends OrmTestCase
|
||||
[
|
||||
'name' => 'group_id',
|
||||
'referencedColumnName' => 'id',
|
||||
'nullable' => false,
|
||||
'unique' => false,
|
||||
'onDelete' => 'CASCADE',
|
||||
'columnDefinition' => null,
|
||||
@@ -551,7 +550,6 @@ class ClassMetadataBuilderTest extends OrmTestCase
|
||||
[
|
||||
'name' => 'user_id',
|
||||
'referencedColumnName' => 'id',
|
||||
'nullable' => false,
|
||||
'unique' => false,
|
||||
'onDelete' => null,
|
||||
'columnDefinition' => null,
|
||||
@@ -742,7 +740,6 @@ class ClassMetadataBuilderTest extends OrmTestCase
|
||||
0 => [
|
||||
'name' => 'group_id',
|
||||
'referencedColumnName' => 'id',
|
||||
'nullable' => false,
|
||||
'unique' => false,
|
||||
'onDelete' => 'CASCADE',
|
||||
'columnDefinition' => null,
|
||||
|
||||
@@ -34,4 +34,17 @@ final class JoinTableMappingTest extends TestCase
|
||||
self::assertSame('bar', $resurrectedMapping->name);
|
||||
self::assertSame(['foo' => 'bar'], $resurrectedMapping->options);
|
||||
}
|
||||
|
||||
public function testConvertingItToAMappingArrayDoesNotContainNullableInformation(): void
|
||||
{
|
||||
$mapping = new JoinTableMapping('bar');
|
||||
|
||||
$mapping->joinColumns = [new JoinColumnMapping('foo_id', 'id')];
|
||||
$mapping->inverseJoinColumns = [new JoinColumnMapping('bar_id', 'id')];
|
||||
|
||||
$mappingArray = $mapping->toArray();
|
||||
foreach ($mappingArray['joinColumns'] as $joinColumn) {
|
||||
self::assertArrayNotHasKey('nullable', $joinColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Mapping;
|
||||
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
|
||||
use Doctrine\ORM\Mapping\JoinTableMapping;
|
||||
use Doctrine\ORM\Mapping\ManyToManyOwningSideMapping;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function assert;
|
||||
@@ -16,6 +18,8 @@ use function unserialize;
|
||||
|
||||
final class ManyToManyOwningSideMappingTest extends TestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
public function testItSurvivesSerialization(): void
|
||||
{
|
||||
$mapping = new ManyToManyOwningSideMapping(
|
||||
@@ -38,22 +42,42 @@ final class ManyToManyOwningSideMappingTest extends TestCase
|
||||
self::assertSame(['bar' => 'baz'], $resurrectedMapping->relationToTargetKeyColumns);
|
||||
}
|
||||
|
||||
/** @param array<string,mixed> $mappingArray */
|
||||
#[DataProvider('mappingsProvider')]
|
||||
public function testNullableDefaults(bool $expectedValue, ManyToManyOwningSideMapping $mapping): void
|
||||
{
|
||||
#[WithoutErrorHandler]
|
||||
public function testNullableDefaults(
|
||||
bool $expectDeprecation,
|
||||
bool $expectedValue,
|
||||
array $mappingArray,
|
||||
): void {
|
||||
$namingStrategy = new DefaultNamingStrategy();
|
||||
if ($expectDeprecation) {
|
||||
$this->expectDeprecationWithIdentifier(
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
);
|
||||
} else {
|
||||
$this->expectNoDeprecationWithIdentifier(
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
);
|
||||
}
|
||||
|
||||
$mapping = ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy(
|
||||
$mappingArray,
|
||||
$namingStrategy,
|
||||
);
|
||||
|
||||
foreach ($mapping->joinTable->joinColumns as $joinColumn) {
|
||||
self::assertSame($expectedValue, $joinColumn->nullable);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{bool, ManyToManyOwningSideMapping}> */
|
||||
/** @return iterable<string, array{bool, bool, array<string,mixed>}> */
|
||||
public static function mappingsProvider(): iterable
|
||||
{
|
||||
$namingStrategy = new DefaultNamingStrategy();
|
||||
|
||||
yield 'defaults to false' => [
|
||||
false,
|
||||
ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy([
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -67,12 +91,13 @@ final class ManyToManyOwningSideMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
],
|
||||
], $namingStrategy),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'explicitly marked as nullable' => [
|
||||
true,
|
||||
false, // user's intent is ignored at the ORM level
|
||||
ManyToManyOwningSideMapping::fromMappingArrayAndNamingStrategy([
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -82,12 +107,75 @@ final class ManyToManyOwningSideMappingTest extends TestCase
|
||||
'joinColumns' => [
|
||||
['name' => 'bar_id', 'referencedColumnName' => 'id', 'nullable' => true],
|
||||
],
|
||||
'inverseJoinColumns' => [
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
],
|
||||
'id' => true,
|
||||
],
|
||||
];
|
||||
|
||||
yield 'explicitly marked as nullable (inverse column)' => [
|
||||
true,
|
||||
false, // user's intent is ignored at the ORM level
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
'isOwningSide' => true,
|
||||
'joinTable' => [
|
||||
'name' => 'bar',
|
||||
'joinColumns' => [
|
||||
['name' => 'bar_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
'inverseJoinColumns' => [
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
|
||||
],
|
||||
],
|
||||
'id' => true,
|
||||
], $namingStrategy),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'explicitly marked as not nullable' => [
|
||||
true,
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
'isOwningSide' => true,
|
||||
'joinTable' => [
|
||||
'name' => 'bar',
|
||||
'joinColumns' => [
|
||||
['name' => 'bar_id', 'referencedColumnName' => 'id', 'nullable' => false],
|
||||
],
|
||||
'inverseJoinColumns' => [
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
],
|
||||
'id' => true,
|
||||
],
|
||||
];
|
||||
|
||||
yield 'explicitly marked as not nullable (inverse column)' => [
|
||||
true,
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
'isOwningSide' => true,
|
||||
'joinTable' => [
|
||||
'name' => 'bar',
|
||||
'joinColumns' => [
|
||||
['name' => 'bar_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
'inverseJoinColumns' => [
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => false],
|
||||
],
|
||||
],
|
||||
'id' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Mapping;
|
||||
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
|
||||
use Doctrine\ORM\Mapping\JoinColumnMapping;
|
||||
use Doctrine\ORM\Mapping\ManyToOneAssociationMapping;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function assert;
|
||||
@@ -16,6 +18,8 @@ use function unserialize;
|
||||
|
||||
final class ManyToOneAssociationMappingTest extends TestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
public function testItSurvivesSerialization(): void
|
||||
{
|
||||
$mapping = new ManyToOneAssociationMapping(
|
||||
@@ -38,22 +42,45 @@ final class ManyToOneAssociationMappingTest extends TestCase
|
||||
self::assertSame(['bar' => 'foo'], $resurrectedMapping->targetToSourceKeyColumns);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $mappingArray */
|
||||
#[DataProvider('mappingsProvider')]
|
||||
public function testNullableDefaults(bool $expectedValue, ManyToOneAssociationMapping $mapping): void
|
||||
{
|
||||
#[WithoutErrorHandler]
|
||||
public function testNullableDefaults(
|
||||
bool $expectDeprecation,
|
||||
bool $expectedValue,
|
||||
array $mappingArray,
|
||||
): void {
|
||||
$namingStrategy = new DefaultNamingStrategy();
|
||||
if ($expectDeprecation) {
|
||||
$this->expectDeprecationWithIdentifier(
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
);
|
||||
} else {
|
||||
$this->expectNoDeprecationWithIdentifier(
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
);
|
||||
}
|
||||
|
||||
$mapping = ManyToOneAssociationMapping::fromMappingArrayAndName(
|
||||
$mappingArray,
|
||||
$namingStrategy,
|
||||
self::class,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
foreach ($mapping->joinColumns as $joinColumn) {
|
||||
self::assertSame($expectedValue, $joinColumn->nullable);
|
||||
}
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{bool, ManyToOneAssociationMapping}> */
|
||||
/** @return iterable<string, array{bool, bool, array<string, mixed>}> */
|
||||
public static function mappingsProvider(): iterable
|
||||
{
|
||||
$namingStrategy = new DefaultNamingStrategy();
|
||||
|
||||
yield 'not part of the identifier' => [
|
||||
false,
|
||||
true,
|
||||
ManyToOneAssociationMapping::fromMappingArrayAndName([
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -62,12 +89,13 @@ final class ManyToOneAssociationMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
'id' => false,
|
||||
], $namingStrategy, self::class, null, false),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'part of the identifier' => [
|
||||
false,
|
||||
ManyToOneAssociationMapping::fromMappingArrayAndName([
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -76,12 +104,13 @@ final class ManyToOneAssociationMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
'id' => true,
|
||||
], $namingStrategy, self::class, null, false),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'part of the identifier, but explicitly marked as nullable' => [
|
||||
true,
|
||||
false, // user's intent is ignored at the ORM level
|
||||
ManyToOneAssociationMapping::fromMappingArrayAndName([
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -90,7 +119,22 @@ final class ManyToOneAssociationMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
|
||||
],
|
||||
'id' => true,
|
||||
], $namingStrategy, self::class, null, false),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'part of the identifier, but explicitly marked as not nullable' => [
|
||||
true,
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
'isOwningSide' => true,
|
||||
'joinColumns' => [
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
|
||||
],
|
||||
'id' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -999,7 +999,7 @@ class User
|
||||
/** @var Collection<int, Group> */
|
||||
#[ORM\ManyToMany(targetEntity: 'Group', cascade: ['all'])]
|
||||
#[ORM\JoinTable(name: 'cms_user_groups')]
|
||||
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', nullable: false, unique: false)]
|
||||
#[ORM\JoinColumn(name: 'user_id', referencedColumnName: 'id', unique: false)]
|
||||
#[ORM\InverseJoinColumn(name: 'group_id', referencedColumnName: 'id', columnDefinition: 'INT NULL')]
|
||||
public $groups;
|
||||
|
||||
|
||||
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Mapping;
|
||||
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Mapping\DefaultNamingStrategy;
|
||||
use Doctrine\ORM\Mapping\JoinColumnMapping;
|
||||
use Doctrine\ORM\Mapping\OneToOneOwningSideMapping;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\WithoutErrorHandler;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
use function assert;
|
||||
@@ -16,6 +18,8 @@ use function unserialize;
|
||||
|
||||
final class OneToOneOwningSideMappingTest extends TestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
public function testItSurvivesSerialization(): void
|
||||
{
|
||||
$mapping = new OneToOneOwningSideMapping(
|
||||
@@ -38,9 +42,33 @@ final class OneToOneOwningSideMappingTest extends TestCase
|
||||
self::assertSame(['bar' => 'foo'], $resurrectedMapping->targetToSourceKeyColumns);
|
||||
}
|
||||
|
||||
/** @param array<string, mixed> $mappingArray */
|
||||
#[DataProvider('mappingsProvider')]
|
||||
public function testNullableDefaults(bool $expectedValue, OneToOneOwningSideMapping $mapping): void
|
||||
{
|
||||
#[WithoutErrorHandler]
|
||||
public function testNullableDefaults(
|
||||
bool $expectDeprecation,
|
||||
bool $expectedValue,
|
||||
array $mappingArray,
|
||||
): void {
|
||||
$namingStrategy = new DefaultNamingStrategy();
|
||||
if ($expectDeprecation) {
|
||||
$this->expectDeprecationWithIdentifier(
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
);
|
||||
} else {
|
||||
$this->expectNoDeprecationWithIdentifier(
|
||||
'https://github.com/doctrine/orm/pull/12126',
|
||||
);
|
||||
}
|
||||
|
||||
$mapping = OneToOneOwningSideMapping::fromMappingArrayAndName(
|
||||
$mappingArray,
|
||||
$namingStrategy,
|
||||
self::class,
|
||||
null,
|
||||
false,
|
||||
);
|
||||
|
||||
foreach ($mapping->joinColumns as $joinColumn) {
|
||||
self::assertSame($expectedValue, $joinColumn->nullable);
|
||||
}
|
||||
@@ -49,11 +77,10 @@ final class OneToOneOwningSideMappingTest extends TestCase
|
||||
/** @return iterable<string, array{bool, OneToOneOwningSideMapping}> */
|
||||
public static function mappingsProvider(): iterable
|
||||
{
|
||||
$namingStrategy = new DefaultNamingStrategy();
|
||||
|
||||
yield 'not part of the identifier' => [
|
||||
false,
|
||||
true,
|
||||
OneToOneOwningSideMapping::fromMappingArrayAndName([
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -62,12 +89,13 @@ final class OneToOneOwningSideMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
'id' => false,
|
||||
], $namingStrategy, self::class, null, false),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'part of the identifier' => [
|
||||
false,
|
||||
OneToOneOwningSideMapping::fromMappingArrayAndName([
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -76,12 +104,13 @@ final class OneToOneOwningSideMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id'],
|
||||
],
|
||||
'id' => true,
|
||||
], $namingStrategy, self::class, null, false),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'part of the identifier, but explicitly marked as nullable' => [
|
||||
true,
|
||||
false, // user's intent ignored at the ORM level
|
||||
OneToOneOwningSideMapping::fromMappingArrayAndName([
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
@@ -90,7 +119,66 @@ final class OneToOneOwningSideMappingTest extends TestCase
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => true],
|
||||
],
|
||||
'id' => true,
|
||||
], $namingStrategy, self::class, null, false),
|
||||
],
|
||||
];
|
||||
|
||||
yield 'part of the identifier, but explicitly marked as not nullable' => [
|
||||
true,
|
||||
false,
|
||||
[
|
||||
'fieldName' => 'foo',
|
||||
'sourceEntity' => self::class,
|
||||
'targetEntity' => self::class,
|
||||
'isOwningSide' => true,
|
||||
'joinColumns' => [
|
||||
['name' => 'foo_id', 'referencedColumnName' => 'id', 'nullable' => false],
|
||||
],
|
||||
'id' => true,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('convertToArrayProvider')]
|
||||
public function testConvertToArray(
|
||||
bool $shouldHaveNullableKey,
|
||||
bool|null $id,
|
||||
): void {
|
||||
$mapping = new OneToOneOwningSideMapping(
|
||||
fieldName: 'foo',
|
||||
sourceEntity: self::class,
|
||||
targetEntity: self::class,
|
||||
);
|
||||
|
||||
$mapping->joinColumns = [new JoinColumnMapping('foo_id', 'id')];
|
||||
$mapping->id = $id;
|
||||
|
||||
$mappingArray = $mapping->toArray();
|
||||
|
||||
foreach ($mappingArray['joinColumns'] as $joinColumn) {
|
||||
if ($shouldHaveNullableKey) {
|
||||
self::assertArrayHasKey('nullable', $joinColumn);
|
||||
} else {
|
||||
self::assertArrayNotHasKey('nullable', $joinColumn);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @return iterable<string, array{shouldHaveNullableKey: bool, id: bool|null}> */
|
||||
public static function convertToArrayProvider(): iterable
|
||||
{
|
||||
yield 'not part of the identifier' => [
|
||||
'shouldHaveNullableKey' => true,
|
||||
'id' => false,
|
||||
];
|
||||
|
||||
yield 'still not part of the identifier' => [
|
||||
'shouldHaveNullableKey' => true,
|
||||
'id' => null,
|
||||
];
|
||||
|
||||
yield 'part of the identifier' => [
|
||||
'shouldHaveNullableKey' => false,
|
||||
'id' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Mapping;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\ORM\Cache\Exception\CacheException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\ClassMetadataFactory;
|
||||
@@ -255,8 +254,8 @@ class XmlMappingDriverTest extends MappingDriverTestCase
|
||||
$class->initializeReflection(new RuntimeReflectionService());
|
||||
$driver->loadMetadataForClass(GH7141Article::class, $class);
|
||||
|
||||
self::assertEquals(
|
||||
Criteria::ASC,
|
||||
self::assertSame(
|
||||
'ASC',
|
||||
$class->getMetadataValue('associationMappings')['tags']->orderBy['position'],
|
||||
);
|
||||
}
|
||||
@@ -269,8 +268,8 @@ class XmlMappingDriverTest extends MappingDriverTestCase
|
||||
$driver = $this->loadDriver();
|
||||
$driver->loadMetadataForClass(GH7316Article::class, $class);
|
||||
|
||||
self::assertEquals(
|
||||
Criteria::ASC,
|
||||
self::assertSame(
|
||||
'ASC',
|
||||
$class->getMetadataValue('associationMappings')['tags']->orderBy['position'],
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
@@ -78,7 +78,7 @@
|
||||
</cascade>
|
||||
<join-table name="cms_users_groups">
|
||||
<join-columns>
|
||||
<join-column name="user_id" referenced-column-name="id" nullable="false" unique="false" />
|
||||
<join-column name="user_id" referenced-column-name="id" unique="false" />
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column name="group_id" referenced-column-name="id" column-definition="INT NULL" />
|
||||
|
||||
@@ -11,6 +11,9 @@ use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\ORMSetup;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\ORM\Tools\SchemaValidator;
|
||||
use Doctrine\Persistence\Mapping\Driver\ClassLocator;
|
||||
use Doctrine\Persistence\Mapping\Driver\FileClassLocator;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\Mocks\EntityManagerMock;
|
||||
use Doctrine\Tests\Models\BinaryPrimaryKey\BinaryIdType;
|
||||
use Doctrine\Tests\Models\BinaryPrimaryKey\Category;
|
||||
@@ -65,7 +68,10 @@ final class BinaryIdPersisterTest extends OrmTestCase
|
||||
return $this->entityManager;
|
||||
}
|
||||
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration([__DIR__ . '/../../Models/BinaryPrimaryKey'], isDevMode: true);
|
||||
$config = ORMSetup::createAttributeMetadataConfiguration(
|
||||
$this->getClassLocator(),
|
||||
isDevMode: true,
|
||||
);
|
||||
$config->enableNativeLazyObjects(PHP_VERSION_ID >= 80400);
|
||||
|
||||
if (! DbalType::hasType(BinaryIdType::NAME)) {
|
||||
@@ -85,4 +91,16 @@ final class BinaryIdPersisterTest extends OrmTestCase
|
||||
|
||||
return $entityManager;
|
||||
}
|
||||
|
||||
/** @return list<string>|ClassLocator */
|
||||
private function getClassLocator(): array|ClassLocator
|
||||
{
|
||||
$paths = [__DIR__ . '/../../Models/BinaryPrimaryKey'];
|
||||
|
||||
if (! AttributeDriverFactory::isClassLocatorSupported()) {
|
||||
return $paths;
|
||||
}
|
||||
|
||||
return FileClassLocator::createFromDirectories($paths);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Query;
|
||||
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
@@ -20,9 +21,12 @@ use Doctrine\Tests\Mocks\NullSqlWalker;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
use PHPUnit\Framework\Attributes\IgnoreDeprecations;
|
||||
|
||||
class LanguageRecognitionTest extends OrmTestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
private EntityManagerInterface $entityManager;
|
||||
private int $hydrationMode = AbstractQuery::HYDRATE_OBJECT;
|
||||
|
||||
@@ -262,8 +266,15 @@ class LanguageRecognitionTest extends OrmTestCase
|
||||
$this->assertValidDQL('SELECT u.name, a.topic, p.phonenumber FROM Doctrine\Tests\Models\CMS\CmsUser u INNER JOIN u.articles a LEFT JOIN u.phonenumbers p');
|
||||
}
|
||||
|
||||
public function testJoinClassPathUsingON(): void
|
||||
{
|
||||
$this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a ON a.user = u.id');
|
||||
}
|
||||
|
||||
#[IgnoreDeprecations]
|
||||
public function testJoinClassPathUsingWITH(): void
|
||||
{
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/issues/12192');
|
||||
$this->assertValidDQL('SELECT u.name FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsArticle a WITH a.user = u.id');
|
||||
}
|
||||
|
||||
@@ -638,7 +649,7 @@ class LanguageRecognitionTest extends OrmTestCase
|
||||
#[Group('DDC-3085')]
|
||||
public function testHavingSupportResultVariableInNullComparisonExpression(): void
|
||||
{
|
||||
$this->assertValidDQL('SELECT u AS user, SUM(a.id) AS score FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN Doctrine\Tests\Models\CMS\CmsAddress a WITH a.user = u GROUP BY u HAVING score IS NOT NULL AND score >= 5');
|
||||
$this->assertValidDQL('SELECT u AS user, SUM(a.id) AS score FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN Doctrine\Tests\Models\CMS\CmsAddress a ON a.user = u GROUP BY u HAVING score IS NOT NULL AND score >= 5');
|
||||
}
|
||||
|
||||
#[Group('DDC-1858')]
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Query;
|
||||
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Query\Exec\AbstractSqlExecutor;
|
||||
use Doctrine\ORM\Query\ParserResult;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
@@ -12,6 +13,8 @@ use PHPUnit\Framework\TestCase;
|
||||
|
||||
class ParserResultTest extends TestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
/** @var ParserResult */
|
||||
public $parserResult;
|
||||
|
||||
@@ -37,6 +40,8 @@ class ParserResultTest extends TestCase
|
||||
|
||||
public function testSetGetSqlExecutor(): void
|
||||
{
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/11188');
|
||||
|
||||
$executor = $this->createMock(AbstractSqlExecutor::class);
|
||||
$this->parserResult->setSqlExecutor($executor);
|
||||
self::assertSame($executor, $this->parserResult->getSqlExecutor());
|
||||
|
||||
@@ -191,7 +191,7 @@ class SelectSqlGenerationTest extends OrmTestCase
|
||||
public function testSupportsJoinOnMultipleComponents(): void
|
||||
{
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT u, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p WITH u = p.user',
|
||||
'SELECT u, p FROM Doctrine\Tests\Models\CMS\CmsUser u JOIN Doctrine\Tests\Models\CMS\CmsPhonenumber p ON u = p.user',
|
||||
'SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, c1_.phonenumber AS phonenumber_4, c0_.email_id AS email_id_5, c1_.user_id AS user_id_6 FROM cms_users c0_ INNER JOIN cms_phonenumbers c1_ ON (c0_.id = c1_.user_id)',
|
||||
);
|
||||
}
|
||||
@@ -199,17 +199,17 @@ class SelectSqlGenerationTest extends OrmTestCase
|
||||
public function testSupportsJoinOnMultipleComponentsWithJoinedInheritanceType(): void
|
||||
{
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e JOIN Doctrine\Tests\Models\Company\CompanyManager m WITH e.id = m.id',
|
||||
'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e JOIN Doctrine\Tests\Models\Company\CompanyManager m ON e.id = m.id',
|
||||
'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c2_.title AS title_5, c0_.discr AS discr_6, c0_.spouse_id AS spouse_id_7, c2_.car_id AS car_id_8 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id INNER JOIN (company_managers c3_ INNER JOIN company_employees c5_ ON c3_.id = c5_.id INNER JOIN company_persons c4_ ON c3_.id = c4_.id) ON (c0_.id = c4_.id)',
|
||||
);
|
||||
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyManager m WITH e.id = m.id',
|
||||
'SELECT e FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyManager m ON e.id = m.id',
|
||||
'SELECT c0_.id AS id_0, c0_.name AS name_1, c1_.salary AS salary_2, c1_.department AS department_3, c1_.startDate AS startDate_4, c2_.title AS title_5, c0_.discr AS discr_6, c0_.spouse_id AS spouse_id_7, c2_.car_id AS car_id_8 FROM company_employees c1_ INNER JOIN company_persons c0_ ON c1_.id = c0_.id LEFT JOIN company_managers c2_ ON c1_.id = c2_.id LEFT JOIN (company_managers c3_ INNER JOIN company_employees c5_ ON c3_.id = c5_.id INNER JOIN company_persons c4_ ON c3_.id = c4_.id) ON (c0_.id = c4_.id)',
|
||||
);
|
||||
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c JOIN c.salesPerson s LEFT JOIN Doctrine\Tests\Models\Company\CompanyEvent e WITH s.id = e.id',
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c JOIN c.salesPerson s LEFT JOIN Doctrine\Tests\Models\Company\CompanyEvent e ON s.id = e.id',
|
||||
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6, c0_.salesPerson_id AS salesPerson_id_7 FROM company_contracts c0_ INNER JOIN company_employees c1_ ON c0_.salesPerson_id = c1_.id LEFT JOIN company_persons c2_ ON c1_.id = c2_.id LEFT JOIN company_managers c3_ ON c1_.id = c3_.id LEFT JOIN (company_events c4_ LEFT JOIN company_auctions c5_ ON c4_.id = c5_.id LEFT JOIN company_raffles c6_ ON c4_.id = c6_.id) ON (c2_.id = c4_.id) WHERE c0_.discr IN ('fix', 'flexible', 'flexultra')",
|
||||
);
|
||||
}
|
||||
@@ -2025,7 +2025,7 @@ SQL,
|
||||
{
|
||||
// Regression test for the bug
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyContract c WITH c.salesPerson = e.id',
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyContract c ON c.salesPerson = e.id',
|
||||
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6, c0_.salesPerson_id AS salesPerson_id_7 FROM company_employees c1_ INNER JOIN company_persons c2_ ON c1_.id = c2_.id LEFT JOIN company_managers c3_ ON c1_.id = c3_.id LEFT JOIN company_contracts c0_ ON (c0_.salesPerson_id = c2_.id) AND c0_.discr IN ('fix', 'flexible', 'flexultra')",
|
||||
);
|
||||
}
|
||||
@@ -2035,7 +2035,7 @@ SQL,
|
||||
{
|
||||
// Ensure other WHERE predicates are passed through to the main WHERE clause
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyContract c WITH c.salesPerson = e.id WHERE e.salary > 1000',
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyEmployee e LEFT JOIN Doctrine\Tests\Models\Company\CompanyContract c ON c.salesPerson = e.id WHERE e.salary > 1000',
|
||||
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6, c0_.salesPerson_id AS salesPerson_id_7 FROM company_employees c1_ INNER JOIN company_persons c2_ ON c1_.id = c2_.id LEFT JOIN company_managers c3_ ON c1_.id = c3_.id LEFT JOIN company_contracts c0_ ON (c0_.salesPerson_id = c2_.id) AND c0_.discr IN ('fix', 'flexible', 'flexultra') WHERE c1_.salary > 1000",
|
||||
);
|
||||
}
|
||||
@@ -2045,7 +2045,7 @@ SQL,
|
||||
{
|
||||
// Test inner joins too
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyEmployee e INNER JOIN Doctrine\Tests\Models\Company\CompanyContract c WITH c.salesPerson = e.id',
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyEmployee e INNER JOIN Doctrine\Tests\Models\Company\CompanyContract c ON c.salesPerson = e.id',
|
||||
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6, c0_.salesPerson_id AS salesPerson_id_7 FROM company_employees c1_ INNER JOIN company_persons c2_ ON c1_.id = c2_.id LEFT JOIN company_managers c3_ ON c1_.id = c3_.id INNER JOIN company_contracts c0_ ON (c0_.salesPerson_id = c2_.id) AND c0_.discr IN ('fix', 'flexible', 'flexultra')",
|
||||
);
|
||||
}
|
||||
@@ -2056,7 +2056,7 @@ SQL,
|
||||
// Test that the discriminator IN() predicate is still added into
|
||||
// the where clause when not joining onto that table
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c LEFT JOIN Doctrine\Tests\Models\Company\CompanyEmployee e WITH e.id = c.salesPerson WHERE c.completed = true',
|
||||
'SELECT c FROM Doctrine\Tests\Models\Company\CompanyContract c LEFT JOIN Doctrine\Tests\Models\Company\CompanyEmployee e ON e.id = c.salesPerson WHERE c.completed = true',
|
||||
"SELECT c0_.id AS id_0, c0_.completed AS completed_1, c0_.fixPrice AS fixPrice_2, c0_.hoursWorked AS hoursWorked_3, c0_.pricePerHour AS pricePerHour_4, c0_.maxPrice AS maxPrice_5, c0_.discr AS discr_6, c0_.salesPerson_id AS salesPerson_id_7 FROM company_contracts c0_ LEFT JOIN (company_employees c1_ INNER JOIN company_persons c2_ ON c1_.id = c2_.id LEFT JOIN company_managers c3_ ON c1_.id = c3_.id) ON (c2_.id = c0_.salesPerson_id) WHERE (c0_.completed = 1) AND c0_.discr IN ('fix', 'flexible', 'flexultra')",
|
||||
);
|
||||
}
|
||||
@@ -2109,7 +2109,7 @@ SQL,
|
||||
public function testHavingSupportResultVariableNullComparisonExpression(): void
|
||||
{
|
||||
$this->assertSqlGeneration(
|
||||
'SELECT u AS user, SUM(a.id) AS score FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN Doctrine\Tests\Models\CMS\CmsAddress a WITH a.user = u GROUP BY u HAVING score IS NOT NULL AND score >= 5',
|
||||
'SELECT u AS user, SUM(a.id) AS score FROM Doctrine\Tests\Models\CMS\CmsUser u LEFT JOIN Doctrine\Tests\Models\CMS\CmsAddress a ON a.user = u GROUP BY u HAVING score IS NOT NULL AND score >= 5',
|
||||
'SELECT c0_.id AS id_0, c0_.status AS status_1, c0_.username AS username_2, c0_.name AS name_3, SUM(c1_.id) AS sclr_4, c0_.email_id AS email_id_5 FROM cms_users c0_ LEFT JOIN cms_addresses c1_ ON (c1_.user_id = c0_.id) GROUP BY c0_.id, c0_.status, c0_.username, c0_.name, c0_.email_id HAVING sclr_4 IS NOT NULL AND sclr_4 >= 5',
|
||||
);
|
||||
}
|
||||
|
||||
@@ -9,6 +9,7 @@ use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Common\Collections\Order;
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\Deprecations\PHPUnit\VerifyDeprecations;
|
||||
use Doctrine\ORM\Cache;
|
||||
use Doctrine\ORM\Query;
|
||||
use Doctrine\ORM\Query\Expr\Join;
|
||||
@@ -23,11 +24,13 @@ 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;
|
||||
use RuntimeException;
|
||||
|
||||
use function array_filter;
|
||||
use function class_exists;
|
||||
|
||||
/**
|
||||
* Test case for the QueryBuilder class used to build DQL query string in a
|
||||
@@ -35,6 +38,8 @@ use function class_exists;
|
||||
*/
|
||||
class QueryBuilderTest extends OrmTestCase
|
||||
{
|
||||
use VerifyDeprecations;
|
||||
|
||||
private EntityManagerMock $entityManager;
|
||||
|
||||
protected function setUp(): void
|
||||
@@ -68,6 +73,26 @@ class QueryBuilderTest extends OrmTestCase
|
||||
$this->assertValidQueryBuilder($qb, 'DELETE Doctrine\Tests\Models\CMS\CmsUser u');
|
||||
}
|
||||
|
||||
public function testDeleteWithLimitNotSupported(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Setting a limit is not supported for delete or update queries.');
|
||||
|
||||
$this->entityManager->createQueryBuilder()
|
||||
->delete(CmsUser::class, 'c')
|
||||
->setMaxResults(1);
|
||||
}
|
||||
|
||||
public function testUpdateWithLimitNotSupported(): void
|
||||
{
|
||||
$this->expectException(RuntimeException::class);
|
||||
$this->expectExceptionMessage('Setting a limit is not supported for delete or update queries.');
|
||||
|
||||
$this->entityManager->createQueryBuilder()
|
||||
->update(CmsUser::class, 'c')
|
||||
->setMaxResults(1);
|
||||
}
|
||||
|
||||
public function testUpdateSetsType(): void
|
||||
{
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
@@ -590,7 +615,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
->from(CmsUser::class, 'u');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria->orderBy(['field' => class_exists(Order::class) ? Order::Descending : Criteria::DESC]);
|
||||
$criteria->orderBy(['field' => Order::Descending]);
|
||||
|
||||
$qb->addCriteria($criteria);
|
||||
|
||||
@@ -607,7 +632,7 @@ class QueryBuilderTest extends OrmTestCase
|
||||
->join('u.article', 'a');
|
||||
|
||||
$criteria = Criteria::create(true);
|
||||
$criteria->orderBy(['a.field' => class_exists(Order::class) ? Order::Descending : Criteria::DESC]);
|
||||
$criteria->orderBy(['a.field' => Order::Descending]);
|
||||
|
||||
$qb->addCriteria($criteria);
|
||||
|
||||
@@ -1031,8 +1056,10 @@ class QueryBuilderTest extends OrmTestCase
|
||||
self::assertEquals('u', $qb->getRootAlias());
|
||||
}
|
||||
|
||||
#[WithoutErrorHandler]
|
||||
public function testBCAddJoinWithoutRootAlias(): void
|
||||
{
|
||||
$this->expectDeprecationWithIdentifier('https://github.com/doctrine/orm/pull/12051');
|
||||
$qb = $this->entityManager->createQueryBuilder()
|
||||
->select('u')
|
||||
->from(CmsUser::class, 'u')
|
||||
@@ -1349,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'));
|
||||
}
|
||||
}
|
||||
|
||||
+152
@@ -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
|
||||
{
|
||||
}
|
||||
}
|
||||
@@ -16,6 +16,8 @@ use Symfony\Component\Console\Application;
|
||||
use Symfony\Component\Console\Tester\CommandCompletionTester;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
use function json_decode;
|
||||
|
||||
/**
|
||||
* Tests for {@see \Doctrine\ORM\Tools\Console\Command\MappingDescribeCommand}
|
||||
*/
|
||||
@@ -56,6 +58,25 @@ class MappingDescribeCommandTest extends OrmFunctionalTestCase
|
||||
self::assertStringContainsString('Root entity name', $display);
|
||||
}
|
||||
|
||||
public function testShowSpecificFuzzySingleJson(): void
|
||||
{
|
||||
$this->tester->execute([
|
||||
'command' => $this->command->getName(),
|
||||
'entityName' => 'AttractionInfo',
|
||||
'--format' => 'json',
|
||||
]);
|
||||
|
||||
$display = $this->tester->getDisplay();
|
||||
$decodedJson = json_decode($display, true);
|
||||
|
||||
self::assertJson($display);
|
||||
self::assertSame(AttractionInfo::class, $decodedJson['name']);
|
||||
self::assertArrayHasKey('rootEntityName', $decodedJson);
|
||||
self::assertArrayHasKey('fieldMappings', $decodedJson);
|
||||
self::assertArrayHasKey('associationMappings', $decodedJson);
|
||||
self::assertArrayHasKey('id', $decodedJson['fieldMappings']);
|
||||
}
|
||||
|
||||
public function testShowSpecificFuzzyAmbiguous(): void
|
||||
{
|
||||
$this->expectException(InvalidArgumentException::class);
|
||||
@@ -111,5 +132,10 @@ class MappingDescribeCommandTest extends OrmFunctionalTestCase
|
||||
'Doctrine\\\\Tests\\\\Models\\\\Cache\\\\Bar',
|
||||
],
|
||||
];
|
||||
|
||||
yield 'format option value' => [
|
||||
['--format='],
|
||||
['text', 'json'],
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,9 +5,9 @@ declare(strict_types=1);
|
||||
namespace Doctrine\Tests\ORM\Tools\Console\Command\SchemaTool;
|
||||
|
||||
use Doctrine\DBAL\Platforms\SQLitePlatform;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Tools\Console\Command\SchemaTool\AbstractCommand;
|
||||
use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
@@ -16,9 +16,8 @@ abstract class CommandTestCase extends OrmFunctionalTestCase
|
||||
/** @param class-string<AbstractCommand> $commandClass */
|
||||
protected function getCommandTester(string $commandClass, string|null $commandName = null): CommandTester
|
||||
{
|
||||
$entityManager = $this->getEntityManager(null, new AttributeDriver([
|
||||
__DIR__ . '/Models',
|
||||
]));
|
||||
$attributeDriver = AttributeDriverFactory::createAttributeDriver([__DIR__ . '/Models']);
|
||||
$entityManager = $this->getEntityManager(null, $attributeDriver);
|
||||
|
||||
if (! $entityManager->getConnection()->getDatabasePlatform() instanceof SQLitePlatform) {
|
||||
self::markTestSkipped('We are testing the symfony/console integration');
|
||||
|
||||
@@ -121,7 +121,7 @@ class CountWalkerTest extends PaginationTestCase
|
||||
public function testCountQueryWithArbitraryJoin(): void
|
||||
{
|
||||
$query = $this->entityManager->createQuery(
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p LEFT JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c WITH p.category = c',
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p LEFT JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c ON p.category = c',
|
||||
);
|
||||
$query->setHint(Query::HINT_CUSTOM_TREE_WALKERS, [CountWalker::class]);
|
||||
$query->setHint(CountWalker::HINT_DISTINCT, true);
|
||||
|
||||
@@ -131,7 +131,7 @@ class LimitSubqueryWalkerTest extends PaginationTestCase
|
||||
*/
|
||||
public function testLimitSubqueryWithArbitraryJoin(): void
|
||||
{
|
||||
$dql = 'SELECT p, c FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c WITH p.category = c';
|
||||
$dql = 'SELECT p, c FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c ON p.category = c';
|
||||
$query = $this->entityManager->createQuery($dql);
|
||||
$limitQuery = clone $query;
|
||||
|
||||
@@ -145,7 +145,7 @@ class LimitSubqueryWalkerTest extends PaginationTestCase
|
||||
|
||||
public function testLimitSubqueryWithSortWithArbitraryJoin(): void
|
||||
{
|
||||
$dql = 'SELECT p, c FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c WITH p.category = c ORDER BY p.title';
|
||||
$dql = 'SELECT p, c FROM Doctrine\Tests\ORM\Tools\Pagination\MyBlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c ON p.category = c ORDER BY p.title';
|
||||
$query = $this->entityManager->createQuery($dql);
|
||||
$limitQuery = clone $query;
|
||||
|
||||
|
||||
@@ -71,12 +71,12 @@ class WhereInWalkerTest extends PaginationTestCase
|
||||
];
|
||||
|
||||
yield 'arbitary join with no WHERE' => [
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c WITH p.category = c',
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c ON p.category = c',
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ INNER JOIN Category c1_ ON (b0_.category_id = c1_.id) WHERE b0_.id IN (?)',
|
||||
];
|
||||
|
||||
yield 'arbitary join with single WHERE' => [
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c WITH p.category = c WHERE 1 = 1',
|
||||
'SELECT p FROM Doctrine\Tests\ORM\Tools\Pagination\BlogPost p JOIN Doctrine\Tests\ORM\Tools\Pagination\Category c ON p.category = c WHERE 1 = 1',
|
||||
'SELECT b0_.id AS id_0, b0_.author_id AS author_id_1, b0_.category_id AS category_id_2 FROM BlogPost b0_ INNER JOIN Category c1_ ON (b0_.category_id = c1_.id) WHERE 1 = 1 AND b0_.id IN (?)',
|
||||
];
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -22,7 +22,6 @@ use Doctrine\ORM\EntityManager;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\ORM\Tools\DebugUnitOfWorkListener;
|
||||
use Doctrine\ORM\Tools\SchemaTool;
|
||||
use Doctrine\ORM\Tools\ToolsException;
|
||||
@@ -31,6 +30,7 @@ use Doctrine\Tests\DbalExtensions\Connection;
|
||||
use Doctrine\Tests\DbalExtensions\QueryLog;
|
||||
use Doctrine\Tests\DbalTypes\Rot13Type;
|
||||
use Doctrine\Tests\EventListener\CacheMetadataListener;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\Models\Cache\Action;
|
||||
use Doctrine\Tests\Models\Cache\Address;
|
||||
use Doctrine\Tests\Models\Cache\Attraction;
|
||||
@@ -187,7 +187,6 @@ use function getenv;
|
||||
use function implode;
|
||||
use function is_object;
|
||||
use function method_exists;
|
||||
use function realpath;
|
||||
use function sprintf;
|
||||
use function str_contains;
|
||||
use function strtolower;
|
||||
@@ -952,12 +951,12 @@ abstract class OrmFunctionalTestCase extends OrmTestCase
|
||||
$config->enableNativeLazyObjects(true);
|
||||
}
|
||||
|
||||
$config->setMetadataDriverImpl(
|
||||
$mappingDriver ?? new AttributeDriver([
|
||||
realpath(__DIR__ . '/Models/Cache'),
|
||||
realpath(__DIR__ . '/Models/GeoNames'),
|
||||
], true),
|
||||
);
|
||||
$mappingDriver ??= AttributeDriverFactory::createAttributeDriver([
|
||||
__DIR__ . '/Models/Cache',
|
||||
__DIR__ . '/Models/GeoNames',
|
||||
]);
|
||||
|
||||
$config->setMetadataDriverImpl($mappingDriver);
|
||||
|
||||
$conn = $connection ?: static::$sharedConn;
|
||||
assert($conn !== null);
|
||||
|
||||
@@ -15,6 +15,7 @@ use Doctrine\ORM\Cache\DefaultCacheFactory;
|
||||
use Doctrine\ORM\Cache\Logging\StatisticsCacheLogger;
|
||||
use Doctrine\ORM\Configuration;
|
||||
use Doctrine\ORM\Mapping\Driver\AttributeDriver;
|
||||
use Doctrine\Tests\Mocks\AttributeDriverFactory;
|
||||
use Doctrine\Tests\Mocks\EntityManagerMock;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Psr\Cache\CacheItemPoolInterface;
|
||||
@@ -22,7 +23,6 @@ use Symfony\Component\Cache\Adapter\ArrayAdapter;
|
||||
|
||||
use function class_exists;
|
||||
use function method_exists;
|
||||
use function realpath;
|
||||
use function sprintf;
|
||||
|
||||
// DBAL 3 compatibility
|
||||
@@ -57,9 +57,10 @@ abstract class OrmTestCase extends TestCase
|
||||
|
||||
private CacheItemPoolInterface|null $secondLevelCache = null;
|
||||
|
||||
/** @param list<string> $paths */
|
||||
protected function createAttributeDriver(array $paths = []): AttributeDriver
|
||||
{
|
||||
return new AttributeDriver($paths);
|
||||
return AttributeDriverFactory::createAttributeDriver($paths);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -96,9 +97,7 @@ abstract class OrmTestCase extends TestCase
|
||||
TestUtil::configureProxies($config);
|
||||
$config->setMetadataCache($metadataCache);
|
||||
$config->setQueryCache(self::getSharedQueryCache());
|
||||
$config->setMetadataDriverImpl(new AttributeDriver([
|
||||
realpath(__DIR__ . '/Models/Cache'),
|
||||
], true));
|
||||
$config->setMetadataDriverImpl(AttributeDriverFactory::createAttributeDriver([__DIR__ . '/Models/Cache']));
|
||||
|
||||
if ($this->isSecondLevelCacheEnabled) {
|
||||
$cacheConfig = new CacheConfiguration();
|
||||
|
||||
Reference in New Issue
Block a user