Как должна быть структурирована модель в MVC?


Я только начинаю понимать структуру 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;
    }
}

Мои модели, как правило, представляют собой класс сущностей, который сопоставляется с таблицей базы данных.

Должен ли объект модели иметь все свойства, сопоставленные с базой данных, а также приведенный выше код, или можно отделить этот код, который на самом деле работает с базой данных?

Будет ли у меня в итоге четыре слои?

Author: i alarmed alien, 2011-05-03

5 answers

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

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

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

Чем НЕ является модель:

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

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

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

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

  • Объекты домена

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

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

  • Картографы данных

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

  • Услуги

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

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

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

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

 

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

Предварительные требования: смотрите лекции "Глобальное состояние и синглеты" и "Не ищите вещей!" из разговоров о чистом коде.

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

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

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

Как вы можете подозревать, контейнер DI является намного более элегантным решением (хотя и не самым простым для новичка). Двумя библиотеками, которые я рекомендую рассмотреть для этой функциональности, будут автономный компонент Syfmony DependencyInjection или Аурин.

Оба решения, использующие фабрику и контейнер 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.

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

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

MVC - это просто конкретное решение для Принципа разделения задач. MVC отделяет пользовательский интерфейс от бизнес-логики, и в пользовательском интерфейсе он отделяет обработку пользовательского ввода и представление. Это имеет решающее значение. В то время как часто люди описывают это как "триаду", на самом деле он не состоит из трех независимых частей. Структура больше похожа на эту:

MVC separation

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

Используя этот подход, пример входа (для 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);
}

Способы создания картографов

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

Mapper diagram

Из: PoEAA книга

На практике они являются реализовано для взаимодействия с определенными классами или суперклассами. Допустим, в вашем коде есть Customer и Admin (оба наследуются от суперкласса User). Оба, вероятно, в конечном итоге будут иметь отдельный сопоставитель соответствия, поскольку они содержат разные поля. Но вы также получите общие и часто используемые операции. Например: обновление времени "последнего просмотра онлайн". И вместо того, чтобы делать существующие картографы более запутанными, более прагматичный подход заключается в том, чтобы иметь общую "Сопоставитель пользователей", который обновляет только эту метку времени.

Некоторые дополнительные комментарии:

  1. Таблицы базы данных и модель

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

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

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

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

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

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

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

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

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

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

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

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

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

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

    Единственное существенное изменение заключается в том, что то, что в старой версии называется Моделью, на самом деле является Сервисом. Остальная часть "библиотечной аналогии" держится довольно хорошо.

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

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

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

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

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

 833
Author: tereško, 2017-10-18 07:54:33

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

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

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

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

 33
Author: netcoder, 2015-06-27 19:38:55

В Web-"MVC" вы можете делать все, что вам заблагорассудится.

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

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

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

 19
Author: mario, 2017-05-23 12:34:45

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

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

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

Контроллер(C)----> Имеет обрабатывающую часть приложения и действует как проводка между M и V, и это зависит от обоих M,V в отличие от M и V.

В целом существует разделение интересов между каждым из них. В будущем любые изменения или усовершенствования могут быть добавлены очень легко.

 5
Author: feel good and programming, 2014-08-19 14:44:27

В моем случае у меня есть класс базы данных, который обрабатывает все прямое взаимодействие с базой данных, такое как запросы, выборки и тому подобное. Поэтому, если бы мне пришлось изменить свою базу данных с 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);
    }

    ....
}

Я надеюсь, что этот пример поможет вам создать хорошую структуру.

 0
Author: Ibu, 2012-06-14 20:24:06