Passed
Pull Request — 2.6 (#7865)
by Marco
07:43
created

testWillFindSongsInPaginatorEvenWithCachedQueryParsing()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 31
Code Lines 22

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 22
c 1
b 0
f 0
dl 0
loc 31
rs 9.568
cc 1
nc 1
nop 0
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Doctrine\Tests\ORM\Functional\Ticket;
6
7
use Doctrine\Common\Cache\ClearableCache;
8
use Doctrine\Common\Collections\Criteria;
9
use Doctrine\DBAL\Platforms\AbstractPlatform;
10
use Doctrine\DBAL\Types\StringType;
11
use Doctrine\DBAL\Types\Type;
12
use Doctrine\ORM\Tools\Pagination\Paginator;
13
use Doctrine\Tests\OrmFunctionalTestCase;
14
use function array_map;
15
use function assert;
16
use function is_string;
17
use function iterator_to_array;
18
19
/**
20
 * @group GH7820
21
 *
22
 * When using a {@see \Doctrine\ORM\Tools\Pagination\Paginator} to iterate over a query
23
 * that has entities with a custom DBAL type used in the identifier, then `$id->__toString()`
24
 * is used implicitly by {@see \PDOStatement::bindValue()}, instead of being converted by the
25
 * expected {@see \Doctrine\DBAL\Types\Type::convertToDatabaseValue()}.
26
 *
27
 * In order to reproduce this, you must have identifiers implementing
28
 * `#__toString()` (to allow {@see \Doctrine\ORM\UnitOfWork} to hash them) and other accessors
29
 * that are used by the custom DBAL type during DB/PHP conversions.
30
 *
31
 * If `#__toString()` and the DBAL type conversions are asymmetric, then the paginator will fail
32
 * to find records.
33
 *
34
 * Tricky situation, but this very much affects `ramsey/uuid-doctrine` and anyone relying on (for
35
 * example) the {@see \Ramsey\Uuid\Doctrine\UuidBinaryType} type.
36
 */
37
class GH7820Test extends OrmFunctionalTestCase
38
{
39
    private const SONG = [
40
        'What is this song all about?',
41
        'Can\'t figure any lyrics out',
42
        'How do the words to it go?',
43
        'I wish you\'d tell me, I don\'t know',
44
        'Don\'t know, don\'t know, don\'t know, I don\'t know!',
45
        'Don\'t know, don\'t know, don\'t know...',
46
    ];
47
48
    protected function setUp() : void
49
    {
50
        parent::setUp();
51
52
        if (! Type::hasType(GH7820LineTextType::class)) {
53
            Type::addType(GH7820LineTextType::class, GH7820LineTextType::class);
54
        }
55
56
        $this->setUpEntitySchema([GH7820Line::class]);
57
58
        $this->_em->createQuery('DELETE FROM ' . GH7820Line::class . ' l')
59
            ->execute();
60
61
        foreach (self::SONG as $index => $line) {
62
            $this->_em->persist(new GH7820Line(GH7820LineText::fromText($line), $index));
63
        }
64
65
        $this->_em->flush();
66
    }
67
68
    public function testWillFindSongsInPaginator() : void
69
    {
70
        $query = $this->_em->getRepository(GH7820Line::class)
71
            ->createQueryBuilder('l')
72
            ->orderBy('l.lineNumber', Criteria::ASC);
73
74
        self::assertSame(
75
            self::SONG,
76
            array_map(static function (GH7820Line $line) : string {
77
                return $line->toString();
78
            }, iterator_to_array(new Paginator($query)))
79
        );
80
    }
81
82
    /** @group GH7837 */
83
    public function testWillFindSongsInPaginatorEvenWithCachedQueryParsing() : void
84
    {
85
        $cache = $this->_em->getConfiguration()
86
            ->getQueryCacheImpl();
87
88
        assert($cache instanceof ClearableCache);
89
90
        $cache->deleteAll();
91
92
        $query = $this->_em->getRepository(GH7820Line::class)
93
            ->createQueryBuilder('l')
94
            ->orderBy('l.lineNumber', Criteria::ASC);
95
96
        self::assertSame(
97
            self::SONG,
98
            array_map(static function (GH7820Line $line) : string {
99
                return $line->toString();
100
            }, iterator_to_array(new Paginator($query))),
101
            'Paginator runs once, query cache is populated with DQL -> SQL translation'
102
        );
103
104
        $query = $this->_em->getRepository(GH7820Line::class)
105
            ->createQueryBuilder('l')
106
            ->orderBy('l.lineNumber', Criteria::ASC);
107
108
        self::assertSame(
109
            self::SONG,
110
            array_map(static function (GH7820Line $line) : string {
111
                return $line->toString();
112
            }, iterator_to_array(new Paginator($query))),
113
            'Paginator runs again, SQL parameters are translated again, even with cached DQL -> SQL translation'
114
        );
115
    }
116
}
117
118
/** @Entity */
119
class GH7820Line
120
{
121
    /**
122
     * @var GH7820LineText
123
     * @Id()
124
     * @Column(type="Doctrine\Tests\ORM\Functional\Ticket\GH7820LineTextType")
125
     */
126
    private $text;
127
128
    /**
129
     * @var int
130
     * @Column(type="integer")
131
     */
132
    private $lineNumber;
133
134
    public function __construct(GH7820LineText $text, int $index)
135
    {
136
        $this->text       = $text;
137
        $this->lineNumber = $index;
138
    }
139
140
    public function toString() : string
141
    {
142
        return $this->text->getText();
143
    }
144
}
145
146
final class GH7820LineText
147
{
148
    /** @var string */
149
    private $text;
150
151
    private function __construct(string $text)
152
    {
153
        $this->text = $text;
154
    }
155
156
    public static function fromText(string $text) : self
157
    {
158
        return new self($text);
159
    }
160
161
    public function getText() : string
162
    {
163
        return $this->text;
164
    }
165
166
    public function __toString() : string
167
    {
168
        return 'Line: ' . $this->text;
169
    }
170
}
171
172
final class GH7820LineTextType extends StringType
173
{
174
    public function convertToPHPValue($value, AbstractPlatform $platform)
175
    {
176
        $text = parent::convertToPHPValue($value, $platform);
177
178
        if (! is_string($text)) {
179
            return $text;
180
        }
181
182
        return GH7820LineText::fromText($text);
183
    }
184
185
    public function convertToDatabaseValue($value, AbstractPlatform $platform)
186
    {
187
        if (! $value instanceof GH7820LineText) {
188
            return parent::convertToDatabaseValue($value, $platform);
189
        }
190
191
        return parent::convertToDatabaseValue($value->getText(), $platform);
192
    }
193
194
    /** {@inheritdoc} */
195
    public function getName() : string
196
    {
197
        return self::class;
198
    }
199
}
200