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