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

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

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

Я получу четыре слоя?

5 ответов

Решение

Отказ от ответственности: ниже приводится описание того, как я понимаю 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, которые не имеют прямого отношения к статьям).

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

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

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

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

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

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

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

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

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

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

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

    ....
}

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

Другие вопросы по тегам