Skip to content

Commit 40d1e7b

Browse files
authored
Merge pull request doctrine#9214 from doctrine/2.7
Merge 2.7 into 2.10.x
2 parents e8275f6 + 66c95a6 commit 40d1e7b

File tree

5 files changed

+260
-64
lines changed

5 files changed

+260
-64
lines changed

docs/en/reference/dql-doctrine-query-language.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -730,7 +730,7 @@ clauses:
730730
- ``SQRT(q)`` - Return the square-root of q.
731731
- ``SUBSTRING(str, start [, length])`` - Return substring of given
732732
string.
733-
- ``TRIM([LEADING \| TRAILING \| BOTH] ['trchar' FROM] str)`` - Trim
733+
- ``TRIM([LEADING | TRAILING | BOTH] ['trchar' FROM] str)`` - Trim
734734
the string by the given trim char, defaults to whitespaces.
735735
- ``UPPER(str)`` - Return the upper-case of the given string.
736736
- ``DATE_ADD(date, value, unit)`` - Add the given time to a given date.

lib/Doctrine/ORM/Internal/Hydration/SingleScalarHydrator.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ protected function hydrateAllData()
3232
throw new NonUniqueResultException('The query returned multiple rows. Change the query or use a different result function like getScalarResult().');
3333
}
3434

35-
if (count($data[key($data)]) > 1) {
35+
$result = $this->gatherScalarRowData($data[key($data)]);
36+
37+
if (count($result) > 1) {
3638
throw new NonUniqueResultException('The query returned a row containing multiple columns. Change the query or use a different result function like getScalarResult().');
3739
}
3840

39-
$result = $this->gatherScalarRowData($data[key($data)]);
40-
4141
return array_shift($result);
4242
}
4343
}

lib/Doctrine/ORM/Persisters/Entity/BasicEntityPersister.php

Lines changed: 24 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1933,36 +1933,42 @@ private function getValues($value): array
19331933
return [$newValue];
19341934
}
19351935

1936-
if (is_object($value) && $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1937-
$class = $this->em->getClassMetadata(get_class($value));
1938-
if ($class->isIdentifierComposite) {
1939-
$newValue = [];
1940-
1941-
foreach ($class->getIdentifierValues($value) as $innerValue) {
1942-
$newValue = array_merge($newValue, $this->getValues($innerValue));
1943-
}
1944-
1945-
return $newValue;
1946-
}
1947-
}
1948-
1949-
return [$this->getIndividualValue($value)];
1936+
return $this->getIndividualValue($value);
19501937
}
19511938

