Эффективная архитектура PHP-приложения
23-03-2024Reading time ~ 8 min.Albireo Framework / CMS 923
Изначально я хотел просто использовать Albireo Framework за основу Albireo CMS, но в процессе поставил перед собой задачи, для которых структура фрейворка не подходила для CMS. Хотя, вообще, если заглянуть намного назад, то был (в общем-то и есть) у меня проект, где я хотел написать свой полноценный PHP-фреймворк, наподобии CodeIgniter, только более гибкий. Разрабатывал я его в свободное время несколько лет и постепенно вырисовывалась очень приятственная картина. Дальше 18+.
Этот php-фреймворк построен по всем канонам ООП. Его роутинг спокойно работает с классами, функциями, файлами. Более того, можно сделать так, чтобы просто в роутинге указать модуль, и тот сам посмотрит какие ротуты в этом модуле используются. То есть можно без проблем создать полностью переносимый (между сайтами) модуль. Для тестирования с сделал todo-модуль, форму простую, форму регистрации, форму авторизации. Всё, есно, рабочее.
Там есть хуки, миграции, парсинги, база через PDO, безопасность, валидация форм и любых данных, полная документация и демо-сайтик. Ах, да ещё и админка с кучей наворотов. В общем, красота, пиздец, можно гордиться!
Свернул я этот проект в итоге.
Проблемка оказалась в том, что я слишком много насмотрелся других фреймворков, где много голимой теории, но мало практики. Нахера вообще нужен фреймворк? Правильно, для того, чтобы делать сайт. И вот если взять тот же CodeIgniter, то он даёт кучу php-кода, но не даёт возможности быстро построить сайт.
Парадокс вот в чём.
Сделать сайт на PHP может любой дурак. Текстовый файл с расширением php и лепи в нём что хочешь. Зачем для сайта вообще существующие php-фреймворки, если на них невозможно просто поднять примитивное hello world?
Свой php-фреймворк я делал из расчёта, что к нему можно будет прикрутить любой шаблон. Ну там шапку, подвал, дизайн какой-нибудь. Архитектурная проблема в том, что шаблон вывода это не что иное, как вьюшка в модели MVC и управлять этой вьюшкой нужно на уровне самого модуля. То есть нельзя просто так раскидать html и php-код в разных частях приложения. Если в модуле нужно поменять css-класс кнопки, то всё, нужно лезть в view. Жопа.
Хрен знает, сколько я перелопатил чужих фреймворков. Всё, что я хотел понять, так это как мне сделать пару страниц, которыми можно управлять с помощью пары опций. Я вообще молчу про админку, поскольку её просто нет в любом фреймворке. А если и есть, что это совершенно отдельные проекты. С ними головняка не оберешься.
Ещё я хотел, чтобы можно было запустить страницу как html-файл без подключения к базе. Выяснилось, что нет таких фреймворков. Тупо нет. Все по умолчанию конектятся к майсэквел.
Потом я изучал Flat File CMS — те которые на файлах. Но что меня удивляет, так это то, что если система на файлах, то в ней не предусмотрена работа с базой, то есть обратная ситуация. Почему, может язык PHP не позволяет?
В итоге я составил ТЗ для будущей CMS, но потом потратил много времени на его переписывание. Я понял, что существующие фреймворки элементарно не предназначены для простого использования. Их авторы делают всё, чтобы создать кучу зависимостей и раскидать файлы по непонятным каталогам. Но самое главное это то, что авторы фреймворков не делают сайты. Просто нет опыта работы с клиентами. Я бы посмотрел, как авторы ларавеля, кодегнайтера или симфони сделают на своих детищах сайт-лендинг из двух страниц, где одна — форма обратной связи. Ну и всё это на обычном шаред-хостинге.
Короче, суть в том, что строить приложение нужно от конечных задач пользователя. То есть вся та мутотень со «стандартами» летит прямиком в помойку.
Есть админка и есть сам сайт. Связаны ли они между собой? Конечно, но с оговорками. Админка работает с файлами сайта и работает с базой. Сайт ничего не знает об админке, но работает с базой. Опять же работа с базой — только по необходимости.
Получаются три каталога (как пример): admin (для админки), website (для сайта), storage (для базы - пока речь об sqlite). То есть когда чел входит в admin, то работает админка. Замечу, что это каталог, а значит его можно легко менять на произвольный. Чтобы достучаться до баз и сайта, нужно вылезти на уровень выше. Технически это dirname()
.
Теперь вопрос будет ли у сайта и админки единое ядро. Да, делаем каталог system, где и будем держать код.
На практике, если пользователю нужна админка, он набирает адрес с admin, а если нужен сайт, то набирает адрес сайта, где корневой index.php подключает website.
Почему так? Всё от итоговых задач. Если нужно разделить сайт, админку, ядро и базы, то это отдельные каталоги. Это намного проще, чем пытаться разделять их функциональность в едином котле.
Из этого вырисовывается интересная возможность: по сути админка и сайт могут иметь одну и туже структуру и функциональность. Иметь свой шаблон вывода, дизайн, конфигурации, библиотеки и т.д. И всё это индивидуально. Если я захочу сделать ещё один сайт в подкаталоге домена, то всё, что нужно сделать, так это создать каталог и повторить структуру admin/website.
Исходя из этого определяем точку входа в приложение. По сути это обычный index.php в admin/website (он одинаковый для каждого каталога).
Что нужно делать в этом файле? Здесь всё будет зависеть от логики приложения, но в общем первый этап будет следующим:
- Вначале подключаем ядро. Это функции из _system.
- Потом выставляем константы приложения.
- Если нужно, подключаем автолоад композера.
- Выполняем функцию автолоада psr4 -
spl_autoload_register()
.
Следующий этап завязывается на роутинг. У меня в Albireo CMS — это файлы, поэтому происходит считывание всех файлов, их анализ на нужные данные, но если речь чем-то другом, то вначале нужно получить текущий URL и раскидать его в массив и сохранить в общем хранилище. (Пока все технические детали опускаю.)
Почему нужно получить текущий URL? Все адреса должны строиться от адреса сайта. Это может быть домен, а может быть каталог домена. Это нужно уметь определять автоматом. При необходимости автоматом будет меняться и .htaccess, поскольку на него завязано ЧПУ.
И вот дальше нужно запустить роутинг в виде функции, которая найдёт то, что нужно подключить для этого URL. У меня это файл, но это может быть функция или класс, или метод класса.
Теперь смотрим с обратной стороны. Как пользователь будет определять адрес страниц сайта? У меня это сам файл (как и в Albireo Framework): в файле указывается поле slug оно и проверяется в роутинге. Если бы я расширил роутинг на базу, то вначале проверял бы именно файлы, а если файла нет, то уже тогда подключил бы базу и проверил в нужной таблице поле slug. В любом случае роутинг находит соответствие и сохраняет его в общем хранилище. Само выполнение файла или данных из базы на уровне роутинга не происходит.
И вот уже после того, как все данные собраны, вызывается функция вывода. Именно в ней и выполняется вся работа по непосредственному выводу или делегируется вывод в функцию, определенную роутингом.
Таким образом точка входа представляет собой последовательность выполнения нескольких функций. В моём случае это примерно так:
require_once SYS_DIR . 'load_functions.php'; // функции ядра setBaseConstants(); // устанавливаем базовые константы if (file_exists(COMPOSER_DIR . 'autoload.php')) require_once COMPOSER_DIR . 'autoload.php'; psr4Autoload(); // PSR4 - autoload getSessionId(); // запуск механизма сессий createHtaccess(); // create .htaccess readPages(); // считать данные всех pages getCurrentUrl(); // получить данные по текущему URL matchUrlPage(); // подключаемый файл страницы pageOut(); // output
Такой подход делает работу приложения очевидной. Нахуй скрытые подключения! В других фреймворках логика совершенно другая — в них ядро подключается скрыто за счёт psr4 и никто не может понять какова последовательность выполнения кода и, как следствие, каков алгоритм работы приложения. Здесь же есть возможность не только понять как работает система, но и нормально его оттестировать на каждом этапе.
Как устроен каталог сайта (ну или админки, не важно)? Как правило, есть постоянные задачи, поэтому доступ для их решения должен быть простым.
Конфиги храним в config. Причём конфиг-файлы это обычные php-файлы, которые возвращают данные по return. Не нужно использовать вcякое говно, вроде ini, yaml, json, toml и т.п. В PHP есть свой формат, который ничем не уступает. Вот им и пользуемся. И это сразу исключает потребность в функциях или библиотеках, которые работают с этим говном.
Дальше, шаблоны с дизайнами храним в templates, где каждый шаблон в отдельном подкаталоге. По шаблонам потом напишу.
Есть каталог service, в котором всякое-такое, что не нужно держать на виду. Например файлы для инсталяции, кэши, бэкапы, а также собственные psr4-библиотеки.
Остальное на свой вкус. У меня есть events (что-то вроде хуков), widgets для виджетов и snippets для снипетов. Просто потому что так удобно. Не нужно всё прятать. Данные у меня хранятся в pages, потому что всё в файлах страниц.
Теперь смотрим со стороны пользователя. Он входит в основной каталог и ему сразу видны доступные возможности. Не где-то на задворках, а всё рядышком, но аккуратно расфасовано.
Как устроено ядро system? Удивительно просто. Есть каталог functions, где хранятся файлы функций. Причём (за редким исключением) каждая функция хранится в отдельном файле. Я к такой схеме пришёл не сразу, но пиздец, как это оказалось удобно! Все функции подключаются автоматом в load_functions.php через glob и foreach. То есть чтобы добавить новую функцию нужно просто сделать её файл и он автоматом подключится.
Каталог psr4 для классов. Тут стандартно работаем по namespace.
Прочие каталоги зависят от функциональности системы. У меня есть parsers, поскольку они могут подключаться отдельно для каждой страницы. Так что им нужно отдельно кучковаться.
Все остальные каталоги произвольно на усмотрение пользователя. Например есть uploads в корне, просто потому что так удобней и привычней с ним работать.
Такой подход решает сразу несколько задач. Однотипные каталоги сайтов. Всё красиво сгруппировано по функциональности. Обновление ядра не затрагивает данные и каталоги сайтов. Конфиги индивидуальны. Базы — отдельно и легко обслуживаются. Большие файлы в uploads — легко синхронизируются по ftp. Расширение функциональности в пределах каждого сайта индивидуально.
Ну и самое главное — это не тупой фреймворк, а полноценное php-приложение.
Слава Украине! Смерть рашистам!
Друже, я ні на чому не наполягаю, але сам трохи ходив цими граблями.
І от до чого прийшов: NodeJS має вже вбудований сервер, тобто не треба Apache, все з коробки працює. А хостинг дешевий знайти так само легко, як і для PHP. Простенький проект на Express ти зможеш написати за кілька годін/днів (залежить від підготовки, наявного часу, складності проекту, тощо). При чому, можеш як БД заюзати, так і статичні файли (мій особистий вибір: Markdown) - хоч окремо, хоч одночасно. На додачу, раджу робити окремо API, і окремо фронт: React або View доволі прості для розуміння. А на перспективу -- TypeScript і NestJS.
Перейшов, і не шкодую.
PS: PHP досі застосовую, в т.ч. веду проекти на WordPress, але все більше люблю власне підхід Headless WP
Окреме "дякую" за роз'яснення про каталог functions, бо в Альбірео я користувався виключно snippets для таких цілей, додаючи потрібне в pages, які інколи виглядали як 2-3 строки з підключенням потрібних сніпетів. Робив ще варіанти зі зміною шаблона сторінки, навіть змішані є (шаблон + сніпети), залежно від потрібного. Доволі гнучка система в цьому плані.