Я подумал, что смогу ответить на свой вопрос. Далее следует лишь один из способов решения проблем 1-3 в моем первоначальном вопросе.
Отказ от ответственности: я не всегда могу использовать правильные термины при описании моделей или методов. Простите за это.
Цели:
- Создайте полный пример базового контроллера для просмотра и редактирования
Users
.
- Весь код должен быть полностью тестируемым и проверяемым.
- Контроллер не должен знать, где хранятся данные (то есть их можно изменить).
- Пример, показывающий реализацию SQL (наиболее распространенная).
- Для максимальной производительности контроллеры должны получать только те данные, которые им нужны, без дополнительных полей.
- Реализация должна использовать некоторые типы картографических данных для простоты разработки.
- Реализация должна иметь возможность выполнять сложные поиски данных.
Решение
Я делю свое взаимодействие с постоянным хранилищем (базой данных) на две категории: R (чтение) и CUD (создание, обновление, удаление). Мой опыт показывает, что чтение действительно вызывает замедление работы приложения. И хотя манипулирование данными (CUD) на самом деле медленнее, это происходит гораздо реже, и, следовательно, гораздо меньше проблем.
CUD (создать, обновить, удалить) легко. Это будет связано с работой с реальными моделями , которые затем будут переданы мне Repositories
для настойчивости. Обратите внимание, что мои репозитории будут по-прежнему предоставлять метод Read, но просто для создания объекта, а не для отображения. Подробнее об этом позже.
R (Читать) не так просто. Здесь нет моделей, только объекты стоимости . Используйте массивы, если хотите . Эти объекты могут представлять одну модель или смесь множества моделей, чего угодно. Они не очень интересны сами по себе, но как они создаются. Я использую то, что я звоню Query Objects
.
Код:
Модель пользователя
Давайте начнем с нашей базовой пользовательской модели. Обратите внимание, что нет расширения ORM или базы данных вообще. Просто чистая модель славы. Добавьте ваши геттеры, сеттеры, валидацию, что угодно.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Интерфейс репозитория
Прежде чем создать свой пользовательский репозиторий, я хочу создать свой интерфейс репозитория. Это определит «контракт», которому должны следовать репозитории, чтобы их мог использовать мой контроллер. Помните, мой контроллер не будет знать, где на самом деле хранятся данные.
Обратите внимание, что мои репозитории будут содержать только эти три метода. Этот save()
метод отвечает как за создание, так и за обновление пользователей, просто в зависимости от того, имеет ли пользовательский объект установленный идентификатор.
interface UserRepositoryInterface
{
public function find($id);
public function save(User $user);
public function remove(User $user);
}
Реализация репозитория SQL
Теперь для создания моей реализации интерфейса. Как уже упоминалось, мой пример будет с базой данных SQL. Обратите внимание на использование средства отображения данных, чтобы избежать необходимости писать повторяющиеся запросы SQL.
class SQLUserRepository implements UserRepositoryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function find($id)
{
// Find a record with the id = $id
// from the 'users' table
// and return it as a User object
return $this->db->find($id, 'users', 'User');
}
public function save(User $user)
{
// Insert or update the $user
// in the 'users' table
$this->db->save($user, 'users');
}
public function remove(User $user)
{
// Remove the $user
// from the 'users' table
$this->db->remove($user, 'users');
}
}
Интерфейс Query Object
Теперь, когда CUD (Create, Update, Delete) заботится о нашем хранилище, мы можем сосредоточиться на R (Read). Объекты запросов - это просто инкапсуляция некоторого типа логики поиска данных. Они не строители запросов. Абстрагируя его, как наш репозиторий, мы можем изменить его реализацию и упростить его тестирование. Примером Объекта Запроса может быть AllUsersQuery
или AllActiveUsersQuery
, или даже MostCommonUserFirstNames
.
Возможно, вы думаете: «Разве я не могу просто создать методы в своих репозиториях для этих запросов?» Да, но вот почему я этого не делаю:
- Мои репозитории предназначены для работы с модельными объектами. В приложении реального мира, зачем мне когда-либо нужно получать
password
поле, если я ищу список всех моих пользователей?
- Хранилища часто зависят от модели, но запросы часто включают в себя несколько моделей. Так в какой репозиторий вы положили свой метод?
- Это делает мои репозитории очень простыми, а не раздутым классом методов.
- Все запросы теперь организованы в свои собственные классы.
- На самом деле, на данный момент, репозитории существуют просто для абстрагирования уровня моей базы данных.
Для моего примера я создам объект запроса для поиска «AllUsers». Вот интерфейс:
interface AllUsersQueryInterface
{
public function fetch($fields);
}
Реализация объекта запроса
Здесь мы можем снова использовать средство отображения данных, чтобы ускорить разработку. Обратите внимание, что я разрешаю одну настройку для возвращенного набора данных - полей. Это примерно столько, сколько я хочу, чтобы манипулировать выполненным запросом. Помните, мои объекты запросов не являются построителями запросов. Они просто выполняют определенный запрос. Тем не менее, поскольку я знаю, что, вероятно, я буду часто использовать его в различных ситуациях, я даю себе возможность указать поля. Я никогда не хочу возвращать поля, которые мне не нужны!
class AllUsersQuery implements AllUsersQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch($fields)
{
return $this->db->select($fields)->from('users')->orderBy('last_name, first_name')->rows();
}
}
Прежде чем перейти к контроллеру, я хочу показать еще один пример, чтобы проиллюстрировать, насколько это мощно. Может быть, у меня есть механизм отчетности и нужно создать отчет для AllOverdueAccounts
. Это может быть сложно с моим картографом данных, и я, возможно, захочу написать некоторые реальные SQL
в этой ситуации. Нет проблем, вот как может выглядеть этот объект запроса:
class AllOverdueAccountsQuery implements AllOverdueAccountsQueryInterface
{
protected $db;
public function __construct(Database $db)
{
$this->db = $db;
}
public function fetch()
{
return $this->db->query($this->sql())->rows();
}
public function sql()
{
return "SELECT...";
}
}
Это хорошо сохраняет всю мою логику для этого отчета в одном классе, и это легко проверить. Я могу издеваться над содержанием моего сердца или даже использовать совсем другую реализацию.
Контроллер
Теперь самое интересное - собрать все кусочки вместе. Обратите внимание, что я использую внедрение зависимостей. Обычно зависимости вводятся в конструктор, но на самом деле я предпочитаю вставлять их прямо в методы контроллера (маршруты). Это минимизирует граф объектов объекта контроллера, и я на самом деле считаю его более разборчивым. Обратите внимание, если вам не нравится этот подход, просто используйте традиционный метод конструктора.
class UsersController
{
public function index(AllUsersQueryInterface $query)
{
// Fetch user data
$users = $query->fetch(['first_name', 'last_name', 'email']);
// Return view
return Response::view('all_users.php', ['users' => $users]);
}
public function add()
{
return Response::view('add_user.php');
}
public function insert(UserRepositoryInterface $repository)
{
// Create new user model
$user = new User;
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the new user
$repository->save($user);
// Return the id
return Response::json(['id' => $user->id]);
}
public function view(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('view_user.php', ['user' => $user]);
}
public function edit(SpecificUserQueryInterface $query, $id)
{
// Load user data
if (!$user = $query->fetch($id, ['first_name', 'last_name', 'gender', 'email'])) {
return Response::notFound();
}
// Return view
return Response::view('edit_user.php', ['user' => $user]);
}
public function update(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Update the user
$user->first_name = $_POST['first_name'];
$user->last_name = $_POST['last_name'];
$user->gender = $_POST['gender'];
$user->email = $_POST['email'];
// Save the user
$repository->save($user);
// Return success
return true;
}
public function delete(UserRepositoryInterface $repository)
{
// Load user model
if (!$user = $repository->find($id)) {
return Response::notFound();
}
// Delete the user
$repository->delete($user);
// Return success
return true;
}
}
Последние мысли:
Здесь важно отметить, что когда я изменяю (создаю, обновляю или удаляю) сущности, я работаю с реальными объектами модели и выполняю персистентность через свои репозитории.
Однако, когда я отображаю (выбираю данные и отправляю их в представления), я работаю не с объектами модели, а с обычными объектами старых значений. Я выбираю только те поля, которые мне нужны, и он спроектирован таким образом, чтобы я мог максимально повысить производительность поиска данных.
Мои репозитории остаются очень чистыми, и вместо этого этот "беспорядок" организован в моих модельных запросах.
Я использую средство отображения данных, чтобы помочь с разработкой, так как просто смешно писать повторяющийся SQL для общих задач. Тем не менее, вы можете написать SQL там, где это необходимо (сложные запросы, отчеты и т. Д.). И когда вы это сделаете, это приятно спрятать в правильно названный класс.
Я хотел бы услышать ваше мнение о моем подходе!
Обновление за июль 2015 года:
В комментариях меня спрашивали, где я все это закончил. Ну, не так уж далеко на самом деле. По правде говоря, я все еще не очень люблю репозитории. Я считаю их излишними для базовых поисков (особенно если вы уже используете ORM) и грязными при работе с более сложными запросами.
Обычно я работаю с ORM в стиле ActiveRecord, поэтому чаще всего я просто ссылаюсь на эти модели непосредственно во всем приложении. Однако в ситуациях, когда у меня есть более сложные запросы, я буду использовать объекты запросов, чтобы сделать их более пригодными для повторного использования. Я также должен отметить, что я всегда внедряю свои модели в свои методы, что облегчает имитацию в моих тестах.