PHP: uso de PDO con matriz de cláusula IN

Estoy usando PDO para ejecutar una declaración con una INcláusula que usa una matriz para sus valores:

$in_array = array(1, 2, 3);
$in_values = implode(',', $in_array);
$my_result = $wbdb->prepare("SELECT * FROM my_table WHERE my_value IN (".$in_values.")");
$my_result->execute();
$my_results = $my_result->fetchAll();

El código anterior funciona perfectamente bien, pero mi pregunta es por qué esto no funciona:
 $in_array = array(1, 2, 3);
    $in_values = implode(',', $in_array);
    $my_result = $wbdb->prepare("SELECT * FROM my_table WHERE my_value IN (:in_values)");
    $my_result->execute(array(':in_values' => $in_values));
    $my_results = $my_result->fetchAll();

Este código devolverá el elemento que my_valuees igual al primer elemento en $in_array(1), pero no los elementos restantes en la matriz (2 y 3).

Answer

PDO no es bueno con esas cosas. Debe crear una cadena con marcadores de posición dinámicamente e insertarla en la consulta, mientras vincula los valores de matriz de la manera habitual. Con marcadores posicionales sería así:

$in  = str_repeat('?,', count($in_array) - 1) . '?';
$sql = "SELECT * FROM my_table WHERE my_value IN ($in)";
$stm = $db->prepare($sql);
$stm->execute($in_array);
$data = $stm->fetchAll();

En caso de que haya otros marcadores de posición en la consulta, puede usar el siguiente enfoque (el código está tomado de mi tutorial de PDO ):

Puede usar array_merge()la función para unir todas las variables en una sola matriz, agregando sus otras variables en forma de matrices, en el orden en que aparecen en su consulta:

$arr = [1,2,3];
$in  = str_repeat('?,', count($arr) - 1) . '?';
$sql = "SELECT * FROM table WHERE foo=? AND column IN ($in) AND bar=? AND baz=?";
$stm = $db->prepare($sql);
$params = array_merge([$foo], $arr, [$bar, $baz]);
$stm->execute($params);
$data = $stm->fetchAll();

En caso de que esté utilizando marcadores de posición con nombre, el código sería un poco más complejo, ya que debe crear una secuencia de marcadores de posición con nombre, p :id0,:id1,:id2. Entonces el código sería:

// other parameters that are going into query
$params = ["foo" => "foo", "bar" => "bar"];

$ids = [1,2,3];
$in = "";
$i = 0; // we are using an external counter 
        // because the actual array keys could be dangerous
foreach ($ids as $item)
{
    $key = ":id".$i++;
    $in .= ($in ? "," : "") . $key; // :id0,:id1,:id2
    $in_params[$key] = $item; // collecting values into a key-value array
}

$sql = "SELECT * FROM table WHERE foo=:foo AND id IN ($in) AND bar=:bar";
$stm = $db->prepare($sql);
$stm->execute(array_merge($params,$in_params)); // just merge two arrays
$data = $stm->fetchAll();

Afortunadamente, para los marcadores de posición con nombre no tenemos que seguir un orden estricto, por lo que podemos fusionar nuestras matrices en cualquier orden.

La sustitución de variables en declaraciones preparadas de PDO no admite matrices. Es uno por uno.

Puede solucionar ese problema generando la cantidad de marcadores de posición que necesita en función de la longitud de la matriz.

$variables = array ('1', '2', '3');
$placeholders = str_repeat ('?, ',  count ($variables) - 1) . '?';

$query = $pdo -> prepare ("SELECT * FROM table WHERE column IN($placeholders)");
if ($query -> execute ($variables)) {
    // ...
}

Como PDO no parece proporcionar una buena solución, también podría considerar usar DBAL, que en su mayoría sigue la API de PDO, pero también agrega algunas funciones útiles http://docs.doctrine-project.org/projects/doctrine-dbal/ es/latest/reference/data-tritrieval-and-manipulation.html#list-of-parameters-conversion

$stmt = $conn->executeQuery('SELECT * FROM articles WHERE id IN (?)',
    array(array(1, 2, 3, 4, 5, 6)),
    array(\Doctrine\DBAL\Connection::PARAM_INT_ARRAY)
);

Probablemente hay algunos otros paquetes que no agregan complejidad y no oscurecen la interacción con la base de datos (como lo hacen la mayoría de los ORM), pero al mismo tiempo hacen que las pequeñas tareas típicas sean un poco más fáciles.

Una versión alternativa de PHP Delusions (@your-common-sense) usando cierres:

$filter      = ["min_price" => "1.98"];
$editions    = [1,2,10];

