Compare commits

...

26 Commits

Author SHA1 Message Date
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
Grégoire Paris
40a0964f06 Merge pull request #11289 from themasch/reproduce-issue-11154-composite-key-eager-fetch-one
Do not use batch loading for collections with composite identifier
2024-03-18 20:12:56 +01:00
Grégoire Paris
08a9e60ed0 Remove outdated git metadata files (#11362)
Some of it seems related to the previous documentation build system,
some of it seems related to IntelliJ.
2024-03-17 23:06:30 +01:00
Benjamin Eberlei
3e3c023c95 Switch join columns around, otherwise index doesnt match 2024-03-17 19:50:56 +01:00
Benjamin Eberlei
5e6d5c06a9 Key on fk 2024-03-17 19:43:26 +01:00
Benjamin Eberlei
1622b7877d Fix entities and mapping. 2024-03-17 18:02:11 +01:00
Benjamin Eberlei
80aae2796d Merge pull request #11373 from kaznovac/patch-3
Minor code style fix in AbstractRemoteControl
2024-03-17 17:20:01 +01:00
Marko Kaznovac
528ef40fc4 Minor code style fix in AbstractRemoteControl 2024-03-17 15:55:54 +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
Benjamin Eberlei
820a0da4c1 Do not schedule batch loading for target classes with composite identifier. 2024-03-16 23:05:28 +01:00
Benjamin Eberlei
fcd02b1ee2 Cleanup tests not to use model sets. 2024-03-16 23:04:57 +01:00
Mark Schmale
8d4718f875 provides a test case for github issue 11154
After 2.17 (some?) EAGER fetched OneToMany associations stopped working, if they have multiple join columns. Loads for these associations will trigger a `MessingPositionalParameter` exception "Positional parameter at index 1 does not have a bound value".

This test case should reproduce this issue, so it can be fixed.
2024-02-22 10:58:50 +01:00
23 changed files with 514 additions and 318 deletions

4
docs/.gitignore vendored
View File

@@ -1,4 +0,0 @@
en/_exts/configurationblock.pyc
build
en/_build
.idea

3
docs/.gitmodules vendored
View File

@@ -1,3 +0,0 @@
[submodule "en/_theme"]
path = en/_theme
url = https://github.com/doctrine/doctrine-sphinx-theme.git

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

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

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

@@ -3166,9 +3166,9 @@ EXCEPTION
if ($hints['fetchMode'][$class->name][$field] === ClassMetadata::FETCH_EAGER) {
$isIteration = isset($hints[Query::HINT_INTERNAL_ITERATION]) && $hints[Query::HINT_INTERNAL_ITERATION];
if (! $isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) {
if ($assoc['type'] === ClassMetadata::ONE_TO_MANY && ! $isIteration && ! $targetClass->isIdentifierComposite && ! isset($assoc['indexBy'])) {
$this->scheduleCollectionForBatchLoading($pColl, $class);
} elseif (($isIteration && $assoc['type'] === ClassMetadata::ONE_TO_MANY) || $assoc['type'] === ClassMetadata::MANY_TO_MANY) {
} else {
$this->loadCollection($pColl);
$pColl->takeSnapshot();
}

View File

@@ -27,8 +27,6 @@ abstract class AbstractRemoteControl
public $id;
/**
* /**
*
* @ORM\Column(type="string")
*
* @var string

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

@@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\EagerFetchedCompositeOneToMany;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="eager_composite_join_root")
*/
class RootEntity
{
/**
* @ORM\Id
* @ORM\Column(type="integer", nullable=false)
*
* @var int|null
*/
private $id = null;
/**
* @ORM\Id
* @ORM\Column(type="string", nullable=false, name="other_key")
*
* @var string
*/
private $otherKey;
/**
* @ORM\OneToMany(mappedBy="root", targetEntity=SecondLevel::class, fetch="EAGER")
*
* @var Collection<int, SecondLevel>
*/
private $secondLevel;
public function __construct(int $id, string $other)
{
$this->otherKey = $other;
$this->secondLevel = new ArrayCollection();
$this->id = $id;
}
public function getId(): ?int
{
return $this->id;
}
public function getOtherKey(): string
{
return $this->otherKey;
}
}

View File

@@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\Models\EagerFetchedCompositeOneToMany;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="eager_composite_join_second_level", indexes={
* @ORM\Index(name="root_other_key_idx", columns={"root_other_key", "root_id"})
* })
*/
class SecondLevel
{
/**
* @ORM\Id
* @ORM\Column(type="integer", nullable=false)
*
* @var int|null
*/
private $upperId;
/**
* @ORM\Id
* @ORM\Column(type="string", nullable=false, name="other_key")
*
* @var string
*/
private $otherKey;
/**
* @ORM\ManyToOne(targetEntity=RootEntity::class, inversedBy="secondLevel")
* @ORM\JoinColumns({
* @ORM\JoinColumn(name="root_id", referencedColumnName="id"),
* @ORM\JoinColumn(name="root_other_key", referencedColumnName="other_key")
* })
*
* @var RootEntity
*/
private $root;
public function __construct(RootEntity $upper)
{
$this->upperId = $upper->getId();
$this->otherKey = $upper->getOtherKey();
$this->root = $upper;
}
public function getId(): ?int
{
return $this->id;
}
}

View File

@@ -0,0 +1,27 @@
<?php
declare(strict_types=1);
namespace Doctrine\Tests\ORM\Functional;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\RootEntity;
use Doctrine\Tests\Models\EagerFetchedCompositeOneToMany\SecondLevel;
use Doctrine\Tests\OrmFunctionalTestCase;
final class EagerFetchOneToManyWithCompositeKeyTest extends OrmFunctionalTestCase
{
/** @ticket 11154 */
public function testItDoesNotThrowAnExceptionWhenTriggeringALoad(): void
{
$this->setUpEntitySchema([RootEntity::class, SecondLevel::class]);
$a1 = new RootEntity(1, 'A');
$this->_em->persist($a1);
$this->_em->flush();
$this->_em->clear();
self::assertCount(1, $this->_em->getRepository(RootEntity::class)->findAll());
}
}

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

@@ -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 */