Как модель должна быть структурирована в 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
@ Элиас Ван Оотегем: вы упустили момент. в этом случае ловить их бессмысленно.
Кароли Хорват

4
@ Элиас Ван Оотегем: а? если это работает с rethrow, это означает, что верхний слой ловит исключение. Но если он есть, он бы поймал его без этого бессмысленного повторного броска ... (если вы все еще не получили его, пожалуйста, смоделируйте небольшой тестовый код)
Кароли Хорват

3
@ Элиас Ван Оотегем: Я понятия не имею, о чем вы говорите, не обрабатывая исключение на определенном слое, это не значит, что оно остановит приложение. пожалуйста, сконструируйте (или, точнее, не сработайте) пример кода, где необходимо это перебрасывание. давайте прекратим этот оффтоп разговор, пожалуйста
Кароли Хорват

6
@drrcknlsn: это допустимый аргумент, но в этом случае, по крайней мере, поймать исключение, которое вы ожидаете, генерик Exceptionне имеет большого значения документации. Лично, если бы я пошел по этому пути, я бы выбрал PHPDoc @exceptionили какой-то подобный механизм, так что это будет показано в сгенерированной документации.
Кароли Хорват

Ответы:


903

Отказ от ответственности: ниже приводится описание того, как я понимаю MVC-подобные шаблоны в контексте веб-приложений на основе PHP. Все внешние ссылки, которые используются в содержании, предназначены для объяснения терминов и понятий, а не для того, чтобы свидетельствовать о моем собственном доверии к предмету.

Первое, что я должен прояснить: модель - это слой .

Второе: есть разница между классическим MVC и тем, что мы используем в веб-разработке. Вот немного более старого ответа, который я написал, который кратко описывает, как они отличаются.

Какая модель НЕ является:

Модель не является классом или каким-либо отдельным объектом. Это очень распространенная ошибка (я тоже сделал, хотя первоначальный ответ был написан, когда я начал учиться иначе) , потому что большинство фреймворков увековечивают это заблуждение.

Это не методика объектно-реляционного отображения (ORM) и не абстракция таблиц базы данных. Любой, кто говорит вам обратное, скорее всего, пытается «продать» другой новый ORM или целую платформу.

Что это за модель:

При правильной адаптации MVC M содержит всю бизнес-логику домена, а уровень модели в основном состоит из трех типов структур:

  • Доменные объекты

    Доменный объект - это логический контейнер с чисто доменной информацией; обычно он представляет логическую сущность в пространстве проблемной области. Обычно упоминается как бизнес-логика .

    В этом месте вы определяете, как проверять данные перед отправкой счета или рассчитывать общую стоимость заказа. В то же время доменные объекты полностью не знают о хранилище - ни откуда (база данных SQL, REST API, текстовый файл и т. Д.), Ни даже если они сохранены или получены.

  • Data Mappers

    Эти объекты несут ответственность только за хранение. Если вы храните информацию в базе данных, это то место, где живет SQL. Или, может быть, вы используете файл XML для хранения данных, и ваши Data Mappers анализируют файлы XML и обратно.

  • Сервисы

    Вы можете думать о них как о «доменных объектах более высокого уровня», но вместо бизнес-логики Сервисы отвечают за взаимодействие между доменными объектами и Mappers . Эти структуры в конечном итоге создают «открытый» интерфейс для взаимодействия с бизнес-логикой домена. Вы можете избежать их, но за счет утечки некоторой логики домена в контроллеры .

    Существует ответ на этот вопрос в вопросе реализации ACL - это может быть полезно.

Связь между уровнем модели и другими частями триады MVC должна происходить только через Сервисы . Четкое разделение имеет несколько дополнительных преимуществ:

  • это помогает обеспечить соблюдение принципа единой ответственности (SRP)
  • обеспечивает дополнительную «комнату для маневра» в случае изменения логики
  • делает контроллер максимально простым
  • дает четкий план, если вам когда-нибудь понадобится внешний API

 

Как взаимодействовать с моделью?

Пререквизиты: смотреть лекции «Глобальное состояние и одиночные игры» и «Не искать вещи!» из Чистых переговоров по коду.

Получение доступа к экземплярам сервисов

Для обоих экземпляров View и Controller (то, что вы могли бы назвать «уровнем пользовательского интерфейса») для доступа к этим службам, существует два основных подхода:

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

Как вы можете подозревать, DI-контейнер - намного более элегантное решение (хотя и не самое простое для новичка). Две библиотеки, которые я рекомендую рассмотреть для этой функциональности, будут автономным компонентом DependencyInjection от Syfmony или 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 Views в PHP .

Но я просто делаю REST API!

Конечно, бывают ситуации, когда это перебор.

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);
}

Хотя это не является устойчивым, когда у вас сложная логика для отображения тела ответа, это упрощение очень полезно для более тривиальных сценариев. Но будьте осторожны , такой подход станет кошмаром, когда вы попытаетесь использовать большие кодовые базы со сложной логикой представления.

 

Как построить модель?

Поскольку не существует ни одного класса «Модель» (как описано выше), вы действительно не «строите модель». Вместо этого вы начинаете создавать Сервисы , которые могут выполнять определенные методы. А затем реализовать доменные объекты и Mappers .

Пример метода обслуживания:

