mirror of
https://github.com/doctrine/orm.git
synced 2026-04-25 23:48:05 +02:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| ccfc97c32f | |||
| d386b43be3 | |||
| 0970ce7072 | |||
| 624c56be72 | |||
| fbc8e6741e | |||
| 7151db3cb8 | |||
| ac24c11808 | |||
| dd478d8662 | |||
| 0b3cd72609 | |||
| 85034699cb | |||
| d98186e2c4 | |||
| a0ed37954b | |||
| 4875f4c878 | |||
| 398ab0547a | |||
| 8f15337b03 | |||
| a8632aca8f | |||
| 3dd3d38857 | |||
| c6b3509aa9 | |||
| a32578b7ea | |||
| e585a92763 | |||
| 26f47cb8d3 | |||
| ebb101009c | |||
| f80ef66ffb | |||
| 85d78f8b0d | |||
| c2886478e8 | |||
| 108fa30db2 | |||
| 393679a479 | |||
| e50ae06fe7 | |||
| 05ef1f4f96 | |||
| 2b91edc525 | |||
| 6af7f9f7bf | |||
| 46cb9a980b | |||
| ed1df148c2 | |||
| 44e943e100 | |||
| 23d36c0d52 | |||
| 212edaa80b | |||
| e5ab18ff80 | |||
| 665ccf1376 |
@@ -1,7 +1,7 @@
|
||||
| [3.0.x][3.0] | [2.16.x][2.16] | [2.15.x][2.15] |
|
||||
| [3.0.x][3.0] | [2.18.x][2.18] | [2.17.x][2.17] |
|
||||
|:----------------:|:----------------:|:----------:|
|
||||
| [![Build status][3.0 image]][3.0] | [![Build status][2.16 image]][2.16] | [![Build status][2.15 image]][2.15] |
|
||||
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.16 coverage image]][2.16 coverage] | [![Coverage Status][2.15 coverage image]][2.15 coverage] |
|
||||
| [![Build status][3.0 image]][3.0] | [![Build status][2.18 image]][2.18] | [![Build status][2.17 image]][2.17] |
|
||||
| [![Coverage Status][3.0 coverage image]][3.0 coverage]| [![Coverage Status][2.18 coverage image]][2.18 coverage] | [![Coverage Status][2.17 coverage image]][2.17 coverage] |
|
||||
|
||||
[<h1 align="center">🇺🇦 UKRAINE NEEDS YOUR HELP NOW!</h1>](https://www.doctrine-project.org/stop-war.html)
|
||||
|
||||
@@ -22,11 +22,11 @@ without requiring unnecessary code duplication.
|
||||
[3.0]: https://github.com/doctrine/orm/tree/3.0.x
|
||||
[3.0 coverage image]: https://codecov.io/gh/doctrine/orm/branch/3.0.x/graph/badge.svg
|
||||
[3.0 coverage]: https://codecov.io/gh/doctrine/orm/branch/3.0.x
|
||||
[2.16 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.16.x
|
||||
[2.16]: https://github.com/doctrine/orm/tree/2.16.x
|
||||
[2.16 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.16.x/graph/badge.svg
|
||||
[2.16 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.16.x
|
||||
[2.15 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.15.x
|
||||
[2.15]: https://github.com/doctrine/orm/tree/2.15.x
|
||||
[2.15 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.15.x/graph/badge.svg
|
||||
[2.15 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.15.x
|
||||
[2.18 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.18.x
|
||||
[2.18]: https://github.com/doctrine/orm/tree/2.18.x
|
||||
[2.18 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.18.x/graph/badge.svg
|
||||
[2.18 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.18.x
|
||||
[2.17 image]: https://github.com/doctrine/orm/actions/workflows/continuous-integration.yml/badge.svg?branch=2.17.x
|
||||
[2.17]: https://github.com/doctrine/orm/tree/2.17.x
|
||||
[2.17 coverage image]: https://codecov.io/gh/doctrine/orm/branch/2.17.x/graph/badge.svg
|
||||
[2.17 coverage]: https://codecov.io/gh/doctrine/orm/branch/2.17.x
|
||||
|
||||
+2
-3
@@ -13,6 +13,5 @@ understand the assumptions we make.
|
||||
- [DBAL Security Page](https://www.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/security.html)
|
||||
- [ORM Security Page](https://www.doctrine-project.org/projects/doctrine-orm/en/stable/reference/security.html)
|
||||
|
||||
If you find a Security bug in Doctrine, please report it on Jira and change the
|
||||
Security Level to "Security Issues". It will be visible to Doctrine Core
|
||||
developers and you only.
|
||||
If you find a Security bug in Doctrine, please follow our
|
||||
[Security reporting guidelines](https://www.doctrine-project.org/policies/security.html#reporting).
|
||||
|
||||
+4
-4
@@ -46,10 +46,10 @@
|
||||
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.6",
|
||||
"psr/log": "^1 || ^2 || ^3",
|
||||
"squizlabs/php_codesniffer": "3.7.2",
|
||||
"symfony/cache": "^4.4 || ^5.4 || ^6.0",
|
||||
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2",
|
||||
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0",
|
||||
"vimeo/psalm": "4.30.0 || 5.15.0"
|
||||
"symfony/cache": "^4.4 || ^5.4 || ^6.4 || ^7.0",
|
||||
"symfony/var-exporter": "^4.4 || ^5.4 || ^6.2 || ^7.0",
|
||||
"symfony/yaml": "^3.4 || ^4.0 || ^5.0 || ^6.0 || ^7.0",
|
||||
"vimeo/psalm": "4.30.0 || 5.16.0"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/annotations": "<1.13 || >= 3.0"
|
||||
|
||||
@@ -36,71 +36,50 @@ Our entities look like:
|
||||
namespace Bank\Entities;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
*/
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
|
||||
#[ORM\Entity]
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="string", unique=true)
|
||||
*/
|
||||
private string $no;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="Entry", mappedBy="account", cascade={"persist"})
|
||||
*/
|
||||
private array $entries;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $maxCredit = 0;
|
||||
|
||||
public function __construct(string $no, int $maxCredit = 0)
|
||||
{
|
||||
$this->no = $no;
|
||||
$this->maxCredit = $maxCredit;
|
||||
$this->entries = new \Doctrine\Common\Collections\ArrayCollection();
|
||||
|
||||
#[ORM\OneToMany(targetEntity: Entry::class, mappedBy: 'account', cascade: ['persist'])]
|
||||
private Collection $entries;
|
||||
|
||||
|
||||
public function __construct(
|
||||
#[ORM\Column(type: 'string', unique: true)]
|
||||
private string $no,
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $maxCredit = 0,
|
||||
) {
|
||||
$this->entries = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
*/
|
||||
|
||||
#[ORM\Entity]
|
||||
class Entry
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private ?int $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Account", inversedBy="entries")
|
||||
*/
|
||||
private Account $account;
|
||||
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
private int $amount;
|
||||
|
||||
public function __construct(Account $account, int $amount)
|
||||
{
|
||||
$this->account = $account;
|
||||
$this->amount = $amount;
|
||||
|
||||
public function __construct(
|
||||
#[ORM\ManyToOne(targetEntity: Account::class, inversedBy: 'entries')]
|
||||
private Account $account,
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $amount,
|
||||
) {
|
||||
// more stuff here, from/to whom, stated reason, execution date and such
|
||||
}
|
||||
|
||||
|
||||
public function getAmount(): Amount
|
||||
{
|
||||
return $this->amount;
|
||||
@@ -193,9 +172,8 @@ relation with this method:
|
||||
public function addEntry(int $amount): void
|
||||
{
|
||||
$this->assertAcceptEntryAllowed($amount);
|
||||
|
||||
$e = new Entry($this, $amount);
|
||||
$this->entries[] = $e;
|
||||
|
||||
$this->entries[] = new Entry($this, $amount);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,18 +191,18 @@ Now look at the following test-code for our entities:
|
||||
{
|
||||
$account = new Account("123456", maxCredit: 200);
|
||||
$this->assertEquals(0, $account->getBalance());
|
||||
|
||||
|
||||
$account->addEntry(500);
|
||||
$this->assertEquals(500, $account->getBalance());
|
||||
|
||||
|
||||
$account->addEntry(-700);
|
||||
$this->assertEquals(-200, $account->getBalance());
|
||||
}
|
||||
|
||||
|
||||
public function testExceedMaxLimit()
|
||||
{
|
||||
$account = new Account("123456", maxCredit: 200);
|
||||
|
||||
|
||||
$this->expectException(Exception::class);
|
||||
$account->addEntry(-1000);
|
||||
}
|
||||
@@ -285,22 +263,19 @@ entries collection) we want to add an aggregate field called
|
||||
<?php
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
*/
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $balance = 0;
|
||||
|
||||
|
||||
public function getBalance(): int
|
||||
{
|
||||
return $this->balance;
|
||||
}
|
||||
|
||||
|
||||
public function addEntry(int $amount): void
|
||||
{
|
||||
$this->assertAcceptEntryAllowed($amount);
|
||||
|
||||
$e = new Entry($this, $amount);
|
||||
$this->entries[] = $e;
|
||||
|
||||
$this->entries[] = new Entry($this, $amount);
|
||||
$this->balance += $amount;
|
||||
}
|
||||
}
|
||||
@@ -331,13 +306,13 @@ potentially lead to inconsistent state. See this example:
|
||||
// The Account $accId has a balance of 0 and a max credit limit of 200:
|
||||
// request 1 account
|
||||
$account1 = $em->find(Account::class, $accId);
|
||||
|
||||
|
||||
// request 2 account
|
||||
$account2 = $em->find(Account::class, $accId);
|
||||
|
||||
|
||||
$account1->addEntry(-200);
|
||||
$account2->addEntry(-200);
|
||||
|
||||
|
||||
// now request 1 and 2 both flush the changes.
|
||||
|
||||
The aggregate field ``Account::$balance`` is now -200, however the
|
||||
@@ -357,10 +332,8 @@ Optimistic locking is as easy as adding a version column:
|
||||
|
||||
class Account
|
||||
{
|
||||
/**
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\Version
|
||||
*/
|
||||
#[ORM\Column(type: 'integer')]
|
||||
#[ORM\Version]
|
||||
private int $version;
|
||||
}
|
||||
|
||||
|
||||
@@ -47,10 +47,8 @@ A Customer entity
|
||||
use Acme\CustomerModule\Entity\Customer as BaseCustomer;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
/**
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="customer")
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'customer')]
|
||||
class Customer extends BaseCustomer implements InvoiceSubjectInterface
|
||||
{
|
||||
// In our example, any methods defined in the InvoiceSubjectInterface
|
||||
@@ -69,19 +67,12 @@ An Invoice entity
|
||||
use Doctrine\ORM\Mapping AS ORM;
|
||||
use Acme\InvoiceModule\Model\InvoiceSubjectInterface;
|
||||
|
||||
/**
|
||||
* Represents an Invoice.
|
||||
*
|
||||
* @ORM\Entity
|
||||
* @ORM\Table(name="invoice")
|
||||
*/
|
||||
#[ORM\Entity]
|
||||
#[ORM\Table(name: 'invoice')]
|
||||
class Invoice
|
||||
{
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="Acme\InvoiceModule\Model\InvoiceSubjectInterface")
|
||||
* @var InvoiceSubjectInterface
|
||||
*/
|
||||
protected $subject;
|
||||
#[ORM\ManyToOne(targetEntity: InvoiceSubjectInterface::class)]
|
||||
protected InvoiceSubjectInterface $subject;
|
||||
}
|
||||
|
||||
An InvoiceSubjectInterface
|
||||
@@ -127,7 +118,7 @@ the targetEntity resolution will occur reliably:
|
||||
// Add the ResolveTargetEntityListener
|
||||
$evm->addEventListener(Doctrine\ORM\Events::loadClassMetadata, $rtel);
|
||||
|
||||
$connection = \Doctrine\DBAL\DriverManager::createConnection($connectionOptions, $config, $evm);
|
||||
$connection = \Doctrine\DBAL\DriverManager::getConnection($connectionOptions, $config, $evm);
|
||||
$em = new \Doctrine\ORM\EntityManager($connection, $config, $evm);
|
||||
|
||||
Final Thoughts
|
||||
|
||||
@@ -464,6 +464,11 @@ hierarchies:
|
||||
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF Doctrine\Tests\Models\Company\CompanyEmployee');
|
||||
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u INSTANCE OF ?1');
|
||||
$query = $em->createQuery('SELECT u FROM Doctrine\Tests\Models\Company\CompanyPerson u WHERE u NOT INSTANCE OF ?1');
|
||||
$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class));
|
||||
|
||||
.. note::
|
||||
To use a class as parameter, you have to bind its class metadata:
|
||||
``$query->setParameter(0, $em->getClassMetadata(CompanyEmployee::class);``.
|
||||
|
||||
Get all users visible on a given website that have chosen certain gender:
|
||||
|
||||
|
||||
@@ -173,6 +173,19 @@ Events Overview
|
||||
| :ref:`onClear<reference-events-on-clear>` | ``$em->clear()`` | No | `OnClearEventArgs`_ |
|
||||
+-----------------------------------------------------------------+-----------------------+-----------+-------------------------------------+
|
||||
|
||||
.. warning::
|
||||
|
||||
Making changes to entities and calling ``EntityManager::flush()`` from within
|
||||
event handlers dispatched by ``EntityManager::flush()`` itself is strongly
|
||||
discouraged, and might be deprecated and eventually prevented in the future.
|
||||
|
||||
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
|
||||
is currently being processed. The ``UnitOfWork`` was never designed to support this,
|
||||
and its behavior in this situation is not covered by any tests.
|
||||
|
||||
This may lead to entity or collection updates being missed, applied only in parts and
|
||||
changes being lost at the end of the commit phase.
|
||||
|
||||
Naming convention
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -699,30 +712,33 @@ Restrictions for this event:
|
||||
postUpdate, postRemove, postPersist
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
These three post* events are called inside ``EntityManager::flush()``.
|
||||
These three ``post*`` events are called inside ``EntityManager::flush()``.
|
||||
Changes in here are not relevant to the persistence in the
|
||||
database, but you can use these events to alter non-persistable items,
|
||||
like non-mapped fields, logging or even associated classes that are
|
||||
not directly mapped by Doctrine.
|
||||
|
||||
- The ``postUpdate`` event occurs after the database
|
||||
update operations to entity data. It is not called for a DQL
|
||||
``UPDATE`` statement.
|
||||
update operations to entity data, but before the database transaction
|
||||
has been committed. It is not called for a DQL ``UPDATE`` statement.
|
||||
- The ``postPersist`` event occurs for an entity after the entity has
|
||||
been made persistent. It will be invoked after all database insert
|
||||
operations for new entities have been performed. Generated primary
|
||||
key values will be available for all entities at the time this
|
||||
event is triggered.
|
||||
operations for new entities have been performed, but before the database
|
||||
transaction has been committed. Generated primary key values will be
|
||||
available for all entities at the time this event is triggered.
|
||||
- The ``postRemove`` event occurs for an entity after the
|
||||
entity has been deleted. It will be invoked after all database
|
||||
delete operations for entity rows have been executed. This event is
|
||||
not called for a DQL ``DELETE`` statement.
|
||||
delete operations for entity rows have been executed, but before the
|
||||
database transaction has been committed. This event is not called for
|
||||
a DQL ``DELETE`` statement.
|
||||
|
||||
.. note::
|
||||
|
||||
At the time ``postPersist`` is called, there may still be collection and/or
|
||||
"extra" updates pending. The database may not yet be completely in
|
||||
sync with the entity states in memory, not even for the new entities.
|
||||
sync with the entity states in memory, not even for the new entities. Similarly,
|
||||
also at the time ``postUpdate`` and ``postRemove`` are called, in-memory collections
|
||||
may still be in a "dirty" state or still contain removed entities.
|
||||
|
||||
.. warning::
|
||||
|
||||
@@ -731,19 +747,6 @@ not directly mapped by Doctrine.
|
||||
cascade remove relations. In this case, you should load yourself the proxy in
|
||||
the associated ``pre*`` event.
|
||||
|
||||
.. warning::
|
||||
|
||||
Making changes to entities and calling ``EntityManager::flush()`` from within
|
||||
``post*`` event handlers is strongly discouraged, and might be deprecated and
|
||||
eventually prevented in the future.
|
||||
|
||||
The reason is that it causes re-entrance into ``UnitOfWork::commit()`` while a commit
|
||||
is currently being processed. The ``UnitOfWork`` was never designed to support this,
|
||||
and its behavior in this situation is not covered by any tests.
|
||||
|
||||
This may lead to entity or collection updates being missed, applied only in parts and
|
||||
changes being lost at the end of the commit phase.
|
||||
|
||||
.. _reference-events-post-load:
|
||||
|
||||
postLoad
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
Limitations and Known Issues
|
||||
============================
|
||||
|
||||
We try to make using Doctrine2 a very pleasant experience.
|
||||
We try to make using Doctrine ORM a very pleasant experience.
|
||||
Therefore we think it is very important to be honest about the
|
||||
current limitations to our users. Much like every other piece of
|
||||
software Doctrine2 is not perfect and far from feature complete.
|
||||
software the ORM is not perfect and far from feature complete.
|
||||
This section should give you an overview of current limitations of
|
||||
Doctrine ORM as well as critical known issues that you should know
|
||||
about.
|
||||
@@ -175,6 +175,18 @@ due to complexity.
|
||||
XML mapping configuration probably needs to completely re-configure or otherwise
|
||||
copy-and-paste configuration for fields used from traits.
|
||||
|
||||
Mapping multiple private fields of the same name
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
When two classes, say a mapped superclass and an entity inheriting from it,
|
||||
both contain a ``private`` field of the same name, this will lead to a ``MappingException``.
|
||||
|
||||
Since the fields are ``private``, both are technically separate and can contain
|
||||
different values at the same time. However, the ``ClassMetadata`` configuration used
|
||||
internally by the ORM currently refers to fields by their name only, without taking the
|
||||
class containing the field into consideration. This makes it impossible to keep separate
|
||||
mapping configuration for both fields.
|
||||
|
||||
Known Issues
|
||||
------------
|
||||
|
||||
|
||||
@@ -12,9 +12,8 @@ page only handles Security issues in the ORM.
|
||||
|
||||
- `DBAL Security Page <https://www.doctrine-project.org/projects/doctrine-dbal/en/current/reference/security.html>`
|
||||
|
||||
If you find a Security bug in Doctrine, please report it on Jira and change the
|
||||
Security Level to "Security Issues". It will be visible to Doctrine Core
|
||||
developers and you only.
|
||||
If you find a Security bug in Doctrine, please follow our
|
||||
`Security reporting guidelines <https://www.doctrine-project.org/policies/security.html#reporting>`_.
|
||||
|
||||
User input and Doctrine ORM
|
||||
---------------------------
|
||||
|
||||
+2
-14
@@ -148,7 +148,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:element name="entity-result" type="orm:entity-result"/>
|
||||
<xs:element name="column-result" type="orm:column-result"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
@@ -226,22 +225,13 @@
|
||||
|
||||
<xs:complexType name="mapped-superclass" >
|
||||
<xs:complexContent>
|
||||
<xs:extension base="orm:entity">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:anyAttribute namespace="##other"/>
|
||||
</xs:extension>
|
||||
<xs:extension base="orm:entity"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
<xs:complexType name="embeddable">
|
||||
<xs:complexContent>
|
||||
<xs:extension base="orm:entity">
|
||||
<xs:choice minOccurs="0" maxOccurs="unbounded">
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
</xs:extension>
|
||||
<xs:extension base="orm:entity"/>
|
||||
</xs:complexContent>
|
||||
</xs:complexType>
|
||||
|
||||
@@ -565,7 +555,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="join-column" type="orm:join-column"/>
|
||||
<xs:element name="join-columns" type="orm:join-columns"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
@@ -583,7 +572,6 @@
|
||||
<xs:choice minOccurs="0" maxOccurs="1">
|
||||
<xs:element name="join-column" type="orm:join-column"/>
|
||||
<xs:element name="join-columns" type="orm:join-columns"/>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
<xs:any minOccurs="0" maxOccurs="unbounded" namespace="##other"/>
|
||||
</xs:choice>
|
||||
|
||||
@@ -987,7 +987,7 @@ class EntityManager implements EntityManagerInterface
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/9961',
|
||||
'%s() is deprecated. To boostrap a DBAL connection, call %s::getConnection() instead. Use the constructor to create an instance of %s.',
|
||||
'%s() is deprecated. To bootstrap a DBAL connection, call %s::getConnection() instead. Use the constructor to create an instance of %s.',
|
||||
__METHOD__,
|
||||
DriverManager::class,
|
||||
self::class
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\ORM\Internal;
|
||||
|
||||
use InvalidArgumentException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_pop;
|
||||
use function array_push;
|
||||
use function min;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
* StronglyConnectedComponents implements Tarjan's algorithm to find strongly connected
|
||||
* components (SCC) in a directed graph. This algorithm has a linear running time based on
|
||||
* nodes (V) and edges between the nodes (E), resulting in a computational complexity
|
||||
* of O(V + E).
|
||||
*
|
||||
* See https://en.wikipedia.org/wiki/Tarjan%27s_strongly_connected_components_algorithm
|
||||
* for an explanation and the meaning of the DFS and lowlink numbers.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class StronglyConnectedComponents
|
||||
{
|
||||
private const NOT_VISITED = 1;
|
||||
private const IN_PROGRESS = 2;
|
||||
private const VISITED = 3;
|
||||
|
||||
/**
|
||||
* Array of all nodes, indexed by object ids.
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $nodes = [];
|
||||
|
||||
/**
|
||||
* DFS state for the different nodes, indexed by node object id and using one of
|
||||
* this class' constants as value.
|
||||
*
|
||||
* @var array<int, self::*>
|
||||
*/
|
||||
private $states = [];
|
||||
|
||||
/**
|
||||
* Edges between the nodes. The first-level key is the object id of the outgoing
|
||||
* node; the second array maps the destination node by object id as key.
|
||||
*
|
||||
* @var array<int, array<int, bool>>
|
||||
*/
|
||||
private $edges = [];
|
||||
|
||||
/**
|
||||
* DFS numbers, by object ID
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $dfs = [];
|
||||
|
||||
/**
|
||||
* lowlink numbers, by object ID
|
||||
*
|
||||
* @var array<int, int>
|
||||
*/
|
||||
private $lowlink = [];
|
||||
|
||||
/** @var int */
|
||||
private $maxdfs = 0;
|
||||
|
||||
/**
|
||||
* Nodes representing the SCC another node is in, indexed by lookup-node object ID
|
||||
*
|
||||
* @var array<int, object>
|
||||
*/
|
||||
private $representingNodes = [];
|
||||
|
||||
/**
|
||||
* Stack with OIDs of nodes visited in the current state of the DFS
|
||||
*
|
||||
* @var list<int>
|
||||
*/
|
||||
private $stack = [];
|
||||
|
||||
/** @param object $node */
|
||||
public function addNode($node): void
|
||||
{
|
||||
$id = spl_object_id($node);
|
||||
$this->nodes[$id] = $node;
|
||||
$this->states[$id] = self::NOT_VISITED;
|
||||
$this->edges[$id] = [];
|
||||
}
|
||||
|
||||
/** @param object $node */
|
||||
public function hasNode($node): bool
|
||||
{
|
||||
return isset($this->nodes[spl_object_id($node)]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new edge between two nodes to the graph
|
||||
*
|
||||
* @param object $from
|
||||
* @param object $to
|
||||
*/
|
||||
public function addEdge($from, $to): void
|
||||
{
|
||||
$fromId = spl_object_id($from);
|
||||
$toId = spl_object_id($to);
|
||||
|
||||
$this->edges[$fromId][$toId] = true;
|
||||
}
|
||||
|
||||
public function findStronglyConnectedComponents(): void
|
||||
{
|
||||
foreach (array_keys($this->nodes) as $oid) {
|
||||
if ($this->states[$oid] === self::NOT_VISITED) {
|
||||
$this->tarjan($oid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function tarjan(int $oid): void
|
||||
{
|
||||
$this->dfs[$oid] = $this->lowlink[$oid] = $this->maxdfs++;
|
||||
$this->states[$oid] = self::IN_PROGRESS;
|
||||
array_push($this->stack, $oid);
|
||||
|
||||
foreach ($this->edges[$oid] as $adjacentId => $ignored) {
|
||||
if ($this->states[$adjacentId] === self::NOT_VISITED) {
|
||||
$this->tarjan($adjacentId);
|
||||
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->lowlink[$adjacentId]);
|
||||
} elseif ($this->states[$adjacentId] === self::IN_PROGRESS) {
|
||||
$this->lowlink[$oid] = min($this->lowlink[$oid], $this->dfs[$adjacentId]);
|
||||
}
|
||||
}
|
||||
|
||||
$lowlink = $this->lowlink[$oid];
|
||||
if ($lowlink === $this->dfs[$oid]) {
|
||||
$representingNode = null;
|
||||
do {
|
||||
$unwindOid = array_pop($this->stack);
|
||||
|
||||
if (! $representingNode) {
|
||||
$representingNode = $this->nodes[$unwindOid];
|
||||
}
|
||||
|
||||
$this->representingNodes[$unwindOid] = $representingNode;
|
||||
$this->states[$unwindOid] = self::VISITED;
|
||||
} while ($unwindOid !== $oid);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param object $node
|
||||
*
|
||||
* @return object
|
||||
*/
|
||||
public function getNodeRepresentingStronglyConnectedComponent($node)
|
||||
{
|
||||
$oid = spl_object_id($node);
|
||||
|
||||
if (! isset($this->representingNodes[$oid])) {
|
||||
throw new InvalidArgumentException('unknown node');
|
||||
}
|
||||
|
||||
return $this->representingNodes[$oid];
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,6 @@ namespace Doctrine\ORM\Internal;
|
||||
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
|
||||
|
||||
use function array_keys;
|
||||
use function array_reverse;
|
||||
use function array_unshift;
|
||||
use function spl_object_id;
|
||||
|
||||
/**
|
||||
@@ -93,18 +91,14 @@ final class TopologicalSort
|
||||
|
||||
/**
|
||||
* Returns a topological sort of all nodes. When we have an edge A->B between two nodes
|
||||
* A and B, then A will be listed before B in the result.
|
||||
* A and B, then B will be listed before A in the result. Visually speaking, when ordering
|
||||
* the nodes in the result order from left to right, all edges point to the left.
|
||||
*
|
||||
* @return list<object>
|
||||
*/
|
||||
public function sort(): array
|
||||
{
|
||||
/*
|
||||
* When possible, keep objects in the result in the same order in which they were added as nodes.
|
||||
* Since nodes are unshifted into $this->>sortResult (see the visit() method), that means we
|
||||
* need to work them in array_reverse order here.
|
||||
*/
|
||||
foreach (array_reverse(array_keys($this->nodes)) as $oid) {
|
||||
foreach (array_keys($this->nodes) as $oid) {
|
||||
if ($this->states[$oid] === self::NOT_VISITED) {
|
||||
$this->visit($oid);
|
||||
}
|
||||
@@ -147,7 +141,7 @@ final class TopologicalSort
|
||||
}
|
||||
|
||||
// We have found a cycle and cannot break it at $edge. Best we can do
|
||||
// is to retreat from the current vertex, hoping that somewhere up the
|
||||
// is to backtrack from the current vertex, hoping that somewhere up the
|
||||
// stack this can be salvaged.
|
||||
$this->states[$oid] = self::NOT_VISITED;
|
||||
$exception->addToCycle($this->nodes[$oid]);
|
||||
@@ -160,6 +154,6 @@ final class TopologicalSort
|
||||
// So we're done with this vertex as well.
|
||||
|
||||
$this->states[$oid] = self::VISITED;
|
||||
array_unshift($this->sortResult, $this->nodes[$oid]);
|
||||
$this->sortResult[] = $this->nodes[$oid];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -123,7 +123,7 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
if ($parent) {
|
||||
$class->setInheritanceType($parent->inheritanceType);
|
||||
$class->setDiscriminatorColumn($parent->discriminatorColumn);
|
||||
$this->inheritIdGeneratorMapping($class, $parent);
|
||||
$class->setIdGeneratorType($parent->generatorType);
|
||||
$this->addInheritedFields($class, $parent);
|
||||
$this->addInheritedRelations($class, $parent);
|
||||
$this->addInheritedEmbeddedClasses($class, $parent);
|
||||
@@ -151,8 +151,12 @@ class ClassMetadataFactory extends AbstractClassMetadataFactory
|
||||
throw MappingException::reflectionFailure($class->getName(), $e);
|
||||
}
|
||||
|
||||
// Complete id generator mapping when the generator was declared/added in this class
|
||||
if ($class->identifier && (! $parent || ! $parent->identifier)) {
|
||||
// If this class has a parent the id generator strategy is inherited.
|
||||
// However this is only true if the hierarchy of parents contains the root entity,
|
||||
// if it consists of mapped superclasses these don't necessarily include the id field.
|
||||
if ($parent && $rootEntityFound) {
|
||||
$this->inheritIdGeneratorMapping($class, $parent);
|
||||
} else {
|
||||
$this->completeIdGeneratorMapping($class);
|
||||
}
|
||||
|
||||
|
||||
@@ -2558,6 +2558,10 @@ class ClassMetadataInfo implements ClassMetadata
|
||||
$overrideMapping['id'] = $mapping['id'];
|
||||
}
|
||||
|
||||
if (isset($mapping['declared'])) {
|
||||
$overrideMapping['declared'] = $mapping['declared'];
|
||||
}
|
||||
|
||||
if (! isset($overrideMapping['type'])) {
|
||||
$overrideMapping['type'] = $mapping['type'];
|
||||
}
|
||||
|
||||
@@ -57,7 +57,15 @@ final class DefaultTypedFieldMapper implements TypedFieldMapper
|
||||
$mapping['enumType'] = $type->getName();
|
||||
|
||||
$reflection = new ReflectionEnum($type->getName());
|
||||
$type = $reflection->getBackingType();
|
||||
if (! $reflection->isBacked()) {
|
||||
throw MappingException::backedEnumTypeRequired(
|
||||
$field->class,
|
||||
$mapping['fieldName'],
|
||||
$mapping['enumType']
|
||||
);
|
||||
}
|
||||
|
||||
$type = $reflection->getBackingType();
|
||||
|
||||
assert($type instanceof ReflectionNamedType);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ declare(strict_types=1);
|
||||
namespace Doctrine\ORM\Mapping\Driver;
|
||||
|
||||
use Doctrine\Common\Collections\Criteria;
|
||||
use Doctrine\Deprecations\Deprecation;
|
||||
use Doctrine\ORM\Mapping\Builder\EntityListenerBuilder;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
@@ -56,15 +55,6 @@ class XmlDriver extends FileDriver
|
||||
);
|
||||
}
|
||||
|
||||
if (! $isXsdValidationEnabled) {
|
||||
Deprecation::trigger(
|
||||
'doctrine/orm',
|
||||
'https://github.com/doctrine/orm/pull/6728',
|
||||
'Using XML mapping driver with XSD validation disabled is deprecated'
|
||||
. ' and will not be supported in Doctrine ORM 3.0.'
|
||||
);
|
||||
}
|
||||
|
||||
if ($isXsdValidationEnabled && ! extension_loaded('dom')) {
|
||||
throw new LogicException(
|
||||
'XSD validation cannot be enabled because the DOM extension is missing.'
|
||||
|
||||
@@ -954,6 +954,16 @@ class MappingException extends ORMException
|
||||
return new self(sprintf('Enum types require PHP 8.1 in %s::$%s', $className, $fieldName));
|
||||
}
|
||||
|
||||
public static function backedEnumTypeRequired(string $className, string $fieldName, string $enumType): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
'Attempting to map a non-backed enum type %s in entity %s::$%s. Please use backed enums only',
|
||||
$enumType,
|
||||
$className,
|
||||
$fieldName
|
||||
));
|
||||
}
|
||||
|
||||
public static function nonEnumTypeMapped(string $className, string $fieldName, string $enumType): self
|
||||
{
|
||||
return new self(sprintf(
|
||||
|
||||
@@ -141,7 +141,9 @@ class ParserResult
|
||||
{
|
||||
foreach (self::LEGACY_PROPERTY_MAPPING as $property => $legacyProperty) {
|
||||
$this->$property = $data[sprintf("\0%s\0%s", self::class, $legacyProperty)]
|
||||
?? $data[self::class][$legacyProperty]
|
||||
?? $data[sprintf("\0%s\0%s", self::class, $property)]
|
||||
?? $data[self::class][$property]
|
||||
?? $this->$property
|
||||
?? null;
|
||||
}
|
||||
|
||||
@@ -31,6 +31,7 @@ class ValidateSchemaCommand extends AbstractEntityManagerCommand
|
||||
->addOption('em', null, InputOption::VALUE_REQUIRED, 'Name of the entity manager to operate on')
|
||||
->addOption('skip-mapping', null, InputOption::VALUE_NONE, 'Skip the mapping validation check')
|
||||
->addOption('skip-sync', null, InputOption::VALUE_NONE, 'Skip checking if the mapping is in sync with the database')
|
||||
->addOption('skip-property-types', null, InputOption::VALUE_NONE, 'Skip checking if property types match the Doctrine types')
|
||||
->setHelp('Validate that the mapping files are correct and in sync with the database.');
|
||||
}
|
||||
|
||||
@@ -39,7 +40,7 @@ class ValidateSchemaCommand extends AbstractEntityManagerCommand
|
||||
$ui = (new SymfonyStyle($input, $output))->getErrorStyle();
|
||||
|
||||
$em = $this->getEntityManager($input);
|
||||
$validator = new SchemaValidator($em);
|
||||
$validator = new SchemaValidator($em, ! $input->getOption('skip-property-types'));
|
||||
$exit = 0;
|
||||
|
||||
$ui->section('Mapping');
|
||||
|
||||
@@ -257,7 +257,7 @@ class LimitSubqueryOutputWalker extends SqlWalker
|
||||
$innerSql
|
||||
);
|
||||
|
||||
// http://www.doctrine-project.org/jira/browse/DDC-1958
|
||||
// https://github.com/doctrine/orm/issues/2630
|
||||
$sql = $this->preserveSqlOrdering($sqlIdentifier, $innerSql, $sql, $orderByClause);
|
||||
|
||||
// Apply the limit and offset.
|
||||
|
||||
@@ -39,6 +39,7 @@ use function count;
|
||||
use function get_class;
|
||||
use function implode;
|
||||
use function in_array;
|
||||
use function interface_exists;
|
||||
use function is_a;
|
||||
use function sprintf;
|
||||
|
||||
@@ -56,6 +57,9 @@ class SchemaValidator
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
/** @var bool */
|
||||
private $validatePropertyTypes;
|
||||
|
||||
/**
|
||||
* It maps built-in Doctrine types to PHP types
|
||||
*/
|
||||
@@ -74,9 +78,10 @@ class SchemaValidator
|
||||
TextType::class => 'string',
|
||||
];
|
||||
|
||||
public function __construct(EntityManagerInterface $em)
|
||||
public function __construct(EntityManagerInterface $em, bool $validatePropertyTypes = true)
|
||||
{
|
||||
$this->em = $em;
|
||||
$this->em = $em;
|
||||
$this->validatePropertyTypes = $validatePropertyTypes;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -136,7 +141,7 @@ class SchemaValidator
|
||||
}
|
||||
|
||||
// PHP 7.4 introduces the ability to type properties, so we can't validate them in previous versions
|
||||
if (PHP_VERSION_ID >= 70400) {
|
||||
if (PHP_VERSION_ID >= 70400 && $this->validatePropertyTypes) {
|
||||
array_push($ce, ...$this->validatePropertiesTypes($class));
|
||||
}
|
||||
|
||||
@@ -371,7 +376,7 @@ class SchemaValidator
|
||||
}
|
||||
|
||||
// If the property type is not a named type, we cannot check it
|
||||
if (! ($propertyType instanceof ReflectionNamedType)) {
|
||||
if (! ($propertyType instanceof ReflectionNamedType) || $propertyType->getName() === 'mixed') {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -389,10 +394,20 @@ class SchemaValidator
|
||||
return null;
|
||||
}
|
||||
|
||||
if (
|
||||
is_a($propertyType, BackedEnum::class, true)
|
||||
&& $metadataFieldType === (string) (new ReflectionEnum($propertyType))->getBackingType()
|
||||
) {
|
||||
if (is_a($propertyType, BackedEnum::class, true)) {
|
||||
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
|
||||
|
||||
if ($metadataFieldType !== $backingType) {
|
||||
return sprintf(
|
||||
"The field '%s#%s' has the property type '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
|
||||
$class->name,
|
||||
$fieldName,
|
||||
$propertyType,
|
||||
$backingType,
|
||||
$metadataFieldType
|
||||
);
|
||||
}
|
||||
|
||||
if (! isset($fieldMapping['enumType']) || $propertyType === $fieldMapping['enumType']) {
|
||||
return null;
|
||||
}
|
||||
@@ -406,6 +421,35 @@ class SchemaValidator
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
isset($fieldMapping['enumType'])
|
||||
&& $propertyType !== $fieldMapping['enumType']
|
||||
&& interface_exists($propertyType)
|
||||
&& is_a($fieldMapping['enumType'], $propertyType, true)
|
||||
) {
|
||||
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();
|
||||
|
||||
if ($metadataFieldType === $backingType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"The field '%s#%s' has the metadata enumType '%s' with a backing type of '%s' that differs from the metadata field type '%s'.",
|
||||
$class->name,
|
||||
$fieldName,
|
||||
$fieldMapping['enumType'],
|
||||
$backingType,
|
||||
$metadataFieldType
|
||||
);
|
||||
}
|
||||
|
||||
if (
|
||||
$fieldMapping['type'] === 'json'
|
||||
&& in_array($propertyType, ['string', 'int', 'float', 'bool', 'true', 'false', 'null'], true)
|
||||
) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return sprintf(
|
||||
"The field '%s#%s' has the property type '%s' that differs from the metadata field type '%s' returned by the '%s' DBAL type.",
|
||||
$class->name,
|
||||
|
||||
@@ -28,6 +28,7 @@ use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
|
||||
use Doctrine\ORM\Id\AssignedGenerator;
|
||||
use Doctrine\ORM\Internal\HydrationCompleteHandler;
|
||||
use Doctrine\ORM\Internal\StronglyConnectedComponents;
|
||||
use Doctrine\ORM\Internal\TopologicalSort;
|
||||
use Doctrine\ORM\Mapping\ClassMetadata;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
@@ -1378,9 +1379,10 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$joinColumns = reset($assoc['joinColumns']);
|
||||
$isNullable = ! isset($joinColumns['nullable']) || $joinColumns['nullable'];
|
||||
|
||||
// Add dependency. The dependency direction implies that "$targetEntity has to go before $entity",
|
||||
// so we can work through the topo sort result from left to right (with all edges pointing right).
|
||||
$sort->addEdge($targetEntity, $entity, $isNullable);
|
||||
// Add dependency. The dependency direction implies that "$entity depends on $targetEntity". The
|
||||
// topological sort result will output the depended-upon nodes first, which means we can insert
|
||||
// entities in that order.
|
||||
$sort->addEdge($entity, $targetEntity, $isNullable);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1390,14 +1392,19 @@ class UnitOfWork implements PropertyChangedListener
|
||||
/** @return list<object> */
|
||||
private function computeDeleteExecutionOrder(): array
|
||||
{
|
||||
$sort = new TopologicalSort();
|
||||
$stronglyConnectedComponents = new StronglyConnectedComponents();
|
||||
$sort = new TopologicalSort();
|
||||
|
||||
// First make sure we have all the nodes
|
||||
foreach ($this->entityDeletions as $entity) {
|
||||
$stronglyConnectedComponents->addNode($entity);
|
||||
$sort->addNode($entity);
|
||||
}
|
||||
|
||||
// Now add edges
|
||||
// First, consider only "on delete cascade" associations between entities
|
||||
// and find strongly connected groups. Once we delete any one of the entities
|
||||
// in such a group, _all_ of the other entities will be removed as well. So,
|
||||
// we need to treat those groups like a single entity when performing delete
|
||||
// order topological sorting.
|
||||
foreach ($this->entityDeletions as $entity) {
|
||||
$class = $this->em->getClassMetadata(get_class($entity));
|
||||
|
||||
@@ -1409,16 +1416,65 @@ class UnitOfWork implements PropertyChangedListener
|
||||
continue;
|
||||
}
|
||||
|
||||
// For associations that implement a database-level cascade/set null operation,
|
||||
assert(isset($assoc['joinColumns']));
|
||||
$joinColumns = reset($assoc['joinColumns']);
|
||||
if (! isset($joinColumns['onDelete'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$onDeleteOption = strtolower($joinColumns['onDelete']);
|
||||
if ($onDeleteOption !== 'cascade') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$targetEntity = $class->getFieldValue($entity, $assoc['fieldName']);
|
||||
|
||||
// If the association does not refer to another entity or that entity
|
||||
// is not to be deleted, there is no ordering problem and we can
|
||||
// skip this particular association.
|
||||
if ($targetEntity === null || ! $stronglyConnectedComponents->hasNode($targetEntity)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$stronglyConnectedComponents->addEdge($entity, $targetEntity);
|
||||
}
|
||||
}
|
||||
|
||||
$stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
// Now do the actual topological sorting to find the delete order.
|
||||
foreach ($this->entityDeletions as $entity) {
|
||||
$class = $this->em->getClassMetadata(get_class($entity));
|
||||
|
||||
// Get the entities representing the SCC
|
||||
$entityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($entity);
|
||||
|
||||
// When $entity is part of a non-trivial strongly connected component group
|
||||
// (a group containing not only those entities alone), make sure we process it _after_ the
|
||||
// entity representing the group.
|
||||
// The dependency direction implies that "$entity depends on $entityComponent
|
||||
// being deleted first". The topological sort will output the depended-upon nodes first.
|
||||
if ($entityComponent !== $entity) {
|
||||
$sort->addEdge($entity, $entityComponent, false);
|
||||
}
|
||||
|
||||
foreach ($class->associationMappings as $assoc) {
|
||||
// We only need to consider the owning sides of to-one associations,
|
||||
// since many-to-many associations can always be (and have already been)
|
||||
// deleted in a preceding step.
|
||||
if (! ($assoc['isOwningSide'] && $assoc['type'] & ClassMetadata::TO_ONE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// For associations that implement a database-level set null operation,
|
||||
// we do not have to follow a particular order: If the referred-to entity is
|
||||
// deleted first, the DBMS will either delete the current $entity right away
|
||||
// (CASCADE) or temporarily set the foreign key to NULL (SET NULL).
|
||||
// Either way, we can skip it in the computation.
|
||||
// deleted first, the DBMS will temporarily set the foreign key to NULL (SET NULL).
|
||||
// So, we can skip it in the computation.
|
||||
assert(isset($assoc['joinColumns']));
|
||||
$joinColumns = reset($assoc['joinColumns']);
|
||||
if (isset($joinColumns['onDelete'])) {
|
||||
$onDeleteOption = strtolower($joinColumns['onDelete']);
|
||||
if ($onDeleteOption === 'cascade' || $onDeleteOption === 'set null') {
|
||||
if ($onDeleteOption === 'set null') {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -1432,9 +1488,17 @@ class UnitOfWork implements PropertyChangedListener
|
||||
continue;
|
||||
}
|
||||
|
||||
// Add dependency. The dependency direction implies that "$entity has to be removed before $targetEntity",
|
||||
// so we can work through the topo sort result from left to right (with all edges pointing right).
|
||||
$sort->addEdge($entity, $targetEntity, false);
|
||||
// Get the entities representing the SCC
|
||||
$targetEntityComponent = $stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($targetEntity);
|
||||
|
||||
// When we have a dependency between two different groups of strongly connected nodes,
|
||||
// add it to the computation.
|
||||
// The dependency direction implies that "$targetEntityComponent depends on $entityComponent
|
||||
// being deleted first". The topological sort will output the depended-upon nodes first,
|
||||
// so we can work through the result in the returned order.
|
||||
if ($targetEntityComponent !== $entityComponent) {
|
||||
$sort->addEdge($targetEntityComponent, $entityComponent, false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3104,9 +3168,10 @@ EXCEPTION
|
||||
$reflField->setValue($entity, $pColl);
|
||||
|
||||
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
|
||||
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY) {
|
||||
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
|
||||
if (! $isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) {
|
||||
$this->scheduleCollectionForBatchLoading($pColl, $class);
|
||||
} elseif ($assoc['type'] === ClassMetadata::MANY_TO_MANY) {
|
||||
} elseif (($isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) || $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
|
||||
$this->loadCollection($pColl);
|
||||
$pColl->takeSnapshot();
|
||||
}
|
||||
|
||||
+11
-53
@@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<files psalm-version="5.15.0@5c774aca4746caf3d239d9c8cadb9f882ca29352">
|
||||
<files psalm-version="5.16.0@2897ba636551a8cb61601cc26f6ccfbba6c36591">
|
||||
<file src="lib/Doctrine/ORM/AbstractQuery.php">
|
||||
<DeprecatedClass>
|
||||
<code>IterableResult</code>
|
||||
@@ -590,7 +590,6 @@
|
||||
<DocblockTypeContradiction>
|
||||
<code><![CDATA[! $mapping['isOwningSide']]]></code>
|
||||
<code><![CDATA[! $this->table]]></code>
|
||||
<code><![CDATA[! class_exists($mapping['targetEntity'])]]></code>
|
||||
<code><![CDATA[$this->table]]></code>
|
||||
<code><![CDATA[isset($mapping['id']) && $mapping['id'] === true && ! $mapping['isOwningSide']]]></code>
|
||||
<code><![CDATA[isset($mapping['orderBy']) && ! is_array($mapping['orderBy'])]]></code>
|
||||
@@ -626,9 +625,6 @@
|
||||
<code>$quotedColumnNames</code>
|
||||
<code><![CDATA[$this->namespace . '\\' . $className]]></code>
|
||||
</LessSpecificReturnStatement>
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<MoreSpecificReturnType>
|
||||
<code>FieldMapping</code>
|
||||
<code>class-string|null</code>
|
||||
@@ -869,6 +865,9 @@
|
||||
<MoreSpecificReturnType>
|
||||
<code>class-string</code>
|
||||
</MoreSpecificReturnType>
|
||||
<NoValue>
|
||||
<code>$metadata</code>
|
||||
</NoValue>
|
||||
<PossiblyNullArrayAccess>
|
||||
<code><![CDATA[$this->tables[$tableName]]]></code>
|
||||
<code><![CDATA[$this->tables[$tableName]]]></code>
|
||||
@@ -1730,11 +1729,6 @@
|
||||
<code>$sqlWalker</code>
|
||||
</ParamNameMismatch>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/AST/Node.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/AST/NullComparisonExpression.php">
|
||||
<ParamNameMismatch>
|
||||
<code>$sqlWalker</code>
|
||||
@@ -1942,36 +1936,15 @@
|
||||
<code>$parts</code>
|
||||
</NonInvariantDocblockPropertyType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Base.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Comparison.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Composite.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<PossiblyInvalidCast>
|
||||
<code>$part</code>
|
||||
</PossiblyInvalidCast>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/From.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Func.php">
|
||||
<LessSpecificReturnStatement>
|
||||
<code><![CDATA[$this->arguments]]></code>
|
||||
</LessSpecificReturnStatement>
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<MoreSpecificReturnType>
|
||||
<code><![CDATA[list<mixed>]]></code>
|
||||
</MoreSpecificReturnType>
|
||||
@@ -1982,9 +1955,6 @@
|
||||
</NonInvariantDocblockPropertyType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Join.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<PossiblyNullArgument>
|
||||
<code><![CDATA[$this->conditionType]]></code>
|
||||
</PossiblyNullArgument>
|
||||
@@ -1994,16 +1964,6 @@
|
||||
<code>$parts</code>
|
||||
</NonInvariantDocblockPropertyType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Math.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/OrderBy.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Expr/Orx.php">
|
||||
<NonInvariantDocblockPropertyType>
|
||||
<code>$allowedClasses</code>
|
||||
@@ -2017,9 +1977,6 @@
|
||||
</NonInvariantDocblockPropertyType>
|
||||
</file>
|
||||
<file src="lib/Doctrine/ORM/Query/Filter/SQLFilter.php">
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<MissingClosureParamType>
|
||||
<code>$value</code>
|
||||
</MissingClosureParamType>
|
||||
@@ -2127,9 +2084,6 @@
|
||||
<code>addNamedNativeQueryResultClassMapping</code>
|
||||
<code>addNamedNativeQueryResultSetMapping</code>
|
||||
</DeprecatedMethod>
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<PossiblyUndefinedArrayOffset>
|
||||
<code><![CDATA[$associationMapping['joinColumns']]]></code>
|
||||
<code><![CDATA[$associationMapping['joinColumns']]]></code>
|
||||
@@ -2192,12 +2146,18 @@
|
||||
<ImplicitToStringCast>
|
||||
<code>$expr</code>
|
||||
</ImplicitToStringCast>
|
||||
<InvalidArrayOffset>
|
||||
<code><![CDATA[$this->queryComponents[$expression]]]></code>
|
||||
</InvalidArrayOffset>
|
||||
<InvalidNullableReturnType>
|
||||
<code>string</code>
|
||||
</InvalidNullableReturnType>
|
||||
<MoreSpecificImplementedParamType>
|
||||
<code>$query</code>
|
||||
</MoreSpecificImplementedParamType>
|
||||
<NoValue>
|
||||
<code>$expression</code>
|
||||
</NoValue>
|
||||
<PossiblyInvalidArgument>
|
||||
<code><![CDATA[$aggExpression->pathExpression]]></code>
|
||||
</PossiblyInvalidArgument>
|
||||
@@ -2293,9 +2253,6 @@
|
||||
<InvalidPropertyAssignmentValue>
|
||||
<code>new ArrayCollection($parameters)</code>
|
||||
</InvalidPropertyAssignmentValue>
|
||||
<MethodSignatureMustProvideReturnType>
|
||||
<code>__toString</code>
|
||||
</MethodSignatureMustProvideReturnType>
|
||||
<PossiblyFalseArgument>
|
||||
<code>$spacePos</code>
|
||||
<code>$spacePos</code>
|
||||
@@ -2748,6 +2705,7 @@
|
||||
<NoValue>
|
||||
<code>$entityState</code>
|
||||
<code>$entityState</code>
|
||||
<code>$object</code>
|
||||
</NoValue>
|
||||
<PossiblyInvalidArgument>
|
||||
<code>$value</code>
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\Enums;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
|
||||
class FaultySwitch
|
||||
{
|
||||
#[Column(type: 'string')]
|
||||
public string $value;
|
||||
|
||||
/**
|
||||
* The following line is ignored on psalm and phpstan so that we can test
|
||||
* that the mapping is throwing an exception when a non-backed enum is used.
|
||||
*
|
||||
* @psalm-suppress InvalidArgument
|
||||
*/
|
||||
#[Column(enumType: SwitchStatus::class)]
|
||||
public SwitchStatus $status;
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\Enums;
|
||||
|
||||
enum SwitchStatus
|
||||
{
|
||||
case ON;
|
||||
case OFF;
|
||||
}
|
||||
@@ -18,6 +18,10 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
|
||||
parent::setUp();
|
||||
|
||||
$this->createSchemaForModels(EagerFetchOwner::class, EagerFetchChild::class);
|
||||
|
||||
// Ensure tables are empty
|
||||
$this->_em->getRepository(EagerFetchChild::class)->createQueryBuilder('o')->delete()->getQuery()->execute();
|
||||
$this->_em->getRepository(EagerFetchOwner::class)->createQueryBuilder('o')->delete()->getQuery()->execute();
|
||||
}
|
||||
|
||||
public function testEagerFetchMode(): void
|
||||
@@ -84,6 +88,20 @@ class EagerFetchCollectionTest extends OrmFunctionalTestCase
|
||||
$query->getResult();
|
||||
}
|
||||
|
||||
public function testEagerFetchWithIterable(): void
|
||||
{
|
||||
$this->createOwnerWithChildren(2);
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$iterable = $this->_em->getRepository(EagerFetchOwner::class)->createQueryBuilder('o')->getQuery()->toIterable();
|
||||
|
||||
// There is only a single record, but use a foreach to ensure the iterator is marked as finished and the table lock is released
|
||||
foreach ($iterable as $owner) {
|
||||
$this->assertCount(2, $owner->children);
|
||||
}
|
||||
}
|
||||
|
||||
protected function createOwnerWithChildren(int $children): EagerFetchOwner
|
||||
{
|
||||
$owner = new EagerFetchOwner();
|
||||
|
||||
@@ -116,6 +116,25 @@ class ParserResultSerializationTest extends OrmFunctionalTestCase
|
||||
yield '2.17.0' => [rtrim(file_get_contents(__DIR__ . '/ParserResults/single_select_2_17_0.txt'), "\n")];
|
||||
}
|
||||
|
||||
public function testSymfony44ProvidedData(): void
|
||||
{
|
||||
$sqlExecutor = $this->createMock(SingleSelectExecutor::class);
|
||||
$resultSetMapping = $this->createMock(ResultSetMapping::class);
|
||||
|
||||
$parserResult = new ParserResult();
|
||||
$parserResult->setSqlExecutor($sqlExecutor);
|
||||
$parserResult->setResultSetMapping($resultSetMapping);
|
||||
$parserResult->addParameterMapping('name', 0);
|
||||
|
||||
$exported = VarExporter::export($parserResult);
|
||||
$unserialized = eval('return ' . $exported . ';');
|
||||
|
||||
$this->assertInstanceOf(ParserResult::class, $unserialized);
|
||||
$this->assertInstanceOf(ResultSetMapping::class, $unserialized->getResultSetMapping());
|
||||
$this->assertEquals(['name' => [0]], $unserialized->getParameterMappings());
|
||||
$this->assertInstanceOf(SingleSelectExecutor::class, $unserialized->getSqlExecutor());
|
||||
}
|
||||
|
||||
private static function parseQuery(Query $query): ParserResult
|
||||
{
|
||||
$r = new ReflectionMethod($query, 'parse');
|
||||
|
||||
@@ -16,32 +16,36 @@ final class GH10661Test extends OrmTestCase
|
||||
/** @var EntityManagerInterface */
|
||||
private $em;
|
||||
|
||||
/** @var SchemaValidator */
|
||||
private $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->em = $this->getTestEntityManager();
|
||||
$this->validator = new SchemaValidator($this->em);
|
||||
$this->em = $this->getTestEntityManager();
|
||||
}
|
||||
|
||||
public function testMetadataFieldTypeNotCoherentWithEntityPropertyType(): void
|
||||
{
|
||||
$class = $this->em->getClassMetadata(InvalidEntity::class);
|
||||
$ce = $this->validator->validateClass($class);
|
||||
$ce = $this->bootstrapValidator()->validateClass($class);
|
||||
|
||||
self::assertEquals(
|
||||
self::assertSame(
|
||||
["The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidEntity#property1' has the property type 'float' that differs from the metadata field type 'string' returned by the 'decimal' DBAL type."],
|
||||
$ce
|
||||
);
|
||||
}
|
||||
|
||||
public function testPropertyTypeErrorsCanBeSilenced(): void
|
||||
{
|
||||
$class = $this->em->getClassMetadata(InvalidEntity::class);
|
||||
$ce = $this->bootstrapValidator(false)->validateClass($class);
|
||||
|
||||
self::assertSame([], $ce);
|
||||
}
|
||||
|
||||
public function testMetadataFieldTypeNotCoherentWithEntityPropertyTypeWithInheritance(): void
|
||||
{
|
||||
$class = $this->em->getClassMetadata(InvalidChildEntity::class);
|
||||
$ce = $this->validator->validateClass($class);
|
||||
$ce = $this->bootstrapValidator()->validateClass($class);
|
||||
|
||||
self::assertEquals(
|
||||
self::assertSame(
|
||||
[
|
||||
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#property1' has the property type 'float' that differs from the metadata field type 'string' returned by the 'decimal' DBAL type.",
|
||||
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH10661\InvalidChildEntity#property2' has the property type 'int' that differs from the metadata field type 'string' returned by the 'string' DBAL type.",
|
||||
@@ -50,4 +54,9 @@ final class GH10661Test extends OrmTestCase
|
||||
$ce
|
||||
);
|
||||
}
|
||||
|
||||
private function bootstrapValidator(bool $validatePropertyTypes = true): SchemaValidator
|
||||
{
|
||||
return new SchemaValidator($this->em, $validatePropertyTypes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,165 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function strpos;
|
||||
|
||||
class GH10912Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10912User::class,
|
||||
GH10912Profile::class,
|
||||
GH10912Room::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testIssue(): void
|
||||
{
|
||||
$user = new GH10912User();
|
||||
$profile = new GH10912Profile();
|
||||
$room = new GH10912Room();
|
||||
|
||||
$user->rooms->add($room);
|
||||
$user->profile = $profile;
|
||||
$profile->user = $user;
|
||||
$room->user = $user;
|
||||
|
||||
$this->_em->persist($room);
|
||||
$this->_em->persist($user);
|
||||
$this->_em->persist($profile);
|
||||
$this->_em->flush();
|
||||
|
||||
/*
|
||||
* This issue is about finding a special deletion order:
|
||||
* $user and $profile cross-reference each other with ON DELETE CASCADE.
|
||||
* So, whichever one gets deleted first, the DBMS will immediately dispose
|
||||
* of the other one as well.
|
||||
*
|
||||
* $user -> $room is the unproblematic (irrelevant) inverse side of
|
||||
* a OneToMany association.
|
||||
*
|
||||
* $room -> $user is a not-nullable, no DBMS-level-cascade, owning side
|
||||
* of ManyToOne. We *must* remove the $room _before_ the $user can be
|
||||
* deleted. And remember, $user deletion happens either when we DELETE the
|
||||
* user (direct deletion), or when we delete the $profile (ON DELETE CASCADE
|
||||
* propagates to the user).
|
||||
*
|
||||
* In the original bug report, the ordering of fields in the entities was
|
||||
* relevant, in combination with a cascade=persist configuration.
|
||||
*
|
||||
* But, for the sake of clarity, let's put these features away and create
|
||||
* the problematic sequence in UnitOfWork::$entityDeletions directly:
|
||||
*/
|
||||
$this->_em->remove($profile);
|
||||
$this->_em->remove($user);
|
||||
$this->_em->remove($room);
|
||||
|
||||
$queryLog = $this->getQueryLog();
|
||||
$queryLog->reset()->enable();
|
||||
|
||||
$this->_em->flush();
|
||||
|
||||
$queries = array_values(array_filter($queryLog->queries, static function (array $entry): bool {
|
||||
return strpos($entry['sql'], 'DELETE') === 0;
|
||||
}));
|
||||
|
||||
self::assertCount(3, $queries);
|
||||
|
||||
// we do not care about the order of $user vs. $profile, so do not check them.
|
||||
self::assertSame('DELETE FROM GH10912Room WHERE id = ?', $queries[0]['sql'], '$room deletion is the first query');
|
||||
|
||||
// The EntityManager is aware that all three entities have been deleted (sanity check)
|
||||
$im = $this->_em->getUnitOfWork()->getIdentityMap();
|
||||
self::assertEmpty($im[GH10912Profile::class]);
|
||||
self::assertEmpty($im[GH10912User::class]);
|
||||
self::assertEmpty($im[GH10912Room::class]);
|
||||
}
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10912User
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity=GH10912Room::class, mappedBy="user")
|
||||
*
|
||||
* @var Collection<int, GH10912Room>
|
||||
*/
|
||||
public $rooms;
|
||||
|
||||
/**
|
||||
* @ORM\OneToOne(targetEntity=GH10912Profile::class)
|
||||
* @ORM\JoinColumn(onDelete="cascade")
|
||||
*
|
||||
* @var GH10912Profile
|
||||
*/
|
||||
public $profile;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->rooms = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10912Profile
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\OneToOne(targetEntity=GH10912User::class)
|
||||
* @ORM\JoinColumn(onDelete="cascade")
|
||||
*
|
||||
* @var GH10912User
|
||||
*/
|
||||
public $user;
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10912Room
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=GH10912User::class, inversedBy="rooms")
|
||||
* @ORM\JoinColumn(nullable=false)
|
||||
*
|
||||
* @var GH10912User
|
||||
*/
|
||||
public $user;
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Internal\TopologicalSort\CycleDetectedException;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
use function array_filter;
|
||||
use function array_values;
|
||||
use function strpos;
|
||||
|
||||
class GH10913Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10913Entity::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testExample1(): void
|
||||
{
|
||||
[$a, $b, $c] = $this->createEntities(3);
|
||||
|
||||
$c->ref = $b;
|
||||
$b->odc = $a;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($a);
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
|
||||
$this->flushAndAssertNumberOfDeleteQueries(3);
|
||||
}
|
||||
|
||||
public function testExample2(): void
|
||||
{
|
||||
[$a, $b, $c] = $this->createEntities(3);
|
||||
|
||||
$a->odc = $b;
|
||||
$b->odc = $a;
|
||||
$c->ref = $b;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($a);
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
|
||||
$this->flushAndAssertNumberOfDeleteQueries(3);
|
||||
}
|
||||
|
||||
public function testExample3(): void
|
||||
{
|
||||
[$a, $b, $c] = $this->createEntities(3);
|
||||
|
||||
$a->odc = $b;
|
||||
$a->ref = $c;
|
||||
$c->ref = $b;
|
||||
$b->odc = $a;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($a);
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
|
||||
self::expectException(CycleDetectedException::class);
|
||||
|
||||
$this->_em->flush();
|
||||
}
|
||||
|
||||
public function testExample4(): void
|
||||
{
|
||||
[$a, $b, $c, $d] = $this->createEntities(4);
|
||||
|
||||
$a->ref = $b;
|
||||
$b->odc = $c;
|
||||
$c->odc = $b;
|
||||
$d->ref = $c;
|
||||
|
||||
$this->_em->persist($a);
|
||||
$this->_em->persist($b);
|
||||
$this->_em->persist($c);
|
||||
$this->_em->persist($d);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($b);
|
||||
$this->_em->remove($c);
|
||||
$this->_em->remove($d);
|
||||
$this->_em->remove($a);
|
||||
|
||||
$this->flushAndAssertNumberOfDeleteQueries(4);
|
||||
}
|
||||
|
||||
private function flushAndAssertNumberOfDeleteQueries(int $expectedCount): void
|
||||
{
|
||||
$queryLog = $this->getQueryLog();
|
||||
$queryLog->reset()->enable();
|
||||
|
||||
$this->_em->flush();
|
||||
|
||||
$queries = array_values(array_filter($queryLog->queries, static function (array $entry): bool {
|
||||
return strpos($entry['sql'], 'DELETE') === 0;
|
||||
}));
|
||||
|
||||
self::assertCount($expectedCount, $queries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<GH10913Entity>
|
||||
*/
|
||||
private function createEntities(int $count = 1): array
|
||||
{
|
||||
$result = [];
|
||||
|
||||
for ($i = 0; $i < $count; $i++) {
|
||||
$result[] = new GH10913Entity();
|
||||
}
|
||||
|
||||
return $result;
|
||||
}
|
||||
}
|
||||
|
||||
/** @ORM\Entity */
|
||||
class GH10913Entity
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=GH10913Entity::class)
|
||||
* @ORM\JoinColumn(nullable=true, onDelete="CASCADE")
|
||||
*
|
||||
* @var GH10913Entity
|
||||
*/
|
||||
public $odc;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity=GH10913Entity::class)
|
||||
* @ORM\JoinColumn(nullable=true)
|
||||
*
|
||||
* @var GH10913Entity
|
||||
*/
|
||||
public $ref;
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
/**
|
||||
* @group GH-10927
|
||||
*/
|
||||
class GH10927Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$platform = $this->_em->getConnection()->getDatabasePlatform();
|
||||
if (! $platform instanceof PostgreSQLPlatform) {
|
||||
self::markTestSkipped('The ' . self::class . ' requires the use of postgresql.');
|
||||
}
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH10927RootMappedSuperclass::class,
|
||||
GH10927InheritedMappedSuperclass::class,
|
||||
GH10927EntityA::class,
|
||||
GH10927EntityB::class,
|
||||
GH10927EntityC::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForRootMappedSuperclass(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927RootMappedSuperclass::class);
|
||||
|
||||
self::assertNull($metadata->sequenceGeneratorDefinition);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForEntityA(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927EntityA::class);
|
||||
|
||||
self::assertSame('GH10927EntityA_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForInheritedMappedSuperclass(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927InheritedMappedSuperclass::class);
|
||||
|
||||
self::assertSame('GH10927InheritedMappedSuperclass_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForEntityB(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927EntityB::class);
|
||||
|
||||
self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
|
||||
public function testSequenceGeneratorDefinitionForEntityC(): void
|
||||
{
|
||||
$metadata = $this->_em->getClassMetadata(GH10927EntityC::class);
|
||||
|
||||
self::assertSame('GH10927EntityB_id_seq', $metadata->sequenceGeneratorDefinition['sequenceName']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\MappedSuperclass()
|
||||
*/
|
||||
class GH10927RootMappedSuperclass
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH10927EntityA extends GH10927RootMappedSuperclass
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="SEQUENCE")
|
||||
* @ORM\Column(type="integer")
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $id = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\MappedSuperclass()
|
||||
*/
|
||||
class GH10927InheritedMappedSuperclass extends GH10927RootMappedSuperclass
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\GeneratedValue(strategy="SEQUENCE")
|
||||
* @ORM\Column(type="integer")
|
||||
*
|
||||
* @var int|null
|
||||
*/
|
||||
private $id = null;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\InheritanceType("JOINED")
|
||||
* @ORM\DiscriminatorColumn(name="discr", type="string")
|
||||
* @ORM\DiscriminatorMap({"B" = "GH10927EntityB", "C" = "GH10927EntityC"})
|
||||
*/
|
||||
class GH10927EntityB extends GH10927InheritedMappedSuperclass
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH10927EntityC extends GH10927EntityB
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11037;
|
||||
|
||||
interface EntityStatus
|
||||
{
|
||||
}
|
||||
@@ -40,8 +40,9 @@ final class GH11037Test extends OrmTestCase
|
||||
|
||||
self::assertEquals(
|
||||
[
|
||||
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status1' has the property type 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus' that differs from the metadata field type 'int' returned by the 'integer' DBAL type.",
|
||||
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status1' has the property type 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus' with a backing type of 'string' that differs from the metadata field type 'int'.",
|
||||
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status2' has the property type 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\IntEntityStatus' that differs from the metadata enumType 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus'.",
|
||||
"The field 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\InvalidEntityWithTypedEnum#status3' has the metadata enumType 'Doctrine\Tests\ORM\Functional\Ticket\GH11037\StringEntityStatus' with a backing type of 'string' that differs from the metadata field type 'int'.",
|
||||
],
|
||||
$ce
|
||||
);
|
||||
|
||||
@@ -28,4 +28,9 @@ class InvalidEntityWithTypedEnum
|
||||
* @Column(type="integer", enumType=StringEntityStatus::class)
|
||||
*/
|
||||
protected IntEntityStatus $status2;
|
||||
|
||||
/**
|
||||
* @Column(type="integer", enumType=StringEntityStatus::class)
|
||||
*/
|
||||
protected EntityStatus $status3;
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11037;
|
||||
|
||||
enum StringEntityStatus: string
|
||||
enum StringEntityStatus: string implements EntityStatus
|
||||
{
|
||||
case ACTIVE = 'active';
|
||||
case INACTIVE = 'inactive';
|
||||
|
||||
@@ -28,4 +28,9 @@ class ValidEntityWithTypedEnum
|
||||
* @Column(type="smallint", enumType=IntEntityStatus::class)
|
||||
*/
|
||||
protected IntEntityStatus $status2;
|
||||
|
||||
/**
|
||||
* @Column(type="string", enumType=StringEntityStatus::class)
|
||||
*/
|
||||
protected EntityStatus $status3;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,146 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\Common\Collections\ArrayCollection;
|
||||
use Doctrine\Common\Collections\Collection;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11058Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11058Parent::class,
|
||||
GH11058Child::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testChildrenInsertedInOrderOfPersistCalls1WhenParentPersistedLast(): void
|
||||
{
|
||||
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
|
||||
|
||||
$this->_em->persist($child1);
|
||||
$this->_em->persist($child2);
|
||||
$this->_em->persist($parent);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertTrue($child1->id < $child2->id);
|
||||
}
|
||||
|
||||
public function testChildrenInsertedInOrderOfPersistCalls2WhenParentPersistedLast(): void
|
||||
{
|
||||
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
|
||||
|
||||
$this->_em->persist($child2);
|
||||
$this->_em->persist($child1);
|
||||
$this->_em->persist($parent);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertTrue($child2->id < $child1->id);
|
||||
}
|
||||
|
||||
public function testChildrenInsertedInOrderOfPersistCalls1WhenParentPersistedFirst(): void
|
||||
{
|
||||
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
|
||||
|
||||
$this->_em->persist($parent);
|
||||
$this->_em->persist($child1);
|
||||
$this->_em->persist($child2);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertTrue($child1->id < $child2->id);
|
||||
}
|
||||
|
||||
public function testChildrenInsertedInOrderOfPersistCalls2WhenParentPersistedFirst(): void
|
||||
{
|
||||
[$parent, $child1, $child2] = $this->createParentWithTwoChildEntities();
|
||||
|
||||
$this->_em->persist($parent);
|
||||
$this->_em->persist($child2);
|
||||
$this->_em->persist($child1);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertTrue($child2->id < $child1->id);
|
||||
}
|
||||
|
||||
private function createParentWithTwoChildEntities(): array
|
||||
{
|
||||
$parent = new GH11058Parent();
|
||||
$child1 = new GH11058Child();
|
||||
$child2 = new GH11058Child();
|
||||
|
||||
$parent->addChild($child1);
|
||||
$parent->addChild($child2);
|
||||
|
||||
return [$parent, $child1, $child2];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH11058Parent
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\OneToMany(targetEntity="GH11058Child", mappedBy="parent")
|
||||
*
|
||||
* @var Collection<int, GH11058Child>
|
||||
*/
|
||||
public $children;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->children = new ArrayCollection();
|
||||
}
|
||||
|
||||
public function addChild(GH11058Child $child): void
|
||||
{
|
||||
if (! $this->children->contains($child)) {
|
||||
$this->children->add($child);
|
||||
$child->setParent($this);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH11058Child
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="GH11058Parent", inversedBy="children")
|
||||
*
|
||||
* @var GH11058Parent
|
||||
*/
|
||||
public $parent;
|
||||
|
||||
public function setParent(GH11058Parent $parent): void
|
||||
{
|
||||
$this->parent = $parent;
|
||||
$parent->addChild($this);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11072;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class GH11072EntityAdvanced
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="integer")
|
||||
* @GeneratedValue
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public mixed $anything;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public true $alwaysTrue = true;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public false $alwaysFalse = false;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public null $alwaysNull = null;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11072;
|
||||
|
||||
use Doctrine\ORM\Mapping\Column;
|
||||
use Doctrine\ORM\Mapping\Entity;
|
||||
use Doctrine\ORM\Mapping\GeneratedValue;
|
||||
use Doctrine\ORM\Mapping\Id;
|
||||
|
||||
/**
|
||||
* @Entity
|
||||
*/
|
||||
class GH11072EntityBasic
|
||||
{
|
||||
/**
|
||||
* @Id
|
||||
* @Column(type="integer")
|
||||
* @GeneratedValue
|
||||
* @var int
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public string $jsonString = 'test';
|
||||
|
||||
/** @Column(type="json") */
|
||||
public int $age = 99;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public float $score = 0.0;
|
||||
|
||||
/** @Column(type="json", nullable=true) */
|
||||
public ?bool $trinary = null;
|
||||
|
||||
/** @Column(type="json") */
|
||||
public array $metadata = [];
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11072;
|
||||
|
||||
use Doctrine\ORM\Tools\SchemaValidator;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
/**
|
||||
* @requires PHP >= 7.4
|
||||
*/
|
||||
final class GH11072Test extends OrmFunctionalTestCase
|
||||
{
|
||||
/** @var SchemaValidator */
|
||||
private $validator;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->_em = $this->getTestEntityManager();
|
||||
$this->validator = new SchemaValidator($this->_em);
|
||||
}
|
||||
|
||||
public function testAcceptsSubsetOfBuiltinTypesWithoutErrors(): void
|
||||
{
|
||||
$class = $this->_em->getClassMetadata(GH11072EntityBasic::class);
|
||||
$ce = $this->validator->validateClass($class);
|
||||
|
||||
self::assertSame([], $ce);
|
||||
}
|
||||
|
||||
/**
|
||||
* @requires PHP >= 8.2
|
||||
*/
|
||||
public function testAcceptsAdvancedSubsetOfBuiltinTypesWithoutErrors(): void
|
||||
{
|
||||
$class = $this->_em->getClassMetadata(GH11072EntityAdvanced::class);
|
||||
$ce = $this->validator->validateClass($class);
|
||||
|
||||
self::assertSame([], $ce);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11135Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11135MappedSuperclass::class,
|
||||
GH11135EntityWithOverride::class,
|
||||
GH11135EntityWithoutOverride::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function testOverrideInheritsDeclaringClass(): void
|
||||
{
|
||||
$cm1 = $this->_em->getClassMetadata(GH11135EntityWithOverride::class);
|
||||
$cm2 = $this->_em->getClassMetadata(GH11135EntityWithoutOverride::class);
|
||||
|
||||
self::assertSame($cm1->getFieldMapping('id')['declared'], $cm2->getFieldMapping('id')['declared']);
|
||||
self::assertSame($cm1->getAssociationMapping('ref')['declared'], $cm2->getAssociationMapping('ref')['declared']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\MappedSuperclass
|
||||
*/
|
||||
class GH11135MappedSuperclass
|
||||
{
|
||||
/**
|
||||
* @ORM\Id
|
||||
* @ORM\Column(type="integer")
|
||||
* @ORM\GeneratedValue
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
private $id;
|
||||
|
||||
/**
|
||||
* @ORM\ManyToOne(targetEntity="GH11135EntityWithoutOverride")
|
||||
*
|
||||
* @var GH11135EntityWithoutOverride
|
||||
*/
|
||||
private $ref;
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
* @ORM\AttributeOverrides({
|
||||
* @ORM\AttributeOverride(name="id", column=@ORM\Column(name="id_overridden"))
|
||||
* })
|
||||
* @ORM\AssociationOverrides({
|
||||
* @ORM\AssociationOverride(name="ref", joinColumns=@ORM\JoinColumn(name="ref_overridden", referencedColumnName="id"))
|
||||
* })
|
||||
*/
|
||||
class GH11135EntityWithOverride extends GH11135MappedSuperclass
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
* @ORM\Entity()
|
||||
*/
|
||||
class GH11135EntityWithoutOverride extends GH11135MappedSuperclass
|
||||
{
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Internal;
|
||||
|
||||
class Node
|
||||
{
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,113 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Internal;
|
||||
|
||||
use Doctrine\ORM\Internal\StronglyConnectedComponents;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
|
||||
class StronglyConnectedComponentsTest extends OrmTestCase
|
||||
{
|
||||
/** @var array<string, Node> */
|
||||
private $nodes = [];
|
||||
|
||||
/** @var StronglyConnectedComponents */
|
||||
private $stronglyConnectedComponents;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$this->stronglyConnectedComponents = new StronglyConnectedComponents();
|
||||
}
|
||||
|
||||
public function testFindStronglyConnectedComponents(): void
|
||||
{
|
||||
// A -> B <-> C -> D <-> E
|
||||
$this->addNodes('A', 'B', 'C', 'D', 'E');
|
||||
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('B', 'C');
|
||||
$this->addEdge('C', 'B');
|
||||
$this->addEdge('C', 'D');
|
||||
$this->addEdge('D', 'E');
|
||||
$this->addEdge('E', 'D');
|
||||
|
||||
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
$this->assertNodesAreInSameComponent('B', 'C');
|
||||
$this->assertNodesAreInSameComponent('D', 'E');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'B');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'D');
|
||||
}
|
||||
|
||||
public function testFindStronglyConnectedComponents2(): void
|
||||
{
|
||||
// A -> B -> C -> D -> B
|
||||
$this->addNodes('A', 'B', 'C', 'D');
|
||||
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('B', 'C');
|
||||
$this->addEdge('C', 'D');
|
||||
$this->addEdge('D', 'B');
|
||||
|
||||
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
$this->assertNodesAreInSameComponent('B', 'C');
|
||||
$this->assertNodesAreInSameComponent('C', 'D');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'B');
|
||||
}
|
||||
|
||||
public function testFindStronglyConnectedComponents3(): void
|
||||
{
|
||||
// v---------.
|
||||
// A -> B -> C -> D -> E
|
||||
// ^--------´
|
||||
|
||||
$this->addNodes('A', 'B', 'C', 'D', 'E');
|
||||
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('B', 'C');
|
||||
$this->addEdge('C', 'D');
|
||||
$this->addEdge('D', 'E');
|
||||
$this->addEdge('E', 'C');
|
||||
$this->addEdge('D', 'B');
|
||||
|
||||
$this->stronglyConnectedComponents->findStronglyConnectedComponents();
|
||||
|
||||
$this->assertNodesAreInSameComponent('B', 'C');
|
||||
$this->assertNodesAreInSameComponent('C', 'D');
|
||||
$this->assertNodesAreInSameComponent('D', 'E');
|
||||
$this->assertNodesAreInSameComponent('E', 'B');
|
||||
$this->assertNodesAreNotInSameComponent('A', 'B');
|
||||
}
|
||||
|
||||
private function addNodes(string ...$names): void
|
||||
{
|
||||
foreach ($names as $name) {
|
||||
$node = new Node($name);
|
||||
$this->nodes[$name] = $node;
|
||||
$this->stronglyConnectedComponents->addNode($node);
|
||||
}
|
||||
}
|
||||
|
||||
private function addEdge(string $from, string $to, bool $optional = false): void
|
||||
{
|
||||
$this->stronglyConnectedComponents->addEdge($this->nodes[$from], $this->nodes[$to], $optional);
|
||||
}
|
||||
|
||||
private function assertNodesAreInSameComponent(string $first, string $second): void
|
||||
{
|
||||
self::assertSame(
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$first]),
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$second])
|
||||
);
|
||||
}
|
||||
|
||||
private function assertNodesAreNotInSameComponent(string $first, string $second): void
|
||||
{
|
||||
self::assertNotSame(
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$first]),
|
||||
$this->stronglyConnectedComponents->getNodeRepresentingStronglyConnectedComponent($this->nodes[$second])
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -34,7 +34,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('E', 'A');
|
||||
|
||||
// There is only 1 valid ordering for this constellation
|
||||
self::assertSame(['E', 'A', 'B', 'C'], $this->computeResult());
|
||||
self::assertSame(['C', 'B', 'A', 'E'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testSkipOptionalEdgeToBreakCycle(): void
|
||||
@@ -44,7 +44,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('A', 'B', true);
|
||||
$this->addEdge('B', 'A', false);
|
||||
|
||||
self::assertSame(['B', 'A'], $this->computeResult());
|
||||
self::assertSame(['A', 'B'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testBreakCycleByBacktracking(): void
|
||||
@@ -57,7 +57,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('D', 'A'); // closes the cycle
|
||||
|
||||
// We can only break B -> C, so the result must be C -> D -> A -> B
|
||||
self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult());
|
||||
self::assertSame(['B', 'A', 'D', 'C'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testCycleRemovedByEliminatingLastOptionalEdge(): void
|
||||
@@ -75,7 +75,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('B', 'D', true);
|
||||
$this->addEdge('D', 'A');
|
||||
|
||||
self::assertSame(['C', 'D', 'A', 'B'], $this->computeResult());
|
||||
self::assertSame(['B', 'A', 'C', 'D'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testGH7180Example(): void
|
||||
@@ -89,7 +89,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('F', 'E');
|
||||
$this->addEdge('E', 'D');
|
||||
|
||||
self::assertSame(['F', 'E', 'D', 'G'], $this->computeResult());
|
||||
self::assertSame(['G', 'D', 'E', 'F'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testCommitOrderingFromGH7259Test(): void
|
||||
@@ -106,9 +106,9 @@ class TopologicalSortTest extends OrmTestCase
|
||||
// the D -> A -> B ordering is important to break the cycle
|
||||
// on the nullable link.
|
||||
$correctOrders = [
|
||||
['D', 'A', 'B', 'C'],
|
||||
['D', 'A', 'C', 'B'],
|
||||
['D', 'C', 'A', 'B'],
|
||||
['C', 'B', 'A', 'D'],
|
||||
['B', 'C', 'A', 'D'],
|
||||
['B', 'A', 'C', 'D'],
|
||||
];
|
||||
|
||||
self::assertContains($this->computeResult(), $correctOrders);
|
||||
@@ -124,12 +124,12 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('B', 'C', true);
|
||||
$this->addEdge('C', 'D', true);
|
||||
|
||||
// Many orderings are possible here, but the bottom line is D must be before A (it's the only hard requirement).
|
||||
// Many orderings are possible here, but the bottom line is A must be before D (it's the only hard requirement).
|
||||
$result = $this->computeResult();
|
||||
|
||||
$indexA = array_search('A', $result, true);
|
||||
$indexD = array_search('D', $result, true);
|
||||
self::assertTrue($indexD < $indexA);
|
||||
self::assertTrue($indexD > $indexA);
|
||||
}
|
||||
|
||||
public function testCommitOrderingFromGH8349Case2Test(): void
|
||||
@@ -141,7 +141,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->addEdge('A', 'B', true);
|
||||
|
||||
// The B -> A requirement determines the result here
|
||||
self::assertSame(['B', 'A'], $this->computeResult());
|
||||
self::assertSame(['A', 'B'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesMaintainOrderWhenNoDepencency(): void
|
||||
@@ -153,6 +153,58 @@ class TopologicalSortTest extends OrmTestCase
|
||||
self::assertSame(['A', 'B', 'C'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrder(): void
|
||||
{
|
||||
$this->addNodes('A', 'B', 'C');
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('A', 'C');
|
||||
|
||||
// We start on A and find that it has two dependencies on B and C,
|
||||
// added (as dependencies) in that order.
|
||||
// So, first we continue the DFS on B, because that edge was added first.
|
||||
// This gives the result order B, C, A.
|
||||
self::assertSame(['B', 'C', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrderWithEdgesInDifferentOrderThanNodes(): void
|
||||
{
|
||||
$this->addNodes('A', 'B', 'C');
|
||||
$this->addEdge('A', 'C');
|
||||
$this->addEdge('A', 'B');
|
||||
|
||||
// This is like testNodesReturnedInDepthFirstOrder, but it shows that for the two
|
||||
// nodes B and C that A depends upon, the result will follow the order in which
|
||||
// the edges were added.
|
||||
self::assertSame(['C', 'B', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrderWithDependingNodeLast(): void
|
||||
{
|
||||
$this->addNodes('B', 'C', 'A');
|
||||
$this->addEdge('A', 'B');
|
||||
$this->addEdge('A', 'C');
|
||||
|
||||
// This again is like testNodesReturnedInDepthFirstOrder, but this
|
||||
// time the node A that depends on B and C is added as the last node.
|
||||
// That means processing can go over B and C in the order they were given.
|
||||
// The order in which edges are added is not relevant (!), since at the time
|
||||
// the edges are evaluated, the nodes they point to have already been finished.
|
||||
self::assertSame(['B', 'C', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testNodesReturnedInDepthFirstOrderWithDependingNodeLastAndEdgeOrderInversed(): void
|
||||
{
|
||||
$this->addNodes('B', 'C', 'A');
|
||||
$this->addEdge('A', 'C');
|
||||
$this->addEdge('A', 'B');
|
||||
|
||||
// This again is like testNodesReturnedInDepthFirstOrderWithDependingNodeLast, but adds
|
||||
// the edges in the opposing order. Still, the result order is the same (!).
|
||||
// This may be surprising when comparing with testNodesReturnedInDepthFirstOrderWithEdgesInDifferentOrderThanNodes,
|
||||
// where the result order depends upon the _edge_ order.
|
||||
self::assertSame(['B', 'C', 'A'], $this->computeResult());
|
||||
}
|
||||
|
||||
public function testDetectSmallCycle(): void
|
||||
{
|
||||
$this->addNodes('A', 'B');
|
||||
@@ -205,7 +257,7 @@ class TopologicalSortTest extends OrmTestCase
|
||||
$this->computeResult();
|
||||
} catch (CycleDetectedException $exception) {
|
||||
self::assertEquals(
|
||||
[$this->nodes['D'], $this->nodes['B'], $this->nodes['C'], $this->nodes['D']],
|
||||
[$this->nodes['B'], $this->nodes['C'], $this->nodes['D'], $this->nodes['B']],
|
||||
$exception->getCycle()
|
||||
);
|
||||
}
|
||||
@@ -235,14 +287,3 @@ class TopologicalSortTest extends OrmTestCase
|
||||
}, array_values($this->topologicalSort->sort()));
|
||||
}
|
||||
}
|
||||
|
||||
class Node
|
||||
{
|
||||
/** @var string */
|
||||
public $name;
|
||||
|
||||
public function __construct(string $name)
|
||||
{
|
||||
$this->name = $name;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,22 +163,7 @@ class BasicInheritanceMappingTest extends OrmTestCase
|
||||
/**
|
||||
* @group DDC-1156
|
||||
* @group DDC-1218
|
||||
*/
|
||||
public function testGeneratedValueFromMappedSuperclass(): void
|
||||
{
|
||||
$class = $this->cmf->getMetadataFor(SuperclassEntity::class);
|
||||
assert($class instanceof ClassMetadata);
|
||||
|
||||
self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
|
||||
self::assertEquals(
|
||||
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
|
||||
$class->sequenceGeneratorDefinition
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-1156
|
||||
* @group DDC-1218
|
||||
* @group GH-10927
|
||||
*/
|
||||
public function testSequenceDefinitionInHierarchyWithSandwichMappedSuperclass(): void
|
||||
{
|
||||
@@ -192,22 +177,6 @@ class BasicInheritanceMappingTest extends OrmTestCase
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @group DDC-1156
|
||||
* @group DDC-1218
|
||||
*/
|
||||
public function testMultipleMappedSuperclasses(): void
|
||||
{
|
||||
$class = $this->cmf->getMetadataFor(MediumSuperclassEntity::class);
|
||||
assert($class instanceof ClassMetadata);
|
||||
|
||||
self::assertInstanceOf(IdSequenceGenerator::class, $class->idGenerator);
|
||||
self::assertEquals(
|
||||
['allocationSize' => 1, 'initialValue' => 10, 'sequenceName' => 'foo'],
|
||||
$class->sequenceGeneratorDefinition
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure indexes are inherited from the mapped superclass.
|
||||
*
|
||||
|
||||
@@ -29,6 +29,8 @@ use Doctrine\Tests\Models\DDC117\DDC117ArticleDetails;
|
||||
use Doctrine\Tests\Models\DDC6412\DDC6412File;
|
||||
use Doctrine\Tests\Models\DDC964\DDC964Admin;
|
||||
use Doctrine\Tests\Models\DDC964\DDC964Guest;
|
||||
use Doctrine\Tests\Models\DirectoryTree\AbstractContentItem;
|
||||
use Doctrine\Tests\Models\DirectoryTree\Directory;
|
||||
use Doctrine\Tests\Models\Routing\RoutingLeg;
|
||||
use Doctrine\Tests\Models\TypedProperties;
|
||||
use Doctrine\Tests\ORM\Mapping\TypedFieldMapper\CustomIntAsStringTypedFieldMapper;
|
||||
@@ -1186,6 +1188,30 @@ class ClassMetadataTest extends OrmTestCase
|
||||
$cm->setAttributeOverride('name', ['type' => 'date']);
|
||||
}
|
||||
|
||||
public function testAttributeOverrideKeepsDeclaringClass(): void
|
||||
{
|
||||
$cm = new ClassMetadata(Directory::class);
|
||||
$cm->mapField(['fieldName' => 'id', 'type' => 'integer', 'declared' => AbstractContentItem::class]);
|
||||
$cm->setAttributeOverride('id', ['columnName' => 'new_id']);
|
||||
|
||||
$mapping = $cm->getFieldMapping('id');
|
||||
|
||||
self::assertArrayHasKey('declared', $mapping);
|
||||
self::assertSame(AbstractContentItem::class, $mapping['declared']);
|
||||
}
|
||||
|
||||
public function testAssociationOverrideKeepsDeclaringClass(): void
|
||||
{
|
||||
$cm = new ClassMetadata(Directory::class);
|
||||
$cm->mapManyToOne(['fieldName' => 'parentDirectory', 'targetEntity' => Directory::class, 'cascade' => ['remove'], 'declared' => Directory::class]);
|
||||
$cm->setAssociationOverride('parentDirectory', ['cascade' => '']);
|
||||
|
||||
$mapping = $cm->getAssociationMapping('parentDirectory');
|
||||
|
||||
self::assertArrayHasKey('declared', $mapping);
|
||||
self::assertSame(Directory::class, $mapping['declared']);
|
||||
}
|
||||
|
||||
/** @group DDC-1955 */
|
||||
public function testInvalidEntityListenerClassException(): void
|
||||
{
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Mapping;
|
||||
|
||||
use Doctrine\ORM\Mapping\DefaultTypedFieldMapper;
|
||||
use Doctrine\ORM\Mapping\MappingException;
|
||||
use Doctrine\Tests\Models\Enums\FaultySwitch;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use ReflectionClass;
|
||||
|
||||
/**
|
||||
* @requires PHP >= 8.1
|
||||
*/
|
||||
class TypedEnumFieldMapperTest extends OrmTestCase
|
||||
{
|
||||
private static function defaultTypedFieldMapper(): DefaultTypedFieldMapper
|
||||
{
|
||||
return new DefaultTypedFieldMapper();
|
||||
}
|
||||
|
||||
public function testNotBackedEnumThrows(): void
|
||||
{
|
||||
$reflectionClass = new ReflectionClass(FaultySwitch::class);
|
||||
|
||||
$this->expectException(MappingException::class);
|
||||
$this->expectExceptionMessage(
|
||||
'Attempting to map a non-backed enum type Doctrine\Tests\Models\Enums\SwitchStatus in entity Doctrine\Tests\Models\Enums\FaultySwitch::$status. Please use backed enums only'
|
||||
);
|
||||
|
||||
self::defaultTypedFieldMapper()->validateAndComplete(['fieldName' => 'status'], $reflectionClass->getProperty('status'));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user