Compare commits

...

30 Commits

Author SHA1 Message Date
Grégoire Paris
94986af284 Merge pull request #11430 from W0rma/fix-deprecation-layer-orm-exception
Fix deprecation layer of Doctrine\ORM\ORMException
2024-04-30 08:49:54 +02:00
W0rma
ad5c8e4bdc Make test compatible with PHP 7.1 2024-04-30 08:35:06 +02:00
W0rma
c363f55ad1 Fix deprecation layer 2024-04-29 14:48:36 +02:00
Grégoire Paris
c973a62272 Merge pull request #11429 from SenseException/unused-test-group
Remove unused test group
2024-04-27 11:42:05 +02:00
Grégoire Paris
8d3446015a Merge pull request #11428 from xificurk/keep-removed-entity-in-identity-map
Prevent creation of new MANAGED entity instance by reloading REMOVED entity from database
2024-04-27 11:40:56 +02:00
Claudio Zizza
4e335f4044 Remove unused test group 2024-04-27 10:46:19 +02:00
Petr Morávek
bb36d49b38 Keep entities in identity map until the scheduled deletions are executed.
If the entity gets reloaded from database before the deletions are
executed UnitOfWork needs to be able to return the original instance in
REMOVED state.
2024-04-26 21:54:02 +02:00
Grégoire Paris
2b81a8e260 Merge pull request #11426 from nasimic/patch-1
Update association-mapping.rst
2024-04-26 21:27:07 +02:00
Nasimi Mammadov
7d3b3f28e9 Update association-mapping.rst
Changed capitalized column names to lowercase for consistency. Other occurances of column names mentioned as lowercase several times at this same page.
2024-04-26 21:24:28 +02:00
Simon Podlipsky
cbec236e8b fix: always cleanup in AbstractHydrator::toIterable() (#11101)
Previously it didn't cleanup anything as long as the iteration hasn't reached the final row.

Co-authored-by: Oleg Andreyev <oleg.andreyev@lampa.lv>
2024-04-25 10:32:40 +02:00
Grégoire Paris
306963fe79 Merge pull request #11422 from tomasz-ryba/bugfix/fetch-eager-order-by
Bugfix: respect orderBy for fetch EAGER mode
2024-04-25 00:09:43 +02:00
Tomasz Ryba
fb4578406f Respect orderBy for EAGER fetch mode
EAGER fetch mode ignores orderBy as of changes introduced with #8391

Fixes #11163
Fixes #11381
2024-04-24 22:44:16 +02:00
Grégoire Paris
bdc41e2b5e Merge pull request #11420 from tyteen4a03/patch-1
fix(docs): typo
2024-04-22 15:40:39 +02:00
Timothy Choi
90376a6431 fix(docs): typo 2024-04-22 15:30:56 +02:00
Alexander M. Turek
b274893486 Fix BIGINT validation (#11414) 2024-04-15 15:11:10 +02:00
Vincent Langlet
8709fb38b0 Fix templated phpdoc return type (#11407)
* Improve getClassMetadata phpdoc

* Update baseline
2024-04-01 12:44:58 +02:00
Grégoire Paris
e9e60f2fbc Merge pull request #11403 from ThomasLandauer/patch-10
[Documentation] Merging "Query Result Formats" with "Hydration Modes"
2024-03-28 07:51:53 +01:00
Thomas Landauer
5f3c1dbab8 [Documentation] Merging "Query Result Formats" with "Hydration Modes"
Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/dql-doctrine-query-language.html#query-result-formats

As announced in https://github.com/doctrine/orm/pull/11372#issue-2190613801, I merged the (mostly) identical sections.

* I changed the `const`s from `Query` to `AbstractQuery`
* I deleted this - mainly cause I didn't find a nice place for it:
    > In parentheses are the constants of the ``Query`` class which you can use with the
general-purpose method ``Query::execute(array $params = [], $hydrationMode = Query::HYDRATE_OBJECT)``.
2024-03-27 13:52:50 +01:00
Grégoire Paris
6090141e0b Merge pull request #11389 from tantegerda1/2.19.x
Fix psalm errors: remove override of template type
2024-03-23 11:13:39 +01:00
Grégoire Paris
e4a6c041b5 Merge pull request #11372 from ThomasLandauer/patch-12
[Documentation] Query Result Formats
2024-03-23 10:07:42 +01:00
Ludwig Rafelsberger
c54c557e02 Fix psalm errors: remove override of template type
See https://github.com/doctrine/collections/issues/368 for the same
issue in doctrine/collections which has been fixed there.

The issue happens when using ->contains(). Running psalm emits

  > InvalidArgument - Argument 1 of Doctrine\ORM\PersistentCollection::contains
  > expects
  > TMaybeContained:fn-doctrine\common\collections\readablecollection::contains
  > as mixed, but … provided.

Solution: we should either not define @template TMaybeContained or
re-define the complete psalm docblock from ReadableCollection.

Repairing the docblock necessitates an update to the psalm baseline:
one "known issue" is no longer an issue and thus removed.
2024-03-22 11:05:00 +01:00
Thomas Landauer
46d0865339 Update dql-doctrine-query-language.rst 2024-03-21 17:55:39 +01:00
Alexander M. Turek
1a5a4c674a Set column length explicitly (#11393) 2024-03-21 12:01:42 +01:00
Alexander M. Turek
95795c87a8 Add missing import 2024-03-21 10:38:59 +01:00
Alexander M. Turek
db6e702088 Remove unused variable (#11391) 2024-03-21 10:32:55 +01:00
Thomas Landauer
5ccbc201bf [Documentation] Removing "Doctrine Mapping Types" ... (#11384)
... in favor of https://www.doctrine-project.org/projects/doctrine-dbal/en/3.8/reference/types.html#reference

Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/basic-mapping.html#doctrine-mapping-types

As announced in https://github.com/doctrine/dbal/pull/6336#issuecomment-2003720361 , the goal is to remove this duplicated type information from ORM and replace it with a link to DBAL.

In https://github.com/doctrine/dbal/pull/6341 , I'm adding any detail which I'm deleting here to the DBAL.
2024-03-20 23:34:10 +01:00
Benjamin Eberlei
d15624f72f [GH-11185] Bugfix: do not use collection batch loading for indexBy assocations. (#11380) 2024-03-20 15:45:47 +01:00
Benjamin Eberlei
9d1a4973ae Improve lazy ghost performance by avoiding self-referencing closure. (#11376)
* Improve lazy ghost performance by avoiding self-referencing closure.

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>

* update baselien

---------

Co-authored-by: Nicolas Grekas <nicolas.grekas@gmail.com>
2024-03-19 09:19:25 +01:00
Thomas Landauer
4b4b9b7b6f Adding NonUniqueResultException 2024-03-17 12:25:05 +01:00
Thomas Landauer
ae842259f5 [Documentation] Query Result Formats
Page: https://www.doctrine-project.org/projects/doctrine-orm/en/2.19/reference/dql-doctrine-query-language.html#query-result-formats

Follow-up of https://github.com/doctrine/orm/pull/11359

The table I suggested is probably not working, since the text for each method is too long. And what I really wanted is to make it more *scanable*. So I tried boldfacing - if this doesn't work, I'll try something else.

Questions:

1. This section here is basically the same as https://www.doctrine-project.org/projects/doctrine-orm/en/2.10/reference/dql-doctrine-query-language.html#hydration-modes ! So I'll try to merge them (in another PR), OK? I think the list is a better format (more scanable) - since those methods all work the same, there's no need for a full-blown code sample for each, IMO.

2. `getSingleColumnResult()` is missing.
2024-03-17 12:24:10 +01:00
26 changed files with 721 additions and 384 deletions

View File

@@ -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.

View File

@@ -1300,8 +1300,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;
// ...
@@ -1333,10 +1333,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>

View File

@@ -300,50 +300,12 @@ and a custom ``Doctrine\ORM\Mapping\TypedFieldMapper`` implementation.
Doctrine Mapping Types
----------------------
The ``type`` option used in the ``@Column`` accepts any of the existing
Doctrine types or even your own custom types. A Doctrine type defines
The ``type`` option used in the ``@Column`` accepts any of the
`existing Doctrine DBAL types <https://docs.doctrine-project.org/projects/doctrine-dbal/en/stable/reference/types.html#reference>`_
or :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`. A Doctrine type defines
the conversion between PHP and SQL types, independent from the database vendor
you are using. All Mapping Types that ship with Doctrine are fully portable
between the supported database systems.
As an example, the Doctrine Mapping Type ``string`` defines the
mapping from a PHP string to a SQL VARCHAR (or VARCHAR2 etc.
depending on the RDBMS brand). Here is a quick overview of the
built-in mapping types:
- ``string``: Type that maps a SQL VARCHAR to a PHP string.
- ``integer``: Type that maps a SQL INT to a PHP integer.
- ``smallint``: Type that maps a database SMALLINT to a PHP
integer.
- ``bigint``: Type that maps a database BIGINT to a PHP string.
- ``boolean``: Type that maps a SQL boolean or equivalent (TINYINT) to a PHP boolean.
- ``decimal``: Type that maps a SQL DECIMAL to a PHP string.
- ``date``: Type that maps a SQL DATETIME to a PHP DateTime
object.
- ``time``: Type that maps a SQL TIME to a PHP DateTime object.
- ``datetime``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object.
- ``datetimetz``: Type that maps a SQL DATETIME/TIMESTAMP to a PHP
DateTime object with timezone.
- ``text``: Type that maps a SQL CLOB to a PHP string.
- ``object``: Type that maps a SQL CLOB to a PHP object using
``serialize()`` and ``unserialize()``
- ``array``: Type that maps a SQL CLOB to a PHP array using
``serialize()`` and ``unserialize()``
- ``simple_array``: Type that maps a SQL CLOB to a PHP array using
``implode()`` and ``explode()``, with a comma as delimiter. *IMPORTANT*
Only use this type if you are sure that your values cannot contain a ",".
- ``json_array``: Type that maps a SQL CLOB to a PHP array using
``json_encode()`` and ``json_decode()``
- ``float``: Type that maps a SQL Float (Double Precision) to a
PHP double. *IMPORTANT*: Works only with locale settings that use
decimal points as separator.
- ``guid``: Type that maps a database GUID/UUID to a PHP string. Defaults to
varchar but uses a specific type if the platform supports it.
- ``blob``: Type that maps a SQL BLOB to a PHP resource stream
A cookbook article shows how to define :doc:`your own custom mapping types
<../cookbook/custom-mapping-types>`.
you are using.
.. note::

View File

@@ -1003,7 +1003,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:
@@ -1020,58 +1020,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
~~~~~~~~~~~~~~~~~~~~~~
@@ -1175,165 +1263,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
~~~~~~~~~~~~~~~~~~~~~~~~~~~

View File

@@ -135,11 +135,6 @@ parameters:
count: 2
path: src/EntityManager.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

View File

@@ -257,12 +257,6 @@
<code><![CDATA[transactional]]></code>
<code><![CDATA[transactional]]></code>
</DeprecatedMethod>
<InvalidReturnStatement>
<code><![CDATA[$this->wrapped->getClassMetadata($className)]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[getClassMetadata]]></code>
</InvalidReturnType>
<MissingParamType>
<code><![CDATA[$entity]]></code>
<code><![CDATA[$lockMode]]></code>
@@ -309,11 +303,9 @@
<code><![CDATA[$entity instanceof $class->name ? $entity : null]]></code>
<code><![CDATA[$persister->load($sortedId, null, null, [], $lockMode)]]></code>
<code><![CDATA[$persister->loadById($sortedId)]]></code>
<code><![CDATA[$this->metadataFactory->getMetadataFor($className)]]></code>
</InvalidReturnStatement>
<InvalidReturnType>
<code><![CDATA[?T]]></code>
<code><![CDATA[getClassMetadata]]></code>
</InvalidReturnType>
<MissingReturnType>
<code><![CDATA[wrapInTransaction]]></code>
@@ -462,9 +454,6 @@
</RedundantConditionGivenDocblockType>
</file>
<file src="src/Internal/Hydration/ObjectHydrator.php">
<InvalidArgument>
<code><![CDATA[$element]]></code>
</InvalidArgument>
<PossiblyFalseArgument>
<code><![CDATA[$index]]></code>
</PossiblyFalseArgument>
@@ -1492,6 +1481,7 @@
<code><![CDATA[getValue]]></code>
<code><![CDATA[setAccessible]]></code>
<code><![CDATA[setValue]]></code>
<code><![CDATA[setValue]]></code>
</PossiblyNullReference>
<TypeDoesNotContainType>
<code><![CDATA[$autoGenerate < 0]]></code>
@@ -1501,13 +1491,8 @@
<code><![CDATA[__wakeup]]></code>
</UndefinedInterfaceMethod>
<UndefinedMethod>
<code><![CDATA[self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
}, $skippedProperties)]]></code>
<code><![CDATA[self::createLazyGhost($initializer, $skippedProperties)]]></code>
</UndefinedMethod>
<UndefinedVariable>
<code><![CDATA[$proxy]]></code>
</UndefinedVariable>
<UnresolvableInclude>
<code><![CDATA[require $fileName]]></code>
</UnresolvableInclude>

View File

@@ -340,7 +340,7 @@ interface EntityManagerInterface extends ObjectManager
* @psalm-param string|class-string<T> $className
*
* @return Mapping\ClassMetadata
* @psalm-return Mapping\ClassMetadata<T>
* @psalm-return ($className is class-string<T> ? Mapping\ClassMetadata<T> : Mapping\ClassMetadata<object>)
*
* @psalm-template T of object
*/

View File

@@ -182,29 +182,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();
}
}

View File

@@ -76,11 +76,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($element)
{

View File

@@ -5,11 +5,31 @@ declare(strict_types=1);
namespace Doctrine\ORM;
use Doctrine\Common\Cache\Cache as CacheDriver;
use Doctrine\Persistence\ObjectRepository;
use Doctrine\ORM\Cache\Exception\InvalidResultCacheDriver;
use Doctrine\ORM\Cache\Exception\MetadataCacheNotConfigured;
use Doctrine\ORM\Cache\Exception\MetadataCacheUsesNonPersistentCache;
use Doctrine\ORM\Cache\Exception\QueryCacheNotConfigured;
use Doctrine\ORM\Cache\Exception\QueryCacheUsesNonPersistentCache;
use Doctrine\ORM\Exception\EntityManagerClosed;
use Doctrine\ORM\Exception\InvalidEntityRepository;
use Doctrine\ORM\Exception\InvalidHydrationMode;
use Doctrine\ORM\Exception\MismatchedEventManager;
use Doctrine\ORM\Exception\MissingIdentifierField;
use Doctrine\ORM\Exception\MissingMappingDriverImplementation;
use Doctrine\ORM\Exception\NamedNativeQueryNotFound;
use Doctrine\ORM\Exception\NamedQueryNotFound;
use Doctrine\ORM\Exception\ProxyClassesAlwaysRegenerating;
use Doctrine\ORM\Exception\UnexpectedAssociationValue;
use Doctrine\ORM\Exception\UnknownEntityNamespace;
use Doctrine\ORM\Exception\UnrecognizedIdentifierFields;
use Doctrine\ORM\Persisters\Exception\CantUseInOperatorOnCompositeKeys;
use Doctrine\ORM\Persisters\Exception\InvalidOrientation;
use Doctrine\ORM\Persisters\Exception\UnrecognizedField;
use Doctrine\ORM\Repository\Exception\InvalidFindByCall;
use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall;
use Doctrine\ORM\Tools\Exception\NotSupported;
use Exception;
use function get_debug_type;
use function implode;
use function sprintf;
/**
@@ -26,8 +46,7 @@ class ORMException extends Exception
*/
public static function missingMappingDriverImpl()
{
return new self("It's a requirement to specify a Metadata Driver and pass it " .
'to Doctrine\\ORM\\Configuration::setMetadataDriverImpl().');
return MissingMappingDriverImplementation::create();
}
/**
@@ -39,11 +58,11 @@ class ORMException extends Exception
*/
public static function namedQueryNotFound($queryName)
{
return new self('Could not find a named query by the name "' . $queryName . '"');
return NamedQueryNotFound::fromName($queryName);
}
/**
* @deprecated Use Doctrine\ORM\Exception\NamedQueryNotFound
* @deprecated Use Doctrine\ORM\Exception\NamedNativeQueryNotFound
*
* @param string $nativeQueryName
*
@@ -51,7 +70,7 @@ class ORMException extends Exception
*/
public static function namedNativeQueryNotFound($nativeQueryName)
{
return new self('Could not find a named native query by the name "' . $nativeQueryName . '"');
return NamedNativeQueryNotFound::fromName($nativeQueryName);
}
/**
@@ -63,7 +82,7 @@ class ORMException extends Exception
*/
public static function unrecognizedField($field)
{
return new self(sprintf('Unrecognized field: %s', $field));
return new UnrecognizedField(sprintf('Unrecognized field: %s', $field));
}
/**
@@ -78,7 +97,7 @@ class ORMException extends Exception
*/
public static function unexpectedAssociationValue($class, $association, $given, $expected)
{
return new self(sprintf('Found entity of type %s on association %s#%s, but expecting %s', $given, $class, $association, $expected));
return UnexpectedAssociationValue::create($class, $association, $given, $expected);
}
/**
@@ -91,7 +110,7 @@ class ORMException extends Exception
*/
public static function invalidOrientation($className, $field)
{
return new self('Invalid order by orientation specified for ' . $className . '#' . $field);
return InvalidOrientation::fromClassNameAndField($className, $field);
}
/**
@@ -101,7 +120,7 @@ class ORMException extends Exception
*/
public static function entityManagerClosed()
{
return new self('The EntityManager is closed.');
return EntityManagerClosed::create();
}
/**
@@ -113,7 +132,7 @@ class ORMException extends Exception
*/
public static function invalidHydrationMode($mode)
{
return new self(sprintf("'%s' is an invalid hydration mode.", $mode));
return InvalidHydrationMode::fromMode($mode);
}
/**
@@ -123,7 +142,7 @@ class ORMException extends Exception
*/
public static function mismatchedEventManager()
{
return new self('Cannot use different EventManager instances for EntityManager and Connection.');
return MismatchedEventManager::create();
}
/**
@@ -135,11 +154,11 @@ class ORMException extends Exception
*/
public static function findByRequiresParameter($methodName)
{
return new self("You need to pass a parameter to '" . $methodName . "'");
return InvalidMagicMethodCall::onMissingParameter($methodName);
}
/**
* @deprecated Doctrine\ORM\Repository\Exception\InvalidFindByCall
* @deprecated Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall::becauseFieldNotFoundIn()
*
* @param string $entityName
* @param string $fieldName
@@ -149,10 +168,7 @@ class ORMException extends Exception
*/
public static function invalidMagicCall($entityName, $fieldName, $method)
{
return new self(
"Entity '" . $entityName . "' has no field '" . $fieldName . "'. " .
"You can therefore not call '" . $method . "' on the entities' repository"
);
return InvalidMagicMethodCall::becauseFieldNotFoundIn($entityName, $fieldName, $method);
}
/**
@@ -165,10 +181,7 @@ class ORMException extends Exception
*/
public static function invalidFindByInverseAssociation($entityName, $associationFieldName)
{
return new self(
"You cannot search for the association field '" . $entityName . '#' . $associationFieldName . "', " .
'because it is the inverse side of an association. Find methods only work on owning side associations.'
);
return InvalidFindByCall::fromInverseSideUsage($entityName, $associationFieldName);
}
/**
@@ -178,7 +191,7 @@ class ORMException extends Exception
*/
public static function invalidResultCacheDriver()
{
return new self('Invalid result cache driver; it must implement Doctrine\\Common\\Cache\\Cache.');
return InvalidResultCacheDriver::create();
}
/**
@@ -188,7 +201,7 @@ class ORMException extends Exception
*/
public static function notSupported()
{
return new self('This behaviour is (currently) not supported by Doctrine 2');
return NotSupported::create();
}
/**
@@ -198,7 +211,7 @@ class ORMException extends Exception
*/
public static function queryCacheNotConfigured()
{
return new self('Query Cache is not configured.');
return QueryCacheNotConfigured::create();
}
/**
@@ -208,7 +221,7 @@ class ORMException extends Exception
*/
public static function metadataCacheNotConfigured()
{
return new self('Class Metadata Cache is not configured.');
return MetadataCacheNotConfigured::create();
}
/**
@@ -218,7 +231,7 @@ class ORMException extends Exception
*/
public static function queryCacheUsesNonPersistentCache(CacheDriver $cache)
{
return new self('Query Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.');
return QueryCacheUsesNonPersistentCache::fromDriver($cache);
}
/**
@@ -228,7 +241,7 @@ class ORMException extends Exception
*/
public static function metadataCacheUsesNonPersistentCache(CacheDriver $cache)
{
return new self('Metadata Cache uses a non-persistent cache driver, ' . get_debug_type($cache) . '.');
return MetadataCacheUsesNonPersistentCache::fromDriver($cache);
}
/**
@@ -238,7 +251,7 @@ class ORMException extends Exception
*/
public static function proxyClassesAlwaysRegenerating()
{
return new self('Proxy Classes are always regenerating.');
return ProxyClassesAlwaysRegenerating::create();
}
/**
@@ -250,9 +263,7 @@ class ORMException extends Exception
*/
public static function unknownEntityNamespace($entityNamespaceAlias)
{
return new self(
sprintf("Unknown Entity namespace alias '%s'.", $entityNamespaceAlias)
);
return UnknownEntityNamespace::fromNamespaceAlias($entityNamespaceAlias);
}
/**
@@ -264,11 +275,7 @@ class ORMException extends Exception
*/
public static function invalidEntityRepository($className)
{
return new self(sprintf(
"Invalid repository class '%s'. It must be a %s.",
$className,
ObjectRepository::class
));
return InvalidEntityRepository::fromClassName($className);
}
/**
@@ -281,7 +288,7 @@ class ORMException extends Exception
*/
public static function missingIdentifierField($className, $fieldName)
{
return new self(sprintf('The identifier %s is missing for a query of %s', $fieldName, $className));
return MissingIdentifierField::fromFieldAndClass($fieldName, $className);
}
/**
@@ -294,10 +301,7 @@ class ORMException extends Exception
*/
public static function unrecognizedIdentifierFields($className, $fieldNames)
{
return new self(
"Unrecognized identifier fields: '" . implode("', '", $fieldNames) . "' " .
"are not present on class '" . $className . "'."
);
return UnrecognizedIdentifierFields::fromClassAndFieldNames($className, $fieldNames);
}
/**
@@ -307,6 +311,6 @@ class ORMException extends Exception
*/
public static function cantUseInOperatorOnCompositeKeys()
{
return new self("Can't use IN operator on entities that have composite keys.");
return CantUseInOperatorOnCompositeKeys::create();
}
}

View File

@@ -412,11 +412,6 @@ final class PersistentCollection extends AbstractLazyCollection implements Selec
return parent::containsKey($key);
}
/**
* {@inheritDoc}
*
* @template TMaybeContained
*/
public function contains($element): bool
{
if (! $this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) {

View File

@@ -360,11 +360,11 @@ EOPHP;
*/
private function createLazyInitializer(ClassMetadata $classMetadata, EntityPersister $entityPersister, IdentifierFlattener $identifierFlattener): Closure
{
return static function (InternalProxy $proxy, InternalProxy $original) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($original);
$entity = $entityPersister->loadById($identifier, $original);
return static function (InternalProxy $proxy) use ($entityPersister, $classMetadata, $identifierFlattener): void {
$identifier = $classMetadata->getIdentifierValues($proxy);
$original = $entityPersister->loadById($identifier);
if ($entity === null) {
if ($original === null) {
throw EntityNotFoundException::fromClassNameAndIdentifier(
$classMetadata->getName(),
$identifierFlattener->flattenIdentifier($classMetadata, $identifier)
@@ -382,7 +382,7 @@ EOPHP;
continue;
}
$property->setValue($proxy, $property->getValue($entity));
$property->setValue($proxy, $property->getValue($original));
}
};
}
@@ -468,9 +468,7 @@ EOPHP;
$identifierFields = array_intersect_key($class->getReflectionProperties(), $identifiers);
$proxyFactory = Closure::bind(static function (array $identifier) use ($initializer, $skippedProperties, $identifierFields, $className): InternalProxy {
$proxy = self::createLazyGhost(static function (InternalProxy $object) use ($initializer, &$proxy): void {
$initializer($object, $proxy);
}, $skippedProperties);
$proxy = self::createLazyGhost($initializer, $skippedProperties);
foreach ($identifierFields as $idField => $reflector) {
if (! isset($identifier[$idField])) {

View File

@@ -64,18 +64,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(EntityManagerInterface $em, bool $validatePropertyTypes = true)
@@ -390,21 +390,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)
);
}
@@ -429,7 +429,7 @@ class SchemaValidator
) {
$backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType();
if ($metadataFieldType === $backingType) {
if (in_array($backingType, $metadataFieldType, true)) {
return null;
}
@@ -439,7 +439,7 @@ class SchemaValidator
$fieldName,
$fieldMapping['enumType'],
$backingType,
$metadataFieldType
implode('|', $metadataFieldType)
);
}
@@ -455,7 +455,7 @@ class SchemaValidator
$class->name,
$fieldName,
$propertyType,
$metadataFieldType,
implode('|', $metadataFieldType),
$fieldMapping['type']
);
},
@@ -468,8 +468,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
private function findBuiltInType(Type $type): ?array
{
$typeName = get_class($type);

View File

@@ -1292,6 +1292,8 @@ class UnitOfWork implements PropertyChangedListener
$eventsToDispatch = [];
foreach ($entities as $entity) {
$this->removeFromIdentityMap($entity);
$oid = spl_object_id($entity);
$class = $this->em->getClassMetadata(get_class($entity));
$persister = $this->getEntityPersister($class->name);
@@ -1667,8 +1669,6 @@ class UnitOfWork implements PropertyChangedListener
return;
}
$this->removeFromIdentityMap($entity);
unset($this->entityUpdates[$oid]);
if (! isset($this->entityDeletions[$oid])) {
@@ -3166,7 +3166,7 @@ EXCEPTION
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite) {
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite && ! isset($assoc['indexBy'])) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} else {
$this->loadCollection($pColl);
@@ -3224,7 +3224,13 @@ EXCEPTION
*
* @param PersistentCollection[] $collections
* @param array<string, mixed> $mapping
* @psalm-param array{targetEntity: class-string, sourceEntity: class-string, mappedBy: string, indexBy: string|null} $mapping
* @psalm-param array{
* targetEntity: class-string,
* sourceEntity: class-string,
* mappedBy: string,
* indexBy: string|null,
* orderBy: array<string, string>|null
* } $mapping
*/
private function eagerLoadCollections(array $collections, array $mapping): void
{
@@ -3241,7 +3247,7 @@ EXCEPTION
$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);

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\BigIntegers;
use Doctrine\ORM\Mapping as ORM;
/** @ORM\Entity */
class BigIntegers
{
/**
* @ORM\Column(type="integer")
* @ORM\Id
* @ORM\GeneratedValue
*/
public ?int $id = null;
/** @ORM\Column(type="bigint") */
public int $one = 1;
/** @ORM\Column(type="bigint") */
public string $two = '2';
/** @ORM\Column(type="bigint") */
public float $three = 3.0;
}

View File

@@ -249,7 +249,6 @@ class QueryDqlFunctionTest extends OrmFunctionalTestCase
self::assertEquals(1600000, $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')

View File

@@ -0,0 +1,42 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("gh11149_eager_product")
*/
class EagerProduct
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\OneToMany(
* targetEntity=EagerProductTranslation::class,
* mappedBy="product",
* fetch="EAGER",
* indexBy="locale_code"
* )
*
* @var Collection<string, EagerProductTranslation>
*/
public $translations;
public function __construct(int $id)
{
$this->id = $id;
$this->translations = new ArrayCollection();
}
}

View File

@@ -0,0 +1,45 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("gh11149_eager_product_translation")
*/
class EagerProductTranslation
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity=EagerProduct::class, inversedBy="translations")
* @ORM\JoinColumn(nullable=false)
*
* @var EagerProduct
*/
public $product;
/**
* @ORM\ManyToOne(targetEntity=Locale::class)
* @ORM\JoinColumn(name="locale_code", referencedColumnName="code", nullable=false)
*
* @var Locale
*/
public $locale;
public function __construct($id, EagerProduct $product, Locale $locale)
{
$this->id = $id;
$this->product = $product;
$this->locale = $locale;
}
}

View File

@@ -0,0 +1,47 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\PersistentCollection;
use Doctrine\Persistence\Proxy;
use Doctrine\Tests\OrmFunctionalTestCase;
class GH11149Test extends OrmFunctionalTestCase
{
protected function setUp(): void
{
parent::setUp();
$this->setUpEntitySchema([
Locale::class,
EagerProduct::class,
EagerProductTranslation::class,
]);
}
public function testFetchEagerModeWithIndexBy(): void
{
// Load entities into database
$this->_em->persist($product = new EagerProduct(11149));
$this->_em->persist($locale = new Locale('fr_FR'));
$this->_em->persist(new EagerProductTranslation(11149, $product, $locale));
$this->_em->flush();
$this->_em->clear();
// Fetch entity from database
$product = $this->_em->find(EagerProduct::class, 11149);
// Assert associated entity is loaded eagerly
static::assertInstanceOf(EagerProduct::class, $product);
static::assertInstanceOf(PersistentCollection::class, $product->translations);
static::assertTrue($product->translations->isInitialized());
static::assertCount(1, $product->translations);
// Assert associated entity is indexed by given property
$translation = $product->translations->get('fr_FR');
static::assertInstanceOf(EagerProductTranslation::class, $translation);
static::assertNotInstanceOf(Proxy::class, $translation);
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional\Ticket\GH11149;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table("gh11149_locale")
*/
class Locale
{
/**
* @ORM\Id
* @ORM\Column(type="string", length=5)
*
* @var string
*/
public $code;
public function __construct(string $code)
{
$this->code = $code;
}
}

View File

@@ -0,0 +1,135 @@
<?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")
*
* @var int
*/
private $id;
/**
* @ORM\OneToMany(
* targetEntity=GH11163BucketItem::class,
* mappedBy="bucket",
* fetch="EAGER"
* )
* @ORM\OrderBy({"position" = "ASC"})
*
* @var Collection<int, GH11163BucketItem>
*/
public $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)
*
* @var GH11163Bucket
*/
private $bucket;
/**
* @ORM\Id
* @ORM\Column(type="integer")
*
* @var int
*/
public $id;
/**
* @ORM\Column(type="integer", nullable=false)
*
* @var int
*/
public $position;
public function __construct(int $id, GH11163Bucket $bucket, int $position)
{
$this->id = $id;
$this->bucket = $bucket;
$this->position = $position;
}
}

View File

@@ -0,0 +1,88 @@
<?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
{
return $this->_em->createQueryBuilder()
->select('e')
->from(GH6123Entity::class, 'e')
->where('e.id = :id')
->setParameter('id', $id)
->getQuery()
->getOneOrNullResult();
}
}
/**
* @ORM\Entity
*/
#[ORM\Entity]
class GH6123Entity
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
* @var int
*/
#[ORM\Id]
#[ORM\GeneratedValue]
#[ORM\Column(type: Types::INTEGER)]
public $id;
}

View File

@@ -12,6 +12,7 @@ use Doctrine\ORM\Events;
use Doctrine\ORM\Internal\Hydration\AbstractHydrator;
use Doctrine\ORM\ORMException;
use Doctrine\ORM\Query\ResultSetMapping;
use Doctrine\Tests\Models\Hydration\SimpleEntity;
use Doctrine\Tests\OrmFunctionalTestCase;
use PHPUnit\Framework\MockObject\MockObject;
@@ -154,4 +155,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));
}
}

View File

@@ -64,8 +64,7 @@ class ProxyFactoryTest extends OrmTestCase
public function testReferenceProxyDelegatesLoadingToThePersister(): void
{
$identifier = ['id' => 42];
$proxyClass = 'Proxies\__CG__\Doctrine\Tests\Models\ECommerce\ECommerceFeature';
$persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['load'])
$persister = $this->getMockBuilderWithOnlyMethods(BasicEntityPersister::class, ['loadById'])
->disableOriginalConstructor()
->getMock();
@@ -75,8 +74,8 @@ class ProxyFactoryTest extends OrmTestCase
$persister
->expects(self::atLeastOnce())
->method('load')
->with(self::equalTo($identifier), self::isInstanceOf($proxyClass))
->method('loadById')
->with(self::equalTo($identifier))
->will(self::returnValue($proxy));
$proxy->getDescription();

View File

@@ -24,6 +24,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;
@@ -240,6 +241,19 @@ class SchemaValidatorTest extends OrmTestCase
$ce
);
}
/**
* @requires PHP 7.4
*/
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 */

View File

@@ -413,12 +413,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));
}