diff --git a/docs/en/reference/dql-doctrine-query-language.rst b/docs/en/reference/dql-doctrine-query-language.rst index 571190072e3..ce06d02b81a 100644 --- a/docs/en/reference/dql-doctrine-query-language.rst +++ b/docs/en/reference/dql-doctrine-query-language.rst @@ -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 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()`` +^^^^^^^^^^^^^^^ + +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): + +.. code-block:: php + + 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. + +- 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. + +``getArrayResult()`` +^^^^^^^^^^^^^^^^^^^^ + +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 + + 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 + + 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 + + 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 + + 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 + + _stmt->fetchAllAssociative(); + } + } -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: - - -- ``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. - - .. 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. - - - -- ``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. - -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. - -The use of the methods mentioned earlier is generally preferred as -it leads to more concise code. +Next you just need to add the class to the ORM configuration: + +.. code-block:: php + + getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator'); + +Now the hydrator is ready to be used in your queries: + +.. code-block:: php + + 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 - - 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 - - createQuery('SELECT u FROM CmsUser u'); - $users = $query->getResult(Query::HYDRATE_ARRAY); - -You can use the ``getArrayResult()`` shortcut as well: - -.. code-block:: php - - 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 - - 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 - - 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 - - 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 - - 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 - - getSingleColumnResult(); - -Custom Hydration Modes -^^^^^^^^^^^^^^^^^^^^^^ - -You can easily add your own custom hydration modes by first -creating a class which extends ``AbstractHydrator``: - -.. code-block:: php - - _stmt->fetchAllAssociative(); - } - } - -Next you just need to add the class to the ORM configuration: - -.. code-block:: php - - getConfiguration()->addCustomHydrationMode('CustomHydrator', 'MyProject\Hydrators\CustomHydrator'); - -Now the hydrator is ready to be used in your queries: - -.. code-block:: php - - createQuery('SELECT u FROM CmsUser u'); - $results = $query->getResult('CustomHydrator'); - Iterating Large Result Sets ~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index c33099dee7a..6f0a73dd5a3 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -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\\&Doctrine\\\\Common\\\\Collections\\\\Selectable\\ but returns Doctrine\\\\ORM\\\\LazyCriteriaCollection\\<\\(int\\|string\\), object\\>\\.$#" count: 1 diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 7a3b729e849..6c70101de13 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -257,12 +257,6 @@ - - wrapped->getClassMetadata($className)]]> - - - - @@ -309,11 +303,9 @@ name ? $entity : null]]> load($sortedId, null, null, [], $lockMode)]]> loadById($sortedId)]]> - metadataFactory->getMetadataFor($className)]]> - @@ -462,9 +454,6 @@ - - - diff --git a/src/EntityManagerInterface.php b/src/EntityManagerInterface.php index 326cb933a59..b3b5ddf9e13 100644 --- a/src/EntityManagerInterface.php +++ b/src/EntityManagerInterface.php @@ -340,7 +340,7 @@ public function hasFilters(); * @psalm-param string|class-string $className * * @return Mapping\ClassMetadata - * @psalm-return Mapping\ClassMetadata + * @psalm-return ($className is class-string ? Mapping\ClassMetadata : Mapping\ClassMetadata) * * @psalm-template T of object */ diff --git a/src/LazyCriteriaCollection.php b/src/LazyCriteriaCollection.php index 5580f971eb0..48040922616 100644 --- a/src/LazyCriteriaCollection.php +++ b/src/LazyCriteriaCollection.php @@ -76,11 +76,11 @@ public function isEmpty() } /** - * {@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) { diff --git a/src/PersistentCollection.php b/src/PersistentCollection.php index 4470a64a5cd..c94bb77496b 100644 --- a/src/PersistentCollection.php +++ b/src/PersistentCollection.php @@ -412,11 +412,6 @@ public function containsKey($key): bool return parent::containsKey($key); } - /** - * {@inheritDoc} - * - * @template TMaybeContained - */ public function contains($element): bool { if (! $this->initialized && $this->getMapping()['fetch'] === ClassMetadata::FETCH_EXTRA_LAZY) { diff --git a/src/Tools/SchemaValidator.php b/src/Tools/SchemaValidator.php index 1ea5e688fab..212ad019846 100644 --- a/src/Tools/SchemaValidator.php +++ b/src/Tools/SchemaValidator.php @@ -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 @@ function (array $fieldMapping) use ($class): ?string { $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 @@ function (array $fieldMapping) use ($class): ?string { ) { $backingType = (string) (new ReflectionEnum($fieldMapping['enumType']))->getBackingType(); - if ($metadataFieldType === $backingType) { + if (in_array($backingType, $metadataFieldType, true)) { return null; } @@ -439,7 +439,7 @@ function (array $fieldMapping) use ($class): ?string { $fieldName, $fieldMapping['enumType'], $backingType, - $metadataFieldType + implode('|', $metadataFieldType) ); } @@ -455,7 +455,7 @@ function (array $fieldMapping) use ($class): ?string { $class->name, $fieldName, $propertyType, - $metadataFieldType, + implode('|', $metadataFieldType), $fieldMapping['type'] ); }, @@ -468,8 +468,10 @@ function (array $fieldMapping) use ($class): ?string { /** * The exact DBAL type must be used (no subclasses), since consumers of doctrine/orm may have their own * customization around field types. + * + * @return list|null */ - private function findBuiltInType(Type $type): ?string + private function findBuiltInType(Type $type): ?array { $typeName = get_class($type); diff --git a/tests/Tests/Models/BigIntegers/BigIntegers.php b/tests/Tests/Models/BigIntegers/BigIntegers.php new file mode 100644 index 00000000000..16b6f58dd0e --- /dev/null +++ b/tests/Tests/Models/BigIntegers/BigIntegers.php @@ -0,0 +1,27 @@ +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 */