19521939
/**
19531940
* Retrieves an individual parameter value.
19541941
*
19551942
* @param mixed $value
19561943
*
1957-
* @return mixed
1944+
* @return array<mixed>
1945+
* @psalm-return list<mixed>
19581946
*/
19591947
private function getIndividualValue($value)
19601948
{
1961-
if (! is_object($value) || ! $this->em->getMetadataFactory()->hasMetadataFor(ClassUtils::getClass($value))) {
1962-
return $value;
1949+
if (! is_object($value)) {
1950+
return [$value];
1951+
}
1952+
1953+
$valueClass = ClassUtils::getClass($value);
1954+
1955+
if ($this->em->getMetadataFactory()->isTransient($valueClass)) {
1956+
return [$value];
1957+
}
1958+
1959+
$class = $this->em->getClassMetadata($valueClass);
1960+
1961+
if ($class->isIdentifierComposite) {
1962+
$newValue = [];
1963+
1964+
foreach ($class->getIdentifierValues($value) as $innerValue) {
1965+
$newValue = array_merge($newValue, $this->getValues($innerValue));
1966+
}
1967+
1968+
return $newValue;
19631969
}
19641970

1965-
return $this->em->getUnitOfWork()->getSingleIdentifierValue($value);
1971+
return [$this->em->getUnitOfWork()->getSingleIdentifierValue($value)];
19661972
}
19671973

19681974
/**
Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Doctrine\Tests\ORM\Functional\Ticket;
6+
7+
use Doctrine\Common\Collections\Collection;
8+
use Doctrine\ORM\Mapping\Column;
9+
use Doctrine\ORM\Mapping\DiscriminatorMap;
10+
use Doctrine\ORM\Mapping\Entity;
11+
use Doctrine\ORM\Mapping\GeneratedValue;
12+
use Doctrine\ORM\Mapping\Id;
13+
use Doctrine\ORM\Mapping\InheritanceType;
14+
use Doctrine\ORM\Mapping\ManyToOne;
15+
use Doctrine\ORM\Mapping\OneToMany;
16+
use Doctrine\Tests\OrmFunctionalTestCase;
17+
18+
class GH7512Test extends OrmFunctionalTestCase
19+
{
20+
protected function setUp(): void
21+
{
22+
parent::setUp();
23+
24+
$this->setUpEntitySchema([
25+
GH7512EntityA::class,
26+
GH7512EntityB::class,
27+
GH7512EntityC::class,
28+
]);
29+
30+
$this->_em->persist(new GH7512EntityA());
31+
$this->_em->persist(new GH7512EntityC());
32+
$this->_em->flush();
33+
$this->_em->clear();
34+
}
35+
36+
public function testFindEntityByAssociationPropertyJoinedChildWithClearMetadata(): void
37+
{
38+
// unset metadata for entity B as though it hasn't been touched yet in application lifecycle.
39+
$this->_em->getMetadataFactory()->setMetadataFor(GH7512EntityB::class, null);
40+
$result = $this->_em->getRepository(GH7512EntityC::class)->findBy([
41+
'entityA' => new GH7512EntityB(),
42+
]);
43+
$this->assertEmpty($result);
44+
}
45+
}
46+
47+
/**
48+
* @Entity()
49+
* @InheritanceType("JOINED")
50+
* @DiscriminatorMap({
51+
* "entitya"=Doctrine\Tests\ORM\Functional\Ticket\GH7512EntityA::class,
52+
* "entityB"=Doctrine\Tests\ORM\Functional\Ticket\GH7512EntityB::class
53+
* })
54+
*/
55+
class GH7512EntityA
56+
{
57+
/**
58+
* @Column(type="integer")
59+
* @Id()
60+
* @GeneratedValue(strategy="AUTO")
61+
* @var int
62+
*/
63+
public $id;
64+
65+
/**
66+
* @OneToMany(targetEntity="Doctrine\Tests\ORM\Functional\Ticket\GH7512EntityC", mappedBy="entityA")
67+
* @var Collection<int, GH7512EntityC>
68+
*/
69+
public $entityCs;
70+
}
71+
72+
/**
73+
* @Entity()
74+
*/
75+
class GH7512EntityB extends GH7512EntityA
76+
{
77+
}
78+
79+
/**
80+
* @Entity()
81+
*/
82+
class GH7512EntityC
83+
{
84+
/**
85+
* @Column(type="integer")
86+
* @Id()
87+
* @GeneratedValue(strategy="AUTO")
88+
* @var int
89+
*/
90+
public $id;
91+
92+
/**
93+
* @ManyToOne(targetEntity="Doctrine\Tests\ORM\Functional\Ticket\GH7512EntityA", inversedBy="entityCs")
94+
* @var GH7512EntityA
95+
*/
96+
public $entityA;
97+
}

tests/Doctrine/Tests/ORM/Hydration/SingleScalarHydratorTest.php

Lines changed: 135 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -9,56 +9,147 @@
99
use Doctrine\ORM\Query\ResultSetMapping;
1010
use Doctrine\Tests\Mocks\ArrayResultFactory;
1111
use Doctrine\Tests\Models\CMS\CmsUser;
12+
use Generator;
1213

1314
use function in_array;
1415

1516
class SingleScalarHydratorTest extends HydrationTestCase
1617
{
17-
/** Result set provider for the HYDRATE_SINGLE_SCALAR tests */
18-
public static function singleScalarResultSetProvider(): array
18+
/**
19+
* @return Generator<int, array{list<array<string,mixed>>,mixed}>
20+
*/
21+
public static function validResultSetProvider(): Generator
1922
{
20-
return [
21-
// valid
22-
'valid' => [
23-
'name' => 'result1',
24-
'resultSet' => [
25-
['u__name' => 'romanb'],
23+
// SELECT u.name FROM CmsUser u WHERE u.id = 1
24+
yield [
25+
[
26+
['u__name' => 'romanb'],
27+
],
28+
'romanb',
29+
];
30+
31+
// SELECT u.id FROM CmsUser u WHERE u.id = 1
32+
yield [
33+
[
34+
['u__id' => '1'],
35+
],
36+
1,
37+
];
38+
39+
// SELECT
40+
// u.id,
41+
// COUNT(u.postsCount + u.likesCount) AS HIDDEN score
42+
// FROM CmsUser u
43+
// WHERE u.id = 1
44+
yield [
45+
[
46+
[
47+
'u__id' => '1',
48+
'score' => 10, // Ignored since not part of ResultSetMapping (cf. HIDDEN keyword)
2649
],
2750
],
28-
// valid
51+
1,
52+
];
53+
}
54+
55+
/**
56+
* @param list<array<string, mixed>> $resultSet
57+
* @param mixed $expectedResult
58+
*
59+
* @dataProvider validResultSetProvider
60+
*/
61+
public function testHydrateSingleScalarFromFieldMappingWithValidResultSet(array $resultSet, $expectedResult): void
62+
{
63+
$rsm = new ResultSetMapping();
64+
$rsm->addEntityResult(CmsUser::class, 'u');
65+
$rsm->addFieldResult('u', 'u__id', 'id');
66+
$rsm->addFieldResult('u', 'u__name', 'name');
67+
68+
$stmt = ArrayResultFactory::createFromArray($resultSet);
69+
$hydrator = new SingleScalarHydrator($this->entityManager);
70+
71+
$result = $hydrator->hydrateAll($stmt, $rsm);
72+
$this->assertEquals($expectedResult, $result);
73+
}
74+
75+
/**
76+
* @param list<array<string, mixed>> $resultSet
77+
* @param mixed $expectedResult
78+
*
79+
* @dataProvider validResultSetProvider
80+
*/
81+
public function testHydrateSingleScalarFromScalarMappingWithValidResultSet(array $resultSet, $expectedResult): void
82+
{
83+
$rsm = new ResultSetMapping();
84+
$rsm->addScalarResult('u__id', 'id', 'string');
85+
$rsm->addScalarResult('u__name', 'name', 'string');
86+
87+
$stmt = ArrayResultFactory::createFromArray($resultSet);
88+
$hydrator = new SingleScalarHydrator($this->entityManager);
89+
90+
$result = $hydrator->hydrateAll($stmt, $rsm);
91+
$this->assertEquals($expectedResult, $result);
92+
}
93+
94+
/**
95+
* @return Generator<int, array{list<array<string,mixed>>}>
96+
*/
97+
public static function invalidResultSetProvider(): Generator
98+
{
99+
// Single row (OK), multiple columns (NOT OK)
100+
yield [
29101
[
30-
'name' => 'result2',
31-
'resultSet' => [
32-
['u__id' => '1'],
102+
[
103+
'u__id' => '1',
104+
'u__name' => 'romanb',
33105
],
34106
],
35-
// invalid
107+
];
108+
109+
// Multiple rows (NOT OK), single column (OK)
110+
yield [
111+
[
112+
['u__id' => '1'],
113+
['u__id' => '2'],
114+
],
115+
];
116+
117+
// Multiple rows (NOT OK), single column with HIDDEN result (OK)
118+
yield [
36119
[
37-
'name' => 'result3',
38-
'resultSet' => [
39-
[
40-
'u__id' => '1',
41-
'u__name' => 'romanb',
42-
],
120+
[
121+
'u__id' => '1',
122+
'score' => 10, // Ignored since not part of ResultSetMapping
123+
],
124+
[
125+
'u__id' => '2',
126+
'score' => 10, // Ignored since not part of ResultSetMapping
43127
],
44128
],
45-
// invalid
129+
1,
130+
];
131+
132+
// Multiple row (NOT OK), multiple columns (NOT OK)
133+
yield [
46134
[
47-
'name' => 'result4',
48-
'resultSet' => [
49-
['u__id' => '1'],
50-
['u__id' => '2'],
135+
[
136+
'u__id' => '1',
137+
'u__name' => 'romanb',
138+
],
139+
[
140+
'u__id' => '2',
141+
'u__name' => 'romanb',
51142
],
52143
],
53144
];
54145
}
55146

56147
/**
57-
* select u.name from CmsUser u where u.id = 1
148+
* @param list<array<string, mixed>> $resultSet
58149
*
59-
* @dataProvider singleScalarResultSetProvider
150+
* @dataProvider invalidResultSetProvider
60151
*/
61-
public function testHydrateSingleScalar($name, $resultSet): void
152+
public function testHydrateSingleScalarFromFieldMappingWithInvalidResultSet(array $resultSet): void
62153
{
63154
$rsm = new ResultSetMapping();
64155
$rsm->addEntityResult(CmsUser::class, 'u');
@@ -68,23 +159,25 @@ public function testHydrateSingleScalar($name, $resultSet): void
68159
$stmt = ArrayResultFactory::createFromArray($resultSet);
69160
$hydrator = new SingleScalarHydrator($this->entityManager);
70161

71-
if ($name === 'result1') {
72-
$result = $hydrator->hydrateAll($stmt, $rsm);
73-
self::assertEquals('romanb', $result);
74-
75-
return;
76-
}
162+
$this->expectException(NonUniqueResultException::class);
163+
$hydrator->hydrateAll($stmt, $rsm);
164+
}
77165

78-
if ($name === 'result2') {
79-
$result = $hydrator->hydrateAll($stmt, $rsm);
80-
self::assertEquals(1, $result);
166+
/**
167+
* @param list<array<string, mixed>> $resultSet
168+
*
169+
* @dataProvider invalidResultSetProvider
170+
*/
171+
public function testHydrateSingleScalarFromScalarMappingWithInvalidResultSet(array $resultSet): void
172+
{
173+
$rsm = new ResultSetMapping();
174+
$rsm->addScalarResult('u__id', 'id', 'string');
175+
$rsm->addScalarResult('u__name', 'name', 'string');
81176

82-
return;
83-
}
177+
$stmt = ArrayResultFactory::createFromArray($resultSet);
178+
$hydrator = new SingleScalarHydrator($this->entityManager);
84179

85-
if (in_array($name, ['result3', 'result4'], true)) {
86-
$this->expectException(NonUniqueResultException::class);
87-
$hydrator->hydrateAll($stmt, $rsm);
88-
}
180+
$this->expectException(NonUniqueResultException::class);
181+
$hydrator->hydrateAll($stmt, $rsm);
89182
}
90183
}

0 commit comments

Comments
 (0)