diff --git a/docs/en/cookbook/aggregate-fields.rst b/docs/en/cookbook/aggregate-fields.rst index 60677522697..67d97c3139e 100644 --- a/docs/en/cookbook/aggregate-fields.rst +++ b/docs/en/cookbook/aggregate-fields.rst @@ -317,7 +317,7 @@ The aggregate field ``Account::$balance`` is now -200, however the SUM over all entries amounts yields -400. A violation of our max credit rule. -You can use both optimistic or pessimistic locking to save-guard +You can use both optimistic or pessimistic locking to safe-guard your aggregate fields against this kind of race-conditions. Reading Eric Evans DDD carefully he mentions that the "Aggregate Root" (Account in our example) needs a locking mechanism. diff --git a/docs/en/reference/inheritance-mapping.rst b/docs/en/reference/inheritance-mapping.rst index e3d853f16a5..77613bd2989 100644 --- a/docs/en/reference/inheritance-mapping.rst +++ b/docs/en/reference/inheritance-mapping.rst @@ -436,7 +436,7 @@ Could be used by an entity that extends a mapped superclass to override a field * column=@Column( * name = "guest_id", * type = "integer", - length = 140 + * length = 140 * ) * ), * @AttributeOverride(name="name", @@ -444,7 +444,7 @@ Could be used by an entity that extends a mapped superclass to override a field * name = "guest_name", * nullable = false, * unique = true, - length = 240 + * length = 240 * ) * ) * }) diff --git a/lib/Doctrine/ORM/EntityManager.php b/lib/Doctrine/ORM/EntityManager.php index 1754032ae2f..c06db61a7d7 100644 --- a/lib/Doctrine/ORM/EntityManager.php +++ b/lib/Doctrine/ORM/EntityManager.php @@ -904,7 +904,7 @@ private function checkLockRequirements(int $lockMode, ClassMetadata $class) : vo if (! $class->isVersioned()) { throw OptimisticLockException::notVersioned($class->getClassName()); } - // Intentional fallthrough + break; case LockMode::PESSIMISTIC_READ: case LockMode::PESSIMISTIC_WRITE: if (! $this->getConnection()->isTransactionActive()) { diff --git a/lib/Doctrine/ORM/EntityRepository.php b/lib/Doctrine/ORM/EntityRepository.php index 59cfed3f612..c1a71c3d0e6 100644 --- a/lib/Doctrine/ORM/EntityRepository.php +++ b/lib/Doctrine/ORM/EntityRepository.php @@ -7,8 +7,8 @@ use Doctrine\Common\Collections\Collection; use Doctrine\Common\Collections\Criteria; use Doctrine\Common\Collections\Selectable; +use Doctrine\Common\Inflector\Inflector; use Doctrine\Common\Persistence\ObjectRepository; -use Doctrine\Common\Util\Inflector; use Doctrine\ORM\Mapping\ClassMetadata; use Doctrine\ORM\Query\ResultSetMappingBuilder; use Doctrine\ORM\Repository\Exception\InvalidMagicMethodCall; diff --git a/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php b/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php index bba97823c62..ebab36818fb 100644 --- a/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php +++ b/lib/Doctrine/ORM/Internal/CommitOrderCalculator.php @@ -142,6 +142,16 @@ private function visit($vertex) case self::IN_PROGRESS: if (isset($adjacentVertex->dependencyList[$vertex->hash]) && $adjacentVertex->dependencyList[$vertex->hash]->weight < $edge->weight) { + // If we have some non-visited dependencies in the in-progress dependency, we + // need to visit them before adding the node. + foreach ($adjacentVertex->dependencyList as $adjacentEdge) { + $adjacentEdgeVertex = $this->nodeList[$adjacentEdge->to]; + + if ($adjacentEdgeVertex->state === self::NOT_VISITED) { + $this->visit($adjacentEdgeVertex); + } + } + $adjacentVertex->state = self::VISITED; $this->sortedNodeList[] = $adjacentVertex->value; diff --git a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php index 7d9abe6bacb..5325773ffd6 100644 --- a/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php +++ b/lib/Doctrine/ORM/Mapping/Driver/DatabaseDriver.php @@ -4,7 +4,7 @@ namespace Doctrine\ORM\Mapping\Driver; -use Doctrine\Common\Util\Inflector; +use Doctrine\Common\Inflector\Inflector; use Doctrine\DBAL\Schema\AbstractSchemaManager; use Doctrine\DBAL\Schema\Column; use Doctrine\DBAL\Schema\ForeignKeyConstraint; diff --git a/lib/Doctrine/ORM/Query/Expr.php b/lib/Doctrine/ORM/Query/Expr.php index 70c34cdaab0..c23c5405d55 100644 --- a/lib/Doctrine/ORM/Query/Expr.php +++ b/lib/Doctrine/ORM/Query/Expr.php @@ -27,8 +27,8 @@ class Expr * // (u.type = ?1) AND (u.role = ?2) * $expr->andX($expr->eq('u.type', ':1'), $expr->eq('u.role', ':2')); * - * @param Expr\Comparison|Expr\Func|Expr\Orx $x Optional clause. Defaults to null, but requires at least one - * defined when converting to string. + * @param Expr\Comparison|Expr\Func|Expr\Orx|string $x Optional clause. Defaults to null, but requires at least one + * defined when converting to string. * * @return Expr\Andx */ diff --git a/lib/Doctrine/ORM/Query/Expr/Base.php b/lib/Doctrine/ORM/Query/Expr/Base.php index 80d6f480485..90ab101e125 100644 --- a/lib/Doctrine/ORM/Query/Expr/Base.php +++ b/lib/Doctrine/ORM/Query/Expr/Base.php @@ -32,7 +32,7 @@ abstract class Base protected $parts = []; /** - * @param mixed[] $args + * @param mixed $args */ public function __construct($args = []) { @@ -40,7 +40,7 @@ public function __construct($args = []) } /** - * @param mixed[] $args + * @param mixed $args * * @return Base */ diff --git a/lib/Doctrine/ORM/Query/Lexer.php b/lib/Doctrine/ORM/Query/Lexer.php index eb1df1a407d..d384c15998e 100644 --- a/lib/Doctrine/ORM/Query/Lexer.php +++ b/lib/Doctrine/ORM/Query/Lexer.php @@ -4,6 +4,7 @@ namespace Doctrine\ORM\Query; +use Doctrine\Common\Lexer\AbstractLexer; use function constant; use function ctype_alpha; use function defined; @@ -18,7 +19,7 @@ /** * Scans a DQL query for tokens. */ -class Lexer extends \Doctrine\Common\Lexer +class Lexer extends AbstractLexer { // All tokens that are not valid identifiers must be < 100 public const T_NONE = 1; diff --git a/lib/Doctrine/ORM/Query/Parser.php b/lib/Doctrine/ORM/Query/Parser.php index e576f0f3b88..deb1e1286a5 100644 --- a/lib/Doctrine/ORM/Query/Parser.php +++ b/lib/Doctrine/ORM/Query/Parser.php @@ -2907,6 +2907,10 @@ public function StringPrimary() case Lexer::T_COALESCE: case Lexer::T_NULLIF: return $this->CaseExpression(); + default: + if ($this->isAggregateFunction($lookaheadType)) { + return $this->AggregateExpression(); + } } $this->syntaxError( diff --git a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php index 0a50473a09d..9ffdc0e2f4e 100644 --- a/lib/Doctrine/ORM/Tools/Pagination/Paginator.php +++ b/lib/Doctrine/ORM/Tools/Pagination/Paginator.php @@ -123,6 +123,7 @@ public function getIterator() $subQuery->setHint(Query::HINT_CUSTOM_OUTPUT_WALKER, LimitSubqueryOutputWalker::class); } else { $this->appendTreeWalker($subQuery, LimitSubqueryWalker::class); + $this->unbindUnusedQueryParams($subQuery); } $subQuery->setFirstResult($offset)->setMaxResults($length); @@ -236,14 +237,21 @@ private function getCountQuery() $countQuery->setResultSetMapping($rsm); } else { $this->appendTreeWalker($countQuery, CountWalker::class); + $this->unbindUnusedQueryParams($countQuery); } $countQuery->setFirstResult(null)->setMaxResults(null); - $parser = new Parser($countQuery); + return $countQuery; + } + + private function unbindUnusedQueryParams(Query $query) : void + { + $parser = new Parser($query); $parameterMappings = $parser->parse()->getParameterMappings(); + /** @var Collection|Parameter[] $parameters */ - $parameters = $countQuery->getParameters(); + $parameters = $query->getParameters(); foreach ($parameters as $key => $parameter) { $parameterName = $parameter->getName(); @@ -253,8 +261,6 @@ private function getCountQuery() } } - $countQuery->setParameters($parameters); - - return $countQuery; + $query->setParameters($parameters); } } diff --git a/tests/Doctrine/Tests/ORM/CommitOrderCalculatorTest.php b/tests/Doctrine/Tests/ORM/CommitOrderCalculatorTest.php index 88c7f115e8f..6756a41882f 100644 --- a/tests/Doctrine/Tests/ORM/CommitOrderCalculatorTest.php +++ b/tests/Doctrine/Tests/ORM/CommitOrderCalculatorTest.php @@ -80,6 +80,39 @@ public function testCommitOrdering2() : void self::assertSame($correctOrder, $sorted); } + + public function testCommitOrdering3() + { + // this test corresponds to the GH7259Test::testPersistFileBeforeVersion functional test + $class1 = new ClassMetadata(NodeClass1::class, $this->metadataBuildingContext); + $class2 = new ClassMetadata(NodeClass2::class, $this->metadataBuildingContext); + $class3 = new ClassMetadata(NodeClass3::class, $this->metadataBuildingContext); + $class4 = new ClassMetadata(NodeClass4::class, $this->metadataBuildingContext); + + $this->calc->addNode($class1->getClassName(), $class1); + $this->calc->addNode($class2->getClassName(), $class2); + $this->calc->addNode($class3->getClassName(), $class3); + $this->calc->addNode($class4->getClassName(), $class4); + + $this->calc->addDependency($class4->getClassName(), $class1->getClassName(), 1); + $this->calc->addDependency($class1->getClassName(), $class2->getClassName(), 1); + $this->calc->addDependency($class4->getClassName(), $class3->getClassName(), 1); + $this->calc->addDependency($class1->getClassName(), $class4->getClassName(), 0); + + $sorted = $this->calc->sort(); + + // There is only multiple valid ordering for this constellation, but + // the class4, class1, class2 ordering is important to break the cycle + // on the nullable link. + $correctOrders = [ + [$class4, $class1, $class2, $class3], + [$class4, $class1, $class3, $class2], + [$class4, $class3, $class1, $class2], + ]; + + // We want to perform a strict comparison of the array + $this->assertContains($sorted, $correctOrders, '', false, true, true); + } } class NodeClass1 diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7259Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7259Test.php new file mode 100644 index 00000000000..b43bbf81d80 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7259Test.php @@ -0,0 +1,167 @@ +setUpEntitySchema([GH7259Space::class, GH7259File::class, GH7259FileVersion::class, GH7259Feed::class]); + } + + /** + * @group 7259 + */ + public function testPersistFileBeforeVersion() : void + { + $space = new GH7259Space(); + + $this->em->persist($space); + $this->em->flush(); + + $feed = new GH7259Feed(); + $feed->space = $space; + + $file = new GH7259File(); + $file->space = $space; + $fileVersion = new GH7259FileVersion(); + $fileVersion->file = $file; + + $this->em->persist($file); + $this->em->persist($fileVersion); + $this->em->persist($feed); + + $this->em->flush(); + + self::assertNotNull($fileVersion->id); + } + + /** + * @group 7259 + */ + public function testPersistFileAfterVersion() : void + { + $space = new GH7259Space(); + + $this->em->persist($space); + $this->em->flush(); + $this->em->clear(); + + $space = $this->em->find(GH7259Space::class, $space->id); + + $feed = new GH7259Feed(); + $feed->space = $space; + + $file = new GH7259File(); + $file->space = $space; + $fileVersion = new GH7259FileVersion(); + $fileVersion->file = $file; + + $this->em->persist($fileVersion); + $this->em->persist($file); + $this->em->persist($feed); + + $this->em->flush(); + + self::assertNotNull($fileVersion->id); + } +} + +/** + * @ORM\Entity() + */ +class GH7259File +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity=GH7259Space::class) + * @ORM\JoinColumn(nullable=false) + * + * @var GH7259Space|null + */ + public $space; +} + +/** + * @ORM\Entity() + */ +class GH7259FileVersion +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity=GH7259File::class) + * @ORM\JoinColumn(nullable=false) + * + * @var GH7259File|null + */ + public $file; +} + +/** + * @ORM\Entity() + */ +class GH7259Space +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity=GH7259File::class) + * @ORM\JoinColumn(nullable=true) + * + * @var GH7259File|null + */ + public $ruleFile; +} + +/** + * @ORM\Entity() + */ +class GH7259Feed +{ + /** + * @ORM\Id + * @ORM\GeneratedValue + * @ORM\Column(type="integer") + * + * @var int + */ + public $id; + + /** + * @ORM\ManyToOne(targetEntity=GH7259Space::class) + * @ORM\JoinColumn(nullable=false) + * + * @var GH7259Space|null + */ + public $space; +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7286Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7286Test.php new file mode 100644 index 00000000000..67429f796fe --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7286Test.php @@ -0,0 +1,138 @@ +setUpEntitySchema( + [ + GH7286Entity::class, + ] + ); + + $this->em->persist(new GH7286Entity('foo', 1)); + $this->em->persist(new GH7286Entity('foo', 2)); + $this->em->persist(new GH7286Entity('bar', 3)); + $this->em->persist(new GH7286Entity(null, 4)); + $this->em->flush(); + $this->em->clear(); + } + + public function testAggregateExpressionInFunction() : void + { + $query = $this->em->createQuery( + 'SELECT CONCAT(e.type, MIN(e.version)) pair' + . ' FROM ' . GH7286Entity::class . ' e' + . ' WHERE e.type IS NOT NULL' + . ' GROUP BY e.type' + . ' ORDER BY e.type' + ); + + self::assertSame( + [ + ['pair' => 'bar3'], + ['pair' => 'foo1'], + ], + $query->getArrayResult() + ); + } + + /** + * @group DDC-1091 + */ + public function testAggregateFunctionInCustomFunction() : void + { + $this->em->getConfiguration()->addCustomStringFunction('CC', GH7286CustomConcat::class); + + $query = $this->em->createQuery( + 'SELECT CC(e.type, MIN(e.version)) pair' + . ' FROM ' . GH7286Entity::class . ' e' + . ' WHERE e.type IS NOT NULL AND e.type != :type' + . ' GROUP BY e.type' + ); + $query->setParameter('type', 'bar'); + + self::assertSame( + ['pair' => 'foo1'], + $query->getSingleResult() + ); + } +} + +/** + * @ORM\Entity + */ +class GH7286Entity +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * @var int + */ + public $id; + + /** + * @ORM\Column(nullable=true) + * @var string|null + */ + public $type; + + /** + * @ORM\Column(type="integer") + * @var int + */ + public $version; + + public function __construct(?string $type, int $version) + { + $this->type = $type; + $this->version = $version; + } +} + +class GH7286CustomConcat extends FunctionNode +{ + /** @var Node */ + private $first; + + /** @var Node */ + private $second; + + public function parse(Parser $parser) : void + { + $parser->match(Lexer::T_IDENTIFIER); + $parser->match(Lexer::T_OPEN_PARENTHESIS); + + $this->first = $parser->StringPrimary(); + $parser->match(Lexer::T_COMMA); + $this->second = $parser->StringPrimary(); + + $parser->match(Lexer::T_CLOSE_PARENTHESIS); + } + + public function getSql(SqlWalker $walker) : string + { + return $walker->getConnection()->getDatabasePlatform()->getConcatExpression( + $this->first->dispatch($walker), + $this->second->dispatch($walker) + ); + } +} diff --git a/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7366Test.php b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7366Test.php new file mode 100644 index 00000000000..a7abd70ac79 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Functional/Ticket/GH7366Test.php @@ -0,0 +1,78 @@ +setUpEntitySchema( + [ + GH7366Entity::class, + ] + ); + + $this->em->persist(new GH7366Entity('baz')); + $this->em->flush(); + $this->em->clear(); + } + + public function testOptimisticLockNoExceptionOnFind() : void + { + try { + $entity = $this->em->find(GH7366Entity::class, 1, LockMode::OPTIMISTIC); + } catch (TransactionRequiredException $e) { + self::fail('EntityManager::find() threw TransactionRequiredException with LockMode::OPTIMISTIC'); + } + self::assertEquals('baz', $entity->getName()); + } +} + +/** + * @ORM\Entity + */ +class GH7366Entity +{ + /** + * @ORM\Id + * @ORM\Column(type="integer") + * @ORM\GeneratedValue + * @var int + */ + public $id; + + /** + * @ORM\Column(type="integer") + * @ORM\Version + */ + protected $lockVersion = 1; + + /** + * @ORM\Column(length=32) + * @var string + */ + protected $name; + + + public function __construct(string $name) + { + $this->name = $name; + } + + public function getName() : string + { + return $this->name; + } +} diff --git a/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php b/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php index 5f7f146825b..096aedad367 100644 --- a/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php +++ b/tests/Doctrine/Tests/ORM/Mapping/XmlMappingDriverTest.php @@ -171,7 +171,7 @@ public function testInvalidMappingFileException() : void public function testValidateXmlSchema($xmlMappingFile) : void { $xsdSchemaFile = __DIR__ . '/../../../../../doctrine-mapping.xsd'; - $dom = new \DOMDocument('UTF-8'); + $dom = new \DOMDocument(); $dom->load($xmlMappingFile); diff --git a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php index e13debb8a5e..cb45daf5a05 100644 --- a/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php +++ b/tests/Doctrine/Tests/ORM/Query/LanguageRecognitionTest.php @@ -696,6 +696,13 @@ public function testNewLiteralWithSubselectExpression() : void { self::assertValidDQL('SELECT new ' . __NAMESPACE__ . "\\DummyStruct(u.id, 'foo', (SELECT 1 FROM Doctrine\Tests\Models\CMS\CmsUser su), true) FROM Doctrine\Tests\Models\CMS\CmsUser u"); } + + public function testStringPrimaryAcceptsAggregateExpression() : void + { + $this->assertValidDQL( + 'SELECT CONCAT(a.topic, MAX(a.version)) last FROM Doctrine\Tests\Models\CMS\CmsArticle a GROUP BY a' + ); + } } /** @ORM\Entity */ diff --git a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php index ed1cf39ea22..f844d26ae64 100644 --- a/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php +++ b/tests/Doctrine/Tests/ORM/Query/SelectSqlGenerationTest.php @@ -849,9 +849,13 @@ public function testLimitAndOffsetFromQueryClass() : void ->setMaxResults(10) ->setFirstResult(0); - self::assertEquals( - 'SELECT t0."id" AS c0, t0."status" AS c1, t0."username" AS c2, t0."name" AS c3, t0."email_id" AS c4 FROM "cms_users" t0 LIMIT 10 OFFSET 0', - $q->getSql() + // DBAL 2.8+ doesn't add OFFSET part when offset is 0 + self::assertThat( + $q->getSql(), + self::logicalOr( + self::identicalTo('SELECT t0."id" AS c0, t0."status" AS c1, t0."username" AS c2, t0."name" AS c3, t0."email_id" AS c4 FROM "cms_users" t0 LIMIT 10'), + self::identicalTo('SELECT t0."id" AS c0, t0."status" AS c1, t0."username" AS c2, t0."name" AS c3, t0."email_id" AS c4 FROM "cms_users" t0 LIMIT 10 OFFSET 0') + ) ); } diff --git a/tests/Doctrine/Tests/ORM/Tools/Pagination/PaginatorTest.php b/tests/Doctrine/Tests/ORM/Tools/Pagination/PaginatorTest.php new file mode 100644 index 00000000000..e9a538b3275 --- /dev/null +++ b/tests/Doctrine/Tests/ORM/Tools/Pagination/PaginatorTest.php @@ -0,0 +1,119 @@ +connection = $this->getMockBuilder(ConnectionMock::class) + ->setConstructorArgs([[], new DriverMock()]) + ->setMethods(['executeQuery']) + ->getMock() + ; + + $this->em = $this->getMockBuilder(EntityManagerDecorator::class) + ->setConstructorArgs([$this->getTestEntityManager($this->connection)]) + ->setMethods(['newHydrator']) + ->getMock() + ; + + $this->hydrator = $this->createMock(AbstractHydrator::class); + $this->em->method('newHydrator')->willReturn($this->hydrator); + } + + public function testExtraParametersAreStrippedWhenWalkerRemovingOriginalSelectElementsIsUsed() : void + { + $paramInWhere = 1; + $paramInSubSelect = 2; + $returnedIds = [10]; + + $this->hydrator->method('hydrateAll')->willReturn([$returnedIds]); + + $query = new Query($this->em); + $query->setDQL( + 'SELECT u, + ( + SELECT MAX(a.version) + FROM Doctrine\\Tests\\Models\\CMS\\CmsArticle a + WHERE a.user = u AND 1 = :paramInSubSelect + ) AS HIDDEN max_version + FROM Doctrine\\Tests\\Models\\CMS\\CmsUser u + WHERE u.id = :paramInWhere' + ); + $query->setParameters(['paramInWhere' => $paramInWhere, 'paramInSubSelect' => $paramInSubSelect]); + $paginator = (new Paginator($query, true))->setUseOutputWalkers(false); + + $this->connection->expects($this->exactly(3))->method('executeQuery'); + + $this->connection->expects($this->at(0)) + ->method('executeQuery') + ->with($this->anything(), [$paramInWhere]) + ; + + $this->connection->expects($this->at(1)) + ->method('executeQuery') + ->with($this->anything(), [$paramInWhere]) + ; + + $this->connection->expects($this->at(2)) + ->method('executeQuery') + ->with($this->anything(), [$paramInSubSelect, $paramInWhere, $returnedIds]) + ; + + $paginator->count(); + $paginator->getIterator(); + } + + public function testPaginatorNotCaringAboutExtraParametersWithoutOutputWalkers() : void + { + $this->connection->expects($this->exactly(3))->method('executeQuery'); + + $this->createPaginatorWithExtraParametersWithoutOutputWalkers([])->count(); + $this->createPaginatorWithExtraParametersWithoutOutputWalkers([[10]])->count(); + $this->createPaginatorWithExtraParametersWithoutOutputWalkers([])->getIterator(); + } + + public function testgetIteratorDoesCareAboutExtraParametersWithoutOutputWalkersWhenResultIsNotEmpty() : void + { + $this->connection->expects($this->exactly(1))->method('executeQuery'); + $this->expectException(Query\QueryException::class); + $this->expectExceptionMessage('Too many parameters: the query defines 1 parameters and you bound 2'); + + $this->createPaginatorWithExtraParametersWithoutOutputWalkers([[10]])->getIterator(); + } + + /** + * @param int[][] $willReturnRows + */ + private function createPaginatorWithExtraParametersWithoutOutputWalkers(array $willReturnRows) : Paginator + { + $this->hydrator->method('hydrateAll')->willReturn($willReturnRows); + $this->connection->method('executeQuery')->with($this->anything(), []); + + $query = new Query($this->em); + $query->setDQL('SELECT u FROM Doctrine\\Tests\\Models\\CMS\\CmsUser u'); + $query->setParameters(['paramInWhere' => 1]); + + return (new Paginator($query, true))->setUseOutputWalkers(false); + } +}