Как следует структурировать модель в 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;
    }
}

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

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

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

Ответ 1

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

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

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

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

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

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

Какая модель:

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

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

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

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

  • Data Mappers

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

  • Сервисы

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

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

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

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

 

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

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

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

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

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

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

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

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

 

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

Поскольку не существует ни одного класса "Модель" (как объяснялось выше), вы действительно не "строите модель". Вместо этого вы начинаете с создания служб, которые могут выполнять определенные методы. А затем реализуйте объекты домена и 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);
}

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

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

Mapper diagram

От: книга PoEAA

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

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

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

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

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

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

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

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

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

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

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

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

  • Просмотр не шаблон

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

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

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

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

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

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

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

    Структура MVC состоит из двух слоев: ui и model. Основными структурами слоя UI являются виды и контроллер.

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

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

Ответ 2

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

Ответ 3

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

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

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

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

Ответ 4

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

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

View (V) → Отображает формат для приложения и знает только о том, как его моделировать, и не беспокоится о C.

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

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

Ответ 5

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

    ....
}

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