Як має бути структурована модель в MVC? [зачинено]


551

Я тільки розумію рамки MVC і мені часто цікаво, скільки коду має входити в модель. Я схильний мати клас доступу до даних, який має такі методи:

public function CheckUsername($connection, $username)
{
    try
    {
        $data = array();
        $data['Username'] = $username;

        //// SQL
        $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE Username = :Username";

        //// Execute statement
        return $this->ExecuteObject($connection, $sql, $data);
    }
    catch(Exception $e)
    {
        throw $e;
    }
}

Мої моделі, як правило, є класом сутності, який відображається в таблиці бази даних.

Чи повинен об'єкт моделі мати всі властивості баз даних, а також код вище, або це нормально відокремлювати цей код, який фактично працює з базою даних?

Чи зрештою я матиму чотири шари?


133
Чому ви ловите винятки, щоб їх знову кинути?
Бейлі Паркер

9
@Elias Van Ootegem: ви пропустили суть. в цьому випадку безглуздо їх ловити.
Каролі Хорват

4
@Elias Van Ootegem: так? якщо він працює з повторним відкиданням, це означає, що верхній шар ловить виняток. Але якщо він є, то він би його наздогнав без того безглуздого переосмислення ... (якщо ви все одно цього не отримаєте, будь ласка, знущайтеся над невеликим тестовим кодом)
Karoly Horvath

3
@Elias Van Ootegem: Я поняття не маю, про що ви говорите, якщо обробка виключення на певному шарі не означає, що це зупинить додаток. будь-ласка, побудуйте (або точніше: не вдалося сконструювати) приклад коду, де необхідне повторне скидання. давайте зупинимо цю офтопічну розмову, будь ласка
Каролі Хорват

6
@drrcknlsn: це коректний аргумент, але в такому випадку принаймні вловить виняток, який ви очікуєте відкинути, загальний Exceptionне має великого значення документації. Особисто, якби я спустився на цю дорогу, я вибрав би PHPDoc @exception, або якийсь подібний механізм, так це відображається в створеній документації.
Каролі Горват

Відповіді:


903

Відмова від відповідальності: далі - опис того, як я розумію MVC-подібні шаблони в контексті веб-додатків на основі PHP. Усі зовнішні посилання, які використовуються у змісті, є для пояснення термінів та понять, а не для того, щоб мати на увазі мою власну достовірність.

Перше, що я повинен прояснити, це: модель - шар .

По-друге: є різниця між класичним MVC і тим, що ми використовуємо в веб-розробці. Ось трохи старшої відповіді, яку я написав, яка коротко описує, чим вони відрізняються.

Яка модель НЕ:

Модель не є класом або будь-яким окремим об'єктом. Це дуже поширена помилка (я теж робив, хоча оригінальна відповідь була написана, коли я почав вчитися інакше) , оскільки більшість рамок увічнюють цю помилку.

Це не є об'єктно-реляційною технікою картографування (ORM) або абстрагуванням таблиць баз даних. Кожен, хто скаже вам інакше, швидше за все, намагається «продати» інший абсолютно новий ORM або цілі рамки.

Що таке модель:

У правильної адаптації MVC, М містить всю область бізнес - логіку і модель рівня в основному зроблений з трьох типів структур:

  • Об'єкти домену

    Об'єкт домену - це логічний контейнер з чисто доменною інформацією; він зазвичай представляє логічну сутність у проблемному доменному просторі. Зазвичай називають діловою логікою .

    Тут ви б визначали, як перевірити дані перед надсиланням рахунку або обчислити загальну вартість замовлення. У той же час, об'єкти домену абсолютно не знають про зберігання - ні звідки (база даних SQL, API REST, текстовий файл тощо), ні навіть якщо вони зберігаються чи отримуються.

  • Картографі даних

    Ці об'єкти відповідають лише за зберігання. Якщо ви зберігаєте інформацію в базі даних, тут живе SQL. Або, можливо, ви використовуєте XML-файл для зберігання даних, а ваші Map Mappers розбирають файли та XML.

  • Послуги

    Ви можете вважати їх "об'єктами домену вищого рівня", але замість бізнес-логіки Служби відповідають за взаємодію між об'єктами домену та картографами . Ці структури в кінцевому підсумку створюють "загальнодоступний" інтерфейс для взаємодії з логікою ділового бізнесу. Ви можете їх уникнути, але за умови проникнення певної логіки домену в Контролери .

    Відповідна відповідь на цю тему є питанням щодо впровадження ACL - це може бути корисним.