$editions = array_combine(
    array_map(function($i){ return ':id'.$i; }, array_keys($editions)),
    $editions
);
$in_placeholders = implode(',', array_keys($editions));
$sql = "SELECT * FROM books WHERE price >= :min_price AND edition IN ($in_placeholders)";
$stm = $pdo->prepare($sql);
$stm->execute(array_merge($filter,$editions));
$data = $stm->fetchAll();

A menudo uso FIND_IN_SET en lugar de IN, así:

$in_array = array(1, 2, 3);
$in_values = implode(',', $in_array);
$my_result = $wbdb->prepare("SELECT * FROM my_table WHERE FIND_IN_SET(my_value, :in_values)");
$my_result->execute(array(':in_values' => $in_values));
$my_results = $my_result->fetchAll();

No es la mejor solución en cuanto a rendimiento, pero si las posibles opciones de número de $in_array son limitadas, por lo general no es un problema. Lo uso a menudo para estados donde my_value es un campo de enumeración. Nunca tuve ningún problema con eso.

Acabo de encontrarme con este problema y codifiqué un pequeño envoltorio. No es el código más bonito ni el mejor, estoy seguro, pero podría ayudar a alguien, así que aquí está:

function runQuery(PDO $PDO, string $sql, array $params = [])
{
    if (!count($params)) {
        return $PDO->query($sql);
    }

    foreach ($params as $key => $values) {
        if (is_array($values)) {
            // get placeholder from array, e.g. ids => [7,12,3] would be ':ids'
            $oldPlaceholder  = ':'.$key;
            $newPlaceholders = '';
            $newParams = [];
            // loop through array to create new placeholders & new named parameters
            for($i = 1; $i <= count($values); $i++) {
                // this gives us :ids1, :ids2, :ids3 etc
                $newKey = $oldPlaceholder.$i;
                $newPlaceholders .= $newKey.', ';
                // this builds an associative array of the new named parameters
                $newParams[$newKey] = $values[$i - 1];
            }
            //trim off the trailing comma and space
            $newPlaceholders = rtrim($newPlaceholders, ', ');

            // remove the old parameter
            unset($params[$key]);

            // and replace with the new ones
            $params = array_merge($params, $newParams);

            // amend the query
            $sql = str_replace($oldPlaceholder, $newPlaceholders, $sql);
        }
    }

    $statement = $PDO->prepare($sql);
    $statement->execute($params);
    return $statement;
}

Por ejemplo, pasando estos en:

SELECT * FROM users WHERE userId IN (:ids)

array(1) {
  ["ids"]=>
  array(3) {
    [0]=>
    int(1)
    [1]=>
    int(2)
    [2]=>
    int(3)
  }
}

se convierte en:

SELECT * FROM users WHERE userId IN (:ids1, :ids2, :ids3)

array(3) {
  [":ids1"]=>
  int(1)
  [":ids2"]=>
  int(2)
  [":ids3"]=>
  int(3)
}

No es a prueba de balas, pero como un único desarrollador para mis necesidades hace el trabajo bien, hasta ahora de todos modos.

Aquí hay una solución para marcadores de posición sin nombre (?). Si pasa $sql con un signo de interrogación como "A=? AND B IN(?) " y $args donde algunos de los elementos son matrices como [1, [1,2,3]], devolverá una cadena SQL con el número apropiado de marcadores de posición - "A=? AND B IN(?,?,?)" . Necesita el parámetro $args solo para encontrar qué elemento es una matriz y cuántos marcadores de posición necesita. Puede encontrar la clase de extensión PDO pequeña con este método que ejecutará su consulta: https://github.com/vicF/pdo/blob/master/src/PDO.php

public function replaceArrayPlaceholders($sql, $args)
{
    $num = 0;
    preg_match_all('/\?/', $sql, $matches, PREG_OFFSET_CAPTURE);  // Captures positions of placeholders
    //echo $matches[0][1][1];
    $replacements = [];
    foreach($args as $arg) {
        if(is_array($arg)) {
            $replacements[$matches[0][$num][1]] = implode(',',array_fill(0, count($arg), '?')); // Create placeholders string
        }
        $num++;
    }
    krsort($replacements);
    foreach($replacements as $position => $placeholders) {
        $sql = substr($sql, 0, $position).$placeholders.substr($sql, $position+1); // Replace single placeholder with multiple
    }
    return $sql;
}

Según tengo entendido, es porque PDO tratará los contenidos de $in_values ​​como un solo elemento y lo hará en consecuencia. PDO verá 1,2,3 como una sola cadena, por lo que la consulta se verá así

