Микро-фреймворк как основа веб-проекта на PHP

Несколько лет назад я делал приватный проект, где было много математики и обработки данных. Поскольку хотелось сразу сделать правильно, то использовал ООП, классы, статические методы и ряд новых функций PHP 5.5. После того, как проект загрузили на «боевой» сервер, всё «разрушилось», поскольку там стояла PHP 5.2, которую (по каким-то особым причинам), нельзя было обновить. Код пришлось переписывать с учётом старой версии. Теперь, если сервер всё-таки обновят до PHP 7.x, код опять придётся переписывать, поскольку между версиями нет полной совместимости.

Не существует идеального и универсального решения для веб-проекта. Они все обязательно окажутся в тупике из-за изначально непродуманной архитектуры или объективных причин, вроде несовместимости между PHP-версиями. Даже если сделать как «по теории», в процессе развития проект окажется неспособным решить какие-то новые задачи, поскольку мы элементарно не можем предсказывать будущее. Единственным способом двигаться дальше будет только революционный подход, который подразумевает полную или частичную потерю совместимости со старым кодом.

Через это прошли все фреймворки, системы, библиотеки (любого ЯП) и т.п., которые продолжают развиваться. По мере роста, вначале идёт эволюционный подход, когда возможностей хватает, но потом резкий скачок к новой версии. Для того, чтобы упростить такие переходы, архитектура приложения должна изначально подразумевать такую «версионность».

За последние лет 10 накопился некоторый мировой опыт того, как грамотно разрабатывать веб-проекты. Например деление файлов на классы и модульность стало фактически «стандартом». Однако проблема в том, что архитектура проекта как правило строится только исходя из предполагаемых потребностей конечных пользователей.

Примеры с файлами вы найдёте в конце статьи.

Возьмём например Laravel, который содержит массу библиотек: авторизация, база данных, работа с почтой, кэширование, логирование и т.д., и т.п. Или Symfony с десятками компонентов. CodeIgniter 4, который выглядит более скромно, но тоже с десятками библиотек и хелперов.

Во всех php-фреймворках сотни или даже тысячи классов, которые могут быть связаны между собой самых причудливым образом. И это речь ещё не идёт о полноценных CMS, которые «навешивают» ещё «тонну» своего кода.

Когда вы создаёте новый веб-проект, то кажется, что лучше использовать наиболее функциональный php-фреймворк, поскольку он «из коробки» предлагает больше функциональных модулей. Но, на самом деле, это не так: за всё нужно платить. Такой фреймворк будет загонять вас в свои жесткие рамки и настанет тот момент, когда они окажутся слишком тесными и чтобы добавить новую возможность, придётся уже что-то решать с самим фреймворком.

Поэтому лучший фреймворк — тот, который позволяет выходить за рамки своих правил.

Нельзя так же не отметить, что существующие php-фреймворки (не все, но многие) обладают явно излишней функциональность и объёмом (шутка ли — дистрибутивы до 30Мб!). Например в CodeIgniter 4 есть замечательный модуль Honeypot, который даже непонятен по своему назначению. Или Format, который предназначен для форматированного вывода JSON или XML-данных. Потребность в таких вещах равна нулю, но команды разработчиков продолжает «пилить» такой код, в надежде «а вдруг кому-то когда-то пригодится!». :-)

Собственно, здесь мы подходим к мысли, что архитектура веб-проекта должна базироваться на чём-то значительно более мелком — микро-фреймворке. Главное его назначение в том, чтобы предоставить лишь только тот код, который будет использоваться в самом микрофрейворке. При этом он должен быть 100% рабочим и самодостаточным, где без труда можно вывести «Hello, World!».

Какова должна быть минимальная функциональность микро-фреймворка?

Конечно же, это будет зависеть от самого веб-проекта, возможно какой-то модуль может оказаться критически важным, но если говорить о чём-то «типовом», то потребуется решить всего несколько задач.

  • организация каталогов
  • базовый автозагрузчик классов
  • поддержка Composer'а
  • поддержка (файлов) конфигурации
  • единая точка входа для сайта и приложения

Такой фреймворк не должен принуждать к какому-то строгому паттерну проектирования. По сути он даже ничего и не должен об этом знать: это уже часть приложения, а не фреймворка. Рассмотрим простой абстрактный пример такого микрофреймворка.

Единая точка входа

Здесь всё достаточно просто: корневой index.php. Этот файл — эдакий диспетчер, который формирует лишь начальные константы для путей, а дальше уже подключает начальное ядро фреймворка.