В обоих вышеописанных подходах использовался метод входа в систему для службы идентификации. Как бы это на самом деле выглядело. Я использую слегка измененную версию той же функциональности из библиотеки , которую я написал .. потому что я ленивый:

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 между таблицей базы данных, Domain Object и Mapper , в более крупных проектах это может быть менее распространенным, чем вы ожидаете:

    • Информация, используемая одним доменным объектом, может отображаться из разных таблиц, тогда как сам объект не хранится в базе данных.

      Пример: если вы генерируете ежемесячный отчет. Это позволит собирать информацию из разных таблиц, но MonthlyReportв базе данных нет волшебных таблиц.

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

      Пример: когда вы храните данные из Userобъекта, этот объект домена может содержать коллекцию других объектов домена - Groupэкземпляров. Если вы измените их и сохраните User, Data Mapper должен будет обновить и / или вставить записи в несколько таблиц.

    • Данные из одного доменного объекта хранятся в нескольких таблицах.

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

    • Для каждого доменного объекта может быть несколько картографов

      Пример: у вас есть новостной сайт с общим кодовым кодом как для публичного, так и для управляющего программного обеспечения. Но, хотя оба интерфейса используют один и тот же Articleкласс, для управления необходимо заполнить его большим количеством информации. В этом случае у вас будет два отдельных преобразователя: «внутренний» и «внешний». Каждый выполняет разные запросы или даже использует разные базы данных (как в master, так и в slave).

  2. Представление не шаблон

    Просмотр экземпляров в MVC (если вы не используете вариацию шаблона MVP) отвечают за логику представления. Это означает, что в каждом представлении обычно используется как минимум несколько шаблонов. Он получает данные с уровня модели, а затем на основе полученной информации выбирает шаблон и устанавливает значения.

    Одним из преимуществ, которые вы получаете от этого, является возможность повторного использования. Если вы создаете ListViewкласс, то с хорошо написанным кодом у вас может быть тот же класс, который передает представление списка пользователей и комментарии под статьей. Потому что они оба имеют одинаковую логику представления. Вы просто переключаете шаблоны.

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

  3. Как насчет старой версии ответа?

    Единственное существенное изменение заключается в том, что то, что в старой версии называется Model , на самом деле является Сервисом . В остальном «библиотечная аналогия» держится довольно неплохо.

    Единственный недостаток, который я вижу, это то, что это будет действительно странная библиотека, потому что она вернет вам информацию из книги, но не позволит вам прикоснуться к самой книге, потому что в противном случае абстракция начнет «просачиваться». Возможно, мне придется подумать о более подходящей аналогии.

  4. Какова связь между экземплярами View и Controller ?

    Структура MVC состоит из двух слоев: пользовательского интерфейса и модели. Основными структурами на уровне пользовательского интерфейса являются представления и контроллер.

    Когда вы имеете дело с веб-сайтами, использующими шаблон проектирования 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 не требует реализации в стиле ООП, хотя в основном он реализован таким образом. Самое главное, чтобы разделить слои и установить правильный поток данных и управления
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 вам редко нужно перехватывать / перебрасывать исключения, потому что обратная трассировка сохраняется, особенно в случае, подобном вашему примеру. Просто позвольте выбросить исключение и вместо этого перехватите его в контроллере.


Моя структура очень похожа, я думаю, я просто выделю ее немного больше. Причина, по которой я передавал соединение, заключалась в том, что мне нужно было выполнять чанки в транзакциях. Я хотел добавить пользователя, а затем добавить пользователя в роль, но вернуть роль в случае неудачи. Единственный способ, которым я мог разобраться в этом, - передать соединение.
Dietpixel

10
-1: это также совершенно неправильно. Модель не является абстракцией для таблицы.
tereško

1
UserКласс в основном расширяет модель, но itsn't объект. Пользователь должен быть объектом и иметь такие свойства, как: идентификатор, имя ... Развертывание Userкласса является помощником.
TomSawyer

1
Я думаю, что вы понимаете MVC, но не понимаете, что такое ООП. В этом сценарии, как я уже сказал, Userобозначает объект, и он должен иметь свойства пользователя, а не такие методы, как CheckUsername, что вы должны делать, если вы хотите создать новый Userобъект? new User($db)
TomSawyer

@TomSawyer ООП не означает, что объекты должны иметь свойства. То, что вы описываете, - это шаблон дизайна, который не имеет отношения к вопросу или ответу на этот вопрос. ООП - это языковая модель, а не шаблон проектирования.
Неткодер

20

В Web- «MVC» вы можете делать все, что угодно.

Первоначальная концепция (1) описывала модель как бизнес-логику. Он должен представлять состояние приложения и обеспечивать некоторую согласованность данных. Этот подход часто называют «жирной моделью».

Большинство PHP-фреймворков используют более поверхностный подход, где модель представляет собой просто интерфейс базы данных. Но, по крайней мере, эти модели должны проверять входящие данные и отношения.

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


8
ссылка недействительна (404)
Kyslik

1

6

Чаще всего большинство приложений будет иметь данные, отображение и обработку, и мы просто помещаем все это в буквы M, Vи C.

Model ( M) -> Имеет атрибуты, которые содержат состояние приложения, и он ничего не знает о Vи C.

View ( V) -> Имеет формат отображения для приложения и знает только о том, как обработать модель, и не беспокоится об этом C.

Контроллер ( C) ----> Имеет обработки части приложения и действует как проводка между М и 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;
    }
}

Таблица объекта classL

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, проблем не будет». Uhhhmmm с приведенным выше кодом у вас будет огромная проблема с изменением чего-либо IMO.
PeeHaa

Я вижу, что мой ответ имеет все меньше смысла после редактирования, и со временем. Но это должно остаться здесь
Ибу

2
Databaseв примере это не класс. Это просто оболочка для функций. Кроме того, как вы можете иметь «класс объекта таблицы» без объекта?
tereško

2
@ tereško Я прочитал много твоих постов, и они великолепны. Но я нигде не могу найти полную структуру для изучения. Знаете ли вы о том, кто "делает это правильно"? Или, по крайней мере, один, который делает это, как вы и некоторые другие здесь на SO говорят, чтобы сделать? Спасибо.
Джонни

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