Если вы вставляете в запрос список числовых параметров, то можно провернуть такое.
$product_list = [30, 32, 38];
$query = " `productId` in (".join(', ', array_map(function($item){return int $item;}, $product_list)).")";
$sql = "SELECT * FROM `products` WHERE ".$query;
// Должен получиться запрос:
// SELECT * FROM `products` WHERE `productId` in (30, 32, 38)
// array_map тут нужен для принудительного перевода всех значений в числовой вид, обеспечив тем самым экранирование.
$query = $db->query($sql);
$products_data = $query->fetchAll(\PDO::FETCH_ASSOC);
Если применять нормальную связку параметров PDO, как должны учить в учебниках:
$product_list = [30, 32, 38];
$in_params = [];
for($i = 0; $i < count($product_list), $i++)
$in_params[':productId'.$i] = $product_list[$i];
// получили массив: $in_params = [':productId0' => 30, ':productId1' => 32, ':productId0' => 38];
$placeholder_string = join(', ', array_keys($in_params));
// получили строку $placeholder_string = ":productId0, :productId1, :productId2";
$sql = "SELECT * FROM `products` WHERE `productId` in (".$placeholder_string.")";
// Должен получиться запрос:
// SELECT * FROM `products` WHERE `productId` in (:productId0, :productId1, :productId2)
$query = $db->prepare($sql);
$query->execute($in_params); // in_params в точности в таком формате, который нужен для связки параметров productId
$products_data = $query->fetchAll(\PDO::FETCH_ASSOC);