Как стать автором
Обновить

Шаблон на Laravel + FrankenPHP

Уровень сложностиСредний
Время на прочтение9 мин
Количество просмотров4K

Всех приветствую, меня зовут Денис, я 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}


Заключение:

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

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

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

Теги:
Хабы:
+9
Комментарии13

Публикации

Работа

PHP программист
78 вакансий

Ближайшие события