Немного философии или крутой класс-обертка над PHP PDO (MySQL, SQlite etc.)

Здравствуйте.

Читатель моего блока наверное недоумевает – блог называется “Философия программирования”, но вот философии в нем что-то не наблюдается. Я хотел написать пост о программистах странных, но позже напишу. Сейчас хочу представить класс-обёртку для PHP/PDO.

Класс-обёртка для PHP/PDO

Если вы давно читаете мой блог (ну или понятное дело нагуглили пару постов в нем) вы знаете что я поклонник минимализма и простоты в программировании. Я пытаюсь упростить везде, где только возможно.

А как же русская народная пословица (мудрость?) “Простота хуже воровства”, спросите вы и поставите меня в тупик.
Когда я думал об этой пословице, я долго бил себя кулаком в грудь, и пытался сам себе доказать, что пословица неверна, что я прав, и лучше всё упрощать, чем усложнять. Но червь сомнения всё равно селился в моей душе и грыз меня изнутри, доводя до слёз.

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

Но в общем случае, в большинстве случаях и объективно говоря лучше всё упрощать, чем усложнять, согласитесь? Обычно усложняет тот, кто не может сделать проще или пытается скрыть какой-то обман.

Что касаемо кода (программирования), то чем проще и понятней код, чем его меньше, тем проще его поддерживать и отлаживать, верно? Тут нет никаких сомнений или исключений.

С другой стороны мы понимаем, что вряд ли возможно сократить/упростить какое-нибудь хитроумное уравнение из ядерной/квантовой физики или высшей математики (хотя кто его знает, может и можно), или молекулы ДНК, состоящие из миллионов нуклеотидов (хотя, наверное всё же можно), а также их кодирование в вычисляемый машинами код (на каком-либо языке программирования).

Хотя изумительно сложные по виду и разнообразные фракталы (например, классический фрактал Мандельброта), как вы знаете, рисуются буквально десятком (или менее) строчек кода, надо просто знать соотв. уравнения.

ОК, может хватить философии и займёмся делом?

Класс-обёртка для PHP/PDO – теперь уже точно! ))

Зачем нужны обертки, спросит некий наивный программист (новичок?). Грамотные обертки могут значительно упростить и классифицировать создания и использование узконаправленного кода.

Недостатки оберток – необходимо запомнить его функциональность (переменные, вызываемые методы и т.п.), а также вычисление машиной (процессором) лишнего кода, так как любая обертка в итоге обращается к своему родителю (то, что она оборачивает).

Преимущества оберток – значительное упрощение вычислений и кода.

Например, случай PHP PDO.

Да, в PDO есть все методы для работы с базами данных, это универсальная обертка над различными драйверами БД. В нем есть такие методы, как PDO::query, PDO::prepare, PDO::exec и т.д.

Также надо знать и учитывать класс PDOStatement, который позволяет подготавливать и вычислять различные запросы к БД и получать из них результаты.

Общие случаи работы с PDO таков:

Создаём экземпляр класса PDO:

try
{
	$db = new PDO(
		"mysql:host=localhost;dbname=my_database;charset=utf8",
		'username',
		'password',
		array(PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES UTF8')
	);
}
catch (PDOException $e)
{
	die('Сайт перегружен. Повторите попытку через 5 секунд'); 
}

После успешного выполнения этого куска кода у нас образуется переменная $db, с помощью которой мы можем выполнять любые операции над открытой базой данных.

Например:

$prep = $db->prepare("SELECT id, name, active FROM partners WHERE email=? AND pass=? LIMIT 1");
$prep->execute(array($email, $pass));
$ar = $prep->fetch(PDO::FETCH_ASSOC);
if(!count($ar)) {
  echo 'Неверное имя или пароль';
  return false;
}
// ... продолжение кода

Таким образом, используя $prep как экземпляр PDOStatement, мы получаем в $ar ассоциативный массив выборки из таблицы partners открытой БД. Теперь в $ar[‘id’], $ar[‘name’, $ar[‘active’] содержат нужные значения.

Используя же обертку, мы можем написать примерно так:

$prep = DB::execute("SELECT id, name, active FROM partners WHERE email=? AND pass=? LIMIT 1", array($email, $pass)); 
$ar = $prep->fetch(PDO::FETCH_ASSOC);
if(!count($ar)) {
  echo 'Неверное имя или пароль';
  return false;
}
// ... продолжение кода

В нашей обёртке все обращения к БД осуществляются через статический класс DB, таким образом нам не нужно следить, чтобы указатель на открытую БД всегда находился в пределах видимости функций работы с ним. Это уже большой плюс.
Кроме того, обертка как мы видим в предыдущем примере упрощает ряд вызовов, например вместо двух вызовов с созданием промежуточной переменной $prep типа PDOStatement мы в данном случае используем один.

Класс DB (static)

Итак, привожу полный пример класса DB, а после – краткое описание методов класса и примеры работы с ним.

//////////////////////////////////////////////////////////
//////////////// PDO DB wrapper	/////////////////////////
////////////////////////////////////////////////////////
class DB
{
// use STATIC rendering
	static private $db;	// db handler
 
	static public function init() {
		try {
			self::$db = new PDO(
				"mysql:host=localhost;dbname=my_database;charset=utf8",
				'username', 'password',
				array(PDO::MYSQL_ATTR_INIT_COMMAND=>'SET NAMES utf8')
			);
			self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);
			//self::$db->exec("SET NAMES utf8"); -- не нужно если сработает PDO::MYSQL_ATTR_INIT_COMMAND?
		}
		catch (PDOException $e)
		{
			die('<h3 style="color: blue">Ошибка соединения с БД. Повторите попытку через полминуты</h3>');
		}
	}
 
	static public function query($sql) {
		return self::$db->query($sql);
	}
 
	static public function exec($sql) {
		return self::$db->exec($sql);
	}
 
	// one column result
	static public function column($sql) {
		return self::$db->query($sql)->fetchColumn();
	}
 
	// intval one column result
	static public function columnInt($sql) {
		return intval(self::$db->query($sql)->fetchColumn());
	}
 
	static public function prepare($sql) {
		return self::$db->prepare($sql);
	}
 
	static public function lastInsertId() {
		return self::$db->lastInsertId();
	}
 
	// prepares and execute one SQL
	static public function execute($sql, $ar) {
		return self::$db->prepare($sql)->execute($ar);
	}
 
	// returns error info on db handler (not stmt handler)
	static public function error() {
		$ar = self::$db->errorInfo();
		return $ar[2] . ' (' . $ar[1] . '/' . $ar[0] . ')';
	}
 
	// returns one row fetched in FETCH_ASSOC mode
	static public function fetchAssoc($sql) {
		return self::$db->query($sql)->fetch(PDO::FETCH_ASSOC);
	}
 
	// returns one row fetched in FETCH_NUM mode
	static public function fetchNum($sql) {
		return self::$db->query($sql)->fetch(PDO::FETCH_NUM);
	}
 
}

Для запуска БД надо написать:

DB::init();

Обычно я делаю это в подключаемом файле с классом, но тут зависит от стиля программирования, иногда стоит собирать инициализирующие мотор сайта функции в одном месте. Так как в большинстве случаев имя/пароль/название БД статические, они захардкожены в самом классе.

Единственная не универсальность, это:

catch (PDOException $e)
{
  die('<h3 style="color: blue">Ошибка соединения с БД. Повторите попытку через полминуты</h3>');
}

Тут естественно Вам надо придумать свой обработчик ошибки открытия БД и уведомления пользователей, но в большинстве случаев сайт не может функционировать, и просит пользователя выполнить дополнительные действия.

Описание методов класса DB

Итак, вот описание всех методов класса:

DB::query($sql) – выполнить один запрос к БД с возвращением результата PDOStatement. его следует использовать только в том случае, если запрос не содержит необработанные данные, например из внешних источников. При использовании данных в запросе от внешних источников всегда следует применять подготовленные запросы (prepared statements) – смотрите ниже – в целях предотвращения SQL Injection (инъекции SQL запроса) и уверенности в целостности конечного запроса.

DB::exec($sql) – выполнение запроса, не требующего возврата результата. К нему относится тоже примечание, что и для DB::query. Так же его всегда следует использовать вместо DB::query, если запрос НЕ возвращает результата (обычно это UPDATE или DELETE), так как он, как указано, работает быстрее.
Возвращает кол-во затронутых строк.

DB::column($sql) – первая вкусность, которая упрощает жизнь. Выбор только одного значения из БД, соотв. запросу.
DB::columnInt($sql) – тоже самое, только возвращается intval значение поля выборки, что бывает полезно.

DB::prepare($sql) – подготовка запроса, возвращает PDOStatement.
DB::lastInsertId() – получение lastInsertId от последнего запроса INSERT.

DB::execute($sql, $ar) – подготавливает и сразу выполняет запрос. Каждый ? или :var в теле запроса должен соотв. значению из массива $ar. Пример смотрите выше. Возвращает true/false.

DB::error – возвращает строку последней ошибки над DB вместе с кодами в скобках. Помните, что при использования PDOSatement’ов ошибки, связанные именно с их исполнением, следует обрабатывать над ними, а не над общей DB.

И, наконец, две последние вкусности:
DB::fetchAssoc($sql)
DB::fetchNum($sql)

Возвращают результат выполнения $sql, в первом случае как PDO::FETCH_ASSOC, во втором – как PDO::FETCH_NUM.
Заметьте, что запрос НЕ подготовлен, поэтому можно использовать только с проверенными данными (НЕ из внешних источников). Но так как в большинстве случаев мы пишем запросы с корректными данными, эти функции часто бывают очень полезны и сокращают код программы.

Пример их использования

Получения данных клиента по его ID. Так как переменная ид у нас всегда целое число, использование его в запросе безопасно.

$id = intval($id); // обычно это уже сделано, то есть $id - у нас целая переменная,
// и не может содержать строки или ещё какой-нибудь мусор
$ar = DB::fetchAssoc("SELECT name,surname,email FROM cleints WHERE id=$id");
extract($ar);
// теперь в переменных $name, $surname, $email у нас необходимые данные клиента.

Ещё пример:

$id = intval($id); // обычно это уже сделано, то есть $id - у нас целая переменная,
// и не может содержать строки или ещё какой-нибудь мусор
 
list($name, $dtm) = DB::fetchNum("SELECT CONCAT(name, ' ', surname), DATE(dk_dtend) 
			FROM clients
            WHERE DATE(dk_dtend)>'2007-01-01' AND id=$id");

Этот прием можно было использовать и в предыдущем примере, всё зависит от стиля, настроения и желания. Мы выбрали из таблицы два поля – имя ($name) и дату ($dtm) одним запросом. Здесь мы также уверенны, что подставляемая в запрос переменная $id не может содержать мусор или непроверенные данные.

Заключение

Ну, пока всё, в будущем сделаю ещё пост про HTML::render – также крутая штука, упрощающая жизнь PHP-программиста (который не использует сомнительные тяжёлые фрэймворки, а делает всё сам, своими руками, как это делаю я, в большинстве случаев). Хотя фрэймворки если уметь с ними работать могут упростить написание конечного кода, но они зачастую бывают слишком тяжелы и неповортливы, кроме того, небезопасны в использовании, так как в них могут находится уязвимости.

Но ничего не обещаю. Ещё Linux – компиляция программ с GCC/G++, make, cmake – ох, это чудо. GNU Utils, я люблю Вас! Сам не знаю за что.

До новых встреч – вкусного горячего кофе, удобной позы в работе, а самое главное – я не устаю это повторять, хотя в зубах завязло – отличного настроения! Это главное. Сейчас – отличное настроение. Не потом, не завтра, не в обед пятницы, не через год, на Гавайях, а сейчас. Отличное настроение – это бодрость духа – решает всё. ИМХО, конечно, по моему скромному мнению.

Удачи на даче!