Шаблон на Laravel + FrankenPHP

от автора

Всех приветствую, меня зовут Денис, я PHP разработчик

Я выступаю на хакатонах за команду «жыбийрыр» — https://жыбийрыр.рф/ и у нас была проблема, что не было готового шаблона, с которым мы могли спокойно выступать и заново не писать один и тот же код

Эта статья будет посвящена тому, как я писал этот шаблон, с какими ошибками столкнулся и в целом есть больше желание, поделиться проделанной работой

Хочу отметить, что это решение не эталонное и буду ждать в комментариях конструктивной критики

репозиторий шаблона

мой github

телеграм для связи: @Deniskorbakov

Что есть в готовом шаблоне:

  • Настроеный многопоточный сервер FrankenPHP

  • Поднятно окружение в докере + multi stage под локально и прод

  • Админка

  • Апи документация проекта

  • Мониторинг системы

  • Логика базовой авторизацию — верификации по почте

  • Веб сокет сервер

  • Настроен pipline — GitHub Actions

Почему был выбран FrankenPHP:

Было желание попробовать многопоточный сервер, поэтому выбор пал на RoadRunner и FrankenPHP

FrankenPHP был выбран вместо RoadRunner, потому что его легче развернуть и во многих бенчмарках он не уступает RR

Стек технологий:

  • Laravel 12

  • FrankenPHP

  • Docker/Docker-compose

  • Redis

  • PostgreSQL

  • Laravel Reverb — вебсокет сервер

  • Horizon — овбертка для очередей

  • PhpStan/PhpCodesniffer/Rector — стат анализаторы

  • Filament — админка

  • Beszel — легковесный мониторинг

  • Scribe — апи документация

  • Traefik

Давайте начнём с настроеного сервера + docker:

Сервер настроен с помощью Laravel Octane + FrankenPHP

Laravel Octane — это провайдер для обслуживания серверов на RR, Swoole, Franken

так же у laravel Octane хорошая документация по каждому серверу + сказано как развернуть это всё дело на проде

Покажу Dockerfile который в итоге получился

FROM dunglas/frankenphp:1.4 AS base  RUN apt-get update \     && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \     git \     unzip \     librabbitmq-dev \     libpq-dev \     supervisor  RUN install-php-extensions \     gd \     pcntl \     opcache \     pdo \     pdo_pgsql \     pgsql \     redis \     zip  WORKDIR /app  COPY --from=composer:2.8 /usr/bin/composer /usr/local/bin/composer  COPY --from=node:23 /usr/local/lib/node_modules /usr/local/lib/node_modules COPY --from=node:23 /usr/local/bin/node /usr/local/bin/node  RUN ln -s /usr/local/lib/node_modules/npm/bin/npm-cli.js /usr/local/bin/npm  FROM base AS dev  COPY ./.docker/supervisor/supervisord.dev.conf /etc/supervisor/conf.d/supervisord.conf  CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]  FROM base AS prod  COPY ./.docker/supervisor/supervisord.prod.conf /etc/supervisor/conf.d/supervisord.conf  CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/supervisord.conf"]

Этот докер файл небольшой, из интересного что в будущем его можно расшить благодаря multi-stage для локальной разработки, так и для прода

Запуск сервера, очередей, вебсокета, крон задач происходит через supervisor

Конфиг:

[supervisord] user=root nodaemon=true logfile=/dev/stdout logfile_maxbytes=0 pidfile=/var/run/supervisord.pid  [program:octane] command=php /app/artisan octane:frankenphp --watch autostart=true autorestart=true stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0  [program:horizon] command=php /app/artisan horizon autostart=true autorestart=true stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0  [program:schedule] command=php /app/artisan schedule:run autostart=true autorestart=true stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0  [program:reverb] command=php /app/artisan reverb:start autostart=true autorestart=true stdout_events_enabled=true stderr_events_enabled=true stdout_logfile=/dev/stdout stdout_logfile_maxbytes=0 stderr_logfile=/dev/stderr stderr_logfile_maxbytes=0 

Так же локально стоит traefik, он выступает как реверс прокси — который просто делает переадресацию по портам

Пример:

  traefik:     image: traefik:v2.10     container_name: traefik.${APP_NAMESPACE}     command:       - --api.insecure=true       - --providers.docker=true       - --entrypoints.web.address=:80     ports:       - "80:80"       - "8080:8080"     volumes:       - /var/run/docker.sock:/var/run/docker.sock     networks:       - app   php:     build:       context: .       dockerfile: .docker/php/Dockerfile       target: dev     volumes:       - .:/app     labels:       - "traefik.enable=true"       - "traefik.http.routers.${APP_NAMESPACE}.rule=Host(`${APP_HOST:-localhost}`)"       - "traefik.http.services${APP_NAMESPACE}.loadbalancer.server.port=${APP_PORT:-8000}"

Хочу подметить два главных нюанса:

  • Hot reload режим сервера

  • Alpine образы