Зв'язок між шаром моделі та іншими частинами тріади MVC має відбуватися лише через Сервіси . Чітке розділення має кілька додаткових переваг:

  • це допомагає виконувати принцип єдиної відповідальності (СРП)
  • надає додатковий "номер", якщо логіка зміниться
  • зберігає контролер максимально просто
  • дає чіткий план, якщо вам коли-небудь потрібен зовнішній API

 

Як взаємодіяти з моделлю?

Необхідні умови: дивіться лекції "Глобальний стан та одинаки" та "Не шукайте речей!" з чистого кодексу переговорів.

Отримання доступу до службових примірників

Як для екземплярів View, так і для Controller (що можна назвати: "рівень інтерфейсу користувача") для доступу до цих служб, є два загальних підходи:

  1. Ви можете вводити потрібні послуги в конструктори ваших поглядів та контролери безпосередньо, бажано, використовуючи контейнер DI.
  2. Використання фабрики послуг як обов'язкова залежність для всіх ваших поглядів та контролерів.

Як ви можете підозрювати, контейнер 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 відокремлює інтерфейс користувача від ділової логіки, і в інтерфейсі він розділив обробку введення користувача та презентації. Це є вирішальним. Хоча часто люди описують це як "тріаду", вона насправді не складається з трьох незалежних частин. Структура виглядає так:

Відділення 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: 1 між таблицею баз даних, об'єктом домену та Mapper , у великих проектах це може бути менш поширеним, ніж ви очікуєте:

    • Інформація, що використовується одним об’єктом домену, може бути відображена з різних таблиць, тоді як сам об’єкт не має стійкості в базі даних.

      Приклад: якщо ви формуєте щомісячний звіт. Це збирало б інформацію з різних таблиць, але MonthlyReportв базі даних немає чарівної таблиці.

    • Один Mapper може впливати на кілька таблиць.

      Приклад: коли ви зберігаєте дані від Userоб'єкта, цей об’єкт домену може містити колекцію інших об'єктів домену - Groupекземплярів. Якщо їх змінити і зберегти User, Mapper повинен буде оновити та / або вставити записи в декілька таблиць.

    • Дані з одного об’єкта домену зберігаються у більш ніж одній таблиці.

      Приклад: у великих системах (думаю: соціальна мережа середнього розміру) може бути прагматичним зберігання даних про автентифікацію користувачів та даних, які часто отримують доступ, окремо від великих фрагментів вмісту, що рідко потрібно. У цьому випадку у вас може бути ще один Userклас, але інформація, яку він містить, залежатиме від того, чи були отримані повні деталі.

    • Для кожного об’єкта домену може бути більше одного картографа

      Приклад: у вас є веб-сайт новин із загальною кодовою базою як для публічного, так і для програмного забезпечення управління. Але, хоча обидва інтерфейси використовують один і той же Articleклас, управління потребує набагато більше інформації, заселеної в ньому. У цьому випадку у вас є два окремі картографи: "внутрішній" та "зовнішній". Кожен виконує різні запити або навіть використовує різні бази даних (як у ведучому чи підлеглому).

  2. Перегляд - це не шаблон

    Перегляд примірників у MVC (якщо ви не використовуєте MVP-зміни шаблону) відповідають за логіку презентації. Це означає, що кожен перегляд зазвичай перемикає принаймні кілька шаблонів. Він отримує дані з шару моделі і потім, виходячи з отриманої інформації, вибирає шаблон і встановлює значення.

    Однією з переваг, які ви отримуєте від цього, є повторне використання. Якщо ви створюєте ListViewклас, то, з добре написаним кодом, ви можете мати той самий клас, що передає список користувачів та коментарі нижче статті. Тому що вони мають однакову логіку презентації. Ви просто перемикаєте шаблони.

    Ви можете використовувати або рідні шаблони PHP, або використовувати інший сторонній двигун шаблонів. Там також може бути деякі сторонні бібліотеки, які здатні повністю замінити View примірників.

  3. Як щодо старої версії відповіді?

    Єдина основна зміна полягає в тому, що те, що називається Модель у старій версії, насправді є Сервісом . Інша частина «бібліотечної аналогії» тримається досить добре.

    Єдиний недолік, який я бачу, це те, що це була б дійсно дивна бібліотека, бо вона поверне вам інформацію з книги, але не дозволить торкатися самої книги, бо інакше абстракція почне «просочуватися». Можливо, я б міг придумати більш придатну аналогію.

  4. Який взаємозв'язок між примірниками View і Controller ?

    Структура MVC складається з двох шарів: ui та моделі. Основними структурами шару інтерфейсу є представлення та контролер.

    Якщо ви маєте справу з веб-сайтами, які використовують схему дизайну MVC, найкращим способом є співвідношення 1: 1 між видами та контролерами. Кожен перегляд представляє цілу сторінку на вашому веб-сайті, і він має спеціальний контролер для обробки всіх вхідних запитів для цього конкретного представлення даних.

    Наприклад, щоб представити відкриту статтю, ви мали б \Application\Controller\Documentі \Application\View\Document. Це містило б усі основні функціональні можливості для шару інтерфейсу, якщо мова йде про роботи зі статтями (звичайно, у вас можуть бути деякі компоненти XHR , які безпосередньо не стосуються статей) .


