Отказ от ответственности: ниже приводится описание того, как я понимаю MVC-подобные шаблоны в контексте веб-приложений на основе PHP. Все внешние ссылки, которые используются в содержании, предназначены для объяснения терминов и понятий, а не для того, чтобы свидетельствовать о моем собственном доверии к предмету.
Первое, что я должен прояснить: модель - это слой .
Второе: есть разница между классическим MVC и тем, что мы используем в веб-разработке. Вот немного более старого ответа, который я написал, который кратко описывает, как они отличаются.
Какая модель НЕ является:
Модель не является классом или каким-либо отдельным объектом. Это очень распространенная ошибка (я тоже сделал, хотя первоначальный ответ был написан, когда я начал учиться иначе) , потому что большинство фреймворков увековечивают это заблуждение.
Это не методика объектно-реляционного отображения (ORM) и не абстракция таблиц базы данных. Любой, кто говорит вам обратное, скорее всего, пытается «продать» другой новый ORM или целую платформу.
Что это за модель:
При правильной адаптации MVC M содержит всю бизнес-логику домена, а уровень модели в основном состоит из трех типов структур:
Доменные объекты
Доменный объект - это логический контейнер с чисто доменной информацией; обычно он представляет логическую сущность в пространстве проблемной области. Обычно упоминается как бизнес-логика .
В этом месте вы определяете, как проверять данные перед отправкой счета или рассчитывать общую стоимость заказа. В то же время доменные объекты полностью не знают о хранилище - ни откуда (база данных SQL, REST API, текстовый файл и т. Д.), Ни даже если они сохранены или получены.
Data Mappers
Эти объекты несут ответственность только за хранение. Если вы храните информацию в базе данных, это то место, где живет SQL. Или, может быть, вы используете файл XML для хранения данных, и ваши Data Mappers анализируют файлы XML и обратно.
Сервисы
Вы можете думать о них как о «доменных объектах более высокого уровня», но вместо бизнес-логики Сервисы отвечают за взаимодействие между доменными объектами и Mappers . Эти структуры в конечном итоге создают «открытый» интерфейс для взаимодействия с бизнес-логикой домена. Вы можете избежать их, но за счет утечки некоторой логики домена в контроллеры .
Существует ответ на этот вопрос в вопросе реализации ACL - это может быть полезно.
Связь между уровнем модели и другими частями триады MVC должна происходить только через Сервисы . Четкое разделение имеет несколько дополнительных преимуществ:
- это помогает обеспечить соблюдение принципа единой ответственности (SRP)
- обеспечивает дополнительную «комнату для маневра» в случае изменения логики
- делает контроллер максимально простым
- дает четкий план, если вам когда-нибудь понадобится внешний API
Как взаимодействовать с моделью?
Пререквизиты: смотреть лекции «Глобальное состояние и одиночные игры» и «Не искать вещи!» из Чистых переговоров по коду.
Получение доступа к экземплярам сервисов
Для обоих экземпляров View и Controller (то, что вы могли бы назвать «уровнем пользовательского интерфейса») для доступа к этим службам, существует два основных подхода:
- Вы можете напрямую внедрить необходимые сервисы в конструкторы ваших представлений и контроллеров, предпочтительно используя контейнер DI.
- Использование фабрики для сервисов в качестве обязательной зависимости для всех ваших представлений и контроллеров.
Как вы можете подозревать, 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 отделяет пользовательский интерфейс от бизнес-логики, а в пользовательском интерфейсе он разделяет обработку пользовательского ввода и представление. Это очень важно. Хотя люди часто называют это «триадой», на самом деле она не состоит из трех независимых частей. Структура больше похожа на это:
Это означает, что, когда логика вашего уровня представления почти не существует, прагматический подход заключается в том, чтобы сохранить их как один уровень. Это также может существенно упростить некоторые аспекты модельного уровня.
Используя этот подход, пример входа в систему (для 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 между таблицей базы данных, Domain Object и Mapper , в более крупных проектах это может быть менее распространенным, чем вы ожидаете:
Информация, используемая одним доменным объектом, может отображаться из разных таблиц, тогда как сам объект не хранится в базе данных.
Пример: если вы генерируете ежемесячный отчет. Это позволит собирать информацию из разных таблиц, но MonthlyReport
в базе данных нет волшебных таблиц.
Один Mapper может влиять на несколько таблиц.
Пример: когда вы храните данные из User
объекта, этот объект домена может содержать коллекцию других объектов домена - Group
экземпляров. Если вы измените их и сохраните User
, Data Mapper должен будет обновить и / или вставить записи в несколько таблиц.
Данные из одного доменного объекта хранятся в нескольких таблицах.
Пример: в больших системах (например, социальная сеть среднего размера) может быть прагматичным хранить данные аутентификации пользователя и часто используемые данные отдельно от больших кусков контента, что редко требуется. В этом случае у вас все еще может быть один User
класс, но информация, которую он содержит, будет зависеть от того, были ли получены полные сведения.
Для каждого доменного объекта может быть несколько картографов
Пример: у вас есть новостной сайт с общим кодовым кодом как для публичного, так и для управляющего программного обеспечения. Но, хотя оба интерфейса используют один и тот же Article
класс, для управления необходимо заполнить его большим количеством информации. В этом случае у вас будет два отдельных преобразователя: «внутренний» и «внешний». Каждый выполняет разные запросы или даже использует разные базы данных (как в master, так и в slave).
Представление не шаблон
Просмотр экземпляров в MVC (если вы не используете вариацию шаблона MVP) отвечают за логику представления. Это означает, что в каждом представлении обычно используется как минимум несколько шаблонов. Он получает данные с уровня модели, а затем на основе полученной информации выбирает шаблон и устанавливает значения.
Одним из преимуществ, которые вы получаете от этого, является возможность повторного использования. Если вы создаете ListView
класс, то с хорошо написанным кодом у вас может быть тот же класс, который передает представление списка пользователей и комментарии под статьей. Потому что они оба имеют одинаковую логику представления. Вы просто переключаете шаблоны.
Вы можете использовать либо собственные шаблоны PHP, либо использовать сторонний шаблонизатор. Также могут быть некоторые сторонние библиотеки, которые могут полностью заменить экземпляры View .
Как насчет старой версии ответа?
Единственное существенное изменение заключается в том, что то, что в старой версии называется Model , на самом деле является Сервисом . В остальном «библиотечная аналогия» держится довольно неплохо.
Единственный недостаток, который я вижу, это то, что это будет действительно странная библиотека, потому что она вернет вам информацию из книги, но не позволит вам прикоснуться к самой книге, потому что в противном случае абстракция начнет «просачиваться». Возможно, мне придется подумать о более подходящей аналогии.
Какова связь между экземплярами View и Controller ?
Структура MVC состоит из двух слоев: пользовательского интерфейса и модели. Основными структурами на уровне пользовательского интерфейса являются представления и контроллер.
Когда вы имеете дело с веб-сайтами, использующими шаблон проектирования MVC, наилучшим способом является соотношение 1: 1 между представлениями и контроллерами. Каждое представление представляет целую страницу на вашем сайте, и у него есть специальный контроллер для обработки всех входящих запросов для этого конкретного представления.
Например, чтобы представить открытую статью, вы должны иметь \Application\Controller\Document
и \Application\View\Document
. Он будет содержать все основные функциональные возможности для уровня пользовательского интерфейса, когда речь идет о работе со статьями (конечно, у вас могут быть некоторые компоненты XHR , которые не имеют прямого отношения к статьям) .