Doctrine按列表排序报错:索引3的位置参数未绑定值
解决Doctrine中使用自定义FIELD函数排序时的参数绑定异常
问题原因
错误Positional parameter at index 3 does not have a bound value.的核心是自定义FIELD函数的解析逻辑与参数传递方式不兼容:你通过:statuses传递数组参数,但当前的FIELD函数会将其拆分为多个位置参数,导致Doctrine无法找到对应的绑定值。
修复方案
1. 修改自定义FIELD函数,支持数组参数解析
调整函数逻辑,使其能识别数组参数并自动展开为多个绑定占位符:
<?php namespace App\DQL\Mysql; use Doctrine\ORM\Query\AST\Functions\FunctionNode; use Doctrine\ORM\Query\Lexer; use Doctrine\ORM\Query\Parser; use Doctrine\ORM\Query\SqlWalker; use Doctrine\ORM\Query\AST\Node; class FieldFunction extends FunctionNode { private Node $field; private array $values = []; private bool $isArrayParameter = false; public function parse(Parser $parser): void { $parser->match(Lexer::T_IDENTIFIER); $parser->match(Lexer::T_OPEN_PARENTHESIS); // 解析排序字段 $this->field = $parser->ArithmeticPrimary(); $lexer = $parser->getLexer(); if ($lexer->lookahead['type'] === Lexer::T_COMMA) { $parser->match(Lexer::T_COMMA); // 判断是否为数组参数(以:开头的标识符) if ($lexer->lookahead['type'] === Lexer::T_IDENTIFIER && str_starts_with($lexer->lookahead['value'], ':')) { $this->values[] = $parser->ArithmeticPrimary(); $this->isArrayParameter = true; } else { // 兼容传统多参数模式 do { $this->values[] = $parser->ArithmeticPrimary(); if ($lexer->lookahead['type'] !== Lexer::T_COMMA) break; $parser->match(Lexer::T_COMMA); } while (true); } } $parser->match(Lexer::T_CLOSE_PARENTHESIS); } public function getSql(SqlWalker $sqlWalker): string { $sql = 'FIELD(' . $this->field->dispatch($sqlWalker); if ($this->isArrayParameter) { // 展开数组参数为多个占位符并绑定值 $paramName = ltrim($this->values[0]->value, ':'); $paramValue = $sqlWalker->getQuery()->getParameter($paramName)->getValue(); $placeholders = []; foreach ($paramValue as $index => $value) { $placeholder = ":{$paramName}_{$index}"; $sqlWalker->getQuery()->setParameter($placeholder, $value); $placeholders[] = $placeholder; } $sql .= ', ' . implode(', ', $placeholders); } else { // 传统多参数拼接逻辑 foreach ($this->values as $value) { $sql .= ', ' . $value->dispatch($sqlWalker); } } $sql .= ')'; return $sql; } }
2. 调整控制器代码,避免参数覆盖
确保只调用一次setParameters,防止之前的权限过滤参数被覆盖:
$em = $doctrine->getManager(); $realEstateRepo = $em->getRepository(RealEstateAd::class); $userAdmin = $this->getUser() && $this->getUser()->isAdmin(); $query = $realEstateRepo->createQueryBuilder('rea'); $query->leftJoin("rea.realEstate", "re"); $parameters = []; if (!$userAdmin) { $parameters['deleted'] = false; $parameters['realEstateDeleted'] = false; $query->where('rea.deleted = :deleted') ->andWhere('re.deleted = :realEstateDeleted'); } // 追加排序参数 $parameters['statuses'] = [ Util::REAL_ESTATE_STATUS_AVAILABLE, Util::REAL_ESTATE_STATUS_RESERVED, Util::REAL_ESTATE_STATUS_LET_AGREED ]; // 统一设置所有参数,避免覆盖 $query->addOrderBy('FIELD(re.status, :statuses)') ->addOrderBy('rea.createdAt', 'DESC') ->setParameters($parameters); // 后续分页逻辑保持不变 $pageSize = 10; $allRealEstate = new Paginator($query); $totalItems = count($allRealEstate); $pagesCount = ceil($totalItems / $pageSize); if ($page > $pagesCount && $pagesCount != 0) { return $this->redirectToRoute('app_home'); } $allRealEstate ->getQuery() ->setFirstResult($pageSize * ($page-1)) ->setMaxResults($pageSize); return $this->render('site/home.html.twig', [ 'pageCount' => $pagesCount, 'page' => $page, 'realEstate' => $allRealEstate ]);
可选替代方案(无需修改FIELD函数)
如果不想调整FIELD函数,也可以直接传递多个独立参数:
// 排序语句改为多个参数 $query->addOrderBy('FIELD(re.status, :status1, :status2, :status3)'); // 参数逐个绑定 $parameters['status1'] = Util::REAL_ESTATE_STATUS_AVAILABLE; $parameters['status2'] = Util::REAL_ESTATE_STATUS_RESERVED; $parameters['status3'] = Util::REAL_ESTATE_STATUS_LET_AGREED;
此方案缺点是排序值数量变化时需同步修改代码,灵活性较差。
内容的提问来源于stack exchange,提问作者Artjom Tjasin
相关产品推荐
相关产品推荐

