使用 Doctrine querybuilder 正确转义 LIKE 查询

Proper escaping of LIKE queries with Doctrine querybuilder

我正在尝试使用 Doctrine QueryBuilder 进行 LIKE 查询。我阅读了其他各种问题和文章,我必须正确地避免这种类型的查询,但我不明白 Doctrine 是否会自己做。以此数据为例:

my_column
ABC
ABCD
A%BCD

及以下输入数据

ABC
ABCD
A
A%

我期望这些结果:

SELECT * FROM my_table WHERE my_column LIKE "%ABC%" => ABC, ABCD
SELECT * FROM my_table WHERE my_column LIKE "%ABCD%" => ABCD
SELECT * FROM my_table WHERE my_column LIKE "%A%" => ABC, ABCD, A, A%
SELECT * FROM my_table WHERE my_column LIKE "%A%%" => A%

我的问题与最新查询(和输入数据)有关。我应该如何正确转义该查询? '%' . addcslashes($input, '%_') . '%' 够吗?

我准备了这个 SQL Fiddle 如果它可以帮助:http://sqlfiddle.com/#!9/35bc8/9

正如 John Kary 在 this gist 中发现的那样,Doctrine 不会转义 LIKE 查询语句。更具体地说,它使用准备好的语句转义参数(像反斜杠或引号这样的字符被正确转义)但是像 %_ 这样的字符是 LIKE 语句语法的一部分而不是转义并且输入未被清理。下面链接的要点提供了一个很好的方法来解决这个问题,用 Symfony 2.6 和 Doctrine 2.5 测试它完美地工作。为了确保要点不会被删除,我将代码复制到这里:

<?php
namespace Foo;

/**
 * Methods for safe LIKE querying.
 */
trait LikeQueryHelpers
{
    /**
     * Format a value that can be used as a parameter for a DQL LIKE search.
     *
     * $qb->where("u.name LIKE (:name) ESCAPE '!'")
     *    ->setParameter('name', $this->makeLikeParam('john'))
     *
     * NOTE: You MUST manually specify the `ESCAPE '!'` in your DQL query, AND the
     * ! character MUST be wrapped in single quotes, else the Doctrine DQL
     * parser will throw an error:
     *
     * [Syntax Error] line 0, col 127: Error: Expected Doctrine\ORM\Query\Lexer::T_STRING, got '"'
     *
     * Using the $pattern argument you can change the LIKE pattern your query
     * matches again. Default is "%search%". Remember that "%%" in a sprintf
     * pattern is an escaped "%".
     *
     * Common usage:
     *
     * ->makeLikeParam('foo')         == "%foo%"
     * ->makeLikeParam('foo', '%s%%') == "foo%"
     * ->makeLikeParam('foo', '%s_')  == "foo_"
     * ->makeLikeParam('foo', '%%%s') == "%foo"
     * ->makeLikeParam('foo', '_%s')  == "_foo"
     *
     * Escapes LIKE wildcards using '!' character:
     *
     * ->makeLikeParam('foo_bar') == "%foo!_bar%"
     *
     * @param string $search        Text to search for LIKE
     * @param string $pattern       sprintf-compatible substitution pattern
     * @return string
     */
    protected function makeLikeParam($search, $pattern = '%%%s%%')
    {
        /**
         * Function defined in-line so it doesn't show up for type-hinting on
         * classes that implement this trait.
         *
         * Makes a string safe for use in an SQL LIKE search query by escaping all
         * special characters with special meaning when used in a LIKE query.
         *
         * Uses ! character as default escape character because \ character in
         * Doctrine/DQL had trouble accepting it as a single \ and instead kept
         * trying to escape it as "\". Resulted in DQL parse errors about "Escape
         * character must be 1 character"
         *
         * % = match 0 or more characters
         * _ = match 1 character
         *
         * Examples:
         *      gloves_pink   becomes  gloves!_pink
         *      gloves%pink   becomes  gloves!%pink
         *      glo_ves%pink  becomes  glo!_ves!%pink
         *
         * @param string $search
         * @return string
         */
        $sanitizeLikeValue = function ($search) {
            $escapeChar = '!';
            $escape = [
                '\' . $escapeChar, // Must escape the escape-character for regex
                '\%',
                '\_',
            ];
            $pattern = sprintf('/([%s])/', implode('', $escape));
            return preg_replace($pattern, $escapeChar . '[=10=]', $search);
        };
        return sprintf($pattern, $sanitizeLikeValue($search));
    }
}

它像这个例子一样使用:

<?php
namespace Foo\Entity;

use Doctrine\ORM\EntityRepository;
use Foo\LikeQueryHelpers;

class ProductRepository extends EntityRepository
{
    use LikeQueryHelpers;
    /**
     * Find Product entities containing searched terms
     *
     * @param string $term
     * @return Product[]
     */
    public function findInSearchableFields($term)
    {
        return $this->createQueryBuilder('p')
            ->where("p.title LIKE :title ESCAPE '!'")
            ->setParameter('title', $this->makeLikeParam($term))
            ->getQuery()
            ->execute();
    }
}