Php classes and database

PHP класс для удобной и безопасной работы с MySQL

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

Есть несколько способов работы с SQL — можно использовать квери-билдер, можно ORM, можно работать с чистым SQL. Я избрал последний вариант, потому что мне он ближе. Я совсем не считаю первые два плохими. Просто лично мне всегда было тесно в их рамках. Но я ни в коем случае не утверждаю, что мой вариант лучше. Это просто ещё один вариант. Который можно использовать, в том числе, и при написании ORM-а. В любом случае, я считаю, что наличие безопасного способа работать с чистым SQL не может принести какой-либо вред. Но при этом, возможно, поможет последним оставшимся приверженцам использования mysql_* в коде приложения, отказаться, наконец, от этой порочной практики.

  1. 100% защита от SQL инъекций
  2. При этом защита очень удобная в применении, делающая код короче, а не длиннее
  3. Универсальность, портабельность и простота освоения

Безопасность

  1. Любые — без исключений! — динамические элементы попадают в запрос только через плейсхолдеры.
  2. Всё, что не получается подставить через плейсхолдеры — прогоняется сначала через белый список.

Удобство и краткость кода приложения

  • query() — возвращает mysqli resource. Может использоваться традиционно, с fetch() и т.д.
  • getOne() — возвращает скаляр, первый элемент первой строки результата
  • getRow() — одномерный массив, первую строку результата
  • getCol() — одномерный массив скаляров — колонку таблицы
  • getAll() — двумерный массив, индексированный числами по порядку
  • getInd() — двумерный массив, индексированный значениями поля, указанного первым параметром
  • getIndCol() — массив скаляров, индексированный полем из первого параметра. Незаменимо для составления словарей вида key => value
$data = $db->getAll("SELECT * FROM ?n WHERE mod=?s LIMIT ?i",$table,$mod,$limit); 

В этом коде есть только необходимые и значащие элементы, но нет ничего лишнего и повторяющегося. Все потроха аккуратно упрятаны внутрь класса: хелпер getAll() позволяет получить сразу нужный результат без написания циклов в коде приложения, а типизованные плейсхолдеры позволяют безопасно добавлять в запрос динамические элементы любых типов без прописывания привязок (bind_param) вручную. Extra DRY код! В случаях использования плейсхолдеров ?a и ?u разница в количестве кода становится ещё больше:

$data = $db->getAll("SELECT * FROM table WHERE category IN (?a)",$ids); 

Универсальность и простота освоения

  1. Очень маленький API — пол-дюжины плейсхолдеров и столько же хелперов.
  2. Мы работаем со старым добрым SQL, который не надо заново учить.
  3. На первый взгляд незаметная, но невероятно полезная функция parse(), которая изначально предназначалась только для отладки, но в итоге выросла до ключевого элемента при составлении сложных запросов.
$sqlpart = ''; if (!empty($var)) < $sqlpart = $db->parse(" AND field = ?s", $var); > $data = $db->getAll("SELECT * FROM table WHERE a=?i ?p", $id, $sqlpart); 

Здесь важно отметить несколько моментов.
Во-первых поскольку мы не связаны родным API, никто не запрещает нам пропарсить не весь запрос целиком, а только его часть. Это оказывается супер-удобно для запросов, собирающихся в соответствии какой-либо логикой: мы парсим только часть запроса, а затем она подставляется в основной запрос через специальный «холостой» плейсхолдер, чтобы избежать повторного парсинга (и соблюсти правило «любые элементы подставляются только через плейсхолдер»).
Но, к сожалению, это является слабым местом всего класса. В отличие от всех остальных плейсхолдеров (которые, даже будучи использованы неверно, никогда не приведут к инъекции) некорректное использование плейсхолдера ?p может к ней привести.
Однако защита от дурака сильно усложнила бы класс, но при этом все равно никак не защитила бы от тупой вставки переменной в строку запроса. Поэтому я решил оставить как есть. Но если вы знаете способ, как без слишком большого оверинжиниринга решить эту проблему — я был бы благодарен за идеи.

Читайте также:  Java util function consumer

