Я думав, що зламаю тріщину, відповідаючи на власне запитання. Далі йде лише один із способів вирішення питань 1-3 в моєму первісному питанні.
Відмова: Я не завжди можу використовувати правильні терміни, коли описую шаблони чи методи. Вибачте за це.
Цілі:
- Створіть повний приклад базового контролера для перегляду та редагування
Users
.
- Весь код повинен бути повністю перевіреним і макетним.
- Контролер не повинен мати уявлення, де зберігаються дані (це означає, що вони можуть бути змінені).
- Приклад для показу реалізації SQL (найпоширеніший).
- Для досягнення максимальної продуктивності контролери повинні отримувати лише потрібні їм дані - без додаткових полів.
- Реалізація повинна використовувати певний тип картографічних даних для зручності розробки.
- Реалізація повинна мати можливість виконувати складні пошукові дані.
Рішення
Я розділяю свою стійку взаємодію із зберіганням (базами даних) на дві категорії: R (читання) та CUD (створення, оновлення, видалення). Мій досвід показав, що читання - це дійсно те, що призводить до уповільнення роботи програми. І хоча обробка даних (CUD) насправді відбувається повільніше, вона відбувається набагато рідше, і тому набагато менше турбує.
CUD (Створити, оновити, видалити) легко. Це передбачає роботу з актуальними моделями , які потім передаються моїм Repositories
для наполегливості. Зауважте, мої сховища все одно надаватимуть метод читання, але просто для створення об'єкта, а не для відображення. Детальніше про це пізніше.
R (Прочитати) не так просто. Тут немає моделей, лише цінуються об'єкти . Використовуйте масиви, якщо хочете . Ці об'єкти можуть представляти одну модель або суміш багатьох моделей, що-небудь справді. Вони самі по собі не дуже цікаві, але як вони генеруються. Я використовую те, що дзвоню Query Objects
.
Код:
Модель користувача
Почнемо просто з нашої базової моделі користувача. Зауважте, що розширення ORM або бази даних взагалі немає. Просто чиста модель слава. Додайте свої геттери, сеттери, валідацію, що завгодно.
class User
{
public $id;
public $first_name;
public $last_name;
public $gender;
public $email;
public $password;
}
Інтерфейс сховища
Перш ніж створити моє сховище користувачів, я хочу створити свій інтерфейс сховища. Це визначить "контракт", якого повинні дотримуватися сховища, щоб використовувати мій контролер. Пам'ятайте, мій контролер не буде знати, де фактично зберігаються дані.
Зауважте, що мої сховища будуть містити лише ці три методи. save()
Метод відповідає за як створення і оновлення користувачів, просто в залежності від того, чи має об'єкт користувача в набір ID.
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');
}
}
Інтерфейс запиту
Тепер, коли 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, тому найчастіше я просто посилаюсь на ці моделі безпосередньо впродовж моєї програми. Однак у ситуаціях, коли у мене є складніші запити, я буду використовувати об'єкти запитів, щоб зробити їх більш використаними. Я також повинен зазначити, що я завжди ввожу свої моделі в свої методи, що полегшує їх глузування під час моїх тестів.