define('BASE_DIR', __DIR__  . '/');
define('APP_DIR',  BASE_DIR . 'app/');
define('SYS_DIR',  BASE_DIR . 'system/');
 
require SYS_DIR . 'core/bootstrap.php';

Из приведенного кода видно, что само приложение будет располагаться в каталоге app, а фреймворк в system. Такое деление полезно тем, что позволяет легко обновлять код фреймворка, не затрагивая приложение. При этом, мы можем совершено произвольно переименовать эти каталоги или поменять их расположение.

«Точка подключения» фреймворка

Файл system/core/bootstrap.php является первым, где можно определить другие константы, базовые функции и т.д. Прежде чем регистрировать автозагрузчик, нужно решить где будут располагаться основные модули фреймворка. Для простоты, будем придерживаться модульной структуры, описанной в прошлой статье. Получится например так:

system/
    
    core/ — «ядро» микрофреймворка
        bootstrap.php — «точка подключения» 
        
    lib/ — дистрибутивы/библиотеки (вне Composer-а)
        Lib1/
        Lib2/
        ...
        autoload.php — автозагрузчик «classmap» для неPSR-4 библиотек
        
    module/ — рабочие модули фреймворка PSR-4 (используют lib и vendor)
        module1/
        module2/
        ...
        
    vendor/ — файлы Composer-а
        autoload.php

Сам по себе микрофреймворк не будет использовать ни Composer, ни module, ни lib, но «full»-фреймворку они скорее всего потребуются. Поэтому мы заранее определяем их расположение. От этого будет зависеть код автозагрузчика (вы его найдёте в готовых файлах к статье).

Разделение фреймворка и приложения

Теперь нужно определить что должно быть в каталоге приложения app. С моей точки зрения, этот каталог должен быть как можно «свободней», чтобы не ограничивать веб-разработчика. Например, если предполагается админ-панель, то она может быть в app/admin. Излишняя строгость со стороны фреймворка создаёт совершенно бесмысленное усложнение каталогов. Например в CodeIgniter волей-неволей, приходится использовать жёстко заданные controllers и views, что в итоге меня вынудило использовать отдельный каталог maxsite для файлов MaxSite CMS.

Сложность в том, что фреймворк (уровень «system») должен как-то использовать файлы приложения (уровень «app»), чтобы корректно выполнить свою работу (например подключить БД).

Поэтому должен быть специальный каталог в app о котором будет знать фреймворк. Например app/framework. В этом каталоге могут размещаться файлы конфигураций, роутинга и т.п.

app/
    framework/
        config/
            config.php — файл конфигурации
            ...
    
    ... прочие каталоги приложения 
    
    app.php — «точка входа» приложения

В system/core/bootstrap.php будет происходить подключение файлов конфигурации приложения, а в конце происходит переход к файлу app/app.php. Важный момент здесь в том, что существует явная цепочка подключаемых файлов:

  1. index.php
  2. system/core/bootstrap.php
  3. app/app.php

Здесь нет скрытой подгрузки еще десятка файлов, которые обычно сопровождают загрузку фреймворка. Если же нужно обеспечить такую загрузку, то классы явно указываются в конфигурации приложения (config.php).

Понятно, что файлов конфигураций может быть сколько угодно.

Файл app/app.php нужен для того, чтобы на уровне приложения можно было бы реализовать его произвольную логику. Фреймворк ничего не должен знать о том, как устроенно веб-приложение. Это может быть примитивная landing page, сложная CMS или вообще backend под какое-то API. После того, как управление перешло к этому файлу, вся работа целиком ложится на приложение.

Роутинг

Роутинг — неотъемлемая часть любого веб-проекта. Поэтому почти все фреймворки предлагают какой-то свой готовый вариант, которому приходится следовать. Но, на самом деле, роутинг — это уровень приложения. Эта «горькая истина» обычно постигается когда уже поздно что-то менять в проекте. На роутинг завязывается ЧПУ (хотя бы в виде .htaccess) и, если мы говорим о каком-то универсальном варианте, то они должны комплексно настраиваться на уровне самого приложения, поскольку потребности и задачи у всех совершенно разные.

Более того, ЧПУ может в конечном итоге настраиваться самим пользователем.