4
@ Rinzler, ви помітите, що ніде в цьому посиланні нічого не сказано про Model (крім одного коментаря). Це лише "об'єктно-орієнтований інтерфейс до таблиць баз даних" . Якщо ви спробуєте сформулювати це у речі, подібній до моделі, ви, в кінцевому рахунку, порушите SRP та LSP .
tereško

8
@hafichuk для прототипування є лише ситуації, коли розумно використовувати шаблон ActiveRecord . Коли ви починаєте писати код, який означає для виробництва, він стає анти-шаблоном, оскільки він змішує логіку зберігання та ділову логіку. А оскільки Model Layer абсолютно не знає про інші частини MVC. Це не змінюється залежно від варіації оригінального шаблону . Навіть при використанні MVVM. Немає "декількох моделей", і вони ні на що не відображені. Модель - шар.
tereško

3
Коротка версія - Моделі є структурами даних .
Едді Б

9
Добре бачити, що він винайшов MVC статтю, можливо, є певна заслуга.
Едді Б

3
... або навіть просто набір функцій. MVC не потрібно реалізовувати у стилі OOP, хоча він здебільшого реалізується саме так. Найголовніше - відокремити шари та встановити правильний потік даних та контролю
hek2mgl

37

Все, що є діловою логікою, належить до моделі, будь то запит до бази даних, обчислення, виклик REST тощо.

Ви можете мати доступ до даних у самій моделі, шаблон MVC не обмежує вас робити це. Ви можете засипати її цукровими послугами, картографами і що ні, але власне визначення моделі - це шар, який обробляє ділову логіку, не більше, нічого менше. Це може бути клас, функція або повний модуль з об'єктами gazillion, якщо це те, що ви хочете.

Завжди простіше мати окремий об'єкт, який фактично виконує запити до бази даних, а не виконувати їх безпосередньо в моделі: це особливо стане в нагоді при тестуванні одиниць (через простоту введення макетної залежності бази даних у вашу модель):

class Database {
   protected $_conn;

   public function __construct($connection) {
       $this->_conn = $connection;
   }

   public function ExecuteObject($sql, $data) {
       // stuff
   }
}

abstract class Model {
   protected $_db;

   public function __construct(Database $db) {
       $this->_db = $db;
   }
}

class User extends Model {
   public function CheckUsername($username) {
       // ...
       $sql = "SELECT Username FROM" . $this->usersTableName . " WHERE ...";
       return $this->_db->ExecuteObject($sql, $data);
   }
}

$db = new Database($conn);
$model = new User($db);
$model->CheckUsername('foo');

Крім того, у PHP вам рідко потрібно ловити / повторно виключати винятки, оскільки зворотний шлях зберігається, особливо у випадку, як ваш приклад. Просто нехай буде викинуто виняток і замість цього вловити його в контролері.


Моя структура дуже схожа, я думаю, що я просто її відокремлюю трохи більше. Причина, через яку я проходив навколо зв'язку, полягала в тому, що мені потрібно було купувати шматки в операціях. Я хотів додати користувача, а потім додати користувача до ролі, але роль назад, якщо один не вдався. Єдиний спосіб, як я міг це розібратися, - це пройти з'єднання.
Діетпіксель

10
-1: це також трапляється абсолютно неправильно. Модель не є абстракцією для таблиці.
терешко

1
UserКлас в основному розширює модель, але itsn't об'єкт. Користувач повинен бути об'єктом і має такі властивості, як: id, ім'я ... Ви розгортаєте Userклас - це помічник.
TomSawyer

1
Я думаю, ви розумієте MVC, але не розумієте, що таке OOP. У цьому сценарії, як я вже говорив, Userстоїть об'єкт, і він повинен мати властивості Користувача, а не методи, як CheckUsername, що робити, якщо ви хочете створити новий Userоб'єкт? new User($db)
TomSawyer

@TomSawyer OOP не означає, що об'єкти повинні мати властивості. Те, що ви описуєте, - це модель дизайну, яка не має значення для питання чи відповіді на це питання. OOP - це мовна модель, а не модель дизайну.
netcoder

