Статьи

Практический рефакторинг PHP: отдельный домен от презентации

К сожалению, PHP славится спагетти-кодом, но примеры логики запутывания являются продуктом программиста, а не языка.

Одним из антишаблонов, которых мы научились избегать, является запутывание логики предметной области и логики представления. Логика домена состоит из моделей данных и поведения, таких как классы User, Group и ForumPost и базы данных для их сохранения; Логика представления касается создания вывода в форме HTML, JSON или любого другого формата. Более того, анализ HTTP-запроса и его заголовков также является частью верхнего уровня приложения, который должен объединять всю логику представления.

Рефакторинг « Отдельный домен от презентации» пытается отвлечь вас от подхода «SQL-и-HTML-в-том-же-сценарии» для нацеливания на архитектурный шаблон, такой как многоуровневая структура или MVC2.

Но я использую фреймворк, он «разделяет проблемы» для меня

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

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

Так почему разделение?

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

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

Вторая причина — тестируемость . Если вы можете изолировать части вашего дизайна, вы можете проверить их сами; Модульные тесты проще в настройке из-за уменьшенной области видимости и более детерминированы (следовательно, автоматизируемы), поскольку у вас нет зависимости от глобальных переменных, таких как дата или случайные числа. Представьте, что вы устанавливаете часы на нужное время, проверьте, что в некоторых ситуациях код верен …

Результат разделения интересов означает, что вы можете:

  • проверить контроллер или действие без Apache, но только с PHPUnit.
  • проверить постоянство модели с поддельной базой данных, заполнив таблицы SQLite.
  • протестируйте шаблон (если хотите), не заполняя таблицы.

меры

  1. Определите назначение запутанной страницы и создайте для нее класс домена . Различные операции могут быть смоделированы как разные методы для одного и того же объекта (например, Репозиторий, удовлетворяющий каждому запросу относительно пользователей), если у вас уже есть много классов домена.
  2. Переместите логику из текущего места в класс домена .
  3. Извлеките файл шаблона или View Helper для генерации HTML (или JSON или из чего состоит ваш вывод).
  4. После второго движения логика внутри исходной страницы или действия должна быть сосредоточена вокруг HTTP-запроса и в соединении других объектов. Это элементарный шаблон MVC для Интернета.

пример

Мы работаем над сценарием .php, для простоты; цель состоит в том, чтобы разделить его на модель, представление и контроллер. Тот же пример можно применить к действию контроллера, встроенному в Symfony или Zend Framework.

Этот скрипт печатает первый не прочитанный пост в ветке, помеченной администраторами как липкая; он возвращает его как фрагмент HTML, который может быть включен клиентами.
Существует необязательный параметр date_visit date: если он присутствует, это означает, что сообщения до этой даты не должны рассматриваться. Если этот параметр отсутствует, это означает, что информация о текущем пользователе отсутствует, поэтому следует выбрать последнее сообщение.

Этот MySQL содержит данные о сообщениях:

mysql> SELECT * FROM posts;
+----+-----------+---------+----------------+------------+
| id | id_thread | author  | text           | date       |
+----+-----------+---------+----------------+------------+
|  1 |        23 | giorgio | My new post... | 2012-02-13 |
|  2 |        23 | giorgio | My old post... | 2012-01-01 |
+----+-----------+---------+----------------+------------+
2 rows in set (0.00 sec)
<?php
$dsn = 'mysql:host=localhost;dbname=practical_php_refactoring';
$username = 'root';
$password = '';
$dbh = new PDO($dsn, $username, $password);
$stmt = $dbh->prepare("SELECT * FROM posts WHERE id_thread = :id_thread AND date >= :date ORDER BY date");
$stmt->bindValue(':id_thread', (int) $_GET['id_thread']);
if (!isset($_GET['last_visit'])) {
    $_GET['last_visit'] = date('Y-m-d');
}
$stmt->bindValue(':date', $_GET['last_visit']);
$stmt->execute();
$post = $stmt->fetch();
?>
<div class="post">
    <div class="author"><?php echo $post['author']; ?></div>
    <div class="date"><?php echo $post['date']; ?></div>
    <div class="text"><?php echo $post['text']; ?></div>
</div>

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

<?php
class Posts
{
    public function __construct(PDO $connection)
    {
        $this->connection = $connection;
    }

    /**
     * @param int $threadId
     * @param string $lastVisit Y-m-d format
     * @return array    fields for the selected post
     */
    public function lastPost($threadId, $lastVisit)
    {
        $stmt = $this->connection->prepare("SELECT * FROM posts WHERE id_thread = :id_thread AND date >= :date ORDER BY date");
        $stmt->bindValue(':id_thread', (int) $_GET['id_thread']);
        $stmt->bindValue(':date', $lastVisit);
        $stmt->execute();
        return $stmt->fetch();
    }
}
$dsn = 'mysql:host=localhost;dbname=practical_php_refactoring';
$username = 'root';
$password = '';
$posts = new Posts(new PDO($dsn, $username, $password));
if (!isset($_GET['last_visit'])) {
    $_GET['last_visit'] = date('Y-m-d');
}
$post = $posts->lastPost($_GET['id_thread'], $_GET['last_visit']);
?>
<div class="post">
    <div class="author"><?php echo $post['author']; ?></div>
    <div class="date"><?php echo $post['date']; ?></div>
    <div class="text"><?php echo $post['text']; ?></div>
</div>

В реальной жизни вы, вероятно, создадите EntityManager или другой Фасад для ORM вместо прямого доступа к PDO. Обратите внимание, что классы, показанные здесь, хранятся в исходном файле ради отдельного примера, но они должны быть перемещены в собственный исходный файл и загружены автоматически.

Теперь мы извлекаем контроллер, но на самом деле это всего лишь одно действие возможного контроллера. Мы назовем класс Action, чтобы отразить этот факт.

<?php
class Posts
{
    public function __construct(PDO $connection)
    {
        $this->connection = $connection;
    }

    /**
     * @param int $threadId
     * @param string $lastVisit Y-m-d format
     * @return array    fields for the selected post
     */
    public function lastPost($threadId, $lastVisit)
    {
        $stmt = $this->connection->prepare("SELECT * FROM posts WHERE id_thread = :id_thread AND date >= :date ORDER BY date");
        $stmt->bindValue(':id_thread', (int) $_GET['id_thread']);
        $stmt->bindValue(':date', $lastVisit);
        $stmt->execute();
        return $stmt->fetch();
    }
}
class LastPostAction
{
    public function __construct(Posts $posts, $template)
    {
        $this->posts = $posts;
        $this->template = $template;
    }

    public function execute(array $getParameters)
    {
        if (!isset($getParameters['last_visit'])) {
            $getParameters['last_visit'] = date('Y-m-d');
        }
        $post = $this->posts->lastPost($getParameters['id_thread'],
                                       $getParameters['last_visit']);
        require $this->template;
    }
}
$dsn = 'mysql:host=localhost;dbname=practical_php_refactoring';
$username = 'root';
$password = '';
$action = new LastPostAction(
                new Posts(new PDO($dsn, $username, $password)),
                'last_post.php'
          );
$action->execute($_GET);
?>

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

<div class="post">
    <div class="author"><?php echo $post['author']; ?></div>
    <div class="date"><?php echo $post['date']; ?></div>
    <div class="text"><?php echo $post['text']; ?></div>
</div>