Начнем, пожалуй, с того, что MVC как такового в PHP нет в виду невозможности реализации. Правда, эта информация может лишь понадобится на собеседовании или общении между разработчиками, чем для реальной жизни. В повседневности для вас будет стоять задача не смешивать бизнес-логику и представление. А каким именно шаблоном вы будете пользоваться не так важно.
Надеюсь, вы уже ознакомились с какой-либо информацией касательно MVC или какой-то информацией для отделения бизнес-логики от представления и т.п. Возможно, прочитали, что все приложения состоят из этих трех частей. Хотя на самом деле это не совсем так. MVC необходим только для взаимодействия с пользовательским интерфейсом (UI), чтобы бизнес-логика не проникала в представление, а представление в бизнес-логику, и больше ни для чего. Помимо шаблона MVC в приложениях, как правило, используется еще множество других шаблонов проектирования, а вся бизнес-логика находится не в модели. Модель лишь транспортирует данные и служит связующим звеном с представлением.
Самое главное, по моему мнению, в изучении парадигм вида MVC, MVP, MVVP/etc и любых других шаблонов.: не пытаться ограничивать себя в реализации. Самого правильного варианта не существует, есть лишь общая модель, идея. А как это называется не особо важно. Не стоит сжимать себя до терминов и определений.
Что есть представление (View)
Представление это та часть нашего приложения, где пользователь наблюдает результат работы программы или сайта. Само по себе представление не делает ничего. Оно лишь отображает и не содержит никаких вычислений. Проще говоря, тупое, как валенок.
Я предполагаю, что читатель знаком с ООП и уже может что-то писать на 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-аттаки и просто защиты от "поехавшей" верстки; - строку итого с общим количеством и суммой.
Как можно заметить никаких вычислений нет. Потому что они сделаны до того, как все попало в представление. Именно таким прозрачным и чистым оно должно быть. Никакой логики. Никаких вычислений.
Что есть модель (Model)
Модель носит транспортный характер и, по сути, не делает ничего, кроме того, что осуществляет доставку данных в представление. Но главную цель, которую несет модель, это - возможность оповещать представление(ия) об изменениях.
Для большинства, прочитавших принципы и идею работы 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
в у.е. В реальных проектах удобно хранить вinteger
в минимальной денежной единице (для рубля РФ это копейки). Еще более удобней вfixed-point
- числах с фиксированной точкой. А для работы с такими значениями используют BCMath или GMP.
У нас получилось два класса. Cart
содержит все необходимые данные для отрисовки корзины, а CartItem
содержит данные позиции товара в корзине. Друг без друга эти два класса смысла не имеют. И используются они только в паре. Вся работа модели сводится к хранению и/или транспортировке. При необходимости модель может что-то считать используя уже полученные данные (как в Cart::getTotalQuantity()
и Cart::getTotalSum()
).
Что есть Контроллер (Controller)
Как я уже сказал выше, реализация 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 не панацея и на нем не держится вся архитектура приложения.
Если у вас остались вопросы - пишите в комментариях и я обязательно отвечу на них.