Тем не менее, в итоге мы получили мощный и лёгкий генератор запросов, который с лихвой оправдывает этот небольшой недостаток.
Мощный потому, что мы не ограничены синтаксисом квери-билдера, «SQL-ем, написанным на PHP» — мы пишем чистый SQL.
Лёгкий потому, что весь API составления запросов состоит из полудюжины плейсхолдеров и функции parse()
Вот мой любимый пример — вставка с использованием функций Mysql

$data = array('field'=>$value,'field2'=>$value); $sql = "INSERT INTO table SET ts=unix_timestamp(), ip=inet_aton(?s),?u"; $db->query($sql, $ip, $data); 

С одной стороны, мы сохраняем синтаксис SQL, с другой — делаем его безопасным, а с третьей — капитально сокращаем количество кода.

Подробнее о типизованных плейсхолдерах

  • самое главное — безопасность. Добавив переменную черз плейсхолдер, мы можем быть уверены в том, что она будет корректно отформатирована.
  • локальность форматирования. Это не менее важный момент. Во-первых, данные форматируются непосредственно перед попаданием в запрос, и не затрагивают исходную переменную, которая потом может быть использована где-то ещё. Во-вторых, данные форматируются ровно там, где нужно, а не до начала работы скрипта, как при magic quotes, и не в десяти возможных местах кода несколькими разработчиками, каждый из которых может понадеяться на другого.

Кроме того, типизованный плейсхолдер — это ОЧЕНЬ удобно!
Во-первых, потому что становится ненужным специальный оператор для привязки значения к плейсхолдеру (но при этом сохраняется возможность указать тип передаваемого значения!)
Во-вторых, раз уж мы изобрели типизованный плейсхолдер — мы можем налепить этих плейсхолдеров огромное количество, для решения множества рутинных задач по составлению SQL запросов.
В первую очередь сделаем плейсхолдер для идентификаторов — нам его отчаянно не хватает в реальной, а не воображаемой авторами стандартных API, жизни. Как только девелопер сталкивается с необходимостью динамически добавить в запрос имя поля — каждый начинает извращаться по-своему, кто в лес, кто по дрова. Здесь же всё унифицировано с остальными элементами запроса, и добавление идентификатора становится не сложнее добавления строки. Но при этом идентификатор форматируется не как строка, а в соответствии со своими собственными правилами — заключается в обратные кавычки, а внутри эти кавычки экранируются удвоением.
Дальше — больше. Следующая головная боль любого разработчика, когда-либо пытавшегося использовать стандартные prepared statements в реальной жизни — оператор IN(). Вуаля, у нас есть плейсхолдер и для этой операции! Подстановка массива становится не сложнее любых других элементов, плюс она унифицирована с ними — никаких отдельных функций, меняется всего лишь буква в плейсхолдере.
Точно таким же образом делаем и плейсхолдер для SET. Не удержусь и продемонстрирую, насколько простым становится код для такого замороченного запроса, как INSERT… ON DUPLICATE:

$data = array('offers_in' => $in, 'offers_out' => $out); $sql = "INSERT INTO stats SET pid=?i,dt=CURDATE(),?u ON DUPLICATE KEY UPDATE ?u"; $db->query($sql,$pid,$data,$data); 
  • ?s («string») — строки (а также DATE, FLOAT и DECIMAL).
  • ?i («integer») — целые числа.
  • ?n («name») — имена полей и таблиц
  • ?p («parsed») — для вставки уже обработанных частей запроса
  • ?a («array») — набор значений для IN (строка вида ‘a’,’b’,’c’)
  • ?u («update») — набор значений для SET (строка вида `field`=’value’,`field`=’value’)
Читайте также:  Python telegram api tutorial