Про Hot reload:

Franken может работать в двух режимах

1) Prod режим — когда сервер сохраняет состояние и надо заново перезапускать сервер, чтоб он изменил состояние (простая экономия ресурсов)

2) Dev режим — когда сервер подтягивает изменения и сам под капотом обновляет сервер (данное решение медленее и подходит для локальной разработки)

При Dev режиме надо чтоб у вас в контейнере обязательно была установлена Node js так как для просмотра изменения файлов выступает js библиотека

Про alpine образы:

В документации четко описывается, почему их не стоит использовать:

Статические бинарники, которые мы предоставляем, а также Alpine Linux-вариант официальных Docker-образов используют библиотеку musl libc.

Известно, что PHP значительно медленнее работает с этой библиотекой по сравнению с традиционной GNU libc, особенно при компиляции в ZTS режиме (потокобезопасный режим), который требуется для FrankenPHP.

Кроме того, некоторые ошибки проявляются исключительно при использовании musl.

Админка:

Мы используем Filament для проектов, так как у нее легкая документация, красивая оболочка и с ее помощью можно быстро и красиво строить админку

Если ее использовать в нормальных проектах, то она особо для этого не подойдет, так как она сильно тормозит

Апи документация:

Один из важных инструментов для нас, так как надо было часто показывать наши ручки кейсодателям и также отгородить себя от лишних вопросов фронтендеров, что должно быть в том или ином запросе

Сейчас лучшее решение для laravel, это — https://scribe.knuckles.wtf/laravel/

Плюсы данной библиотеке:

  • Автогенерация ручек

  • Автогенерация query params

  • Автогенерация url params

  • Автогенерация body params

  • Тонкая настройка конфига

  • Кастомизация темы

  • Возможность указыть явно те или иные параметры

  • Поддержка атрибутов

Автогенерация будет работать только в том случае, если вы используете Laravel Request и Resource — то есть встроенные классы laravel

Так как в шаблоне я использую DTO от https://spatie.be/docs/laravel-data/v4/introduction

То приходиться ручками описать все параметры

Мониторинг:

В этот проект, я хотел добавить популярный стек — grafana, loki, prometeus, promtail
Но потом понял что это будет пустая трата времени и ресурсов сервера

Поэтому подключил простенький, но зато функциональный https://github.com/henrygd/beszel

Для меня он подошел идеально — так как красивый интерфейс и очень простая установка

Базовая авторизация:

Главная проблема и головная боль, что мы с каждым хакатоном заново писали авторизацию и связывали с фронтом

Поэтому было принято решение сразу в шаблон добавить авторизацию + верификацию пользователей

Также есть ролевка и добавлена роль — разработчик, при которой можно получить доступ только к системным сервисам, как, например дашборд Horizon

Пример сервиса для авторизации:

<?php  declare(strict_types=1);  namespace App\Services\Controllers;  use Illuminate\Validation\ValidationException; use Illuminate\Support\Facades\Hash; use App\DTO\User\UserAuthShowDTO; use App\DTO\Auth\AuthRegisterDTO; use App\DTO\Auth\AuthLoginDTO; use App\Models\User;  final class AuthService {     /** @return array<string, mixed> */     public function register(AuthRegisterDTO $authRegisterDTO): array     {         $user = User::query()->create([             'name'     => $authRegisterDTO->name,             'role'     => $authRegisterDTO->role,             'email'    => $authRegisterDTO->email,             'password' => Hash::make($authRegisterDTO->password),         ]);          return UserAuthShowDTO::from($user)->toArray();     }      /**      * @return array<string, mixed>      * @throws ValidationException      */     public function login(AuthLoginDTO $authLoginDTO): array     {         $user = User::query()->where('email', $authLoginDTO->email)->firstOrFail();          if (! Hash::check($authLoginDTO->password, $user->password)) {             throw ValidationException::withMessages(['bad credentials']);         }          return UserAuthShowDTO::from($user)->toArray();     } }

Вебсокет сервер:

На последнем хакатоне нам надо было использовать вебсокеты для передачи данных для забега в real time

Поэтому также решил его добавить в шаблон, чтоб в будущем на это не тратить время

На данный этап для вебсокет сервера стоит использовать:

  • Centrifugo

  • Larvel reverb

Самое лучшее решение на пхп в использовании вебсокетов — это использовать Centrifugo, так как она очень производительная и хорошо подойдет для больших проектов

Но у меня не получилось на хакатоне быстро его поднять и связать с Laravel

Поэтому я решил включить в сборку Laravel Reverb, неплохое решение, если ваш сервис будет небольшой — так же в будущем можно расширяться благодря очердям, что позволить улучшить производительность вебсокетов

GitHub Actions:

Настройка пайпланов была самой легкой, так как в интернете очень много примеров — поэтому покажу, что у меня получилось