Многие фреймворки не просто «навязывают» свой роутинг, но и не могут сами без его работать. Просто приведу пару примеров, чтобы было понятно о чём речь. Например: Slim, Flight, Fat-Free Framework и другие. Такие фреймворки практически не годятся для проектов с произвольными адресами, особенно теми, которые назначает конечный пользователь. Такой типичный подход реализован в стареньком фреймворке с забавным названием Limonade, где всё крутится возле функции dispatch. То есть просто не получится сделать ни единой страницы приложения, пока не будет прописаны правила роутинга.

Когда правила формируются на уровне самого приложения, то нет никаких проблем — ваше приложение и вам решать какие будут адреса. Но если это уровень фреймворка, то придётся строго следовать его идеологии и загонять себя в жёсткие рамки.

Модули приложения

На уровне «system» мы предусмотрели расположение модулей, библиотек и файлов Composer'а. Аналогично нужно сделать и для «app», для тех случаев, когда нужно «расширить» возможности фреймворка. Лучшее место, конечно же app/framework. В этом случае фреймворк самостоятельно организует автозагрузку классов, причём можно сделать даже с приоритетом приложения. Тогда можно будет использовать «перезагрузку» system-классов.

Функции микрофреймворка

Как правило фреймворки содержат множество готовых для использования функций. Речь именно о функциях, а не php-классах. В CodeIgniter есть даже отдельный вид — хелперы (helpers). Кажется, что это круто, но на самом деле такая практика порочна, поскольку не просто бессмысленно раздувает код, ни и часто дублирует то, что уже есть в PHP. Если стоит задача получить какой-то функционал, то он должен быть оформлен именно как модуль. Для простоты можно вообще использовать статические методы. «Голые» функции нужны на уровне приложения, поскольку с ними проще работать. Например при выводе html-кода в шаблоне удобней простая функция, чем класс — это и меньше кода и более высокая читабельность. Но вот на уровне самого фреймворка функции не стоит создавать, или ограничиться лишь теми, которые использует сам фреймворк.

То есть не нужно создавать лишних сущностей.

Уровень «public»

Некоторые фреймворки предлагают размещать «system» и «app» на уровень выше чем каталог «public_html», что делает их недоступными при прямом url-обращении. С точки зрения безопасности, наверное такое решение может использоваться, но на практике оно не очень удобно и скорее всего бессмысленно.

Проблема ещё в том, что веб-приложение будет содержать файлы, которые должны быть доступны по прямой ссылке: изображения, css-стили, шрифты и js-скрипты. В таком случае приходится решать где именно должны располагаться такие файлы.

Создаётся впечатление, что разработчики фреймворков просто не в курсе, что современный WEB — это и возможность быстрой смены шаблона (читай дизайна и функциональности), то есть то, что реализуется в любой CMS. Когда мы начинаем использовать уровень «не-public» возникает довольно серьёзная проблема разделения шаблона на такие же уровни. Решить её, естественно, можно, только можно и не создавать проблему на ровном месте. :-)

Что в итоге

В целом получается такая структура:

app/
    framework/
        config/
            config.php
 
        lib/        
        module/
        vendor/
        
    app.php
        
system/    
    core/
        bootstrap.php
        
    lib/
        ...
        
    module/
        ...
        
    vendor/
        autoload.php
 
index.php
.htaccess

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

Естественно данный «микрофреймворк» играет больше демонстрационную роль, но всё равно хорошо показывает слабую зависимость между приложением и фреймворком. При этом сохраняется возможность расширения самого фреймворка новыми модулями и библиотеками без изменения приложения. На базе такой архитектуры можно решать самые разные практические задачи.

Комментариев: 2 RSS

1Ярик29-06-2019 18:17

https://github.com/bcosca/fatfree#named-routes это разве не то, в отсутствии чего ты обвиняешь Fat-Free? также у них есть какой-то плагин Access Route access control for the PHP Fat-Free Framework

This plugin for Fat-Free Framework helps you control access to your routes.

2MAX29-06-2019 18:53

Это — именованные правила роутинга. F3 не может работать без роутинга в принципе. Он прямо во фронт-контролере (корневой index.php) жестко прошит. Суть моих «обвинений» в том, что роутинг должен быть на уровне приложения, а не как база всего фреймворка. Это можно сравнить и с жестким требованием подключения БД. Например если есть страницы, где база вообще не нужна, но фреймворк всё равно требует подключения и сам подключает базу для всех страниц. С роутингом тоже самое. Должна быть возможность использовать произвольный вариант на уровне приложения.

Оставьте комментарий!

Комментарий будет опубликован после проверки. Вы соглашаетесь с правилами сайта.

(обязательно)