MVC в PHP

В современном мире программистов только и слышно, что разговоры о MVC. Что же это за штука такая, о которой так много и часто говорят. Давайте разбираться.

Теория MVC

MVC - это сокращение названия шаблона программирования Model-View-Controller. Этот шаблон используется для отделения данных от логики и представления. Допустим, есть у нас пользователь, он будет моделью, так как содержит данные. Чтобы показать данные, мы создаём форму, в которую передаём этого пользователя. Это будет вид. Форма может содержать разные кнопки для изменения данных, то есть модели. Чтобы управлять кнопками, к форме привязывают контроллер, который ждёт нажатия кнопок и меняет модель. Таким образом достигается чёткое разделение обязанностей: модель хранит данные, вид показывает, а контроллер изменяет. При таком подходе одна модель может иметь много разных видов, а они, в свою очередь, контроллеров.

MVC

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

MVC и PHP-фреймворки

Не могу точно сказать, кому пришла "гениальная" идея взять шаблон для десктопа и перенести его в веб, но, начиная с первого Zend Framework, MVC стало уже чем-то вроде стандарта. Все современные фреймворки так или иначе используют этот шаблон. Правда, как это часто бывает, при переносе перевернули всё с ног на голову. Теперь контроллер принимает запросы, инициализирует модель, выполняет какую-то логику и передаёт данные виду. В общем-то и шаблон надо было бы переименовать в CMV, но этого не произошло, все фреймворки гордо именуются MVC.

По факту MVC в современном PHP означает, что HTTP-запрос всегда обрабатывается объектом контроллера, а наличие модели и вида вовсе не обязательно. На мой взгляд, от изначальной идеи MVC остался "огрызок". Если изобразить на схеме, то выглядит это примерно так:

MCV в PHP

Именно такую схему реализуют фреймворки, гордо именуя её MVC. Где тут модель и вид? А их тут просто нет, кому они вообще нужны...

Проблемы MVC в PHP

Из-за подмены понятий изначального шаблона и "того, что имеем" возникает масса путаниц. Например, где хранить бизнес-логику? Вики говорит, что в модели, а здравый смысл шепчет: "в контроллере". Потому что контроллер есть всегда, а вот модель и вид... как карта ляжет. А вот ещё вопрос, почему на второй схеме есть роутер и действие, а в аббревиатуре никаких букв по этому поводу нет? Наверное, можно придумать массу теорий на этот счёт, но ответ очевиден, это другой шаблон.

Но даже, если отбросить роутеры, действия и прочие нестыковки, то всё равно остаётся один серьёзный вопрос - что является моделью в MVC для веба? Может быть, таблица базы данных, массив, композитный объект? А что делать, если на странице ДВА объекта-модели? На эти вопросы современные фрейворки не любят давать ответы, мол, не нравиться, не ешь. Но при этом активно гнобят тех, кто пытается засунуть логику (например отправку письма или запрос к базе) в контроллер, называя это "плохой практикой".

Хорошая практика MVC по версии фреймворков

Руководство Yii 2 гласит: "Yii приложения организованы согласно шаблону проектирования модель-представление-контроллер (MVC)". На практике там действительно есть папки, которые называются "модели", "виды", "контроллеры". Но, если копнуть глубже, то, вида может и не быть, модель тоже опциональна. Зато всегда есть контроллер, действие и много чего ещё.

Остальные фреймворки, плюс-минус, организованны по тому же принципу: на основании URL определяется контроллер (controller) и функция (action или действие), которая затем вызывается. В качестве аргументов в функцию может передаваться объект запроса или какие-то части URL. В теле функции можно обработать входные данные (через модели или абы как) и подключить вид. Некоторые фреймворки подключают вид сами, более порядочные ждут, когда вид вызовут.

Рассмотрим примеры контроллеров популярных фреймворков:

// Yii2
class PostController extends Controller
{
    public function actionView($id)
    {
        $model = Post::findOne($id);
        
        return $this->render('view', ['model' => $model]);
    }
}

// Symfony 4
class LuckyController
{
    /**
     * @Route("/lucky/number/{max}", name="app_lucky_number")
     */
    public function number($max)
    {
        $number = random_int(0, $max);

        return new Response(
            '<html><body>Lucky number: '.$number.'</body></html>'
        );
    }
}

