Недавнооткрыл миру 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-окружения.

Основные проблемы:

  1. Глобальное состояние: Фреймворки хранят конфигурацию, сервис-контейнеры и другие данные в статических свойствах. В долгоживущем процессе Swoole это приводит к утечкам памяти и неожиданному поведению.

  2. Блокирующие операции: Оба фреймворка содержат множество синхронных вызовов (файловые операции, тяжелые вычисления в сервис-провайдерах). В Swoole это требует создания десятков воркеров для параллельной обработки.

  3. Отсутствие пулов соединений: Традиционные ORM (Eloquent, Doctrine) создают новое соединение с БД для каждого запроса, что в Swoole приводит к:

    • Исчерпанию лимитов соединений на стороне БД

    • Накладным расходам на установку соединения

    • Потере преимуществ долгоживущих процессов

  4. 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. поддержите проект поставьте звездочку, вам не сложно, а мне приятно.

Комментарии (0)