Статика
У ProfileService появился фронтенд. HTML, CSS, скрипты, аватарки — обычное дело. Раньше их раздавал nginx, но nginx мы торжественно проводили в первой главе. Кто теперь?
Первый порыв понятен: написать обработчик, который открывает файлы по URL. Остановитесь. Прикиньте, что это значит: тысячи запросов к логотипу в день, и на каждый — корутина, вход в PHP, fopen, выход. PHP здесь не делает ничего, что требовало бы PHP.
Сервер решает это радикально. Статический маршрут обслуживается целиком в C. Запрос к нему в PHP не заходит вообще:
use TrueAsync\StaticHandler;
$server->addStaticHandler(
new StaticHandler('/assets/', '/var/www/profile/public')
);Префикс URL, каталог на диске, готово. GET /assets/css/app.css превращается в асинхронное чтение файла прямо в сокет, через libuv, мимо PHP. Обработчики из прошлых глав продолжают получать всё остальное. Маунтов может быть несколько, совпадения ищутся в порядке регистрации. Весь HTTP-этикет из sendFile здесь тоже есть: Content-Type, ETag с 304, докачка по Range.
Настройка маунта
StaticHandler настраивается цепочкой, до подключения к серверу:
$static = (new StaticHandler('/assets/', '/var/www/profile/public'))
->setCacheControl('public, max-age=86400')
->enablePrecompressed('br', 'zstd', 'gzip')
->hide('*.map', 'drafts/**')
->setOnMissing(StaticOnMissing::NEXT);
$server->addStaticHandler($static);Пройдёмся по строчкам.
setCacheControl — заголовок кэширования на каждый ответ. В паре со включённым по умолчанию ETag браузер скачивает файл заново только тогда, когда файл действительно изменился.
enablePrecompressed — мой любимый пункт. Если рядом с app.css лежит app.css.br, клиенту с подходящим Accept-Encoding уйдёт готовый сжатый файл. Вдумайтесь в экономику: сжали один раз на этапе сборки фронтенда, самым дорогим и качественным уровнем, а раздаёте миллион раз, не тратя на компрессию ни такта.
hide — глобы, которые получают 404 независимо от того, существует файл или нет. Карты сорцов и черновики наружу не пойдут.
setOnMissing(NEXT) — судьба запросов мимо файлов. По умолчанию промах отвечает 404 прямо из C. NEXT вместо этого передаёт запрос дальше, в обычный PHP-обработчик. Зачем? SPA. Файл /assets/app.js отдаётся с диска, а несуществующий /assets/whatever проваливается в приложение, которое ответит своим index.html.
После addStaticHandler объект запирается: сервер уже построил по нему структуры для горячего пути. Попытка дёрнуть сеттер после этого — исключение.
Безопасность по умолчанию
Небольшое лирическое отступление. Отдача файлов по URL — исторически одна из самых урожайных дыр веб-серверов. ../../etc/passwd в адресной строке — приём старше многих читателей этой главы.
Поэтому политика из коробки параноидальная. Запросы с .. — 404. Пути через файлы с точкой в начале — 404: ни .env, ни .git не утекут, даже если случайно окажутся в каталоге. Символические ссылки не разыменовываются вовсе: файл должен физически лежать внутри корня маунта, и никакой симлинк наружу его не вытащит.
Всё это ослабляется осознанно (setDotfilePolicy, setSymlinkPolicy), но умолчания подобраны так, чтобы вариант «подключил и забыл» был безопасным.
Когда файлов очень много
Для горячих маунтов есть ещё один рычаг:
$static->setOpenFileCache(maxEntries: 1024, ttlSeconds: 60);Кэш запоминает разрешённый путь, метаданные и заголовки последних файлов и срезает syscall-прогулку на повторных запросах. На большом каталоге или сетевой файловой системе — ощутимо. На маленьком локальном сайте — нет, потому и выключен по умолчанию.
Вот и вся глава, я обещал короткую. Статика летит мимо PHP, PHP занимается делом. И вот теперь, когда у сервиса есть лицо, пора его оживить. Помните полоску прогресса из самой первой главы первой серии, ту, что рисовалась в терминале? Она сейчас переедет в браузер. И рисовать её будет сам сервер.