SELECCIONE * DESDE la tabla DONDE my_value IN ("1,2,3")

Puede pensar que cambiar la implosión para que tenga comillas y comas lo arreglará, pero no es así. PDO verá las comillas y cambiará la forma en que cita la cadena.

En cuanto a por qué su consulta coincide con el primer valor, no tengo explicación.

Aquí está mi código completo, perdón por mi estúpida codificación, mala estructura y líneas de comentarios. Tal vez alguien recodifique mi estúpido código :)

enviar a clase:

$veri_sinifi = new DB_Connect;
                          $veri_sorgu_degerleri=array(
                            "main_user_id" => (int)$_SESSION['MM_UserId'],
                            "cari_grup_id" => [71,72,73],
                            "cari_grup_adi" => ['fatih','ahmet','ali']                       
                          );                              
                          $siteler =$veri_sinifi->query("Select cari_grup_adi,cari_grup_id FROM m_cari_gruplar WHERE main_user_id= :main_user_id and cari_grup_id in (:cari_grup_id) and cari_grup_adi in (:cari_grup_adi)",$veri_sorgu_degerleri) ; 

clase obtener este sql:

Select cari_grup_adi,cari_grup_id FROM m_cari_gruplar WHERE main_user_id= :main_user_id and cari_grup_id in (:cari_grup_id) and cari_grup_adi in (:cari_grup_adi) 

clase convertir este sql a esto.

Select cari_grup_adi,cari_grup_id FROM m_cari_gruplar WHERE main_user_id= :main_user_id and cari_grup_id in (:cari_grup_id0,:cari_grup_id1,:cari_grup_id2) and cari_grup_adi in (:cari_grup_adi0,:cari_grup_adi1,:cari_grup_adi2)

parámetros de enlace de clase:

 Select cari_grup_adi,cari_grup_id FROM m_cari_gruplar WHERE main_user_id= 1 and cari_grup_id in (71,72,73) and cari_grup_adi in ('fatih','ahmet','ali')

código:

