PDO является акронимом для PHP Data Objects: это PHP расширение для работы с базами данных используя объекты. Одно из его преимуществ лежит в том, что оно не привязано напрямую к определенной базе данных: его интерфейс позволяет получить доступ к нескольким разным средам, включая: MySQL, SQLite, PostgreSQL, Microsoft SQL Server.
Это руководство нацелено предоставить полный обзор PDO и провести читателя шаг за шагом от создания и подключения к базе данных, до выбора наиболее подходящих методов выборки, демонстрируя как создать подготовленные запросы и описывая возможные режимы ошибок.
Создание тестовой базы данных и таблицы
В первую очередь, мы создадим базу данных:
CREATE DATABASE solar_system;
GRANT ALL PRIVILEGES ON solar_system.* TO 'testuser'@'localhost'
IDENTIFIED BY 'testpassword';
Мы выдали пользователю testuser
все привилегии в базе данных solar_system
, используя testpassword
для пароля. Теперь давайте создадим таблицу и заполним её какой-нибудь информацией:
USE solar_system;
CREATE TABLE planets (
id TINYINT(1) UNSIGNED NOT NULL AUTO_INCREMENT,
PRIMARY KEY(id),
name VARCHAR(10) NOT NULL,
color VARCHAR(10) NOT NULL
);
INSERT INTO planets(name, color) VALUES('earth', 'blue'), ('mars', 'red'), ('jupiter', 'strange');
Описание соединения DSN (Data Source Name)
Теперь, когда у нас есть база данных, мы должны задать DSN
. DSN расшифровывается как Data Source Name
, и является набором информации, необходимой для подключения к базе данных, DSN имеет форму строки. Синтаксис отличается в зависимости от базы данных, подключение к которой требуется, но так как мы используем MySQL/MariaDB, нам нужно задать следующие:
- Тип драйвера, используемого для подключения;
- Имя компьютера-хоста, на котором запущена база данных;
- Порт для подключения (необязательно);
- Название базы данных;
- Кодировка (необязательно).
Формат строки в нашем случае будет таким (мы будем хранить его в переменной $dsn
):
$dsn = "mysql:host=localhost;port=3306;dbname=solar_system;charset=utf8";
В первую очередь мы задали database prefix
или префикс базы данных. В этом случае, так как мы подключаемся к базе данных типа MySQL/MariaDB, мы используем mysql
. Затем мы отделили префикс от остальной строки двоеточием и каждая последующая секция отделена от остальных точкой с запятой.
В следующих двух секциях мы задали hostname
, на котором запущена база данных и port
используемый для подключения. Если порт не указан, использован будет порт по умолчанию, в данном случае это 3306
. Сразу после database name
указывается charset
.
Создание PDO объекта
Теперь, когда наш DSN готов, мы приступим к созданию PDO object
. Конструктор PDO использует строку DSN как первый параметр, имя пользователя базы данных вторым параметром, пароль – третьим, и необязательный массив настроек – четвертым.
$options = [
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
];
$pdo = new PDO($dsn, 'testuser', 'testpassword', $options);
Настройки так же можно задать и после создания объекта, пользуясь методом SetAttribute()
:
$pdo->SetAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
Настройка PDO повидения при ошибках
Давайте взглянем на некоторые опции доступные для PDO::ATTR_ERRMODE
. Эти опции крайне важны, потому что они определяют поведение PDO в случае возникновения ошибок. Возможные опции:
PDO::ERRMODE_SILENT
Опция по умолчанию. PDO просто выдаст код ошибки и сообщение об ошибке. Их можно будет получить используя методы errorCode()
и errorInfo()
.
PDO::ERRMODE_EXCEPTION
Эта опция, на мой взгляд, рекомендуема для использования. С её помощью, помимо выдачи кода ошибки и информации, PDO выдаст исключение PDOException
, которое прервет ход выполнения скрипта, а ещё она полезна при PDO transactions
(их мы рассмотрим чуть позже).
PDO::ERRMODE_WARNING
С этой опцией, PDO выдаст код ошибки и сообщение о ней, как и при PDO::ERRMODE_SILENT
, но еще и покажет предупреждение WARNING
, которое не прерывает работу скрипта.
Настройка метода выборки по умолчанию
Еще одна важная настройка, регулируется с помощью константы PDO::DEFAULT_FETCH_MODE
. Она позволяет настроить по умолчанию работу метода fetch()
, который будет использоваться для получения результатов запроса. Вот самые часто используемые опции:
PDO::FETCH_BOTH
При его использовании, полученные результаты будут индексированы и по целым числам, и по названиям столбцов. Использование его в методе для получения ряда из таблицы планет выдаст нам такие результаты:
$stmt = $pdo->query("SELECT * FROM planets");
$results = $stmt->fetch(PDO::FETCH_BOTH);
Array
(
[id] => 1
[0] => 1
[name] => earth
[1] => earth
[color] => blue
[2] => blue
)
PDO::FETCH_ASSOC
С этой константой, результаты будут записаны в ассоциативный массив в котором каждый ключ будет именем столбца, а каждое значение – обозначать определенное значение в ряду:
$stmt = $pdo->query("SELECT * FROM planets");
$results = $stmt->fetch(PDO::FETCH_ASSOC);
Array
(
[id] => 1
[name] => earth
[color] => blue
)
PDO::FETCH_NUM
Используя PDO::FETCH_NUM
константу мы получим 0-indexed array:
Array
(
[0] => 1
[1] => earth
[2] => blue
)
PDO::FETCH_COLUMN
Эта константа полезна для получения только значений из столбца, а метод вернет все результаты внутри простого одномерного массива. Например, вот такой запрос:
$stmt = $pdo->query("SELECT name FROM planets");
В результате:
Array
(
[0] => earth
[1] => mars
[2] => jupiter
)
PDO::FETCH_KEY_PAIR
Эта константа полезна, когда нужно получить значения из двух столбцов. Метод fetchAll()
вернет результаты в виде ассоциативного массива. В этом массиве, данные из первого столбца будут указаны в форме ключей, а из второго – в качестве значений:
$stmt = $pdo->query("SELECT name, color FROM planets");
$result = $stmt->fetchAll(PDO::FETCH_KEY_PAIR);
В результате:
Array
(
[earth] => blue
[mars] => red
[jupiter] => strange
)
PDO::FETCH_OBJECT
При использовании константы PDO::FETCH_OBJECT
, будет создан anonymous object
за каждый полученный ряд. Его (публичные) свойства будут названы также, как и столбцы, а результаты запроса будут использованы в качестве значений. Использование этого метода для того же запроса, что и выше, приведет к следующему результату:
$results = $stmt->fetch(PDO::FETCH_OBJ);
stdClass Object
(
[name] => earth
[color] => blue
)
PDO::FETCH_CLASS
Как и предыдущая константа, назначит значения столбцов свойствами объекта, но в этом случае мы должны настроить существующий класс, который будет использован для создания объекта. Для демонстрации, сначала мы создадим класс:
class Planet
{
private $name;
private $color;
public function setName($planet_name)
{
$this->name = $planet_name;
}
public function setColor($planet_color)
{
$this->color = $planet_color;
}
public function getName()
{
return $this->name;
}
public function getColor()
{
return $this->color;
}
}
Не обращайте внимание на простоту кода, лучше займемся осмотром класса Planet, который мы создали: у него в свойствах прописано private
и у класса отсутствует конструктор. Теперь же попробуем получить результаты.
Используя fetch()
с PDO::FETCH_CLASS
необходимо использовать метод setFetchMode()
на объект, перед тем как пытаться получить данные, например:
$stmt = $pdo->query("SELECT name, color FROM planets");
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Planet');
Мы задаём константу PDO::FETCH_CLASS
как первый аргумент метода setFetchMode()
и название класса, использовано для создания объекта (в нашем случае «Planet») – вторым аргументом. Теперь запускаем код:
$planet = $stmt->fetch();
Должен получиться объект Planet:
var_dump($planet);
Planet Object
(
[name:Planet:private] => earth
[color:Planet:private] => blue
)
Заметьте, как значения, полученные из запроса, были назначены к соответствующим характеристикам объекта, несмотря на то, что они приватные.
Назначение характеристик после создания объекта
Класс «Planet», не обладал никаким определенным конструктором, так что проблем с назначением характеристик не возникло; но что если у класса есть конструктор, в котором характеристики задаются и изменяются? Так как значения назначены до запуска конструктора, они будут перезаписаны.
PDO помогает предоставить константу FETCH_PROPS_LATE
: при её использовании, значения будут назначены после создания объекта. Пример:
class Planet
{
private $name;
private $color;
public function __construct($name = moon, $color = grey)
{
$this->name = $name;
$this->color = $color;
}
public function setName($planet_name)
{
$this->name = $planet_name;
}
public function setColor($planet_color)
{
$this->color = $planet_color;
}
public function getName()
{
return $this->name;
}
public function getColor()
{
return $this->color;
}
}
Мы изменили наш класс Planet, создав конструктор, который возьмет два аргумента: name
name и сolor
. Эти аргументы имеют базовые значения: moon и gray, что значит, что, если других значений не будет задано, будут установлены эти.
В этом случае, если мы не используем FETCH_PROPS_LATE
, то не важно, какие значения будут получены из базы данных, все характеристики останутся базовыми, потому что в процессе создания объекта, они будут перезаписаны. Для того чтобы это проверить, запустим следующий запрос:
$stmt = $pdo->query("SELECT name, color FROM solar_system WHERE name = 'earth'");
$stmt->setFetchMode(PDO::FETCH_CLASS, 'Planet');
$planet = $stmt->fetch();
А теперь рассмотрим объект Planet и проверим, какие значения соответствуют его характеристикам:
var_dump($planet);
object(Planet)#2 (2) {
["name":"Planet":private]=>
string(4) "moon"
["color":"Planet":private]=>
string(4) "gray"
}
Как и ожидалось, полученные из базы данных значения были перезаписаны значениями по умолчанию. Теперь, мы продемонстрируем решение проблем, используя константу FETCH_PROPS_LATE
(и тот же запрос, что и предыдущий):
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'Planet');
$planet = $stmt->fetch();
var_dump($planet);
object(Planet)#4 (2) {
["name":"Planet":private]=>
string(5) "earth"
["color":"Planet":private]=>
string(4) "blue"
}
Наконец, получен желаемый результат. Но что если у конструктора класса нет базовых значений, и они должны быть заданы? Это уже проще: мы можем задать параметры конструктора в форме массива, как третий аргумент, после имени класса, используя метод setFetchMode()
. Например, давайте изменим конструктор:
class Planet
{
private $name;
private $color;
public function __construct($name, $color)
{
$this->name = $name;
$this->color = $color;
}
[...]
}
Аргументы конструктора теперь обязательны, так что мы запускаем:
$stmt->setFetchMode(PDO::FETCH_CLASS|PDO::FETCH_PROPS_LATE, 'Planet', ['moon', 'gray']);
В этом случае, указанные нами параметры служат только как базовые значения, требуемые для работы объекта без ошибок: они будут переписаны значениями из базы данных.
Получение нескольких объектов
Конечно же, возможно получить сразу несколько результатов в форме объектов, или используя метод fetch()
, или посредством цикла:
while ($planet = $stmt->fetch()) {
// Что-то делам с результатами
}
Или получив все результаты сразу. В этом случае, как говорилось ранее, используя метод fetchAll()
вам нужно будет указывать fetch режим не перед запуском метода, но в тот момент, когда он запустится:
$stmt->fetchAll(PDO::FETCH_CLASS|PDO_FETCH_PROPS_LATE, 'Planet', ['moon', 'gray']);
PDO::FETCH_INTO
При использовании этой константы, PDO не создает новый объект, взамен обновляя характеристики уже существующего, но только если он public
или в случае использования метода __set()
внутри объекта.
Подготовленные против прямых запросов
У PDO есть два пути работы с запросами: использовать прямые и более надежный - подготовленные.
Прямые запросы
Для использования прямых запросов существует два главных метода: query()
и exec()
. Первый из них создает объект PDOStatemnt
, доступ к которому можно получить через методы fetch()
или fetchAll()
: если вы используете их в случаях, когда таблица не меняется, таких как SELECT
.
Второй метод, взамен, возвращает номер ряда, который был изменен запросом: мы используем его в случаях, которые заменяют ряды, таких как INSERT
, DELETE
или UPDATE
. Прямые запросы должны быть использованы, только в случаях отсутствия переменных в запросах, и в безопасности метода нет никаких сомнений.
Подготовленные запросы
PDO также поддерживает запросы исполняемые в два шага, подготовленные: они полезны, когда в запросах есть переменные, и более безопасны в целом, поскольку метод prepare()
сделает все необходимые действия за нас. Давайте взглянем, как используются переменные. Представьте, что мы хотим вставить характеристики планеты в таблицу Planets
. Для начала, подготовим запрос:
$stmt = $pdo->prepare("INSERT INTO planets(name, color) VALUES(?, ?)");
Как и говорилось ранее, мы используем метод prepare()
который использует SQL запрос в качестве аргумента, используя временные значения для переменных. Временные значения могут быть двух типов: позиционные и именные.
Позиционные
Используя ?
позиционные временные значения, код получается более краткий, но мы должны задать данные, которые будут вставлены, в том же порядке, что и имена столбцов, в массиве представленном как аргумент метода execute()
:
$stmt->execute([$planet->name, $planet->color]);
Именные
Используя именные временные значения named placeholders
, нам не нужен определенный порядок, но мы получим больше кода в результате. При запуске метода execute()
, мы должны задать данные в форме ассоциативного массива, в котором каждый ключ – имя использованного временного значения, а ассоциирующееся значения будет тем, что перенесется в запрос. Например, предыдущий запрос станет таким:
$stmt = $pdo->prepare("INSERT INTO planets(name, color) VALUES(:name, :color)");
$stmt->execute(['name' => $planet->name, 'color' => $planet->color]);
Методы prepare()
и execute()
оба могут быть использованы для запросов, которые изменяют или просто получают информацию из базы данных. В первом случае, мы используем fetch методы перечисленные сверху для получения информации, а во втором – используя метод rowCount()
.
Методы bindValue() и bindParam()
Для предоставления значений, которые будут вставлены в запрос, могут также использоваться методы bindValue()
и bindParam()
. Первый привязывает значение заданной переменной к позиционному или именному временному значению, использованному при подготовке запроса. Взяв за пример предыдущий случай, мы сделаем:
$stmt->bindValue('name', $planet->name, PDO::PARAM_STR);
Мы привязываем значение $planet->name
к временному значению :name
. Замете, что используя оба метода bindValue()
и bindParam()
мы можем также задать тип переменной, как третий аргумент, используя подходящую PDO константу, в этом случае PDO::PARAM_STR
.
Используя взамен bindParam()
мы можем привязать переменную к подходящему временному значению, используемому в подготовке запроса. Заметьте, что в этом случае, переменная связана с reference
и её значение будет изменено на временное, только когда запустится метод execute()
. Синтаксис такой же, как и в прошлый раз:
$stmt->bindParam('name', $planet->name, PDO::PARAM_STR)
Мы привязали переменную, а не её значение $planet->name
к :name
! Как сказано выше, замена произойдет только при запуске метода execute()
, так что временное значение будет заменено на значение переменной в тот момент.
PDO Транзакции
Транзакции позволяют сохранить последовательность при запуске множественных запросов. Все запросы выполняются «партиями» и относятся к базе данных, только если они все удачно выполнены. Транзакции не будут работать со всеми базами данных, и не со всеми sql
конструкциями, поскольку некоторые из них вызывают проблемы.
В качестве экстремального и странного примера, представьте, что пользователь должен выбрать список планет и каждый раз, когда он делает новый выбор, вам нужно будет удалить предыдущий из базы данных, прежде чем вставлять новый. Что если удаление произойдет, а вставка – нет? Мы получим пользователя без планет! В основном, транзакции применяются так:
$pdo->beginTransaction();
try {
$stmt1 = $pdo->exec("DELETE FROM planets");
$stmt2 = $pdo->prepare("INSERT INTO planets(name, color) VALUES (?, ?)");
foreach ($planets as $planet) {
$stmt2->execute([$planet->getName(), $planet->getColor()]);
}
$pdo->commit();
} catch (PDOException $e) {
$pdo->rollBack();
}
В первую очередь, метод beginTransaction()
в объекте PDO отключает autocommit
запроса, затем запросы запускаются в необходимом порядке. В этот момент, если не возникает исключение PDOException
запросы автоматически пропускаются через метод commit()
, в противном случае – через метод rollBack()
транзакции отменяются и autocommit
восстанавливается.
Таким образом, при множественных запросах, всегда будет последовательность. Это довольно очевидно, но PDO транзакции могут быть использованы только PDO::ATTR_ERRMODE
установлен на PDO::ERRMODE_EXCEPTION
.