Простая обертка для 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($dsnDB_USERDB_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)