Эффективное использование PDO. Защита от SQL инъекций. Решение проблем

КодингБД2015-01-08 14:20:57

Введение в PDO

PDO является расширением PHP, предоставляющим разработчикам простой и универсальный интерфейс для доступа к различным БД, а так же берет на себя задачу по обработке внешних данных, значительно упрощая данный процесс. Использование для разных БД их "родных" драйверов позволяет добиться высокой производительности. PDO поддерживает любую систему управления базами данных, для которой существует PDO драйвер, что позволяет легко переходить от одной СУБД к другой, а так же работать одновременно с несколькими БД. Для того, чтобы проверить, какие из драйверов есть на вашем сервере, можно воспользоваться следующей конструкцией:

print_r(PDO::getAvailableDrivers());

Подключение к БД

Способы подключения к различным БД могут немного отличаться, но, в основном, синтаксис совпадает.

try{ $DB = new PDO("mysql:host=$host;dbname=$dbname", $user, $pass); }catch(PDOException $e){ $e->getMessage(); } $DB = null; # закрытие соединения;

PDO умеет выбрасывать исключения при ошибках, поэтому не забываем любые действия оборачивать в конструкцию try/catch. Всего существует 3 режима ошибок, которые включаются с помощью метода setAttribute(); и параметра PDO::ATTR_ERRMODE. Но, стоит помнить, что ошибка при попытке соединения будет всегда вызывать исключение.

PDO::ERRMODE_SILENT: Стандартный режим, включенный по умолчанию. Именно этот режим обычно используется для отлавливания ошибок в расширениях mysql и mysqli.

PDO::ERRMODE_WARNING: Вызывает стандартный Warning, позволяя скрипту продолжить выполнение. Крайне удобен при отладке.

PDO::ERRMODE_EXCEPTION: Наиболее предпочтительный тип контроля, который позволяет выбрасывать исключения и обрабатывать ошибки в скрытом режиме.

$DB->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

Вставка и обновление данных

Самый простой вариант вставки новых данных может состоять из одного или двух шагов. Двухшаговый метод имеет большие преимущества, так как помогает в защите от SQL инъекций

$STH = $DB->exec("INSERT INTO table_name(name) VALUES('Майк')"); # небезопасный метод; $STH = $DB->prepare("INSERT INTO table_name(name) VALUES('Майк')"); # метод с prepared statements; $STH->execute();

Prepared statement — это заранее обработанный SQL-запрос, который может быть многократно выполнен путем отправки серверу различных наборов данных. Дополнительным преимуществом является невозможность провести SQL-инъекцию через данные, используемые в placeholders. Есть два варианта передачи placeholders

# безымянные placeholders $STH = $DB->prepare("INSERT INTO table_name(name, addr, city) values(?, ?, ?)"); # назначаем переменные каждому placeholder, с индексами от 1 до 3 $STH->bindParam(1, $name); $STH->bindParam(2, $addr); $STH->bindParam(3, $city); # вставляем одну строку $name = "Антон"; $addr = "д. 16 ул. Весеняя"; $city = "Москва"; $STH->execute(); # вставляем еще одну строку, уже с другими данными $name = "Вован"; $addr = "д. 15 ул. Осеняя"; $city = "Амстердам"; $STH->execute();

Первым шагом мы составляем SQL запрос, после чего назначаем всем placeholders переменные. Далее назначаем переменным значения и выполняем запрос. Для последующих запросов достаточно изменить значения переменных и выполнить запрос еще раз. В случае большого количества параметров гораздо удобнее использовать именные placeholders. В данном случае можно передать данные в виде массива непосредственно в execute.

# именные placeholders $STH = $DB->prepare("INSERT INTO table_name(name, addr, city) values(:name, :addr, :city)"); # массив значений $data = array('name'=>'Anton', 'addr'=>'Район булгакова', 'city' => 'China'); # выполняем запрос $STH->execute($data);

Удобством использования именных placeholders является возможность вставки объектов напрямую в БД, если названия свойств совпадают с именами параметров. При преобразовании объекта в массив в execute, свойства считаются ключами массива.

class person{ public $name; public $addr; public $city; function __construct($n,$a,$c){ $this->name = $n; $this->addr = $a; $this->city = $c; } } $cathy = new person('Cathy','9 Dark and Twisty','Cardiff'); # выполняем запрос, преобразовав объект в массив $STH = $DB->prepare("INSERT INTO folks (name, addr, city) values (:name, :addr, :city)"); $STH->execute((array)$cathy);

Получение данных

Для получения данных используется метод fetch() с указанием вида получаемых данных.
PDO::FETCH_ASSOC: возвращает массив с названиями столбцов в виде ключей
PDO::FETCH_BOTH (по умолчанию): возвращает массив с индексами как в виде названий стобцов, так и их порядковых номеров
PDO::FETCH_BOUND: присваивает значения столбцов соответствующим переменным, заданным с помощью метода bindColumn()
PDO::FETCH_CLASS: присваивает значения столбцов соответствующим свойствам указанного класса. Если для какого-то столбца свойства нет, оно будет создано
PDO::FETCH_INTO: обновляет существующий экземпляр указанного класса
PDO::FETCH_LAZY: объединяет в себе PDO::FETCH_BOTH и PDO::FETCH_OBJ
PDO::FETCH_NUM: возвращает массив с ключами в виде порядковых номеров столбцов
PDO::FETCH_OBJ: возвращает анонимный объект со свойствами, соответствующими именам столбцов
На практике наиболее используемые: FETCH_ASSOC, FETCH_CLASS, и FETCH_OBJ. Вид данных можно установить через метод setFetchMode или непосредственно в самом методе fetch().

$STH->setFetchMode(PDO::FETCH_ASSOC);

При использовании FETCH_CLASS данные заносятся в экземпляры указанного класса. При этом значения назначаются свойствам объекта ДО вызова конструктора. Если свойства с именами, соответствующими названиям столбцов, не существуют, они будут созданы автоматически (с областью видимости public).
Если полученные данные нуждаются в обязательной обработке сразу после их получения из БД, необходимо реализовать функцию обработки в конструкторе класса.

# преобразуем значение адреса к секретному виду; class secret_person{ public $name; public $addr; public $city; public $other_data; function __construct($other = ''){ $this->addr = preg_replace('/[a-z]/', 'x', $this->addr); $this->other_data = $other; } } $STH = $DB->query('SELECT name, addr, city FROM table_name'); $STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person'); while($obj = $STH->fetch()) echo $obj->addr;

В результате на экран будут выведены преобразованные данные. Чтобы выполнить конструктор класса до присваивания значений, необходимо указать параметр PDO::FETCH_PROPS_LATE. В результате, сохраненные данные преобразованы не будут.

$STH->setFetchMode(PDO::FETCH_CLASS | PDO::FETCH_PROPS_LATE, 'secret_person');

При необходимости можно передать необходимые для конструктора аргументы

$STH->setFetchMode(PDO::FETCH_CLASS, 'secret_person', array('staff'));

Полезные методы PDO

Следующий метод позволяет получить идентификатор последней вставленной записи. Данный метод вызывается у объекта БД, а не у объекта выражения.

$DB->lastInsertId();

Для операций, не возвращающих никаких данных, кроме количества затронутых в них строк используется следующий метод:

$DBH->exec('DELETE FROM table_name WHERE 1');

Для получения количества затронутых в операции строк, используется следующий метод, применяемый к объекту выражения:

$rows_affected = $STH->rowCount();

Сложности с оператором LIKE в PDO

У оператора LIKE есть два собственных спецсимвола «_» и «%». Чтобы искать буквальное совпадение с этими символами, их необходимо прослэшить функцией addCslashes().

$data = addCslashes($data, '\%_');

В подготовленных выражениях плейсхолдер может заменять только строку или число. Ключевое слово, идентификатор, часть строки или набор строк через плейсхолдер подставить нельзя. Поэтому для LIKE необходимо подготавливать строку поиска целиком перед внедрением в запрос:

$name = "%$name%"; $STH = $DB->prepare("SELECT*FROM table_name WHERE name LIKE ?"); $STH->execute(array($name)); $data = $STH->fetchAll();

Использование оператора LIMIT в PDO

При передаче данных напрямую в execute() в режиме PDO эмуляции, они форматируются как строки - эскейпятся и обрамляются кавычками. Поэтому LIMIT ?,? превращается в LIMIT '10', '10', что вызывает ошибку синтаксиса. Есть два варианта решения этой проблемы:

# отключение эмуляции $DB->setAttribute(PDO::ATTR_EMULATE_PREPARES, false); # биндить параметры, принудительно присваиваивая им тип PDO::PARAM_INT $STH->bindParam(1, $limit_from, PDO::PARAM_INT); $STH->bindParam(2, $per_page, PDO::PARAM_INT);

Использование оператора IN в PDO

Как уже говорилось выше, подставить набор данных на место одного плейсхолдера не получится. Поэтому для IN придется изворачиваться, динамически формируя две переменные:

# набор знаков вопроса через запятую по числу элементов в IN() $in = str_repeat('?,', count($arr) - 1) . '?'; $sql = "SELECT*FROM table_name WHERE column IN($in)"; # массив данных для подстановки. $arr = array(1,2,3);

Собственно говоря, это основные знания для полноценной работы с PDO. Назовем данную статью справочником понятий и рецептов работы с PDO, который я собирал и продолжаю собирать, по мере поступления новых проблем, со всего интернета в одном месте.

Все представленные на данном сайте статьи являются моим личным справочником, составленным на основе анализа интернет форумов и блогов, для упрощения поиска необходимой для меня информации. Содержание может корректироваться по мере поступления более свежих и простых решений. Просьба указывать ссылку на источник, при полном или частичном копировании материала!
Идет загрузка...еще чуть чуть...