name: DEPLOY AND BUILD on:   push:     branches: ["main"]   pull_request:     branches: ["main"]  jobs:   coding-standard:     name: Coding Standard     runs-on: ubuntu-latest     steps:       - uses: actions/checkout@v2        - name: Setup PHP         uses: shivammathur/setup-php@v2         with:           php-version: '8.4'           coverage: none        - name: Get composer cache directory         id: composer-cache         run: |           echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT        - name: Cache composer dependencies         uses: actions/cache@v4         with:           path: ${{ steps.composer-cache.outputs.dir }}           key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}           restore-keys: |             ${{ runner.os }}-composer-        - name: Install dependencies         run: composer install --no-progress --no-suggest --prefer-dist --no-interaction --ignore-platform-reqs        - name: Check coding style         run: composer cs-check        - name: Check code rector         run: composer cs-rector        - name: Perform a static analysis of the code base         run: ./vendor/bin/phpstan analyse --memory-limit=2G        - name: Test         run: php artisan test   deploy:     runs-on: [ ubuntu-latest ]     environment: deniskorbakov     needs: coding-standard     if: github.ref == 'refs/heads/main'     steps:       - uses: actions/checkout@v4.2.2       - name: Push to server         uses: appleboy/ssh-action@master         with:           host: ${{ secrets.SERVER_IP }}           username: ${{ secrets.SERVER_USERNAME }}           password: ${{ secrets.SERVER_PASSWORD }}           script: |             cd ${{ secrets.PROJECT_PATH }}             make update-project 

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

Я решил все нужные мне команды вынести в makefile, чтобы не писать слишком длинный конфиг, да и в целом неплохая идея вынести нужные команды для работы с деплоем в отдельную прослойку

Makefile:

include .env  # набор команд для обновление проекта в продакшене update-project: pull composer-install db-migrate build-front rm-images build-prod doc-generate restart  # набор команд для инициализации проекта локально init: build composer-install build-front key-generate storage-link db-migrate seed doc-generate restart build-wait  # набор команд для инициализации проекта на проде init-prod: build-prod composer-install build-front key-generate storage-link db-migrate seed doc-generate restart build-prod  build: @echo "Building containers" @docker compose --env-file .env up -d --build build-wait: @echo "Building containers" @docker compose --env-file .env up -d --build --wait up: @echo "Starting containers" @docker compose --env-file .env up -d --remove-orphans build-prod: @echo "Building containers" @docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d --wait --build up-prod: @echo "Starting containers" @docker compose -f docker-compose.yml -f docker-compose.prod.yml --env-file .env up -d --wait --remove-orphans shell: @docker exec -it $$(docker ps -q -f name=php.${APP_NAMESPACE}) /bin/bash code-check: @echo "Perform a static analysis of the code base" @DOCKER_CLI_HINTS=false docker exec -it $$(docker ps -q -f name=php.${APP_NAMESPACE}) vendor/bin/phpstan analyse --memory-limit=2G @echo "Perform a code rector" @DOCKER_CLI_HINTS=false docker exec -it $$(docker ps -q -f name=php.${APP_NAMESPACE}) composer cs-rector @echo "Perform a code style check" @DOCKER_CLI_HINTS=false docker exec -it $$(docker ps -q -f name=php.${APP_NAMESPACE}) composer cs-check rector-fix: @echo "Fix code with rector" @DOCKER_CLI_HINTS=false docker exec -it $$(docker ps -q -f name=php.${APP_NAMESPACE}) composer cs-rector-fix code-baseline: @echo "Perform phpstan generate-baseline" @DOCKER_CLI_HINTS=false docker exec -it $$(docker ps -q -f name=php.${APP_NAMESPACE}) vendor/bin/phpstan analyse --generate-baseline --memory-limit=2G composer-install: @echo "Running composer install" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) composer install --ignore-platform-reqs db-migrate: @echo "Running database migrations" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) php artisan migrate --force build-front: @echo "Building admin frontend for production" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) npm i @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) npm run build pull: @echo "Updating project from git and rebuild" @git pull rm-images: @echo "Delete extra images" @docker system prune -f key-generate: @echo "Key generate" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) php artisan key:generate storage-link: @echo "Storage Link" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) php artisan storage:link seed: @echo "Db Seed" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) php artisan db:seed doc-generate: @echo "Key generate" @docker exec -i $$(docker ps -q -f name=php.${APP_NAMESPACE}) php artisan scribe:generate restart: @echo "restart container" @docker restart php.${APP_NAMESPACE}   

Заключение:

В целом всё — в этой статье я рассказал про ключевые моменты, если нужно что‑то раскрыть более подробно, напишите об этом комментарий.

Для более детального ознакомления прошу перейти в репозиторий.

Благодарю всех, кто прочитал этот пост. Это моя первая публикация на Хабре, поэтому не судите строго, постарался раскрыть только самые интересные моменты


ссылка на оригинал статьи https://habr.com/ru/articles/902066/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *