Простая обертка для PDO

Проблема N1, многословность
Проблема N2, доступность
Код
Примеры
Комментарии (21)

Вообще, из-за того, что библиотека PDO сама по себе уже является обёрткой, придумать какие-либо улучшения для неё довольно трудно. Попытки новичков расширить функционал редко приводят к чему-то умному или полезному. Но, тем не менее, немного улучшить эту библиотеку можно.

Проблема N1, многословность
Главный недостаток и PDO, и mysqli при работе с подготовленными выражениями состоит в том, что их встроенные функции заточены на множественное исполнение подготовленного запроса (когда prepare вызывается только раз, а потом много раз вызывается execute() с разными данными). Из-за этого они так многословны. Но беда в том, что на практике в РНР выполнять такие запросы приходится довольно редко. И в результате для большинства запросов приходится каждый раз писать ненужный код:

$stmt
= $pdo->prepare($sql);
$stmt->execute($params);
$data = $stmt->fetch();

Что называется - за что боролись, на то и напоролись. Никакого сокращения кода по сравнению с приснопамятной mysql_query() не произошло. А очень бы хотелось!
При этом Wes Furlong подложил пользователям PDO еще одну свинью - execute() возвращает тупо булево значение вместо того, чтобы возвращать стейтмент, что позволило бы реализовать method chaining и получать красивый код вида

$data
= $pdo->prepare($sql)->execute($params)->fetch();

Но увы, даже такой подход почти недоступен. Да и в любом случае он избыточен, поскольку в большинстве случаев нам не надо делать ни prepare, ни execute - нам надо тупо выполнить чертов запрос, передав в него чертовы данные для плейсхолдеров.

Поэтому, чтобы сократить количество писанины, добавим к PDO одну функцию, run(), вся функция которой будет сводиться приведенному выше коду - выполнить prepare/execute и вернуть стейтмент:

class MyPDO extends PDO
{
public function
run($sql, $args = NULL)
{
$stmt = $this->prepare($sql);
$stmt->execute($args);
return
$stmt;
}
}

А уже из стейтмента мы можем получить любые данные, любым стандартным способом:

$data = $pdo->run("SELECT * FROM users WHERE sex='male'")->fetchAll();

Проблема N2, доступность
Еще одним неприятным открытием для новичков является то, что к ПДО нельзя обратиться в любом месте скрипта, как к mysql_query().

Поэтому следующим улучшением будет реализация доступа к ПДО через статический синглтон. Этот паттерн частенько ругают в интернете, и за дело. Но тут надо понимать одну простую вещь:
Если ваш код подвержен тем проблемам, которые может породить синглтон - значит, вы уже пользуетесь высокоуровневым драйвером БД, скорее всего из состава популярно фремворка.
Но если вы только-только вылезли из пещеры с mysql_query(), привыкнув писать её по всему коду не заботясь о передаче соединения, то необходимость таскать за собой инстанс PDO станет серьёзным испытанием. При том, что и сам синтаксис PDO, и подготовленных выражений и сами по себе образуют не хилый такой порог вхождения. Так что попробуем подсластить пилюлю возможностью обратиться к БД из любого места скрипта, как в старые добрые времена.

В итоге у нас получилась очень компактная надстройка над PDO, которая, будучи такой же простой в использовании, как и mysql_query(), при этом сокращает код и обеспечивает безопасность подготовленных выражений.

Код

<?php
define
('DB_HOST', 'localhost');
define('DB_NAME', 'test');
define('DB_USER', 'root');
define('DB_PASS', '');
define('DB_CHAR', 'utf8');

class
DB
{
protected static
$instance = null;

public function
__construct() {}
public function
__clone() {}

public static function
instance()
{
if (
self::$instance === null)
{
$opt = array(
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC,
PDO::ATTR_EMULATE_PREPARES => TRUE,
);
$dsn = 'mysql:host='.DB_HOST.';dbname='.DB_NAME.';charset='.DB_CHAR;
self::$instance = new PDO($dsn, DB_USER, DB_PASS, $opt);
}
return
self::$instance;
}

public static function
__callStatic($method, $args)
{
return
call_user_func_array(array(self::instance(), $method), $args);
}

public static function
run($sql, $args = [])
{
if (!
$args)
{
return
self::instance()->query($sql);
}
$stmt = self::instance()->prepare($sql);
$stmt->execute($args);
return
$stmt;
}
}


Примеры

# Создаем таблицу
DB::query("CREATE temporary TABLE pdowrapper (id int auto_increment primary key, name varchar(255))");

# множественное исполнение подготовленных выражений
$stmt = DB::prepare("INSERT INTO pdowrapper VALUES (NULL, ?)");
foreach ([
'Sam','Bob','Joe'] as $name)
{
$stmt->execute([$name]);
}
var_dump(DB::lastInsertId());
//string(1) "3"

# Получение строк в цикле
$stmt = DB::run("SELECT * FROM pdowrapper");
while (
$row = $stmt->fetch(PDO::FETCH_LAZY))
{
echo
$row['name'],",";
echo
$row->name,",";
echo
$row[1], PHP_EOL;
}
/*
Sam,Sam,Sam
Bob,Bob,Bob
Joe,Joe,Joe
*/

# Получение одной строки
$id = 1;
$row = DB::run("SELECT * FROM pdowrapper WHERE id=?", [$id])->fetch();
var_export($row);
/*
array (
'id' => '1',
'name' => 'Sam',
)
*/

# Получение одного поля
$name = DB::run("SELECT name FROM pdowrapper WHERE id=?", [$id])->fetchColumn();
var_dump($name);
//string(3) "Sam"

# Получение всех строк в массив
$all = DB::run("SELECT name, id FROM pdowrapper")->fetchAll(PDO::FETCH_KEY_PAIR);
var_export($all);
/*
array (
'Sam' => '1',
'Bob' => '2',
'Joe' => '3',
)
*/

# Обновление таблицы
$new = 'Sue';
$stmt = DB::run("UPDATE pdowrapper SET name=? WHERE id=?", [$new, $id]);
var_dump($stmt->rowCount());
//int(1)