class DB_Connect{
var $dbh;
function __construct(){
    $host = "";
    $db = "";
    $user = "";
    $password = "";
    $this -> dbh = $this -> db_connect($host, $db, $user, $password);
}
public function getDBConnection(){
    return $this -> dbh;
}
protected function db_connect($host, $db, $user, $password){
    //var_dump($host, $db, $user, $password);exit();
    try {
        $dbh = new PDO("mysql:host=$host;dbname=$db", $user, $password);
    }
    catch(PDOException $err) {
        echo "Error: ".$err->getMessage()."<br/>";
        die();
    }
    return $dbh;
}
public function query($statement,$bind_params){
    $keyword = substr(strtoupper($statement), 0, strpos($statement, " ")); // sql in en başındaki kelimeye bakıyor SELECT UPDATE vs gibi ordaki ilk boşluğa kadar olan kelimeyi alıyor.
  //echo $keyword;
    $dbh = $this->getDBConnection();        
    if($dbh){
        try{
            $sql = $statement;
            /*GELEN PARAMETRELERE BAKIP İÇİNDE ARRAY VAR İSE SQL STATEMENT KISMINI ONA GÖRE DEĞİŞTİRİYORUZ.
            Alttaki döngünün yaptığı işlem şu. Eğer alttaki gibi bir sorgu değerleri gönderilirse
            $veri_sorgu_degerleri=array(
                                    "main_user_id" => (int)$_SESSION['MM_UserId'],
                                    "cari_grup_id" => [71,72,73],
                                    "cari_grup_adi" => ['fatih','ahmet','ali']                       
                                  );    
            burada main_user_id tek bir değer diğerleri sise array olarak gönderiliyor. Where IN sorgusu birden fazla değer alabileceği için bunları PDO kabul ederken string olarak kabul ediyor yani yukardaki 71,72,73 değerini tırnak içine tek değermiş gib '71,72,73' şeklinde alıyor yapılması gereken sorgunun değiştirilmesi. bu döngü ile 

            Select cari_grup_adi,cari_grup_id FROM m_cari_gruplar WHERE main_user_id= :main_user_id and cari_grup_id in (:cari_grup_id) and cari_grup_adi in (:cari_grup_adi) 

            şeklindeki sorgu in kısımları değiştirilerek

            Select cari_grup_adi,cari_grup_id FROM m_cari_gruplar WHERE main_user_id= :main_user_id and cari_grup_id in (:cari_grup_id0,:cari_grup_id1,:cari_grup_id2) and cari_grup_adi in (:cari_grup_adi0,:cari_grup_adi1,:cari_grup_adi2)

            halini alıyor bir sonraki foreach de ise yine benzer yapı ile arary olarak gelen değerler in için tek tek bind ediliyor, normal gelen değerler ise normal bind yapılıyor.


            */
            foreach($bind_params as $paramkey => $param_value) {
              //echo "Key=" . $paramkey ."-".($param_value)."-, Value=" . $param_value;
              //echo "<br>";                 
              //echo "is_numeric($param_value)>".is_numeric($param_value)."<br>";
              //echo "is_string($param_value)>".is_string($param_value)."<br>";
              //echo "is_array($param_value)>".is_array($param_value)."<br>";
              $in_key="";
              $in_parameters="";
              if (is_array($param_value)) // Gelan parametre array ise yeniden yapılandır.
              {
              foreach ($param_value as $i => $item)
                {
                    $in_key = ":$paramkey".$i;
                    //echo "<br>$in_key = ".$paramkey.".$i";
                    $in_parameters .= "$in_key,";
                    //echo "<br>$in_parameters = ".$in_key;
                }
                $in_parameters = rtrim($in_parameters,","); // :id0,:id1,:id2
                //echo "<br>in_parameters>$in_parameters";
                $sql = str_replace(":".$paramkey, $in_parameters,$sql);
                //echo "<br>oluşan sql>".$sql."<br>"; 
               }
            }
            $sql = $dbh->prepare($sql);
            foreach($bind_params as $paramkey => $param_value) {
              //echo "Key=" . $paramkey ."-".($param_value)."-, Value=" . $param_value;
              //echo "<br>";                 
              //echo "is_numeric($param_value)>".is_numeric($param_value)."<br>";
              //echo "is_string($param_value)>".is_string($param_value)."<br>";
              //echo "is_array($param_value)>".is_array($param_value)."<br>";
              if (is_numeric($param_value)==1)  // gelen veri  numerik ise
                {
                    $param_value = (int)$param_value;
                    $pdo_param_type = PDO::PARAM_INT;
                } 
              elseif (is_string($param_value)==1)// gelen veri  string ise
                {$pdo_param_type = PDO::PARAM_STR; }
              if (is_array($param_value)) // gelen veri array tipinde ise
              {
              foreach ($param_value as $i => $param_array_value) // bu döngünün açıklaması yukardaki döngü için yazılan açıklama içinde mevcut
                {
                    $in_key = ":$paramkey".$i;
                                if (is_numeric($param_array_value)==1)  // gelen veri  numerik ise
                                    {
                                        $param_array_value = (int)$param_array_value;
                                        $pdo_param_type = PDO::PARAM_INT;
                                    } 
                                  elseif (is_string($param_array_value)==1)// gelen veri  string ise
                                    {$pdo_param_type = PDO::PARAM_STR; }
                                    $sql->bindValue($in_key, $param_array_value, $pdo_param_type );
                    //echo "<br>oldu1";
                    //echo "<br> -$in_key-";
                }
                //$sql = str_replace(":".$paramkey, $in_parameters, $sql);
                //echo "oluşan sql>".$sql."<br>"; 
               }
              else // array değilse aşağıdaki şekilde bind yap.
                {
                  //echo "<br>oldu2";
                  $sql->bindValue(":$paramkey", $param_value, $pdo_param_type ); // bindparam foreach içinde kullanılmaz çünkü execute esnasında bind yaptığı için yani anlık olarak değerleri atamaddığı için for döngüsünde en sonda value değişkeni neyse tüm parametrelere onu atıyor, bu sebeple bindvalue kullanıyoruz.PDO::PARAM_INT
                }
            } // foreach                
            $exe = $sql->execute();
            $sql->debugDumpParams();
            //echo $exe;

        }
        catch(PDOException $err){
            return $err->getMessage();
        }

        //BU KISMA AİT AÇIKLAMA AŞAĞIDAIR.
        switch($keyword){ // sorgu çalıştıktan sonra sorgu sonucuna göre gerekli işlemler yapılıyor.
            case "SELECT":  // Sorgu select sorgusu ise                
                $result = array(); //sonuçları diziye aktaracak.
                while($row = $sql->fetch(PDO::FETCH_ASSOC)){     // sonuç satırlarını tek tek okuyup                   
                    //echo $row;
                    $result[] = $row; // her bir satırı dizinin bir elemanına aktarıyor.bu değer diziden nasıl okunur açıklaması aşağıda                      
                }
                return $result; // sorgudan dönen diziyi doğrudan ana programa aktarıyor orada dizi olarak okunabilir.                
                break;
            default: 
                return $exe;
                break;
        }
    }
    else{
        return false;
    }
}

}