11 июня 2020 г., 8:59:06 PHP mvc php pattern 0 Комментариев
Начнем, пожалуй, с того, что MVC как такового в PHP нет в виду невозможности реализации. Правда, эта информация может лишь понадобится на собеседовании или общении между разработчиками, чем для реальной жизни. В повседневности для вас будет стоять задача не смешивать бизнес-логику и представление. А каким именно шаблоном вы будете пользоваться не так важно.
Надеюсь, вы уже ознакомились с какой-либо информацией касательно MVC или какой-то информацией для отделения бизнес-логики от представления и т.п. Возможно, прочитали, что все приложения состоят из этих трех частей. Хотя на самом деле это не совсем так. MVC необходим только для взаимодействия с пользовательским интерфейсом (UI), чтобы бизнес-логика не проникала в представление, а представление в бизнес-логику, и больше ни для чего. Помимо шаблона MVC в приложениях, как правило, используется еще множество других шаблонов проектирования, а вся бизнес-логика находится не в модели. Модель лишь транспортирует данные и служит связующим звеном с представлением.
Самое главное, по моему мнению, в изучении парадигм вида MVC, MVP, MVVP/etc и любых других шаблонов.: не пытаться ограничивать себя в реализации. Самого правильного варианта не существует, есть лишь общая модель, идея. А как это называется не особо важно. Не стоит сжимать себя до терминов и определений.
Представление это та часть нашего приложения, где пользователь наблюдает результат работы программы или сайта. Само по себе представление не делает ничего. Оно лишь отображает и не содержит никаких вычислений. Проще говоря, тупое, как валенок.
Я предполагаю, что читатель знаком с ООП и уже может что-то писать на PHP. Если нет, то материал в данной статье может вызвать вопросы. Для примеров нужно быть знакомым с PHP 7.1 или выше.
Для своих примеров я возьму модель интернет-корзины, т.к. сейчас каждый знаком с тем, как она работает. Здесь не должно возникнуть сложностей.
Наше представление для корзины выглядело бы как-то так:
// /views/cart.php <DOCTYPE! html> <html lang="ru"> <head> <meta charset="utf-8"> <title>Корзина</title> </head> <body> <h1>Корзина</h1> <table> <thead> <tr> <th>№</th> <th>Наименование</th> <th>Количество</th> <th>Сумма</th> </tr> </thead> <tbody> <?php foreach ($cart->getItems() as $index => $data) : ?> <tr> <td><?= $index + 1; ?></td> <td><?= htmlspecialchars($data->getName(), ENT_QUOTES | ENT_SUBSTITUTE); ?></td> <td><?= $data->getQuantity(); ?></td> <td><?= $data->getSum(); ?></td> </tr> <?php endforeach; ?> </tbody> <tfoot> <tr> <td colspan="2">Итого</td> <td><?= $cart->getTotalQuantity(); ?></td> <td><?= $cart->getTotalSum(); ?></td> </tr> </tfoot> </table> </body> </html>
Что мы видим в этом куске кода:
$cart
со всеми данными;htmlspecialchars()
с константами ENT_QUOTES
и ENT_SUBSTITUTE
для защиты от XSS-аттаки и просто защиты от "поехавшей" верстки;Как можно заметить никаких вычислений нет. Потому что они сделаны до того, как все попало в представление. Именно таким прозрачным и чистым оно должно быть. Никакой логики. Никаких вычислений.
Модель носит транспортный характер и, по сути, не делает ничего, кроме того, что осуществляет доставку данных в представление. Но главную цель, которую несет модель, это - возможность оповещать представление(ия) об изменениях.
Для большинства, прочитавших принципы и идею работы MVC/MVP/etc, сложилось стойкое правило, что там должна быть бизнес-логика: обращение к базе данных, наличие ключевых логических функций, валидация входящих данных и т.п. На самом деле, это не так (если, конечно, это не упрощенная модель парадигмы MVC/MVP/etc; хотя и в данном случае все получается более запутанным, нежели простым). Если вернуться в начало статьи, где я говорю, что MVC это шаблон взаимодействия с пользовательским интерфейсом (UI), то все становится ясным: бизнес-логики в модели быть не может.
Давайте напишем модель для нашего представления из примера выше:
// /src/View/Model/CartItem.php namespace App\View\Model; class CartItem { /** * @var string */ private $name = ''; /** * @var int */ private $quantity = 0; /** * @var float */ private $sum = 0.0; public function __construct(string $name, int $quantity, float $sum) { $this->name = $name; $this->quantity = $quantity; $this->sum = $sum; } public function getName(): string { return $this->name; } public function setName(string $name): CartItem { $this->name = $name; return $this; } public function getQuantity(): int { return $this->quantity; } public function setQuantity(int $quantity): CartItem { $this->quantity = $quantity; return $this; } public function getSum(): float { return $this->sum; } public function setSum(float $sum): CartItem { $this->sum = $sum; return $this; } }
// /src/View/Model/Cart.php namespace App\View\Model; class Cart { /** * @var CartItem[] */ private $items = []; /** * @return CartItem[] */ public function getItems(): array { return $this->items; } public function addItem(CartItem $item): Cart { $this->items[] = $item; return $this; } public function getTotalQuantity(): int { return array_sum(array_map(function (CartItem $item) { return $item->getQuantity(); }, $this->items)); } public function getTotalSum(): float { return array_sum(array_map(function (CartItem $item) { return $item->getSum(); }, $this->items)); } }
В примере валюта хранится в
float
в у.е. В реальных проектах на PHP удобно хранить вstring
в минимальной денежной единице. Например, для рубля РФ это копейки. А для работы с такими значениями используют BCMath или GMP.
У нас получилось два класса. Cart
содержит все необходимые данные для отрисовки корзины, а CartItem
содержит данные позиции товара в корзине. Друг без друга эти два класса смысла не имеют. И используются они только в паре. Вся работа модели сводится к хранению и/или транспортировке. При необходимости модель может что-то считать используя уже полученные данные (как в Cart::getTotalQuantity()
и Cart::getTotalSum()
).
Как я уже сказал выше, реализация MVC в PHP невозможно в силу особенности языка: на каждый запрос создается отдельный экземпляр PHP и по окончанию выполнения всей работы он "погибает". Поэтому нельзя запустить приложении и повешать наблюдателей на модель, чтобы можно было обновлять представление. MVC в PHP не совсем то, как это в теории, потому что:
Но исторически сложилось так, что в PHP это называют MVC. В целом, задача по разделению зон ответственностей не меняется и результат не страдает. А более и не нужно.
Контроллер играет роль посредника и сам по себе ничего не умеет. Он всегда делегирует работу. Делегаторами (не путать с шаблоном Delegator) могут выступать репозитории, службы, различные клиенты и команды. Все напрямую зависит от поставленной задачи.
После того, как вся работа контроллером выполнена он передает полученные данные в представление. Ну, или просто вызывает его без ничего. Всякое бывает. Иногда и данных-то никаких нет.
Для нашего примера контроллер будет иметь следующий вид:
// /src/Controller/CartController.php namespace App\Controller; use App\View\Model\Cart; use App\View\Model\CartItem; class CartController { public function load(): void { $cart = new Cart(); $cart->addItem(new CartItem('Item 1', 1, 600.96)); $cart->addItem(new CartItem('Item 2', 2, 399.00)); ob_start(); require __DIR__ . '/views/cart.php'; echo ob_get_clean(); } }
У нас получился класс с одним методом load()
в котором мы наполнили данными нашу модель и отрисовали представление. В реальности, конечно, у нас не будет статических данных. Как минимум номера номенклатуры и количество будут храниться в куках (cookies), а сама номенклатура в базе данных.
Первое, что придет в голову, это - сделать все нужные операции в контроллере. Но мы не можем так сделать, потому что бизнес-логика не может содержаться в контроллере. И в модели не можем, т.к. она занимается только транспортировкой. А в представлении тем более. Поэтому должна быть еще одна часть приложения помимо уже существующего контроллера, модели и представления. Она выходит за рамки шаблона MVC. И это часть приложения отвечающая за управление корзиной будет называться той самой бизнес-логикой. А доступ к ней будет через контроллер.
Доработаем наш пример и добавим бизнес-логику. Для этого потребуется служба, которая будет отвечать за корзину.
// /src/Service/CartService.php namespace App\Service; use App\View\Model\Cart; use App\View\Model\CartItem; class CartService { public function getData(): Cart { $cart = new Cart(); $cart->addItem(new CartItem('Item 1', 1, 600.96)); $cart->addItem(new CartItem('Item 2', 2, 399.00)); return $cart; } }
// /src/Controller/CartController.php namespace App\Controller; use App\Service\CartService; class CartController { /** * @var CartService */ private $cartService; public function __construct(CartService $cartService) { $this->cartService = $cartService; } public function load(): void { $cart = $this->cartService->getData(); ob_start(); require __DIR__ . '/views/cart.php'; echo ob_get_clean(); } }
Конечно, этот пример с службой также содержит статически данные, но мы скрыли реализацию того откуда поступают данные и все свели к результату. Нет никаких проблем, чтобы позднее формировать данные на основе кук и базы данных, ведь всегда будет возвращаться один и тот же тип данных, что не приведет к неожиданным ситуациям. Плюсом, мы сузили ответственность контроллера. Теперь он обычный "дирижер", каким и должен быть.
И вроде бы проблема решена. Но нет. Мы "размыли" границу между слоем модели и бизнес-логикой: модель пользовательского интерфейса используется в бизнес-логике. А такого быть не должно из-за правила взаимодействия со слоями приложения (об этом мы поговорим в другой раз). Отсюда следует, что для того, чтобы передать данные в представление, нам нужно, как минимум, две модели в разных слоях приложения: там где наша бизнес-логика и там где пользовательский интерфейс. Сделаем так.
// /src/Model/CartItem.php namespace App\Model; class CartItem { /** * @var string */ private $name = ''; /** * @var int */ private $quantity = 0; /** * @var float */ private $sum = 0.0; public function __construct(string $name, int $quantity, float $sum) { $this->name = $name; $this->quantity = $quantity; $this->sum = $sum; } public function getName(): string { return $this->name; } public function setName(string $name): CartItem { $this->name = $name; return $this; } public function getQuantity(): int { return $this->quantity; } public function setQuantity(int $quantity): CartItem { $this->quantity = $quantity; return $this; } public function getSum(): float { return $this->sum; } public function setSum(float $sum): CartItem { $this->sum = $sum; return $this; } }
// /src/Model/Cart.php namespace App\Model; class Cart { /** * @var CartItem[] */ private $items = []; /** * @return CartItem[] */ public function getItems(): array { return $this->items; } public function addItem(CartItem $item): Cart { $this->items[] = $item; return $this; } }
Легким движением руки у нас получились те же самые модели, но в другом слое приложения. Осталось доработаем службу и контроллер.
// /src/Service/CartService.php namespace App\Service; use App\Model\Cart; use App\Model\CartItem; class CartService { public function getData(): Cart { $cart = new Cart(); $cart->addItem(new CartItem('Item 1', 1, 600.96)); $cart->addItem(new CartItem('Item 2', 2, 399.00)); return $cart; } }
// /src/Controller/CartController.php namespace App\Controller; use App\Service\CartService; use App\View\Model\Cart as ViewModelCart; use App\View\Model\CartItem as ViewModelCartItem; use App\Model\Cart; class CartController { /** * @var CartService */ private $cartService; public function __construct(CartService $cartService) { $this->cartService = $cartService; } public function load(): void { $cart = $this->createViewModel($this->cartService->getData()); ob_start(); require __DIR__ . '/views/cart.php'; echo ob_get_clean(); } private function createViewModel(Cart $cart): ViewModelCart { $viewModelCart = new ViewModelCart(); foreach ($cart->getItems() as $item) { $viewModelCart->addItem(new ViewModelItem($item->getName(), $item->getQuantity(), $item->getSum())); } return $viewModelCart; } }
Проблема "размытой" границы слоев решена. Появилась другая. Избыточность (или "бойлерплейт"). Теперь нам нужно проделывать двойную работу, чтобы передавать данные из одной части приложения в другую. Резонный вопрос возникающий в голове программиста: можно ли как-то это сделать лучше? Да, можно, если мы говорим о PHP.
Как вы уже заметили, данные в представление попадают в виде переменных и позже используются для отрисовки. И на объекты, содержащие данные, никто не вешает наблюдателей за изменениями. Это просто не нужно в виду особенности работы PHP: после каждого запроса "умирать". Именно поэтому данные идут в одном направлении и достигая "конечной" прекращают свое существование. Если бы это было приложение на Java, то там наличие модели для представления является обязательным для реализации MVC. В PHP это аппендикс.
Правда, модели представления иногда необходимы, но не так часто. Когда именно они нужны подсказывает логика. Какого-то определенного правила нет.
На основании вышесказанного делаем вывод, что от модели пользовательского интерфейса можно отказаться в пользу модели бизнес-логики. Работать будет проще и писать код быстрей. Но, если что-то где-то поменяется, то придется ковырять представления вдоль и поперек, чтобы внести изменения.
Мы отказались от модели пользовательского интерфейса в пользу модели бизнес-логики, но не наоборот. Почему? Если хочешь узнать, напиши, что тебе понравилась статья и я напишу новую, где мы детально обсудим взаимодействия между слоями приложения.
Упраздним модели представления и внесем изменения:
// /src/Model/Cart.php namespace App\Model; class Cart { /** * @var CartItem[] */ private $items = []; /** * @return CartItem[] */ public function getItems(): array { return $this->items; } public function addItem(CartItem $item): Cart { $this->items[] = $item; return $this; } public function getTotalQuantity(): int { return array_sum(array_map(function (CartItem $item) { return $item->getQuantity(); }, $this->items)); } public function getTotalSum(): float { return array_sum(array_map(function (CartItem $item) { return $item->getSum(); }, $this->items)); } }
// /src/Controller/CartController.php namespace App\Controller; use App\Service\CartService; class CartController { /** * @var CartService */ private $cartService; public function __construct(CartService $cartService) { $this->cartService = $cartService; } public function load(): void { $cart = $this->cartService->getData(); ob_start(); require __DIR__ . '/views/cart.php'; echo ob_get_clean(); } }
Теперь моделью пользовательского интерфейса являются наши переменные передаваемые в представления. Сами переменные могут содержать любые наборы данных необходимых для отрисовки.
Добавим точку входа для запуска:
// /public/index.php header('HTTP/1.1 200 OK'); header('Content-type: text/html; charset=UTF-8'); $ctrl = new App\Controller\CartController(new App\Service\CartService()); $ctrl->load(); exit(0);
И запустим:
$ php -S 127.0.0.1:8080 -t public
Такой вот MVC в PHP.
Тема шаблона MVC в PHP заезжена вдоль и поперек. Множество реализаций и мнений. Да и саму тему охватить в одной статье тяжело. Много нюансов, много особенностей. В своей статье я хотел дать пищу для размышления и рассуждения в контексте PHP, сделал упор на то, что это шаблон пользовательского интерфейса и никак не относится к бизнес-логике; бизнес-логика находится вне шаблона MVC и в основу её лежит огромный пласт других практик и шаблонов проектирования с которыми стоит ознакомиться; и что MVC не панацея и на нем не держится вся архитектура приложения.
Если у вас остались вопросы - пишите в комментариях и я обязательно отвечу на них.
Комментарии [0]
Новый комментарий