falbar Как настроить и использовать PDO (PHP Data Objects) для доступа к базе

Как настроить и использовать PDO (PHP Data Objects) для доступа к базе

3 июня 2018 Перевод Туториал 1069 0

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, создав конструктор, который возьмет два аргумента: namename и с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.

Реклама
Комментариев еще не оставлено
no_avatar