Відмова від відповідальності: далі - опис того, як я розумію MVC-подібні шаблони в контексті веб-додатків на основі PHP. Усі зовнішні посилання, які використовуються у змісті, є для пояснення термінів та понять, а не для того, щоб мати на увазі мою власну достовірність.
Перше, що я повинен прояснити, це: модель - шар .
По-друге: є різниця між класичним MVC і тим, що ми використовуємо в веб-розробці. Ось трохи старшої відповіді, яку я написав, яка коротко описує, чим вони відрізняються.
Яка модель НЕ:
Модель не є класом або будь-яким окремим об'єктом. Це дуже поширена помилка (я теж робив, хоча оригінальна відповідь була написана, коли я почав вчитися інакше) , оскільки більшість рамок увічнюють цю помилку.
Це не є об'єктно-реляційною технікою картографування (ORM) або абстрагуванням таблиць баз даних. Кожен, хто скаже вам інакше, швидше за все, намагається «продати» інший абсолютно новий ORM або цілі рамки.
Що таке модель:
У правильної адаптації MVC, М містить всю область бізнес - логіку і модель рівня в основному зроблений з трьох типів структур:
Об'єкти домену
Об'єкт домену - це логічний контейнер з чисто доменною інформацією; він зазвичай представляє логічну сутність у проблемному доменному просторі. Зазвичай називають діловою логікою .
Тут ви б визначали, як перевірити дані перед надсиланням рахунку або обчислити загальну вартість замовлення. У той же час, об'єкти домену абсолютно не знають про зберігання - ні звідки (база даних SQL, API REST, текстовий файл тощо), ні навіть якщо вони зберігаються чи отримуються.
Картографі даних
Ці об'єкти відповідають лише за зберігання. Якщо ви зберігаєте інформацію в базі даних, тут живе SQL. Або, можливо, ви використовуєте XML-файл для зберігання даних, а ваші Map Mappers розбирають файли та XML.
Послуги
Ви можете вважати їх "об'єктами домену вищого рівня", але замість бізнес-логіки Служби відповідають за взаємодію між об'єктами домену та картографами . Ці структури в кінцевому підсумку створюють "загальнодоступний" інтерфейс для взаємодії з логікою ділового бізнесу. Ви можете їх уникнути, але за умови проникнення певної логіки домену в Контролери .
Відповідна відповідь на цю тему є питанням щодо впровадження ACL - це може бути корисним.
Зв'язок між шаром моделі та іншими частинами тріади MVC має відбуватися лише через Сервіси . Чітке розділення має кілька додаткових переваг:
- це допомагає виконувати принцип єдиної відповідальності (СРП)
- надає додатковий "номер", якщо логіка зміниться
- зберігає контролер максимально просто
- дає чіткий план, якщо вам коли-небудь потрібен зовнішній API
Як взаємодіяти з моделлю?
Необхідні умови: дивіться лекції "Глобальний стан та одинаки" та "Не шукайте речей!" з чистого кодексу переговорів.
Отримання доступу до службових примірників
Як для екземплярів View, так і для Controller (що можна назвати: "рівень інтерфейсу користувача") для доступу до цих служб, є два загальних підходи:
- Ви можете вводити потрібні послуги в конструктори ваших поглядів та контролери безпосередньо, бажано, використовуючи контейнер DI.
- Використання фабрики послуг як обов'язкова залежність для всіх ваших поглядів та контролерів.
Як ви можете підозрювати, контейнер DI - це набагато більш елегантне рішення (при цьому не є найпростішим для початківця). Дві бібліотеки, які я рекомендую врахувати для цієї функціональності, будуть окремим компонентом Syfmony DependencyInjection або Auryn .
Як рішення, що використовують заводський, так і DI-контейнер, дозволять вам також ділитися екземплярами різних серверів, якими слід ділитися між вибраним контролером, і переглядати заданий цикл відповіді на запит.
Зміна стану моделі
Тепер, коли ви можете отримати доступ до шару моделі в контролерах, вам потрібно почати фактично їх використовувати:
public function postLogin(Request $request)
{
$email = $request->get('email');
$identity = $this->identification->findIdentityByEmailAddress($email);
$this->identification->loginWithPassword(
$identity,
$request->get('password')
);
}
Ваші контролери мають дуже чітке завдання: взяти користувацький вклад і, виходячи з цього вводу, змінити поточний стан бізнес-логіки. У цьому прикладі стану, які змінюються між "анонімним користувачем" та "користувачем, який увійшов у систему".
Контролер не несе відповідальності за перевірку даних користувачів, оскільки це є частиною бізнес-правил, а контролер, безумовно, не викликає SQL-запити, як, наприклад, те, що ви бачили б тут чи тут (будь ласка, не ненавидійте їх, вони неправильно керуються, а не злі).
Показано користувачеві зміну стану.
Гаразд, користувач увійшов (або не вдався). А тепер що? Згаданий користувач досі не знає про це. Тож вам потрібно насправді дати відповідь, і це відповідальність погляду.
public function postLogin()
{
$path = '/login';
if ($this->identification->isUserLoggedIn()) {
$path = '/dashboard';
}
return new RedirectResponse($path);
}
У цьому випадку подання дало одну з двох можливих відповідей на основі поточного стану шару моделі. Для іншого випадку використання у вас буде можливість вибору різних шаблонів для візуалізації, спираючись на щось на зразок "поточного обраного статті".
Шар презентації насправді може бути досить детальним, як описано тут: Розуміння MVC-поглядів у PHP .
Але я просто роблю API REST!
Звичайно, бувають ситуації, коли це надмірність.
MVC - це лише конкретне рішення для принципу розділення проблем . MVC відокремлює інтерфейс користувача від ділової логіки, і в інтерфейсі він розділив обробку введення користувача та презентації. Це є вирішальним. Хоча часто люди описують це як "тріаду", вона насправді не складається з трьох незалежних частин. Структура виглядає так:
Це означає, що коли логіка вашого презентаційного шару близька до неіснуючої, прагматичним підходом є збереження їх як єдиного шару. Це також може істотно спростити деякі аспекти шару моделі.
Використовуючи такий підхід, приклад входу (для API) можна записати так:
public function postLogin(Request $request)
{
$email = $request->get('email');
$data = [
'status' => 'ok',
];
try {
$identity = $this->identification->findIdentityByEmailAddress($email);
$token = $this->identification->loginWithPassword(
$identity,
$request->get('password')
);
} catch (FailedIdentification $exception) {
$data = [
'status' => 'error',
'message' => 'Login failed!',
]
}
return new JsonResponse($data);
}
Хоча це не є стійким, коли у вас є складна логіка для надання органу відповідей, це спрощення є дуже корисним для більш тривіальних сценаріїв. Але будьте попереджені , такий підхід стане кошмаром при спробах використання у великих кодових базах зі складною логікою подання.
Як побудувати модель?
Оскільки не існує жодного класу «Модель» (як пояснено вище), ви дійсно не «будуєте модель». Натомість ви починаєте з створення Служб , які здатні виконувати певні методи. А потім реалізуйте об’єкти домену та картографи .
Приклад способу обслуговування:
В обох вище підходах був такий метод входу для служби ідентифікації. Як би насправді це виглядало. Я використовую трохи змінену версію того ж функціоналу з бібліотеки , яку я написав .. бо я лінивий:
public function loginWithPassword(Identity $identity, string $password): string
{
if ($identity->matchPassword($password) === false) {
$this->logWrongPasswordNotice($identity, [
'email' => $identity->getEmailAddress(),
'key' => $password, // this is the wrong password
]);
throw new PasswordMismatch;
}
$identity->setPassword($password);
$this->updateIdentityOnUse($identity);
$cookie = $this->createCookieIdentity($identity);
$this->logger->info('login successful', [
'input' => [
'email' => $identity->getEmailAddress(),
],
'user' => [
'account' => $identity->getAccountId(),
'identity' => $identity->getId(),
],
]);
return $cookie->getToken();
}
Як бачимо, на цьому рівні абстракції немає вказівок, звідки отримані дані. Це може бути база даних, але це також може бути просто макетним об’єктом для тестування. Навіть картографи даних, які фактично використовуються для цього, приховані у private
методах цієї послуги.
private function changeIdentityStatus(Entity\Identity $identity, int $status)
{
$identity->setStatus($status);
$identity->setLastUsed(time());
$mapper = $this->mapperFactory->create(Mapper\Identity::class);
$mapper->store($identity);
}
Шляхи створення картографів
Для здійснення абстрагування наполегливості на найбільш гнучких підходах є створення спеціальних картографічних даних .
Від: Книга PoEAA
На практиці вони реалізуються для взаємодії з конкретними класами або суперкласами. Скажімо, у вас є Customer
і Admin
у вашому коді (обидва успадковані від User
суперкласу). Можливо, обидва, маючи окремий відповідний картограф, оскільки містять різні поля. Але ви також закінчите спільні та часто використовувані операції. Наприклад: оновлення часу "востаннє в Інтернеті" . І замість того, щоб зробити існуючі картографи більш звивистими, більш прагматичним підходом є загальний "User Mapper", який лише оновлює цю часову позначку.
Деякі додаткові коментарі:
Таблиці та модель баз даних
Хоча іноді існує пряме відношення 1: 1: 1 між таблицею баз даних, об'єктом домену та Mapper , у великих проектах це може бути менш поширеним, ніж ви очікуєте:
Інформація, що використовується одним об’єктом домену, може бути відображена з різних таблиць, тоді як сам об’єкт не має стійкості в базі даних.
Приклад: якщо ви формуєте щомісячний звіт. Це збирало б інформацію з різних таблиць, але MonthlyReport
в базі даних немає чарівної таблиці.
Один Mapper може впливати на кілька таблиць.
Приклад: коли ви зберігаєте дані від User
об'єкта, цей об’єкт домену може містити колекцію інших об'єктів домену - Group
екземплярів. Якщо їх змінити і зберегти User
, Mapper повинен буде оновити та / або вставити записи в декілька таблиць.
Дані з одного об’єкта домену зберігаються у більш ніж одній таблиці.
Приклад: у великих системах (думаю: соціальна мережа середнього розміру) може бути прагматичним зберігання даних про автентифікацію користувачів та даних, які часто отримують доступ, окремо від великих фрагментів вмісту, що рідко потрібно. У цьому випадку у вас може бути ще один User
клас, але інформація, яку він містить, залежатиме від того, чи були отримані повні деталі.
Для кожного об’єкта домену може бути більше одного картографа
Приклад: у вас є веб-сайт новин із загальною кодовою базою як для публічного, так і для програмного забезпечення управління. Але, хоча обидва інтерфейси використовують один і той же Article
клас, управління потребує набагато більше інформації, заселеної в ньому. У цьому випадку у вас є два окремі картографи: "внутрішній" та "зовнішній". Кожен виконує різні запити або навіть використовує різні бази даних (як у ведучому чи підлеглому).
Перегляд - це не шаблон
Перегляд примірників у MVC (якщо ви не використовуєте MVP-зміни шаблону) відповідають за логіку презентації. Це означає, що кожен перегляд зазвичай перемикає принаймні кілька шаблонів. Він отримує дані з шару моделі і потім, виходячи з отриманої інформації, вибирає шаблон і встановлює значення.
Однією з переваг, які ви отримуєте від цього, є повторне використання. Якщо ви створюєте ListView
клас, то, з добре написаним кодом, ви можете мати той самий клас, що передає список користувачів та коментарі нижче статті. Тому що вони мають однакову логіку презентації. Ви просто перемикаєте шаблони.
Ви можете використовувати або рідні шаблони PHP, або використовувати інший сторонній двигун шаблонів. Там також може бути деякі сторонні бібліотеки, які здатні повністю замінити View примірників.
Як щодо старої версії відповіді?
Єдина основна зміна полягає в тому, що те, що називається Модель у старій версії, насправді є Сервісом . Інша частина «бібліотечної аналогії» тримається досить добре.
Єдиний недолік, який я бачу, це те, що це була б дійсно дивна бібліотека, бо вона поверне вам інформацію з книги, але не дозволить торкатися самої книги, бо інакше абстракція почне «просочуватися». Можливо, я б міг придумати більш придатну аналогію.
Який взаємозв'язок між примірниками View і Controller ?
Структура MVC складається з двох шарів: ui та моделі. Основними структурами шару інтерфейсу є представлення та контролер.
Якщо ви маєте справу з веб-сайтами, які використовують схему дизайну MVC, найкращим способом є співвідношення 1: 1 між видами та контролерами. Кожен перегляд представляє цілу сторінку на вашому веб-сайті, і він має спеціальний контролер для обробки всіх вхідних запитів для цього конкретного представлення даних.
Наприклад, щоб представити відкриту статтю, ви мали б \Application\Controller\Document
і \Application\View\Document
. Це містило б усі основні функціональні можливості для шару інтерфейсу, якщо мова йде про роботи зі статтями (звичайно, у вас можуть бути деякі компоненти XHR , які безпосередньо не стосуються статей) .