20

У веб- "MVC" ви можете робити все, що завгодно.

Початкова концепція (1) описувала модель як бізнес-логіку. Він повинен представляти стан програми та застосовувати певну узгодженість даних. Цей підхід часто описують як "жирну модель".

Більшість фреймворків PHP дотримуються більш дрібного підходу, де модель є лише інтерфейсом бази даних. Але принаймні ці моделі все ж повинні підтверджувати вхідні дані та відносини.

Так чи інакше, вам не дуже далеко, якщо ви розділите речі SQL або дзвінки до бази даних в інший шар. Таким чином, вам потрібно потурбуватися лише про реальні дані / поведінку, а не про фактичний API зберігання. (Однак це нерозумно перестаратися. Ви, наприклад, ніколи не зможете замінити бекенд бази даних на зберігання файлів, якщо це не було розроблено наперед)


8
посилання недійсне (404)
Kyslik


6

Більш oftenly більшість додатків матиме дані, відображення і обробку частина , і ми просто покласти все ті , в листах M, Vі C.

Model ( M) -> має атрибути, які містять стан застосування, і вони не знають нічого про Vі C.

View ( V) -> Має відображення формату програми та знає лише про те, як засвоювати модель на ній, і не турбується C.

Контролер ( C) ----> Обробляє частину програми та виконує функції проводки між M і V, і це залежить від обох M, Vна відміну від Mі V.

Взагалі між ними існує роз'єднання проблем. Надалі будь-які зміни або вдосконалення можуть бути додані дуже легко.


0

У моєму випадку у мене є клас бази даних, який обробляє всі прямі взаємодії з базою даних, такі як запити, отримання даних тощо. Тож якщо мені довелося змінити базу даних з MySQL на PostgreSQL, не виникне жодних проблем. Отже, додаючи, що додатковий шар може бути корисним

Кожна таблиця може мати свій клас і мати свої конкретні методи, але для того, щоб фактично отримати дані, вона дозволяє класу бази даних обробляти це:

Файл Database.php

class Database {
    private static $connection;
    private static $current_query;
    ...

    public static function query($sql) {
        if (!self::$connection){
            self::open_connection();
        }
        self::$current_query = $sql;
        $result = mysql_query($sql,self::$connection);

        if (!$result){
            self::close_connection();
            // throw custom error
            // The query failed for some reason. here is query :: self::$current_query
            $error = new Error(2,"There is an Error in the query.\n<b>Query:</b>\n{$sql}\n");
            $error->handleError();
        }
        return $result;
    }
 ....

    public static function find_by_sql($sql){
        if (!is_string($sql))
            return false;

        $result_set = self::query($sql);
        $obj_arr = array();
        while ($row = self::fetch_array($result_set))
        {
            $obj_arr[] = self::instantiate($row);
        }
        return $obj_arr;
    }
}

Клас об'єкта таблиціL

class DomainPeer extends Database {

    public static function getDomainInfoList() {
        $sql = 'SELECT ';
        $sql .='d.`id`,';
        $sql .='d.`name`,';
        $sql .='d.`shortName`,';
        $sql .='d.`created_at`,';
        $sql .='d.`updated_at`,';
        $sql .='count(q.id) as queries ';
        $sql .='FROM `domains` d ';
        $sql .='LEFT JOIN queries q on q.domainId = d.id ';
        $sql .='GROUP BY d.id';
        return self::find_by_sql($sql);
    }

    ....
}

Я сподіваюся, що цей приклад допоможе вам створити гарну структуру.


12
"Отже, якщо мені доведеться змінити свою базу даних з MySQL на PostgreSQL, проблем не виникне." Ухммммм, з вищезгаданим кодом у вас виникне величезна проблема змінити що-небудь imo.
PeeHaa

Я бачу, що моя відповідь має все менше сенсу після редагування, і з часом йде. Але він повинен залишитися тут
Ібу

2
Databaseу прикладі - не клас. Це просто обгортка для функцій. Крім того, як можна мати "клас об'єктних таблиць" без об'єкта?
tereško

2
@ tereško Я прочитав багато ваших публікацій, і вони чудові. Але я не можу знайти жодної повної рамки, де можна було б вивчити. Чи знаєте ви одного, який "робить це правильно"? Або хоча б одне, що вам подобається, а ви і деякі інші тут на ТАК говорите, що потрібно зробити? Дякую.
johnny

Я, можливо, запізнююся, але я хотів би зазначити, що PDO майже вирішує питання про необхідність створення "рівня" БД, щоб полегшити майбутні зміни.
Метью Гуларт
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.