Pull to refresh

Entity “фреймворк” для PHP из одного класса

Reading time5 min
Views15K
Поскольку развитие технологий привело к тому, что у каждого программиста теперь есть собственный компьютер, в качестве побочного эффекта имеем тысячи разнообразных библиотек, фреймворков, сервисов, API и т.д. на все случаи жизни. Но когда этот случай жизни наступает, возникает проблема — что их этого использовать и что делать если оно не совсем подходит — переписывать, писать с нуля свое или прикручивать несколько решений для разных вариантов использования.

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

Перейдем к конкретной «ходовой» задаче — объектная прослойка для работы с базами данных в PHP. Решений великое множество, начиная от PDO и заканчивая многоуровневыми (и, на мой взгляд, не совсем уместными в PHP) ORM движками.

Большинство этих решений перекочевали в PHP из других платформ. Но зачастую авторы не учитывают особенности PHP, которые позволили бы резко упростить как написание, так и использование портируемых конструкций.
Одной из распространенных архитектур для данного класса задач является паттерн Active Record. В частности, по этому шаблону строятся так называемые Entity (сущности), в том или ином виде использующиеся в ряде платформ, начиная от персистентных бинов в EJB3 заканчивая EF в .NET.

Итак, построим подобную конструкцию для PHP. Соединим между собой две клёвые штуки — готовую библиотеку ADODB и слаботипизированность и динамические свойства объектов языка PHP.
Одной из многочисленных фич ADODB является так называемая автогенерация SQL запросов для вставки (INSERT) и обновления (UPDATE) записей на основе ассоциативных массивов с данными.
Собственно нет ничего военного взять массив, где ключи — имена полей а значения — соответственно, данные и сгенерировать строку SQL запроса. Но ADODB делает это более интеллектуально. Запрос строится на основе структуры таблицы, которая предварительно считывается с схемы БД. В результате во-первых, в sql попадают только существующие поля а не всё подряд, во-вторых, учитывается тип поля — для строк добавляются кавычки, форматы дат могут формироваться на основе timestamp если ADODB видит оный вместо строки в передаваемом значении и т.д.

Теперь зайдем со стороны PHP.
Представим такой класс (упрощенно).

class Entity{
   protected $fields = array();
   public final function __set($name, $value) {
        $this->fields[$name] = $value;
   }
   public final function __get($name) {
        return $this->fields[$name];
   }

}


Передавая внутренний массив библиотеке ADODB мы можем автоматически генерировать SQL запросы для обновления записи в БД данным объектом, При этом, не нужны громоздкие конструкции маппинга полей таблиц БД на поля объекта сущности на основе XML и тому подобного. Нужно только чтобы имя поля совпадало со свойством объекта. Поскольку то, как именуются поля в БД и поля объекта, для компьютера значения не имеет, то нет никаких причин чтобы они не совпадали.

Покажем, как это работает в конечном варианте.
Код законченного класса расположен на Gist. Это абстрактный класс, содержащий минимум необходимого для работы с БД. Отмечу, что данный класс — упрощенная версия решения, отработанного на нескольких десятках проектов.

Представим, что у нас такая табличка:

CREATE TABLE   `users` (
  `username` varchar(255) ,
  `created` date  ,
  `user_id` int(11) NOT NULL AUTO_INCREMENT,
  PRIMARY KEY (`user_id`)
)

Тип БД не имеет значения — ADODB обеспечивает переносимость для всех распространенных серверов БД.

Создадим класс сущности Пользователь, на основе класса Entity

/**
 * @table=users
 * @keyfield=user_id
 */
class User extends Entity{

}


Собственно и все.
Используется просто:


$user = new User();
$user->username='Вася Пупкин';
$user->created=time();
$user->save(); //сохраняем в  хранилище

// загрузим  опять
$thesameuser = User::load($user->user_id);
echo $thesameuser ->username;



Таблицу и ключевое поле указываем в псевдоаннотациях.
Также можем указать представление (к примеру, view=usersview) если, как это часто бывает, сущность выбирается на основе своей таблицы с приджойнеными или вычисляемыми полями. В этом случае выбираться данные будут из представления а обновляться будет таблица. Кому не нравятся такие аннотации могут переопределить метод getMetatada() и указать параметры таблицы в возвращаемом массиве.

Что еще полезного представляет класс Entity в данной реализации?

Например, мы можем переопределить метод init(), который вызывается после создания экземпляра Entity, чтобы инициализировать дату создания по умолчанию.
Или перегрузить метод afterLoad(), который автоматически вызывается после загрузки сущности из БД, чтобы преобразовать дату в timestamp для дальнейшего более удобного использования.
В результате получим не намного более сложную конструкцию.

/**
 * @table=users
 * @view=usersview
 * @keyfield=user_id
 */
class User extends Entity{
    protected function init() {
        $this->created = time();
    }
    protected function afterLoad() {
        $this->created = strtotime($this->created);
    }
}


Также можно перегрузить методы beforeSave и beforeDelete и другие события жизненного цикла где можно, например, выполнить валидацию перед сохранением или какие нибудь другие действия — например, убрать за собой картинки из аплоада при удалении пользователя.

Загружаем список сущностей по критерию (по сути условия для WHERE ).
$users = User::load("username like 'Пупкин' ");

Также класс Entity позволяет выполнить произвольный, «нативный» так сказать SQL запрос. Например, мы хотим вернуть список пользователей с какими нибудь группировками по статистике. Какие конкретно поля вернутся, не имеет значения (главное чтобы там было user_id, если есть необходимость дальнейшей манипуляции сущностью), нужно только знать их наименования чтобы обратится к выбранным полям. При сохранении сущности, как очевидно из вышеприведенного, тоже не надо заполнять все поля, какие будут присутствовать в объекте сущности, те и пойдут в БД. То есть нам не нужно создавать дополнительные классы для произвольных выборок. Примерно как анонимные структуры при выборке в EF только здесь это тот же класс сущности со всеми методами бизнес-логики.

Строго говоря, вышеприведенные методы получения списков несколько выходят за пределы паттерна AR. По сути — это фабричные методы. Но как завещал старик Оккама, не будем плодить сущности сверх необходимого и городить отдельный Entity Manager или типа того.

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

Заметим что вышеприведенное — просто классы PHP и их можно как угодно расширять и модифицировать, дописывать в сущности (или базовый класс Entity) свойства и методы бизнес-логики. То есть мы получаем не просто копию строки таблицы БД, а именно бизнес-сущность как часть объектной архитектуры приложения.

Кому это может быть полезно? Разумеется, не прокачанным разрабам, которые считают что использование чего то проще доктрины — не солидно, и не перфекционистам, уверенным, что если решение не вытягивает миллиард обращений к Бд в секунду то это не решение. Судя по форумам, перед многими обычными разработчиками, работающими над обычными (коих 99.9%) проектами рано или поздно возникает проблема найти простой и удобный объектный способ для доступа к БД. Но сталкиваются с тем, что большинство решений либо неоправданно наворочены, либо являются частью какого-либо фреймворка.

P.S. Вынес решение из фреймворка отдельным проектом GitHub
Tags:
Hubs:
-3
Comments239

Articles