Кстати, как многие могли заметить, этот класс во многом напоминает библиотеку DbSimple Дмитрия Котерова. Но у меня есть принципиальные расхождения с некоторыми идеями, заложенными в неё.
Во-первых, я противник любой магии, когда одна и та же функция может возвращать различный результат в зависимости от типа переданных данных. Это, возможно, чуть упрощает написание, но при этом чудовищно затрудняет сопровождение и отладку кода. Поэтому в моем классе вся магия сведена к минимуму, а все операции и типы данных всегда прописываются явно.
Во-вторых, в DbSimple немного, на мой взгляд, переусложнённый синтаксис. С одной стороны, фигурные скобки — гениальная идея. С другой — а зачем, если в нашем распоряжении вся мощь PHP? Поэтому я решил пойти другим путём и весто «внутренней» — заведомо ограниченной — логики ввёл «внешнюю», ограниченную лишь синтаксисом РНР. Главное, чтобы любые динамические элементы попадали в запрос только через плейсхолдеры, а остальное зависит лишь от фантазии разработчика (и функции parse()).

Код класса доступен на Гитхабе, github.com/colshrapnel/safemysql/blob/master/safemysql.class.php
Cheat sheet с основными командами и примерами: phpfaq.ru/misc/safemysql_cheatsheet_ru.pdf
Хорошее представление о возможностях можно получить на странице примеров документации (к сожалению, ещё не законченной), phpfaq.ru/safemysql
Там же есть ответы на часто задаваемые вопросы, такие как «почему ты не используешь родные prepared statements?» и пр.
Тем не менее, буду рад ответить на любые вопросы в комментариях, а так же улучшить по вашим замечаниям как сам класс, так и эту статью.

Источник

PHP Class MySQLi — СУБД MySQL

На сегодняшний день существует множество статей с готовыми решениями, как работать с СУБД MySQL. Однако, почти в каждой статье приводятся примеры процедурного вызова функций. Начиная с php-версии 5.5.0 расширение MySQL будет удалено из сборки и вообще, это расширение уже устарело. Вместо него будем использовать расширение MySQLi в ООП.
Данная статья является готовым решением для работы с СУБД.

При разработке очередного проекта, я столкнулся с тем, что в каждом методе следующего написанного класса с обращением к СУБД, мне нужно было обращаться к классу работы с СУБД, для очередного select, insert, update и т.п., то есть тягать в функции параметра global с объявленной переменной вне класса. А устанавливать новое соединение с базой — слишком затратно, да и не выгодно, только ресурсы системы тратить. И решил написать переписать класс работы с базой, с сохранением экземпляра класса, чтобы обращаться сразу напрямую к методам класса из любой области видимости переменной.

Для просмотра исходника смотрим под кат.

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

 catch (Exception $e) < exit($e->getMessage()); > ?> 

Ниже приведу содержимое класса DB.class.php

connect_error) throw new Exception("Connect failed: %s", $mysqli->connect_error); self::$mysqli = &$mysqli; self::$mysqli->query('SET NAMES utf8 COLLATE utf8_general_ci'); > public static function GetRows($rows, $single = false) < $result = array(); if ($rows === false) return $result; if ($single) return $rows->fetch_assoc(); while ($row = $rows->fetch_assoc()) array_push($result, $row); $rows->free(); return $result; > public static function Select($sql, $single = false) < $result = self::$mysqli->query($sql); return self::$db->GetRows($result, $single); > public static function Update($data, $table, $where) < $sets = ''; foreach ($data as $column =>$value) < $sets .= $sets ? ', ' : ''; $sets .= "`$column` = '$value'"; >$sql = "UPDATE $table SET $sets WHERE $where"; self::$mysqli->query($sql); > public static function Insert($data, $table) < $columns = ""; $values = ""; foreach ($data as $column =>$value) < $columns .= $columns ? ', ' : ''; $columns .= "`$column`"; $values .= $values ? ', ' : ''; $values .= "'$value'"; >$sql = "INSERT INTO $table ($columns) VALUES ($values)"; self::$mysqli->query($sql); return self::$mysqli->insert_id; > public static function CountRows($table, $request = false) < $sql = "SELECT COUNT(*) FROM $table "; $sql .= $request ? $request : ''; $result = self::$mysqli->query($sql); $count = $result->fetch_array(); $result->free(); return $count[0]; > public static function Query($sql) < return self::$mysqli->query($sql); > public static function Delete($table, $where) < $sql = "DELETE FROM $table WHERE $where"; self::$mysqli->query($sql); > public static function Close() < self::$mysqli->close(); > function __destruct() < self::$db->Close(); > > ?> 

Источник

Оцените статью