mirror of
https://github.com/doctrine/orm.git
synced 2026-04-24 15:08:12 +02:00
Compare commits
34 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ca99fdfdc | |||
| 2d8e466636 | |||
| 94986af284 | |||
| ad5c8e4bdc | |||
| c363f55ad1 | |||
| c973a62272 | |||
| 8d3446015a | |||
| 4e335f4044 | |||
| bb36d49b38 | |||
| 2b81a8e260 | |||
| 7d3b3f28e9 | |||
| cbec236e8b | |||
| 306963fe79 | |||
| fb4578406f | |||
| bdc41e2b5e | |||
| 90376a6431 | |||
| f79d166a4e | |||
| 9c22814cfa | |||
| b274893486 | |||
| e0e55dc9c5 | |||
| 010b1e0886 | |||
| 93eb8a1bcb | |||
| 1464827220 | |||
| 8709fb38b0 | |||
| cbb6c897de | |||
| e9e60f2fbc | |||
| 5f3c1dbab8 | |||
| 753bc16c0b | |||
| 6090141e0b | |||
| e4a6c041b5 | |||
| c54c557e02 | |||
| 46d0865339 | |||
| 4b4b9b7b6f | |||
| ae842259f5 |
@@ -11,7 +11,7 @@ What we offer are hooks to execute any kind of validation.
|
||||
.. note::
|
||||
|
||||
You don't need to validate your entities in the lifecycle
|
||||
events. Its only one of many options. Of course you can also
|
||||
events. It is only one of many options. Of course you can also
|
||||
perform validations in value setters or any other method of your
|
||||
entities that are used in your code.
|
||||
|
||||
|
||||
@@ -18,7 +18,7 @@ well.
|
||||
Requirements
|
||||
------------
|
||||
|
||||
Doctrine ORM requires a minimum of PHP 7.1. For greatly improved
|
||||
Doctrine ORM requires a minimum of PHP 8.1. For greatly improved
|
||||
performance it is also recommended that you use APC with PHP.
|
||||
|
||||
Doctrine ORM Packages
|
||||
|
||||
@@ -870,8 +870,8 @@ This is essentially the same as the following, more verbose, mapping:
|
||||
* @var Collection<int, Group>
|
||||
*/
|
||||
#[JoinTable(name: 'User_Group')]
|
||||
#[JoinColumn(name: 'User_id', referencedColumnName: 'id')]
|
||||
#[InverseJoinColumn(name: 'Group_id', referencedColumnName: 'id')]
|
||||
#[JoinColumn(name: 'user_id', referencedColumnName: 'id')]
|
||||
#[InverseJoinColumn(name: 'group_id', referencedColumnName: 'id')]
|
||||
#[ManyToMany(targetEntity: Group::class)]
|
||||
private Collection $groups;
|
||||
// ...
|
||||
@@ -884,10 +884,10 @@ This is essentially the same as the following, more verbose, mapping:
|
||||
<many-to-many field="groups" target-entity="Group">
|
||||
<join-table name="User_Group">
|
||||
<join-columns>
|
||||
<join-column id="User_id" referenced-column-name="id" />
|
||||
<join-column id="user_id" referenced-column-name="id" />
|
||||
</join-columns>
|
||||
<inverse-join-columns>
|
||||
<join-column id="Group_id" referenced-column-name="id" />
|
||||
<join-column id="group_id" referenced-column-name="id" />
|
||||
</inverse-join-columns>
|
||||
</join-table>
|
||||
</many-to-many>
|
||||
|
||||
@@ -976,7 +976,7 @@ The Query class
|
||||
---------------
|
||||
|
||||
An instance of the ``Doctrine\ORM\Query`` class represents a DQL
|
||||
query. You create a Query instance be calling
|
||||
query. You create a Query instance by calling
|
||||
``EntityManager#createQuery($dql)``, passing the DQL query string.
|
||||
Alternatively you can create an empty ``Query`` instance and invoke
|
||||
``Query#setDQL($dql)`` afterwards. Here are some examples:
|
||||
@@ -993,58 +993,146 @@ Alternatively you can create an empty ``Query`` instance and invoke
|
||||
$q = $em->createQuery();
|
||||
$q->setDQL('select u from MyProject\Model\User u');
|
||||
|
||||
Query Result Formats
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
Query Result Formats (Hydration Modes)
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
The format in which the result of a DQL SELECT query is returned
|
||||
can be influenced by a so-called ``hydration mode``. A hydration
|
||||
mode specifies a particular way in which a SQL result set is
|
||||
transformed. Each hydration mode has its own dedicated method on
|
||||
the Query class. Here they are:
|
||||
The way in which the SQL result set of a DQL SELECT query is transformed
|
||||
to PHP is determined by the so-called "hydration mode".
|
||||
|
||||
``getResult()``
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
- ``Query#getResult()``: Retrieves a collection of objects. The
|
||||
result is either a plain collection of objects (pure) or an array
|
||||
where the objects are nested in the result rows (mixed).
|
||||
- ``Query#getSingleResult()``: Retrieves a single object. If the
|
||||
result contains more than one object, an ``NonUniqueResultException``
|
||||
is thrown. If the result contains no objects, an ``NoResultException``
|
||||
is thrown. The pure/mixed distinction does not apply.
|
||||
- ``Query#getOneOrNullResult()``: Retrieve a single object. If the
|
||||
result contains more than one object, a ``NonUniqueResultException``
|
||||
is thrown. If no object is found null will be returned.
|
||||
- ``Query#getArrayResult()``: Retrieves an array graph (a nested
|
||||
array) that is largely interchangeable with the object graph
|
||||
generated by ``Query#getResult()`` for read-only purposes.
|
||||
Retrieves a collection of objects. The result is either a plain collection of objects (pure) or an array
|
||||
where the objects are nested in the result rows (mixed):
|
||||
|
||||
.. note::
|
||||
.. code-block:: php
|
||||
|
||||
An array graph can differ from the corresponding object
|
||||
graph in certain scenarios due to the difference of the identity
|
||||
semantics between arrays and objects.
|
||||
<?php
|
||||
use Doctrine\ORM\AbstractQuery;
|
||||
|
||||
$query = $em->createQuery('SELECT u FROM User u');
|
||||
$users = $query->getResult();
|
||||
// same as:
|
||||
$users = $query->getResult(AbstractQuery::HYDRATE_OBJECT);
|
||||
|
||||
- Objects fetched in a FROM clause are returned as a Set, that means every
|
||||
object is only ever included in the resulting array once. This is the case
|
||||
even when using JOIN or GROUP BY in ways that return the same row for an
|
||||
object multiple times. If the hydrator sees the same object multiple times,
|
||||
then it makes sure it is only returned once.
|
||||
|
||||
- ``Query#getScalarResult()``: Retrieves a flat/rectangular result
|
||||
set of scalar values that can contain duplicate data. The
|
||||
pure/mixed distinction does not apply.
|
||||
- ``Query#getSingleScalarResult()``: Retrieves a single scalar
|
||||
value from the result returned by the dbms. If the result contains
|
||||
more than a single scalar value, an exception is thrown. The
|
||||
pure/mixed distinction does not apply.
|
||||
- If an object is already in memory from a previous query of any kind, then
|
||||
then the previous object is used, even if the database may contain more
|
||||
recent data. This even happens if the previous object is still an unloaded proxy.
|
||||
|
||||
Instead of using these methods, you can alternatively use the
|
||||
general-purpose method
|
||||
``Query#execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
|
||||
Using this method you can directly supply the hydration mode as the
|
||||
second parameter via one of the Query constants. In fact, the
|
||||
methods mentioned earlier are just convenient shortcuts for the
|
||||
execute method. For example, the method ``Query#getResult()``
|
||||
internally invokes execute, passing in ``Query::HYDRATE_OBJECT`` as
|
||||
the hydration mode.
|
||||
``getArrayResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
The use of the methods mentioned earlier is generally preferred as
|
||||
it leads to more concise code.
|
||||
Retrieves an array graph (a nested array) for read-only purposes.
|
||||
|
||||
.. note::
|
||||
|
||||
An array graph can differ from the corresponding object
|
||||
graph in certain scenarios due to the difference of the identity
|
||||
semantics between arrays and objects.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $query->getArrayResult();
|
||||
// same as:
|
||||
$users = $query->getResult(AbstractQuery::HYDRATE_ARRAY);
|
||||
|
||||
``getScalarResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a flat/rectangular result set of scalar values that can contain duplicate data. The
|
||||
pure/mixed distinction does not apply.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $query->getScalarResult();
|
||||
// same as:
|
||||
$users = $query->getResult(AbstractQuery::HYDRATE_SCALAR);
|
||||
|
||||
Fields from classes are prefixed by the DQL alias in the result.
|
||||
A query of the kind `SELECT u.name ...` returns a key `u_name` in the result rows.
|
||||
|
||||
``getSingleScalarResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a single scalar value from the result returned by the database. If the result contains
|
||||
more than a single scalar value, a ``NonUniqueResultException`` is thrown. The pure/mixed distinction does not apply.
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT COUNT(u.id) FROM User u');
|
||||
$numUsers = $query->getSingleScalarResult();
|
||||
// same as:
|
||||
$numUsers = $query->getResult(AbstractQuery::HYDRATE_SINGLE_SCALAR);
|
||||
|
||||
``getSingleColumnResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves an array from a one-dimensional array of scalar values:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT a.id FROM User u');
|
||||
$ids = $query->getSingleColumnResult();
|
||||
// same as:
|
||||
$ids = $query->getResult(AbstractQuery::HYDRATE_SCALAR_COLUMN);
|
||||
|
||||
``getSingleResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
|
||||
is thrown. If the result contains no objects, a ``NoResultException`` is thrown. The pure/mixed distinction does not apply.
|
||||
|
||||
``getOneOrNullResult()``
|
||||
^^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
Retrieves a single object. If the result contains more than one object, a ``NonUniqueResultException``
|
||||
is thrown. If no object is found, ``null`` will be returned.
|
||||
|
||||
Custom Hydration Modes
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can easily add your own custom hydration modes by first
|
||||
creating a class which extends ``AbstractHydrator``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\Hydrators;
|
||||
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
|
||||
class CustomHydrator extends AbstractHydrator
|
||||
{
|
||||
protected function _hydrateAll()
|
||||
{
|
||||
return $this->_stmt->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
|
||||
Next you just need to add the class to the ORM configuration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
|
||||
|
||||
Now the hydrator is ready to be used in your queries:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$results = $query->getResult('CustomHydrator');
|
||||
|
||||
Pure and Mixed Results
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
@@ -1148,165 +1236,6 @@ will return the rows iterating the different top-level entities.
|
||||
[2] => Object (User)
|
||||
[3] => Object (Group)
|
||||
|
||||
|
||||
Hydration Modes
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Each of the Hydration Modes makes assumptions about how the result
|
||||
is returned to user land. You should know about all the details to
|
||||
make best use of the different result formats:
|
||||
|
||||
The constants for the different hydration modes are:
|
||||
|
||||
|
||||
- ``Query::HYDRATE_OBJECT``
|
||||
- ``Query::HYDRATE_ARRAY``
|
||||
- ``Query::HYDRATE_SCALAR``
|
||||
- ``Query::HYDRATE_SINGLE_SCALAR``
|
||||
- ``Query::HYDRATE_SCALAR_COLUMN``
|
||||
|
||||
Object Hydration
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
Object hydration hydrates the result set into the object graph:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$users = $query->getResult(Query::HYDRATE_OBJECT);
|
||||
|
||||
Sometimes the behavior in the object hydrator can be confusing, which is
|
||||
why we are listing as many of the assumptions here for reference:
|
||||
|
||||
- Objects fetched in a FROM clause are returned as a Set, that means every
|
||||
object is only ever included in the resulting array once. This is the case
|
||||
even when using JOIN or GROUP BY in ways that return the same row for an
|
||||
object multiple times. If the hydrator sees the same object multiple times,
|
||||
then it makes sure it is only returned once.
|
||||
|
||||
- If an object is already in memory from a previous query of any kind, then
|
||||
then the previous object is used, even if the database may contain more
|
||||
recent data. Data from the database is discarded. This even happens if the
|
||||
previous object is still an unloaded proxy.
|
||||
|
||||
This list might be incomplete.
|
||||
|
||||
Array Hydration
|
||||
^^^^^^^^^^^^^^^
|
||||
|
||||
You can run the same query with array hydration and the result set
|
||||
is hydrated into an array that represents the object graph:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$users = $query->getResult(Query::HYDRATE_ARRAY);
|
||||
|
||||
You can use the ``getArrayResult()`` shortcut as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$users = $query->getArrayResult();
|
||||
|
||||
Scalar Hydration
|
||||
^^^^^^^^^^^^^^^^
|
||||
|
||||
If you want to return a flat rectangular result set instead of an
|
||||
object graph you can use scalar hydration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$users = $query->getResult(Query::HYDRATE_SCALAR);
|
||||
echo $users[0]['u_id'];
|
||||
|
||||
The following assumptions are made about selected fields using
|
||||
Scalar Hydration:
|
||||
|
||||
|
||||
1. Fields from classes are prefixed by the DQL alias in the result.
|
||||
A query of the kind 'SELECT u.name ..' returns a key 'u_name' in
|
||||
the result rows.
|
||||
|
||||
Single Scalar Hydration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have a query which returns just a single scalar value you can use
|
||||
single scalar hydration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT COUNT(a.id) FROM CmsUser u LEFT JOIN u.articles a WHERE u.username = ?1 GROUP BY u.id');
|
||||
$query->setParameter(1, 'jwage');
|
||||
$numArticles = $query->getResult(Query::HYDRATE_SINGLE_SCALAR);
|
||||
|
||||
You can use the ``getSingleScalarResult()`` shortcut as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$numArticles = $query->getSingleScalarResult();
|
||||
|
||||
Scalar Column Hydration
|
||||
^^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
If you have a query which returns a one-dimensional array of scalar values
|
||||
you can use scalar column hydration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT a.id FROM CmsUser u');
|
||||
$ids = $query->getResult(Query::HYDRATE_SCALAR_COLUMN);
|
||||
|
||||
You can use the ``getSingleColumnResult()`` shortcut as well:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$ids = $query->getSingleColumnResult();
|
||||
|
||||
Custom Hydration Modes
|
||||
^^^^^^^^^^^^^^^^^^^^^^
|
||||
|
||||
You can easily add your own custom hydration modes by first
|
||||
creating a class which extends ``AbstractHydrator``:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
namespace MyProject\Hydrators;
|
||||
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
|
||||
class CustomHydrator extends AbstractHydrator
|
||||
{
|
||||
protected function _hydrateAll()
|
||||
{
|
||||
return $this->_stmt->fetchAllAssociative();
|
||||
}
|
||||
}
|
||||
|
||||
Next you just need to add the class to the ORM configuration:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$em->getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator');
|
||||
|
||||
Now the hydrator is ready to be used in your queries:
|
||||
|
||||
.. code-block:: php
|
||||
|
||||
<?php
|
||||
$query = $em->createQuery('SELECT u FROM CmsUser u');
|
||||
$results = $query->getResult('CustomHydrator');
|
||||
|
||||
Iterating Large Result Sets
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ What is Doctrine?
|
||||
-----------------
|
||||
|
||||
Doctrine ORM is an `object-relational mapper (ORM) <https://en.wikipedia.org/wiki/Object-relational_mapping>`_
|
||||
for PHP 7.1+ that provides transparent persistence for PHP objects. It uses the Data Mapper
|
||||
for PHP that provides transparent persistence for PHP objects. It uses the Data Mapper
|
||||
pattern at the heart, aiming for a complete separation of your domain/business
|
||||
logic from the persistence in a relational database management system.
|
||||
|
||||
|
||||
@@ -115,11 +115,6 @@ parameters:
|
||||
count: 1
|
||||
path: src/EntityManagerInterface.php
|
||||
|
||||
-
|
||||
message: "#^Template type T of method Doctrine\\\\ORM\\\\EntityManagerInterface\\:\\:getClassMetadata\\(\\) is not referenced in a parameter\\.$#"
|
||||
count: 1
|
||||
path: src/EntityManagerInterface.php
|
||||
|
||||
-
|
||||
message: "#^Method Doctrine\\\\ORM\\\\EntityRepository\\:\\:matching\\(\\) should return Doctrine\\\\Common\\\\Collections\\\\AbstractLazyCollection\\<int, T of object\\>&Doctrine\\\\Common\\\\Collections\\\\Selectable\\<int, T of object\\> but returns Doctrine\\\\ORM\\\\LazyCriteriaCollection\\<\\(int\\|string\\), object\\>\\.$#"
|
||||
count: 1
|
||||
|
||||
@@ -20,10 +20,6 @@ parameters:
|
||||
message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
|
||||
path: src/Persisters/Entity/BasicEntityPersister.php
|
||||
|
||||
-
|
||||
message: '~^Unreachable statement \- code above always terminates\.$~'
|
||||
path: src/Mapping/AssociationMapping.php
|
||||
|
||||
- '~^Class Doctrine\\DBAL\\Platforms\\SQLitePlatform not found\.$~'
|
||||
|
||||
# To be removed in 4.0
|
||||
|
||||
@@ -20,10 +20,6 @@ parameters:
|
||||
message: '~^Method Doctrine\\ORM\\Persisters\\Entity\\BasicEntityPersister\:\:getArrayBindingType\(\) never returns .* so it can be removed from the return type\.$~'
|
||||
path: src/Persisters/Entity/BasicEntityPersister.php
|
||||
|
||||
-
|
||||
message: '~^Unreachable statement \- code above always terminates\.$~'
|
||||
path: src/Mapping/AssociationMapping.php
|
||||
|
||||
# Compatibility with DBAL 3
|
||||
# See https://github.com/doctrine/dbal/pull/3480
|
||||
-
|
||||
|
||||
+4
-13
@@ -154,14 +154,6 @@
|
||||
<code><![CDATA[$className]]></code>
|
||||
</ArgumentTypeCoercion>
|
||||
</file>
|
||||
<file src="src/Decorator/EntityManagerDecorator.php">
|
||||
<InvalidReturnStatement>
|
||||
<code><![CDATA[$this->wrapped->getClassMetadata($className)]]></code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType>
|
||||
<code><![CDATA[ClassMetadata]]></code>
|
||||
</InvalidReturnType>
|
||||
</file>
|
||||
<file src="src/EntityManager.php">
|
||||
<ArgumentTypeCoercion>
|
||||
<code><![CDATA[$className]]></code>
|
||||
@@ -174,11 +166,9 @@
|
||||
<code><![CDATA[$persister->load($sortedId, null, null, [], $lockMode)]]></code>
|
||||
<code><![CDATA[$persister->loadById($sortedId)]]></code>
|
||||
<code><![CDATA[$this->metadataFactory]]></code>
|
||||
<code><![CDATA[$this->metadataFactory->getMetadataFor($className)]]></code>
|
||||
</InvalidReturnStatement>
|
||||
<InvalidReturnType>
|
||||
<code><![CDATA[ClassMetadataFactory]]></code>
|
||||
<code><![CDATA[Mapping\ClassMetadata]]></code>
|
||||
</InvalidReturnType>
|
||||
<PossiblyNullArgument>
|
||||
<code><![CDATA[$config->getProxyDir()]]></code>
|
||||
@@ -251,9 +241,6 @@
|
||||
</UnsupportedReferenceUsage>
|
||||
</file>
|
||||
<file src="src/Internal/Hydration/ObjectHydrator.php">
|
||||
<InvalidArgument>
|
||||
<code><![CDATA[$element]]></code>
|
||||
</InvalidArgument>
|
||||
<PossiblyFalseArgument>
|
||||
<code><![CDATA[$index]]></code>
|
||||
</PossiblyFalseArgument>
|
||||
@@ -298,6 +285,10 @@
|
||||
<code><![CDATA[$this->columnNames]]></code>
|
||||
</DeprecatedProperty>
|
||||
<InvalidArgument>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
<code><![CDATA[$mapping]]></code>
|
||||
|
||||
@@ -234,7 +234,7 @@ interface EntityManagerInterface extends ObjectManager
|
||||
*
|
||||
* @psalm-param string|class-string<T> $className
|
||||
*
|
||||
* @psalm-return Mapping\ClassMetadata<T>
|
||||
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
|
||||
*
|
||||
* @psalm-template T of object
|
||||
*/
|
||||
|
||||
@@ -104,29 +104,31 @@ abstract class AbstractHydrator
|
||||
|
||||
$this->prepare();
|
||||
|
||||
while (true) {
|
||||
$row = $this->statement()->fetchAssociative();
|
||||
try {
|
||||
while (true) {
|
||||
$row = $this->statement()->fetchAssociative();
|
||||
|
||||
if ($row === false) {
|
||||
$this->cleanup();
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
$this->hydrateRowData($row, $result);
|
||||
|
||||
$this->cleanupAfterRowIteration();
|
||||
if (count($result) === 1) {
|
||||
if (count($resultSetMapping->indexByMap) === 0) {
|
||||
yield end($result);
|
||||
} else {
|
||||
yield from $result;
|
||||
if ($row === false) {
|
||||
break;
|
||||
}
|
||||
|
||||
$result = [];
|
||||
|
||||
$this->hydrateRowData($row, $result);
|
||||
|
||||
$this->cleanupAfterRowIteration();
|
||||
if (count($result) === 1) {
|
||||
if (count($resultSetMapping->indexByMap) === 0) {
|
||||
yield end($result);
|
||||
} else {
|
||||
yield from $result;
|
||||
}
|
||||
} else {
|
||||
yield $result;
|
||||
}
|
||||
} else {
|
||||
yield $result;
|
||||
}
|
||||
} finally {
|
||||
$this->cleanup();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,11 +64,11 @@ class LazyCriteriaCollection extends AbstractLazyCollection implements Selectabl
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* Do an optimized search of an element
|
||||
*
|
||||
* @template TMaybeContained
|
||||
* @param mixed $element The element to search for.
|
||||
*
|
||||
* @return bool TRUE if the collection contains $element, FALSE otherwise.
|
||||
*/
|
||||
public function contains(mixed $element): bool
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ abstract class AssociationMapping implements ArrayAccess
|
||||
/**
|
||||
* The fetching strategy to use for the association, usually defaults to FETCH_LAZY.
|
||||
*
|
||||
* @var ClassMetadata::FETCH_*
|
||||
* @var ClassMetadata::FETCH_*|null
|
||||
*/
|
||||
public int|null $fetch = null;
|
||||
|
||||
@@ -96,13 +96,26 @@ abstract class AssociationMapping implements ArrayAccess
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed[] $mappingArray
|
||||
* @psalm-param array{
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): static
|
||||
{
|
||||
|
||||
@@ -1152,7 +1152,7 @@ class ClassMetadata implements PersistenceClassMetadata, Stringable
|
||||
* fieldName?: string,
|
||||
* columnName?: string,
|
||||
* id?: bool,
|
||||
* generated?: int,
|
||||
* generated?: self::GENERATED_*,
|
||||
* enumType?: class-string,
|
||||
* } $mapping The field mapping to validate & complete.
|
||||
*
|
||||
|
||||
@@ -49,10 +49,12 @@ final class EmbeddedClassMapping implements ArrayAccess
|
||||
|
||||
/**
|
||||
* @psalm-param array{
|
||||
* class: class-string,
|
||||
* columnPrefix?: false|string|null,
|
||||
* declaredField?: string|null,
|
||||
* originalField?: string|null
|
||||
* class: class-string,
|
||||
* columnPrefix?: false|string|null,
|
||||
* declaredField?: string|null,
|
||||
* originalField?: string|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): self
|
||||
|
||||
@@ -26,7 +26,7 @@ final class FieldMapping implements ArrayAccess
|
||||
public bool|null $notInsertable = null;
|
||||
public bool|null $notUpdatable = null;
|
||||
public string|null $columnDefinition = null;
|
||||
/** @psalm-var ClassMetadata::GENERATED_* */
|
||||
/** @psalm-var ClassMetadata::GENERATED_*|null */
|
||||
public int|null $generated = null;
|
||||
/** @var class-string<BackedEnum>|null */
|
||||
public string|null $enumType = null;
|
||||
@@ -83,7 +83,34 @@ final class FieldMapping implements ArrayAccess
|
||||
) {
|
||||
}
|
||||
|
||||
/** @param array{type: string, fieldName: string, columnName: string} $mappingArray */
|
||||
/**
|
||||
* @param array<string, mixed> $mappingArray
|
||||
* @psalm-param array{
|
||||
* type: string,
|
||||
* fieldName: string,
|
||||
* columnName: string,
|
||||
* length?: int|null,
|
||||
* id?: bool|null,
|
||||
* nullable?: bool|null,
|
||||
* notInsertable?: bool|null,
|
||||
* notUpdatable?: bool|null,
|
||||
* columnDefinition?: string|null,
|
||||
* generated?: ClassMetadata::GENERATED_*|null,
|
||||
* enumType?: string|null,
|
||||
* precision?: int|null,
|
||||
* scale?: int|null,
|
||||
* unique?: bool|null,
|
||||
* inherited?: string|null,
|
||||
* originalClass?: string|null,
|
||||
* originalField?: string|null,
|
||||
* quoted?: bool|null,
|
||||
* declared?: string|null,
|
||||
* declaredField?: string|null,
|
||||
* options?: array<string, mixed>|null,
|
||||
* version?: bool|null,
|
||||
* default?: string|int|null,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): self
|
||||
{
|
||||
$mapping = new self(
|
||||
|
||||
@@ -31,7 +31,17 @@ final class JoinColumnMapping implements ArrayAccess
|
||||
|
||||
/**
|
||||
* @param array<string, mixed> $mappingArray
|
||||
* @psalm-param array{name: string, referencedColumnName: string, ...} $mappingArray
|
||||
* @psalm-param array{
|
||||
* name: string,
|
||||
* referencedColumnName: string,
|
||||
* unique?: bool|null,
|
||||
* quoted?: bool|null,
|
||||
* fieldName?: string|null,
|
||||
* onDelete?: string|null,
|
||||
* columnDefinition?: string|null,
|
||||
* nullable?: bool|null,
|
||||
* options?: array<string, mixed>|null,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): self
|
||||
{
|
||||
|
||||
@@ -35,10 +35,10 @@ final class JoinTableMapping implements ArrayAccess
|
||||
* @param mixed[] $mappingArray
|
||||
* @psalm-param array{
|
||||
* name: string,
|
||||
* quoted?: bool,
|
||||
* quoted?: bool|null,
|
||||
* joinColumns?: mixed[],
|
||||
* inverseJoinColumns?: mixed[],
|
||||
* schema?: string,
|
||||
* schema?: string|null,
|
||||
* options?: array<string, mixed>
|
||||
* } $mappingArray
|
||||
*/
|
||||
|
||||
@@ -41,9 +41,21 @@ final class ManyToManyOwningSideMapping extends ToManyOwningSideMapping implemen
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndNamingStrategy(array $mappingArray, NamingStrategy $namingStrategy): self
|
||||
{
|
||||
|
||||
@@ -12,9 +12,21 @@ final class OneToManyAssociationMapping extends ToManyInverseSideMapping
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): static
|
||||
{
|
||||
@@ -33,9 +45,21 @@ final class OneToManyAssociationMapping extends ToManyInverseSideMapping
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndName(array $mappingArray, string $name): static
|
||||
{
|
||||
|
||||
@@ -13,8 +13,21 @@ abstract class ToOneInverseSideMapping extends InverseSideMapping
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool,
|
||||
* } $mappingArray
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndName(
|
||||
array $mappingArray,
|
||||
|
||||
@@ -31,8 +31,22 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool,
|
||||
* joinColumns?: mixed[]|null,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArray(array $mappingArray): static
|
||||
{
|
||||
@@ -64,8 +78,22 @@ abstract class ToOneOwningSideMapping extends OwningSideMapping implements ToOne
|
||||
* fieldName: string,
|
||||
* sourceEntity: class-string,
|
||||
* targetEntity: class-string,
|
||||
* cascade?: list<'persist'|'remove'|'detach'|'refresh'|'all'>,
|
||||
* fetch?: ClassMetadata::FETCH_*|null,
|
||||
* inherited?: class-string|null,
|
||||
* declared?: class-string|null,
|
||||
* cache?: array<mixed>|null,
|
||||
* id?: bool|null,
|
||||
* isOnDeleteCascade?: bool|null,
|
||||
* originalClass?: class-string|null,
|
||||
* originalField?: string|null,
|
||||
* orphanRemoval?: bool,
|
||||
* unique?: bool|null,
|
||||
* joinTable?: mixed[]|null,
|
||||
* type?: int,
|
||||
* isOwningSide: bool,
|
||||
* joinColumns?: mixed[]|null,
|
||||
* isOwningSide: bool, ...} $mappingArray
|
||||
* } $mappingArray
|
||||
*/
|
||||
public static function fromMappingArrayAndName(
|
||||
array $mappingArray,
|
||||
|
||||
@@ -350,11 +350,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
|
||||
return parent::containsKey($key);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*
|
||||
* @template TMaybeContained
|
||||
*/
|
||||
public function contains(mixed $element): bool
|
||||
{
|
||||
if (! $this->initialized && $this->getMapping()->fetch === ClassMetadata::FETCH_EXTRA_LAZY) {
|
||||
|
||||
@@ -52,18 +52,18 @@ class SchemaValidator
|
||||
* It maps built-in Doctrine types to PHP types
|
||||
*/
|
||||
private const BUILTIN_TYPES_MAP = [
|
||||
AsciiStringType::class => 'string',
|
||||
BigIntType::class => 'string',
|
||||
BooleanType::class => 'bool',
|
||||
DecimalType::class => 'string',
|
||||
FloatType::class => 'float',
|
||||
GuidType::class => 'string',
|
||||
IntegerType::class => 'int',
|
||||
JsonType::class => 'array',
|
||||
SimpleArrayType::class => 'array',
|
||||
SmallIntType::class => 'int',
|
||||
StringType::class => 'string',
|
||||
TextType::class => 'string',
|
||||
AsciiStringType::class => ['string'],
|
||||
BigIntType::class => ['int', 'string'],
|
||||
BooleanType::class => ['bool'],
|
||||
DecimalType::class => ['string'],
|
||||
FloatType::class => ['float'],
|
||||
GuidType::class => ['string'],
|
||||
IntegerType::class => ['int'],
|
||||
JsonType::class => ['array'],
|
||||
SimpleArrayType::class => ['array'],
|
||||
SmallIntType::class => ['int'],
|
||||
StringType::class => ['string'],
|
||||
TextType::class => ['string'],
|
||||
];
|
||||
|
||||
public function __construct(
|
||||
@@ -353,21 +353,21 @@ class SchemaValidator
|
||||
$propertyType = $propertyType->getName();
|
||||
|
||||
// If the property type is the same as the metadata field type, we are ok
|
||||
if ($propertyType === $metadataFieldType) {
|
||||
if (in_array($propertyType, $metadataFieldType, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (is_a($propertyType, BackedEnum::class, true)) {
|
||||
$backingType = (string) (new ReflectionEnum($propertyType))->getBackingType();
|
||||
|
||||
if ($metadataFieldType !== $backingType) {
|
||||
if (! in_array($backingType, $metadataFieldType, true)) {
|
||||
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,
|
||||
implode('|', $metadataFieldType),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -392,7 +392,7 @@ class SchemaValidator
|
||||
) {
|
||||
$backingType = (string) (new ReflectionEnum($fieldMapping->enumType))->getBackingType();
|
||||
|
||||
if ($metadataFieldType === $backingType) {
|
||||
if (in_array($backingType, $metadataFieldType, true)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -402,7 +402,7 @@ class SchemaValidator
|
||||
$fieldName,
|
||||
$fieldMapping->enumType,
|
||||
$backingType,
|
||||
$metadataFieldType,
|
||||
implode('|', $metadataFieldType),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -418,7 +418,7 @@ class SchemaValidator
|
||||
$class->name,
|
||||
$fieldName,
|
||||
$propertyType,
|
||||
$metadataFieldType,
|
||||
implode('|', $metadataFieldType),
|
||||
$fieldMapping->type,
|
||||
);
|
||||
},
|
||||
@@ -431,8 +431,10 @@ class SchemaValidator
|
||||
/**
|
||||
* The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own
|
||||
* customization around field types.
|
||||
*
|
||||
* @return list<string>|null
|
||||
*/
|
||||
private function findBuiltInType(Type $type): string|null
|
||||
private function findBuiltInType(Type $type): array|null
|
||||
{
|
||||
$typeName = $type::class;
|
||||
|
||||
|
||||
+3
-3
@@ -1143,6 +1143,8 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$eventsToDispatch = [];
|
||||
|
||||
foreach ($entities as $entity) {
|
||||
$this->removeFromIdentityMap($entity);
|
||||
|
||||
$oid = spl_object_id($entity);
|
||||
$class = $this->em->getClassMetadata($entity::class);
|
||||
$persister = $this->getEntityPersister($class->name);
|
||||
@@ -1484,8 +1486,6 @@ class UnitOfWork implements PropertyChangedListener
|
||||
return;
|
||||
}
|
||||
|
||||
$this->removeFromIdentityMap($entity);
|
||||
|
||||
unset($this->entityUpdates[$oid]);
|
||||
|
||||
if (! isset($this->entityDeletions[$oid])) {
|
||||
@@ -2653,7 +2653,7 @@ class UnitOfWork implements PropertyChangedListener
|
||||
$entities[] = $collection->getOwner();
|
||||
}
|
||||
|
||||
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities]);
|
||||
$found = $this->getEntityPersister($targetEntity)->loadAll([$mappedBy => $entities], $mapping['orderBy'] ?? null);
|
||||
|
||||
$targetClass = $this->em->getClassMetadata($targetEntity);
|
||||
$targetProperty = $targetClass->getReflectionProperty($mappedBy);
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\Models\BigIntegers;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
|
||||
#[ORM\Entity]
|
||||
class BigIntegers
|
||||
{
|
||||
#[ORM\Column]
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
public int|null $id = null;
|
||||
|
||||
#[ORM\Column(type: Types::BIGINT)]
|
||||
public int $one = 1;
|
||||
|
||||
#[ORM\Column(type: Types::BIGINT)]
|
||||
public string $two = '2';
|
||||
|
||||
#[ORM\Column(type: Types::BIGINT)]
|
||||
public float $three = 3.0;
|
||||
}
|
||||
@@ -252,7 +252,6 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
|
||||
self::assertEquals(1_600_000, $result[3]['op']);
|
||||
}
|
||||
|
||||
#[Group('test')]
|
||||
public function testOperatorDiv(): void
|
||||
{
|
||||
$result = $this->_em->createQuery('SELECT m, (m.salary/0.5) AS op FROM Doctrine\Tests\Models\Company\CompanyManager m ORDER BY m.salary ASC')
|
||||
|
||||
@@ -0,0 +1,112 @@
|
||||
<?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\ORM\PersistentCollection;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH11163Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->setUpEntitySchema([
|
||||
GH11163Bucket::class,
|
||||
GH11163BucketItem::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function tearDown(): void
|
||||
{
|
||||
parent::tearDown();
|
||||
|
||||
$conn = static::$sharedConn;
|
||||
$conn->executeStatement('DELETE FROM GH11163BucketItem');
|
||||
$conn->executeStatement('DELETE FROM GH11163Bucket');
|
||||
}
|
||||
|
||||
public function testFetchEagerModeWithOrderBy(): void
|
||||
{
|
||||
// Load entities into database
|
||||
$this->_em->persist($bucket = new GH11163Bucket(11163));
|
||||
$this->_em->persist(new GH11163BucketItem(1, $bucket, 2));
|
||||
$this->_em->persist(new GH11163BucketItem(2, $bucket, 3));
|
||||
$this->_em->persist(new GH11163BucketItem(3, $bucket, 1));
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
// Fetch entity from database
|
||||
$dql = 'SELECT bucket FROM ' . GH11163Bucket::class . ' bucket WHERE bucket.id = :id';
|
||||
$bucket = $this->_em->createQuery($dql)
|
||||
->setParameter('id', 11163)
|
||||
->getSingleResult();
|
||||
|
||||
// Assert associated entity is loaded eagerly
|
||||
static::assertInstanceOf(GH11163Bucket::class, $bucket);
|
||||
static::assertInstanceOf(PersistentCollection::class, $bucket->items);
|
||||
static::assertTrue($bucket->items->isInitialized());
|
||||
|
||||
static::assertCount(3, $bucket->items);
|
||||
|
||||
// Assert order of entities
|
||||
static::assertSame(1, $bucket->items[0]->position);
|
||||
static::assertSame(3, $bucket->items[0]->id);
|
||||
|
||||
static::assertSame(2, $bucket->items[1]->position);
|
||||
static::assertSame(1, $bucket->items[1]->id);
|
||||
|
||||
static::assertSame(3, $bucket->items[2]->position);
|
||||
static::assertSame(2, $bucket->items[2]->id);
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11163Bucket
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
private int $id;
|
||||
|
||||
/** @var Collection<int, GH11163BucketItem> */
|
||||
#[ORM\OneToMany(
|
||||
targetEntity: GH11163BucketItem::class,
|
||||
mappedBy: 'bucket',
|
||||
fetch: 'EAGER',
|
||||
)]
|
||||
#[ORM\OrderBy(['position' => 'ASC'])]
|
||||
public Collection $items;
|
||||
|
||||
public function __construct(int $id)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->items = new ArrayCollection();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH11163BucketItem
|
||||
{
|
||||
#[ORM\ManyToOne(targetEntity: GH11163Bucket::class, inversedBy: 'items')]
|
||||
#[ORM\JoinColumn(nullable: false)]
|
||||
private GH11163Bucket $bucket;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\Column(type: 'integer')]
|
||||
public int $id;
|
||||
|
||||
#[ORM\Column(type: 'integer')]
|
||||
public int $position;
|
||||
|
||||
public function __construct(int $id, GH11163Bucket $bucket, int $position)
|
||||
{
|
||||
$this->id = $id;
|
||||
$this->bucket = $bucket;
|
||||
$this->position = $position;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace Doctrine\Tests\ORM\Functional\Ticket;
|
||||
|
||||
use Doctrine\DBAL\Types\Types;
|
||||
use Doctrine\ORM\Mapping as ORM;
|
||||
use Doctrine\ORM\UnitOfWork;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
|
||||
class GH6123Test extends OrmFunctionalTestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
||||
$this->createSchemaForModels(
|
||||
GH6123Entity::class,
|
||||
);
|
||||
}
|
||||
|
||||
public function testLoadingRemovedEntityFromDatabaseDoesNotCreateNewManagedEntityInstance(): void
|
||||
{
|
||||
$entity = new GH6123Entity();
|
||||
$this->_em->persist($entity);
|
||||
$this->_em->flush();
|
||||
|
||||
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity));
|
||||
self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
|
||||
|
||||
$this->_em->remove($entity);
|
||||
|
||||
$freshEntity = $this->loadEntityFromDatabase($entity->id);
|
||||
self::assertSame($entity, $freshEntity);
|
||||
|
||||
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($freshEntity));
|
||||
self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($freshEntity));
|
||||
}
|
||||
|
||||
public function testRemovedEntityCanBePersistedAgain(): void
|
||||
{
|
||||
$entity = new GH6123Entity();
|
||||
$this->_em->persist($entity);
|
||||
$this->_em->flush();
|
||||
|
||||
$this->_em->remove($entity);
|
||||
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_em->getUnitOfWork()->getEntityState($entity));
|
||||
self::assertTrue($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
|
||||
|
||||
$this->loadEntityFromDatabase($entity->id);
|
||||
|
||||
$this->_em->persist($entity);
|
||||
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_em->getUnitOfWork()->getEntityState($entity));
|
||||
self::assertFalse($this->_em->getUnitOfWork()->isScheduledForDelete($entity));
|
||||
|
||||
$this->_em->flush();
|
||||
}
|
||||
|
||||
private function loadEntityFromDatabase(int $id): GH6123Entity|null
|
||||
{
|
||||
return $this->_em->createQueryBuilder()
|
||||
->select('e')
|
||||
->from(GH6123Entity::class, 'e')
|
||||
->where('e.id = :id')
|
||||
->setParameter('id', $id)
|
||||
->getQuery()
|
||||
->getOneOrNullResult();
|
||||
}
|
||||
}
|
||||
|
||||
#[ORM\Entity]
|
||||
class GH6123Entity
|
||||
{
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column(type: Types::INTEGER)]
|
||||
public int $id;
|
||||
}
|
||||
@@ -13,6 +13,7 @@ use Doctrine\ORM\Events;
|
||||
use Doctrine\ORM\Exception\ORMException;
|
||||
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
|
||||
use Doctrine\ORM\Query\ResultSetMapping;
|
||||
use Doctrine\Tests\Models\Hydration\SimpleEntity;
|
||||
use Doctrine\Tests\OrmFunctionalTestCase;
|
||||
use PHPUnit\Framework\Attributes\CoversClass;
|
||||
use PHPUnit\Framework\Attributes\Group;
|
||||
@@ -149,4 +150,33 @@ class AbstractHydratorTest extends OrmFunctionalTestCase
|
||||
$this->expectException(ORMException::class);
|
||||
$this->hydrator->hydrateAll($this->mockResult, $this->mockResultMapping);
|
||||
}
|
||||
|
||||
public function testToIterableIfYieldAndBreakBeforeFinishAlwaysCleansUp(): void
|
||||
{
|
||||
$this->setUpEntitySchema([SimpleEntity::class]);
|
||||
|
||||
$entity1 = new SimpleEntity();
|
||||
$this->_em->persist($entity1);
|
||||
$entity2 = new SimpleEntity();
|
||||
$this->_em->persist($entity2);
|
||||
|
||||
$this->_em->flush();
|
||||
$this->_em->clear();
|
||||
|
||||
$evm = $this->_em->getEventManager();
|
||||
|
||||
$q = $this->_em->createQuery('SELECT e.id FROM ' . SimpleEntity::class . ' e');
|
||||
|
||||
// select two entities, but do no iterate
|
||||
$q->toIterable();
|
||||
self::assertCount(0, $evm->getListeners(Events::onClear));
|
||||
|
||||
// select two entities, but abort after first record
|
||||
foreach ($q->toIterable() as $result) {
|
||||
self::assertCount(1, $evm->getListeners(Events::onClear));
|
||||
break;
|
||||
}
|
||||
|
||||
self::assertCount(0, $evm->getListeners(Events::onClear));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@ use Doctrine\ORM\Mapping\OneToOne;
|
||||
use Doctrine\ORM\Mapping\OrderBy;
|
||||
use Doctrine\ORM\Mapping\Table;
|
||||
use Doctrine\ORM\Tools\SchemaValidator;
|
||||
use Doctrine\Tests\Models\BigIntegers\BigIntegers;
|
||||
use Doctrine\Tests\Models\ECommerce\ECommerceCart;
|
||||
use Doctrine\Tests\OrmTestCase;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
@@ -228,6 +229,16 @@ class SchemaValidatorTest extends OrmTestCase
|
||||
$ce,
|
||||
);
|
||||
}
|
||||
|
||||
public function testBigIntProperty(): void
|
||||
{
|
||||
$class = $this->em->getClassMetadata(BigIntegers::class);
|
||||
|
||||
self::assertSame(
|
||||
['The field \'Doctrine\Tests\Models\BigIntegers\BigIntegers#three\' has the property type \'float\' that differs from the metadata field type \'int|string\' returned by the \'bigint\' DBAL type.'],
|
||||
$this->validator->validateClass($class),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[MappedSuperclass]
|
||||
|
||||
@@ -288,12 +288,18 @@ class UnitOfWorkTest extends OrmTestCase
|
||||
$entity->id = 123;
|
||||
|
||||
$this->_unitOfWork->registerManaged($entity, ['id' => 123], []);
|
||||
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($entity));
|
||||
self::assertFalse($this->_unitOfWork->isScheduledForDelete($entity));
|
||||
self::assertTrue($this->_unitOfWork->isInIdentityMap($entity));
|
||||
|
||||
$this->_unitOfWork->remove($entity);
|
||||
self::assertFalse($this->_unitOfWork->isInIdentityMap($entity));
|
||||
self::assertSame(UnitOfWork::STATE_REMOVED, $this->_unitOfWork->getEntityState($entity));
|
||||
self::assertTrue($this->_unitOfWork->isScheduledForDelete($entity));
|
||||
self::assertTrue($this->_unitOfWork->isInIdentityMap($entity));
|
||||
|
||||
$this->_unitOfWork->persist($entity);
|
||||
self::assertSame(UnitOfWork::STATE_MANAGED, $this->_unitOfWork->getEntityState($entity));
|
||||
self::assertFalse($this->_unitOfWork->isScheduledForDelete($entity));
|
||||
self::assertTrue($this->_unitOfWork->isInIdentityMap($entity));
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user