Почему я отказался от Laravel и собрал свой шаблон для простых сайтов
от aNNiMON
PHP/MySQL
php, фреймворки, framework, framework-less, router, template engine, шаблонизатор, twig, fastroute
Для своей персональной странички некоторое время я использовал Laravel. Было чувство, что я стреляю из пушки по воробьям, потому что огромный пласт функций этого фреймворка у меня не использовался. На сайте нет форм, подключение к базе данных используется лишь для парочки CRUD'ов, не нужны ни сессии, ни валидаторы, ни логирование, ни ORM.
Поначалу я хотел всё оставить как есть, с "заделом на будущее", но прошло два года, а будущее всё никак не наступало. Тогда-то я и задался вопросом о смене фреймворка на что-то простое.
Вот так выглядит упрощённая структура веб-приложения. Если из Laravel выкинуть те части, которые на сайте не используются, и упростить остальные компоненты, то получится точно такая же диаграмма. Проблема в моём случае заключается в том, что компоненты фреймворка не так просто убрать или заменить, но, поскольку php богат библиотеками на все случаи жизни, можно собрать из них свой аналог Laravel.
Сторонние компоненты
В качестве роутера можно использовать FastRoute. Шаблонизатор можно взять вообще любой, который нравится. Мой выбор — Twig. Также будет полезен DI контейнер. Опять же выбор не ограничен, но я взял Pimple.
Теперь у нас в проекте три зависимости. Минусы: нужно будет потратить немного времени на изучение работы с ними. Плюсы: этих трёх зависимостей вполне достаточно, все они легковесные.
Файлы проекта
Предлагаю взять за основу структуру проектов на Laravel с некоторыми упрощениями:
В cache/ будут храниться кэш шаблонов и прочие временные файлы.
В public/ — статические ресурсы приложения.
В src/ будет php-код и файлы шаблонов. Там же можно хранить исходные файлы стилей, скриптов и даже иконок, а потом генерировать/транспилировать/минифицировать в public/.
В vendor/ как обычно автолоадер и библиотеки.
В app.php настройка (бутстрапинг) приложения и вызов соответствующего контроллера.
Настройка nginx/Apache
Первым делом настроим сервер, чтобы стартовой считалась директория public/.
Nginx:
Apache:
Теперь создадим public/index.php:
Он будет вызывать app.php, в котором будет бутстрапинг и настройка роутера. Но пока что можно просто проверить, что сервер правильно перенаправляет все запросы:
Если всё сделано верно, то при переходе на любой адрес сайта app.site будет выведено сообщение "It works!!!".
Установка зависимостей
Устанавливаем все три зависимости при помощи composer:
composer.json:
Бутстрапинг
Теперь подгрузим конфигурацию, настроим автолоадер, подключение к базе данных и включим отображение ошибок для режима отладки.
Начнём с конфигурации src/config.php:
Теперь настроим автолоадер, прочитаем конфиг и настроим подключение к базе данных:
База данных будет инициализироваться только при первом обращении к $container['db']. Для теста можно вывести данные из конфига и из БД:
Шаблонизатор
Twig настраивается очень легко, достаточно указать директорию с шаблонами, место для кэширования (не забудьте создать папку cache/views) и пробросить глобальный контейнер в шаблонизатор под глобальной переменной app.
Поскольку в конфигурации у нас есть возможность включать режим отладки, то можно добавить в Twig соответствующее расширение.
Теперь проверим работу шаблонизатора. Переносим вывод тестовых данных в шаблон src/views/index.twig:
Заполняем шаблон данными и рендерим:
Роутер
Наконец, настраиваем роутер. Добавим главную страницу и страницу о сайте. Также обработаем ситуацию, когда роут не найден, для этого покажем страницу 404.twig.
Полезно иметь базовый контроллер, на случай, если их понадобится несколько. src/Controllers/AbstractController.php:
Наконец, основной контроллер src/Controllers/AppController.php:
И шаблоны src/views/about.twig:
src/views/404.twig:
Разметка
Пришло время использовать Twig по назначению, создадим разметку src/views/layout/main.twig:
Внутри разметки есть блоки, которые можно переопределить и заполнить. Так код страницы будет проще понять.
src/views/index.twig:
JavaScript и CSS
В качестве бонуса покажу вариант с препроцессором SCSS и минификацией JS.
src/package.json:
Установим какой-нибудь CSS-фреймворк (я взял bootstrap) и dev-зависимость uglify-js для минификации js.
Подключим некоторые компоненты бутстрапа в src/scss/styles.scss:
Теперь можно преобразовать scss в минифицированный css командой
Либо вместе с минификацией js:
Как использовать шаблон
Чтобы не проходить все перечисленные шаги, можно воспользоваться проектом как шаблоном. Репозиторий: https://github.com/annimon-tutorials/php-unframework-template
Клонируем репо, устанавливаем зависимости и собираем стили и js. Останется только настроить сервер, воспользовавшись конфигами из раздела Настройка nginx/Apache.
Поначалу я хотел всё оставить как есть, с "заделом на будущее", но прошло два года, а будущее всё никак не наступало. Тогда-то я и задался вопросом о смене фреймворка на что-то простое.
Вот так выглядит упрощённая структура веб-приложения. Если из Laravel выкинуть те части, которые на сайте не используются, и упростить остальные компоненты, то получится точно такая же диаграмма. Проблема в моём случае заключается в том, что компоненты фреймворка не так просто убрать или заменить, но, поскольку php богат библиотеками на все случаи жизни, можно собрать из них свой аналог Laravel.
Содержание
Сторонние компоненты
В качестве роутера можно использовать FastRoute. Шаблонизатор можно взять вообще любой, который нравится. Мой выбор — Twig. Также будет полезен DI контейнер. Опять же выбор не ограничен, но я взял Pimple.
Теперь у нас в проекте три зависимости. Минусы: нужно будет потратить немного времени на изучение работы с ними. Плюсы: этих трёх зависимостей вполне достаточно, все они легковесные.
Файлы проекта
Предлагаю взять за основу структуру проектов на Laravel с некоторыми упрощениями:
- /app/
- - cache/
- - public/
- - dist/
- - css/
- - js/
- - icons/
- - index.php
- - src/
- - Controllers/
- - scss/
- - js/
- - views/
- - config.php
- - vendor/
- - app.php
- - composer.json
- - composer.lock
В public/ — статические ресурсы приложения.
В src/ будет php-код и файлы шаблонов. Там же можно хранить исходные файлы стилей, скриптов и даже иконок, а потом генерировать/транспилировать/минифицировать в public/.
В vendor/ как обычно автолоадер и библиотеки.
В app.php настройка (бутстрапинг) приложения и вызов соответствующего контроллера.
Настройка nginx/Apache
Первым делом настроим сервер, чтобы стартовой считалась директория public/.
Nginx:
- server {
- listen 80;
- listen [::]:80;
- listen 443 ssl;
- root /var/www/app/public;
- index index.php index.html index.htm;
- server_name app.site;
- autoindex off;
- location / {
- try_files $uri $uri/ /index.php$is_args$args;
- }
- location ~ \.php$ {
- include snippets/fastcgi-php.conf;
- fastcgi_pass unix:/var/run/php/php7.4-fpm.sock;
- }
- location ~* ^.+\.(svg|svgz|eot|otf|woff|jpg|jpeg|gif|png|ico)$ {
- access_log off;
- expires 30d;
- }
- }
Apache:
- <VirtualHost *:80>
- ServerName app.site
- ServerAdmin admin@app.site
- DocumentRoot /var/www/app/public
- <Directory /var/www/app>
- Options Indexes FollowSymLinks MultiViews
- AllowOverride None
- Require all granted
- </Directory>
- </VirtualHost>
Теперь создадим public/index.php:
- <?php
- require_once __DIR__ . '/../app.php';
- <?php
- echo 'It works!!!';
Установка зависимостей
Устанавливаем все три зависимости при помощи composer:
- composer require nikic/fast-route
- composer require pimple/pimple ~3.0
- composer require twig/twig ^3.0
composer.json:
- {
- "require": {
- "nikic/fast-route": "^1.3",
- "pimple/pimple": "~3.0",
- "twig/twig": "3.0"
- }
- }
Бутстрапинг
Теперь подгрузим конфигурацию, настроим автолоадер, подключение к базе данных и включим отображение ошибок для режима отладки.
Начнём с конфигурации src/config.php:
- <?php
- return [
- 'metadata' => [
- 'name' => 'App Site',
- 'message' => 'It Works!!!',
- 'copyright' => '© aNNiMON 2020'
- ],
- 'database' => [
- 'host' => '127.0.0.1',
- 'user' => 'root',
- 'pass' => '',
- 'name' => 'app'
- ],
- 'debug' => true
- ];
Теперь настроим автолоадер, прочитаем конфиг и настроим подключение к базе данных:
- <?php
- use Pimple\Container;
- const SRC_DIR = __DIR__ . '/src/';
- // Autoloader
- require 'vendor/autoload.php';
- spl_autoload_register(function ($class) {
- include SRC_DIR . str_replace('\\', DIRECTORY_SEPARATOR, $class) . '.php';
- });
- // Container
- $container = new Container();
- $container['config'] = require SRC_DIR . 'config.php';
- $container['db'] = function($c) {
- $db = $c['config']['database'];
- $url = 'mysql:host=' . $db['host'] . ';dbname=' . $db['name'] . ';charset=utf8mb4';
- return new PDO($url, $db['user'], $db['pass'], [
- PDO::ATTR_ERRMODE => PDO::ERRMODE_WARNING,
- PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC
- ]);
- };
- if ($container['config']['debug']) {
- error_reporting(E_ALL);
- ini_set('display_errors', '1');
- }
База данных будет инициализироваться только при первом обращении к $container['db']. Для теста можно вывести данные из конфига и из БД:
- $meta = $container['config']['metadata'];
- echo '<b>Name</b>: ' . $meta['name'];
- echo '<br/><b>Message</b>: ' .$meta['message'];
- echo '<br/><b>Copyright</b>: ' .$meta['copyright'];
- $user = $container['db']
- ->query('SELECT * FROM users WHERE id = 1')
- ->fetch();
- echo '<br/><b>Username</b>: ' .$user['name'];
Шаблонизатор
Twig настраивается очень легко, достаточно указать директорию с шаблонами, место для кэширования (не забудьте создать папку cache/views) и пробросить глобальный контейнер в шаблонизатор под глобальной переменной app.
- use Twig\Environment;
- use Twig\Extension\DebugExtension;
- use Twig\Loader\FilesystemLoader;
- $container['twig'] = function ($c) {
- $loader = new FilesystemLoader(SRC_DIR . 'views');
- $twig = new Environment($loader, [
- 'cache' => __DIR__ . '/cache/views',
- 'auto_reload' => true,
- 'debug' => $c['config']['debug'],
- ]);
- $twig->addGlobal('app', $c);
- if ($c['config']['debug']) {
- $twig->addExtension(new DebugExtension());
- }
- return $twig;
- };
Поскольку в конфигурации у нас есть возможность включать режим отладки, то можно добавить в Twig соответствующее расширение.
Теперь проверим работу шаблонизатора. Переносим вывод тестовых данных в шаблон src/views/index.twig:
- {% set meta = app.config.metadata %}
- <b>Name</b> {{ meta.name }}<br/>
- <b>Message</b>: {{ meta.message }}<br/>
- <b>Copyright</b>: {{ meta.copyright }}<br/>
- <b>Username</b>: {{ user.name }}<br/>
- <b>User</b>: <pre>{{ dump(user) }}</pre>
Заполняем шаблон данными и рендерим:
- $user = $container['db']
- ->query('SELECT * FROM users WHERE id = 1')
- ->fetch();
- echo $container['twig']->render('index.twig', ['user' => $user]);
Роутер
Наконец, настраиваем роутер. Добавим главную страницу и страницу о сайте. Также обработаем ситуацию, когда роут не найден, для этого покажем страницу 404.twig.
- use Controllers\AbstractController;
- use Controllers\AppController;
- use FastRoute\Dispatcher;
- use FastRoute\RouteCollector;
- $dispatcher = \FastRoute\simpleDispatcher(function(RouteCollector $r) {
- $r->addRoute('GET', '/', [AppController::class, 'index']);
- $r->addRoute('GET', '/about[/]', [AppController::class, 'about']);
- });
- $uri = $_SERVER['REQUEST_URI'];
- $pos = strpos($uri, '?');
- if ($pos !== false) {
- $uri = substr($uri, 0, $pos);
- }
- $uri = rawurldecode($uri);
- $routeInfo = $dispatcher->dispatch($_SERVER['REQUEST_METHOD'], $uri);
- switch ($routeInfo[0]) {
- case Dispatcher::NOT_FOUND:
- http_response_code(404);
- echo $container['twig']->render('404.twig');
- break;
- case Dispatcher::FOUND:
- [$class, $method] = $routeInfo[1];
- /** @var AbstractController $instance */
- $instance = new $class;
- $instance->setContainer($container);
- call_user_func_array([$instance, $method], [$routeInfo[2]]);
- break;
- }
Полезно иметь базовый контроллер, на случай, если их понадобится несколько. src/Controllers/AbstractController.php:
- <?php
- namespace Controllers;
- use Pimple\Container;
- use Twig\Environment;
- class AbstractController {
- protected Container $container;
- public function setContainer(Container $container) : void {
- $this->container = $container;
- }
- protected function db() : PDO {
- return $this->container['db'];
- }
- protected function twig() : Environment {
- return $this->container['twig'];
- }
- protected function render(string $view, array $data = []) : void {
- echo $this->twig()->render($view, $data);
- }
- }
Наконец, основной контроллер src/Controllers/AppController.php:
- <?php
- namespace Controllers;
- class AppController extends AbstractController {
- public function index() {
- $user = $this->db()
- ->query('SELECT * FROM users WHERE id = 1')
- ->fetch();
- $this->render('index.twig', ['user' => $user]);
- }
- public function about() {
- $this->render('about.twig');
- }
- }
И шаблоны src/views/about.twig:
- {% set meta = app.config.metadata %}
- <h2>About page</h2>
- <b>Name</b> {{ meta.name }}<br/>
- <b>Copyright</b>: {{ meta.copyright }}<br/>
src/views/404.twig:
- <h2>404</h2>
- Sorry, the page you are looking for could not be found.
Разметка
Пришло время использовать Twig по назначению, создадим разметку src/views/layout/main.twig:
- {% set meta = app.config.metadata %}
- <!DOCTYPE html>
- <html lang="en">
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
- <meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1, user-scalable=yes" />
- <link rel="stylesheet" href="/dist/css/styles.css" />
- <title>{% block title %}{% endblock %} | {{ meta.name }}</title>
- {% block head %}{% endblock %}
- </head>
- <body>
- <div class="container">
- {% block content %}{% endblock %}
- <div class="footer p-1 bg-light text-muted">{{ meta.copyright }} {{ "now"|date('Y') }}</div>
- </div>
- {% block scripts %}
- <script src="/dist/js/app.js"></script>
- {% endblock %}
- </body>
- </html>
src/views/index.twig:
- {% extends "layout/main.twig" %}
- {% block title %}Index{% endblock %}
- {% block content %}
- <nav class="nav navbar-light bg-light">
- <a class="nav-link active" href="/">{{ meta.name}}</a>
- <a class="nav-link" href="/about/">About</a>
- </nav>
- <main class="col-md-8 py-md-2 pl-md-3 bd-content">
- <b>Name</b>: {{ meta.name }}<br/>
- <b>Message</b>: {{ meta.message }}<br/>
- <b>Copyright</b>: {{ meta.copyright }}<br/>
- <b>Username</b>: {{ user.name }}<br/>
- <b>User</b>: <pre>{{ dump(user) }}</pre>
- </main>
- {% endblock %}
JavaScript и CSS
В качестве бонуса покажу вариант с препроцессором SCSS и минификацией JS.
src/package.json:
- {
- "name": "app-site",
- "version": "1.0.0",
- "description": "",
- "main": "index.js",
- "dependencies": {
- "bootstrap": "^4.5.3"
- },
- "devDependencies": {
- "uglify-js": "^3.12.0"
- },
- "scripts": {
- "test": "echo \"Error: no test specified\" && exit 1",
- "scss": "sass --no-source-map --style=compressed scss/styles.scss:../public/dist/css/styles.css",
- "uglify": "uglifyjs js/*.js -c -m -o ../public/dist/js/app.js",
- "dist": "npm run scss && npm run uglify"
- },
- "author": "aNNiMON",
- "license": "MIT"
- }
Установим какой-нибудь CSS-фреймворк (я взял bootstrap) и dev-зависимость uglify-js для минификации js.
- cd src && npm install
Подключим некоторые компоненты бутстрапа в src/scss/styles.scss:
- @import "../node_modules/bootstrap/scss/functions";
- @import "../node_modules/bootstrap/scss/variables";
- @import "../node_modules/bootstrap/scss/mixins";
- @import "../node_modules/bootstrap/scss/reboot";
- @import "../node_modules/bootstrap/scss/type";
- @import "../node_modules/bootstrap/scss/alert";
- @import "../node_modules/bootstrap/scss/card";
- @import "../node_modules/bootstrap/scss/grid";
- @import "../node_modules/bootstrap/scss/nav";
- @import "../node_modules/bootstrap/scss/utilities";
Теперь можно преобразовать scss в минифицированный css командой
- npm run scss
- npm run dist
Как использовать шаблон
Чтобы не проходить все перечисленные шаги, можно воспользоваться проектом как шаблоном. Репозиторий: https://github.com/annimon-tutorials/php-unframework-template
- git clone https://github.com/annimon-tutorials/php-unframework-template.git my-app
- cd my-app
- composer install
- chmod -R 777 cache
- cd src && npm install
- npm run dist
Клонируем репо, устанавливаем зависимости и собираем стили и js. Останется только настроить сервер, воспользовавшись конфигами из раздела Настройка nginx/Apache.