Недавнооткрыл миру SwooleApp — минималистичного фреймворка для PHP, построенного на базе Swoole. Если вы уже работали с Swoole напрямую, то знаете, что это мощный инструмент для создания высокопроизводительных приложений, но иногда хочется иметь чуть больше структуры и удобства, чем предлагает чистый Swoole. Именно эту нишу и занимает SwooleApp.
В этой статье я кратко расскажу, что это за проект, как его использовать, и поделюсь ссылкой на рабочий пример приложения, который можно запустить в Docker за несколько минут.
Что такое SwooleApp?
SwooleApp — это каркас для создания долгоживущих PHP-приложений на движке Swoole. В отличие от традиционных PHP-фреймворков, где каждый HTTP-запрос запускает интерпретатор с нуля, здесь приложение работает как сервер, который загружается один раз и затем обрабатывает запросы в рамках одного процесса (или пула процессов). Это позволяет:
Избежать накладных расходов на инициализацию PHP при каждом запросе.
Хранить состояние между запросами (например, пулы соединений с БД, кэши).
Использовать асинхронные и фоновые задачи без дополнительных очередей.
Фреймворк предоставляет:
Маршрутизацию через атрибуты PHP 8.
Middleware для сквозной логики.
Встроенную систему фоновых задач (Task Workers).
Поддержку циклических джобов (встроенный «cron»).
Контейнер состояния (State Container) для общих ресурсов (Redis, БД и т.д.).
Пример приложения: что внутри?
Чтобы быстро понять, как работать с SwooleApp, я подготовил пример приложения. Он включает:
REST-эндпоинты для работы с Redis (get/set).
Фоновую задачу, которая имитирует долгую обработку.
Циклическую задачу, которая пишет лог раз в 10 секунд.
Middleware для логирования времени выполнения запросов.
Docker-конфигурацию для быстрого запуска.
Структура:
text
src/
├── Controllers/ # HTTP-контроллеры
├── Middleware/ # Промежуточное ПО
├── Tasks/ # Фоновые задачи
├── CyclicJobs/ # Планируемые задачи
└── StateContainerInitializers/ # Инициализаторы ресурсов
Как это работает на практике?
1. Маршрутизация через атрибуты
Контроллер объявляется так:
php
#[Route(uri: "/redis/get/{key}", method: 'GET')]
#[Middleware(LoggingMiddleware::class)]
class RedisGetter extends AbstractController
{
public function execute(): \Swoole\Http\Response
{
$redisPool = $this->application->getStateContainer()
->getContainer(RedisConnectionPoolInitializer::class);
$key = $this->uri_params['key'];
$redis = $redisPool->get();
$data = $redis->get($key);
$redisPool->put($redis);
$this->response->end($data);
return $this->response;
}
}
Маршрут, параметры пути и Middleware задаются декларативно.
2. State Container и пулы соединений: как Swoole хранит состояние
Ключевое отличие Swoole от традиционного PHP — воркеры (worker processes) живут долго и имеют общую память. В обычном PHP-FPM каждый запрос — это отдельный процесс, который умирает после ответа. В Swoole воркеры работают постоянно, что позволяет:
State Container — это глобальное хранилище данных в памяти, которое инициализируется при старте сервера и доступно всем запросам, обрабатываемым одним воркером.
Пул соединений — вместо создания нового подключения к БД/Redis для каждого запроса, мы создаём пул соединений один раз при старте и переиспользуем его.
php
class RedisConnectionPoolInitializer extends AbstractContainerInitiator
{
public function init(\Sidalex\SwooleApp\Application $param): void
{
$redisConfig = $param->getConfig()->getConfigFromKey('REDIS');
$this->result = new RedisPool(
(new RedisConfig)
->withHost($redisConfig->HOST)
->withPort($redisConfig->PORT),
$redisConfig->POOL_SIZE ?? 10
);
$this->key = self::class;
}
}
Затем контроллеры получают доступ к пулу через $this->application->getStateContainer(). Это не только быстрее (нет накладных расходов на установку соединения), но и снижает нагрузку на саму БД.
Важный нюанс: данные в State Container уникальны для каждого воркера. Если у вас 4 воркера — будет 4 независимых пула соединений. Это архитектурное ограничение Swoole.
3. Зачем в Swoole нужны Task Workers?
Swoole работает в асинхронном режиме, но блокирующие операции (sleep, file_put_contents, медленные SQL-запросы) всё равно блокируют воркер. Если воркер заблокирован — он не может обрабатывать новые HTTP-запросы.
Решение: выносить блокирующие операции в отдельные Task Workers:
php
class TasksExample extends AbstractTaskExecutor
{
public function execute(): TaskResulted
{
sleep(3); // Имитация тяжёлой операции
return new TaskResulted(['success' => 'ok']);
}
}
И запускать из контроллера:
php
$task = new BasicTaskData(TasksExample::class, ['data' => $body]);
$result = $this->server->taskwait($task, 10);
Как это работает:
HTTP Worker отправляет задачу в очередь Task Workers
Продолжает обрабатывать другие запросы
Task Worker выполняет долгую операцию асинхронно
Результат возвращается обратно
4. Циклические задачи (Cyclic Jobs)
Аналог cron, работающий внутри Swoole:
php
class CyclicJob extends AbstractCyclicJob
{
protected float $timeSleep = 10;
public function runJob(): void
{
file_put_contents('Job.log', date('Y-m-d H:i:s') . "\n", FILE_APPEND);
}
}
Эти задачи запускаются в корутинах и не блокируют основные воркеры.
Почему существующие бандлы для Laravel/Symfony неэффективны с Swoole?
Популярные решения вроде laravel-swoole или symfony-swoole-bundle сталкиваются с фундаментальной проблемой: Laravel и Symfony изначально проектировались для stateless-окружения.
Основные проблемы:
Глобальное состояние: Фреймворки хранят конфигурацию, сервис-контейнеры и другие данные в статических свойствах. В долгоживущем процессе Swoole это приводит к утечкам памяти и неожиданному поведению.
Блокирующие операции: Оба фреймворка содержат множество синхронных вызовов (файловые операции, тяжелые вычисления в сервис-провайдерах). В Swoole это требует создания десятков воркеров для параллельной обработки.
-
Отсутствие пулов соединений: Традиционные ORM (Eloquent, Doctrine) создают новое соединение с БД для каждого запроса, что в Swoole приводит к:
Исчерпанию лимитов соединений на стороне БД
Накладным расходам на установку соединения
Потере преимуществ долгоживущих процессов
Middleware и события: В Laravel/Symfony многие компоненты предполагают, что они "живут" только во время одного запроса. В Swoole они сохраняются между запросами, что может приводить к накоплению состояния.
Результат: Чтобы запустить Laravel/Symfony на Swoole, нужно либо:
Запускать десятки или сотни воркеров (теряем преимущества Swoole)
Значительно переписывать код приложения
Мириться с утечками памяти и нестабильностью
SwooleApp решает эти проблемы, предлагая архитектуру "с нуля" заточенную под долгоживущие процессы.
Зачем это нужно?
SwooleApp подойдёт, если:
Вы хотите быстро начать писать на Swoole без boilerplate-кода.
Нужен легковесный каркас для микросервиса или API.
Хочется использовать преимущества долгоживущих процессов (пулы соединений, кэши в памяти).
Нужны встроенные фоновые и циклические задачи.
Вы разочаровались в производительности Laravel/Symfony на Swoole.
Это не монстр вроде Laravel или Symfony — это именно минималистичный каркас, который даёт структуру, но не диктует архитектуру.
Ссылки
Итог
SwooleApp — это попытка сделать работу с Swoole более структурированной, сохранив при этом простоту и производительность. Если вы уже смотрите в сторону Swoole для высоконагруженных проектов — попробуйте этот фреймворк. А если только начинаете — пример в Docker поможет быстро всё развернуть и понять основы.
P.S. Проект открыт для contribution. Если есть идеи или баги — welcome в Issues и Pull Requests.
P.P.S. поддержите проект поставьте звездочку, вам не сложно, а мне приятно.