// Laravel 5
class UserController extends Controller
{
    /**
     * @param  int  $id
     */
    public function show($id)
    {
        return view('user.profile', ['user' => User::findOrFail($id)]);
    }
}

// Zend Framework 3
class AlbumController extends AbstractActionController
{
    public function indexAction()
    {
    }
}

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

Проклятье MVC

Когда мне предлагают использовать тот или иной "суперклёвый" фреймворк (в основном это MVC-фреймворки), я сразу уточняю, а куда там можно запихать две тонны и маленькую тележку говнокода. Я пока не видел крупных проектов, в которых был бы только чистый код.

Если контроллер сделан в виде класса, то он должен подчиняться правилам написания классов (принципы SOLID). Это значит, что туда нельзя запихать даже маленькую тележку, не говоря уже о двух тоннах.

На практике люди пихают в свои контроллеры даже не тонны, а мегатонны всего, что только можно и нельзя. По какому-то неведомому принципу контроллер = справочник. Назвался AlbumController, будь добр уметь создавать, обновлять и удалять альбомы. Это хорошо, когда справочник простой и понятный, а что делать, когда нужен справочник-матрёшка? Например, справочник исполнителей в альбоме. А у исполнителей есть свои альбомы, да ещё и с обложками и историей покупок.

Когда задача выходит за рамки документации, то начинается полёт буйной фантазии разработчиков. Обычно такие полёты и порождают монстров в тысячи строк. Это, увы, не редкость в современном веб-MVC.

Некоторые оправдывают использование ООП для контроллеров возможностью писать юнит-тесты и повторно использовать отдельные методы класса. Но в своей практике я не видел ни того, ни другого. А вот тонны говнокода в контроллерах видел. Куда ж его ещё девать? Но это, как мы знаем, фу-фу-фу и так делать нельзя. А куда тогда девать ценнейшее удобрение умственной деятельности сильных мира сего? В модели? Так ведь они тоже классы и подчиняются тем же правилам. Дробить большую какашку на много маленьких и делать вид, что это хэлперы? Наверное можно, но обилие лишних классов само по себе превращает проект в сплошной кусок сами-знаете-чего.

Есть ли жизнь без MVC?

Есть. Не используйте MVC-шаблон для веба в реализации от популярных фреймворков. Изобретайте своё, фантазируйте. Помните, что "своё" не "пахнет".

Для себя я написал свой фреймворк. Там нет MVC, там есть просто файлы-действия, шаблоны и модули. В действия можно запихать хоть десять тонн говнокода, хуже проекту от этого не станет.

Вот так может выглядеть action без MVC:

Module('widgets')->execute('handle.php');

$searchParams = [
    'name' => Request()->get('name'),
];

$pagination = Pagination();

$items = Site\ConsultantRepository::findMany($searchParams, $pagination);

$params = [];

Template()->render('~/search-form.php', $searchParams);
Template()->render('@totalFound', $pagination, $params);
Template()->render('@pagination', $pagination);
Template()->render('~/list.php' , $items, $pagination->startFrom() + 1);
Template()->render('@pagination', $pagination);

Пример вида:

/* @var $this ApCode\Template\Template */
/* @var $profile \Site\Consultant */

$profile = $this->argument(0);

if ($profile->isDeleted()) {
    Alert('Пользователь удлён. Вы не можете его редактировать.', 'danger');
}
?>
<div class="row widgets">
  <div class="col-md-7">
    <?php echo Widget('consultant.information', $profile, $this->params)?>
  </div>
  <div class="col-md-5">
  </div>
</div>

А вот пример разработки кабинета без MVC:

Сделать подобный интерфейсе на "обычном" фреймворке, на мой взгляд, очень проблематично. Это хорошо, когда у вас много программистов, включая фронтэндеров, но этот проект я делал один при сильно ограниченном лимите часов разработки. Если бы я попытался идти по пути, например, Yii 2, то только представьте сколько форм, моделей, контроллеров пришлось бы создавать и как-то связывать вместе.

Хорошая статья, мне понравилась. Оставлю отзыв!