Это старая версия урока, которая больше не обновляется. Новая версия расположения тут: https://github.com/codedokode/pasta/blob/master/db/patterns-oop.md
Разберемся, как правильно с применением ООП сохранять и загружать данные из базы. Существуют такие подходы:
- примитивный подход с использованием PDO/массивов
- ActiveRecord http://design-pattern.ru/patterns/active-record.html
- DataMapper http://design-pattern.ru/patterns/data-mapper.html
- TableDataGateway http://design-pattern.ru/patterns/table-data-gateway.html
В этом варианте мы не используем никаких классов, а просто загружаем/вставляем данные в базу с использованием PDO:
<?php
$query = $pdo->prepare("SELECT * FROM news WHERE categId = :categId LIMIT 20");
$query->execute(array(':categId' => $categId));
$news = $query->fetchAll(); // получаем массив массивов
Этот способ не требует создания никаких классов, но он очень ограничен и ведет к плохому коду если в нашем приложении больше 1-2 таблиц: ты начнешь путаться, где какой массив и какие у него поля. В общем, с таким подходом ничего хорошего нам не светит.
Однако этот подход наиболее эффективен при работе с огромным количеством записей.
Такой подход например поддерживается классом Zend_Db_Select
в Zend Framework 1: http://framework.zend.com/manual/1.12/ru/zend.db.select.html
Остальные подходы подразумевают создание класса-сущности (entity), который представляет собой одну запись в таблице. Например, для работы с таблицей новостей мы можем создать сущность News
, представляющую собой одну новость:
<?php
class News
{
public $id;
public $title;
public $text;
public $categId;
/**
* Дата в виде объекта DateTime
*/
public $date;
/**
* Возвращает возраст новости в днях
*/
public function getAgeDays()
{
// Находим разницу между сегодня и датой публикации
$today = new DateTime();
$interval = $today->diff($this->date);
return $interval->d;
}
/**
* Проверяет все ли поля заполнены перед вставкой в БД
*/
public function validate(ErrorList $errors)
{
if (!$this->title) {
$errors->add('title', 'Необходимо указать название новости');
}
if (!$this->text) {
$errors->add('text', 'Необходимо заполнить текст новости');
}
}
}
Заметь, что в класс мы можем поместить вспомогательные методы, работающие с этой новостью — удобно (хотя стоит ли вставлять валидацию в сущность — спорный вопрос так как в нашем варианте у нее нет доступа к БД и она например не может проверить заголовок на уникальность — чтобы это было возможно, надо переносить валидацию в другое место). Теперь у нас есть новость, давай посмотрим, как можно сохранить или загрузить ее из базы данных.
Код, реализующий загрузку и сохранение сущностей в SQL базу данных еще называется ORM (Object-Relational Mapper). ORM пытаются избавить нас от необходимости писать однотипные примитивные SQL запросы, позволяя нам работать на более высоком уровне.
Это более простой способ. При его использовании методы для сохранения/загрузки сущности из БД добавляются прямо в нее. Чтобы не копипастить их в каждый класс, их обычно добавляют в базовый класс, а сущность наследуют от него. При этом обычно в сущности делается метод, возвращающий информацию о соответствии полей объекта таблице и полям в базе данных (чтобы можно было правильно составить SQL запрос):
<?php
class News extends ActiveRecordBase
{
.....
protected function getTableName()
{
return 'news'; // имя таблицы с новостями
}
protected function getFields()
{
// список полей, которые отображаются на таблицу
return array('id', 'title', 'text', 'date', 'categId');
}
/**
* Вызывается перед вставкой в таблицу
*/
protected function beforeInsert()
{
if (!$this->date) {
// ставим дату создания если не задана
$this->date = new DateTime();
}
}
}
Соответственно, вот как выглядит пример поиска записей, вставки и обновления записи:
<?php
$news = new News($pdo); // в некоторых фреймворках передавать объект БД не надо −
// сущность сама берет объект откуда-нибудь
// возвращает массив объектов-новостей
$lastestNews = $news->findLatestNews();
// возвращает новость с id = 10
$someNews = $news->getById(10);
// меняем название
$someNews->title = 'Новое название';
// Обновляем запись в БД
$someNews->save();
$newNews = new News($pdo);
$newNews->title = 'Сенсация!';
$newNews->text = 'Текст новости';
// вставка в БД. После нее поля id и date заполняются автоматически
$newNews->save();
Такой подход используется, например в Yii 1: http://www.yiiframework.com/doc/guide/1.1/ru/database.ar
Также он использовался в Doctrine 1.
Этот подход относительно прост, но он имеет недостаток: мы смешиваем бизнес-логику (методы работы со свойствами новости) и работу с БД в одном классе. Хотя объект-новость вполе может сущестовать и сам по себе. Для решения этой проблемы нам нужен DataMapper.
В DataMapper мы выносим код сохранения/загрузки сущностей (и все что связано с базой данных) в отдельный класс. Вот пример такого класса:
<?php
class NewsMapper
{
....
public function save(News $news) { ... }
public function getById($id) { ... }
public function findLatestNews() { ... }
}
И вот пример использования:
<?php
$mapper = new NewsMapper($pdo);
// Поиск новости по id
$someNews = $mapper->getById(10);
// меняем название
$someNews->title = 'Новое название';
// Обновляем запись в БД
$mapper->save($someNews);
// создание новой
$newNews = new News();
$newNews->title = 'Сенсация!';
$newNews->text = 'Текст новости';
// вставка в БД
$mapper->save($newNews);
Этот подход используется в ORM Doctrine2: http://odiszapc.ru/doctrine/ Только там Mapper называется Repository.
Это что-то напоминающее DataMapper, но он может быть реализован без объектов-сущностей. Например, в ZF есть Zend_Db_Table
который его реализует: http://framework.zend.com/manual/1.12/ru/zend.db.table.html — там результаты возвращаются в виде объектов класса Zend_Db_Table_Row
. Соответственно, для любых сущностей используется один и тот же класс и это сильно напоминает подход с массивами.
Doctrine 2 — это библиотека реализующая паттерн DataMapper. Ты просто добавляешь в свои сущности аннотации, задающие соответствие полей объектов и полей в базе данных, а Doctrine дает тебе классы-репозитории, которые позволяют загружать и сохранять твои объекты в базу данных. Doctrine 2 — очень мощная и популярная, хотя и непростая для начинающего, библиотека. Чтобы с ней работать, надо понимать саму идею ORM, паттерны UnitOfWork и IdentityMap. И придется много читать мануал по ней.
Напишу еще несколько вещей, которые мы не рассмотрели, но которые есть в больших ORM вроде Doctrine 2:
- свой язык запросов DQL, похожий на SQL
- описание через конфиг или аннотации: ты можешь с помощью специальных комментариев-аннотаций указать, как поля объекта связаны с полями в таблице: http://odiszapc.ru/doctrine/basic_mapping/
- IdentityMap ( http://design-pattern.ru/patterns/identity-map.html ): если ты повторно выбираешь ту же самую сущность из базы, тебе возвращается ссылка на существующую сущность. Доктрина следит чтобы каждая сущность существовала ровно в одном экземпляре, и это помогает избежать противоречий когда есть несколько экземпляров и непонятно в каком из них актуальные данные
- UnitOfWork ( http://design-pattern.ru/patterns/unit-of-work.html ): когда ты делаешь изменения в сущностях, они не сохраняются автоматически. Ты должен явно вызвать метод flush() и тогда EntityManager найдет все изменившиеся, новые и удаленные сущности и соответственно обновит/вставит/удалит записи в базе одной транзакцией.
- работа с ассоциациями (связями). Например, Новость может относиться к Категории и быть помечена Тегами, а также под ней могут быть оставлены Комментарии (у которых в свою очередь есть Авторы). При этом если мы должны иметь возможность создавать такие связи и разрывать их. Представь, как сложно такое реализовать самому (трудно представить? попробуй напиши код).
Doctrine 2 не требует от тебя унаследовать класс-сущность от какого-то базового класса, он позволяет связать любой класс с базой данных — главное чтобы в нем были методы get../set.. для чтения и записи полей. Также, придется потратить время на то, чтобы разобраться, как правильно использовать этот ORM и как настроить в нем кеширование метаданных, чтобы он работал с приемлемой скоростью.
В общем, если у тебя маленькое число таблиц, то ты можешь попробовать обойтись простым DataMapper. Но если у тебя много таблиц, и есть связи между ними то использование Doctrine 2 поможет отойти от написания SQL запросов к манипуляции объектами, сделать код проще и короче и сэкономить твое время. Если же у тебя высоконагруженный проект, то возможно от сложных ORM придется отказаться.
eloquent it is active record pattern