Compare commits

..

24 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
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
19 changed files with 543 additions and 322 deletions
+1 -1
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.
+4 -4
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>
+131 -202
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
~~~~~~~~~~~~~~~~~~~~~~~~~~~
-5
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
-11
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>
+1 -1
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
*/
+22 -20
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();
}
}
+3 -3
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)
{
+49 -45
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();
}
}
-5
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) {
+21 -19
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);
+10 -4
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])) {
@@ -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);
@@ -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;
}
@@ -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')
@@ -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;
}
}
@@ -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;
}
@@ -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));
}
}
@@ -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 */
+7 -1
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));
}