Como aplicar o método bindValue na cláusula LIMIT?

117

Aqui está um instantâneo do meu código:

$fetchPictures = $PDO->prepare("SELECT * 
    FROM pictures 
    WHERE album = :albumId 
    ORDER BY id ASC 
    LIMIT :skip, :max");

$fetchPictures->bindValue(':albumId', $_GET['albumid'], PDO::PARAM_INT);

if(isset($_GET['skip'])) {
    $fetchPictures->bindValue(':skip', trim($_GET['skip']), PDO::PARAM_INT);    
} else {
    $fetchPictures->bindValue(':skip', 0, PDO::PARAM_INT);  
}

$fetchPictures->bindValue(':max', $max, PDO::PARAM_INT);
$fetchPictures->execute() or die(print_r($fetchPictures->errorInfo()));
$pictures = $fetchPictures->fetchAll(PDO::FETCH_ASSOC);

eu recebo

Você tem um erro em sua sintaxe SQL; verifique o manual que corresponde à versão do seu servidor MySQL para a sintaxe correta para usar próximo a '' 15 ', 15' na linha 1

Parece que o PDO está adicionando aspas simples às minhas variáveis ​​na parte LIMIT do código SQL. Eu pesquisei e encontrei este bug que acho que está relacionado: http://bugs.php.net/bug.php?id=44639

É isso que estou olhando? Este bug está aberto desde abril de 2008! O que devemos fazer enquanto isso?

Preciso construir alguma paginação e ter certeza de que os dados estão limpos e seguros para injeção de sql antes de enviar a instrução sql.

Nathan H
fonte

Respostas:

165

Eu me lembro de ter esse problema antes. Converta o valor em um inteiro antes de transmiti-lo à função de vinculação. Eu acho que isso resolve tudo.

$fetchPictures->bindValue(':skip', (int) trim($_GET['skip']), PDO::PARAM_INT);
Stephen Curran
fonte
37
Obrigado! Mas no PHP 5.3, o código acima gerava um erro dizendo "Erro fatal: não é possível passar o parâmetro 2 por referência". Não gosta de lançar um int ali. Em vez de (int) trim($_GET['skip']), tente intval(trim($_GET['skip'])).
Will Martin,
5
seria legal se alguém fornecesse a explicação do motivo disso ... do ponto de vista de design / segurança (ou outro).
Ross
6
Isso só funcionará se as instruções preparadas emuladas estiverem habilitadas . Ele irá falhar se for desativado (e deve ser desativado!)
Madara's Ghost
4
@Ross não posso responder especificamente a isso - mas posso apontar que LIMIT e OFFSET são recursos que foram colados DEPOIS de toda essa loucura de PHP / MYSQL / PDO chegar ao circuito de desenvolvimento ... Na verdade, acredito que foi o próprio Lerdorf quem supervisionou LIMIT implementação alguns anos atrás. Não, não responde à pergunta, mas indica que é um complemento do mercado de reposição, e você sabe como eles podem funcionar bem às vezes ...
FredTheWebGuy
2
@Ross PDO não permite vinculação a valores - ao invés, variáveis. Se você tentar bindParam (': something', 2), você terá um erro porque PDO usa um ponteiro para a variável que um número não pode ter (se $ i for 2, você pode ter um ponteiro para $ i, mas não para o número 2).
Kristijan de
44

A solução mais simples seria desligar o modo de emulação. Você pode fazer isso simplesmente adicionando a seguinte linha

$PDO->setAttribute( PDO::ATTR_EMULATE_PREPARES, false );

Além disso, este modo pode ser definido como um parâmetro do construtor ao criar uma conexão PDO . Poderia ser uma solução melhor, já que alguns relatam que o driver não suporta a setAttribute()função.

Isso não apenas resolverá seu problema com vinculação, mas também permitirá que você envie valores diretamente para o execute(), o que tornará seu código muito mais curto. Supondo que o modo de emulação já tenha sido definido, todo o caso levará até meia dúzia de linhas de código

$skip = isset($_GET['skip']) ? (int)trim($_GET['skip']) : 0;
$sql  = "SELECT * FROM pictures WHERE album = ? ORDER BY id LIMIT ?, ?";
$stmt  = $PDO->prepare($sql);
$stmt->execute([$_GET['albumid'], $skip, $max]);
$pictures = $stmt->fetchAll(PDO::FETCH_ASSOC);
Seu senso comum
fonte
SQLSTATE[IM001]: Driver does not support this function: This driver doesn't support setting attributes... Por que nunca é tão simples para mim :) Embora tenha certeza de que isso levará a maioria das pessoas até lá, no meu caso acabei tendo que usar algo semelhante à resposta aceita. Apenas um aviso aos futuros leitores!
Matthew Johnson
@MatthewJohnson que driver é?
Seu senso comum
Não tenho certeza, mas no manual diz PDO::ATTR_EMULATE_PREPARES Enables or disables emulation of prepared statements. Some drivers do not support native prepared statements or have limited support for them. É um novo para mim, mas, novamente, estou apenas começando com o PDO. Normalmente uso mysqli, mas achei melhor tentar ampliar meus horizontes.
Matthew Johnson
@MatthewJohnson se você estiver usando PDO para mysql, o driver suporta esta função corretamente. Então, você está recebendo esta mensagem devido a algum engano
Seu senso comum
1
Se você recebeu uma mensagem de problema de suporte ao driver, verifique novamente se você chama setAttributea instrução ($ stm, $ stmt) não para o objeto pdo.
Jehong Ahn
17

Olhando para o relatório de bug, o seguinte pode funcionar:

$fetchPictures->bindValue(':albumId', (int)$_GET['albumid'], PDO::PARAM_INT);

$fetchPictures->bindValue(':skip', (int)trim($_GET['skip']), PDO::PARAM_INT);  

mas tem certeza de que seus dados de entrada estão corretos? Porque na mensagem de erro, parece haver apenas uma citação após o número (em oposição ao número inteiro sendo colocado entre aspas). Isso também pode ser um erro com os dados de entrada. Você pode fazer um print_r($_GET);para descobrir?

Pekka
fonte
1
'' 15 ', 15'. O primeiro número está totalmente entre aspas. O segundo número não tem aspas. Sim, os dados são bons.
Nathan H
8

Isso é apenas um resumo.
Existem quatro opções para parametrizar os valores LIMIT / OFFSET:

  1. Desative PDO::ATTR_EMULATE_PREPARESconforme mencionado acima .

    O que evita que os valores passados ​​por ->execute([...])sempre apareçam como strings.

  2. Mude para ->bindValue(..., ..., PDO::PARAM_INT)população de parâmetro manual .

    O que, entretanto, é menos conveniente do que um -> execute list [].

  3. Basta abrir uma exceção aqui e apenas interpolar inteiros simples ao preparar a consulta SQL.

     $limit = intval($limit);
     $s = $pdo->prepare("SELECT * FROM tbl LIMIT {$limit}");

    O elenco é importante. Mais comumente você vê ->prepare(sprintf("SELECT ... LIMIT %d", $num))usado para tais fins.

  4. Se você não estiver usando MySQL, mas por exemplo SQLite ou Postgres; você também pode lançar parâmetros vinculados diretamente no SQL.

     SELECT * FROM tbl LIMIT (1 * :limit)

    Novamente, MySQL / MariaDB não oferece suporte a expressões na cláusula LIMIT. Ainda não.

mario
fonte
1
Eu teria usado sprintf () com% d para 3, eu diria que é um pouco mais estável do que com a variável.
hakre
Sim, o varfunc cast + interpolation não é o exemplo mais prático. Costumo usar meu preguiçoso {$_GET->int["limit"]}para esses casos.
mario
7

para LIMIT :init, :end

Você precisa ligar dessa forma. se você tiver algo parecido $req->execute(Array());, não funcionará, pois será lançado PDO::PARAM_STRpara todos os vars no array e, para isso, LIMITvocê absolutamente precisa de um inteiro. bindValue ou BindParam como você deseja.

$fetchPictures->bindValue(':albumId', (int)$_GET['albumid'], PDO::PARAM_INT);
Nicolas Manzini
fonte
2

Como ninguém explicou por que isso está acontecendo, estou adicionando uma resposta. A razão está se comportando dessa era é porque você está usando trim(). Se você olhar o manual do PHP para trim, o tipo de retorno é string. Você está então tentando passar isso como PDO::PARAM_INT. Algumas maneiras de contornar isso são:

  1. Use filter_var($integer, FILTER_VALIDATE_NUMBER_INT)para certificar-se de que está passando um número inteiro.
  2. Como outros disseram, usando intval()
  3. Fundição com (int)
  4. Verificar se é um inteiro com is_int()

Existem muitas outras maneiras, mas esta é basicamente a causa raiz.

Melissa Williams
fonte
3
Isso acontece mesmo quando a variável sempre foi um inteiro.
felwithe
1

bindValue offset and limit usando PDO :: PARAM_INT e vai funcionar

Karel
fonte
-1

// BEFORE (Erro presente) $ query = ".... LIMIT: p1, 30;"; ... $ stmt-> bindParam (': p1', $ limiteInferior);

// AFTER (erro corrigido) $ query = ".... LIMIT: p1, 30;"; ... $ limiteInferior = (int) $ limiteInferior; $ stmt-> bindParam (': p1', $ limiteInferior, PDO :: PARAM_INT);

Brayan Josue Medina Melendez
fonte
-1

PDO::ATTR_EMULATE_PREPARES me deu o

O driver não suporta esta função: Este driver não suporta erro de configuração de atributos.

Minha solução alternativa foi definir uma $limitvariável como uma string e, em seguida, combiná-la na instrução prepare como no exemplo a seguir:

$limit = ' LIMIT ' . $from . ', ' . $max_results;
$stmt = $pdo->prepare( 'SELECT * FROM users WHERE company_id = :cid ORDER BY name ASC' . $limit . ';' );
try {
    $stmt->execute( array( ':cid' => $company_id ) );
    ...
}
catch ( Exception $e ) {
    ...
}
Barbatanas
fonte
-1

Há muita coisa acontecendo entre as diferentes versões do PHP e as esquisitices do PDO. Tentei 3 ou 4 métodos aqui, mas não consegui fazer o LIMIT funcionar.
Minha sugestão é usar formatação / concatinação de string COM um filtro intval () :

$sql = 'SELECT * FROM `table` LIMIT ' . intval($limitstart) . ' , ' . intval($num).';';

É muito importante usar intval () para prevenir injeção de SQL, particularmente se você está obtendo seu limite de $ _GET ou algo parecido. Se você fizer isso, esta é a maneira mais fácil de fazer o LIMIT funcionar.

Fala-se muito sobre 'O problema com LIMIT em PDO', mas meu pensamento aqui é que os parâmetros de PDO nunca foram usados ​​para LIMIT, pois eles sempre serão inteiros e um filtro rápido funciona. Ainda assim, é um pouco enganador, pois a filosofia sempre foi não fazer nenhuma filtragem por injeção de SQL, mas sim 'Faça com que o PDO cuide disso'.

Tycon
fonte