cesurapp / swoole-bundle
Symfony Swoole Bundle
Package info
github.com/cesurapp/swoole-bundle
Type:symfony-bundle
pkg:composer/cesurapp/swoole-bundle
Requires
- php: >=8.4
- ext-pcntl: *
- ext-posix: *
- ext-swoole: *
- dragonmantank/cron-expression: ^3.3
- symfony/console: ^8.1
- symfony/dependency-injection: ^8.1
- symfony/dotenv: ^8.1
- symfony/framework-bundle: ^8.1
- symfony/http-client-contracts: ^3.7
- symfony/http-kernel: ^8.1
- symfony/lock: ^8.1
- symfony/runtime: ^8.1
Requires (Dev)
- doctrine/doctrine-bundle: ^3.3.2
- doctrine/orm: ^3.7.2
- php-cs-fixer/shim: ^3.95
- phpstan/phpstan: ^2.2
- phpunit/phpunit: ^13.3
- symfony/process: ^8.1
- symfony/uid: ^8.1
Suggests
None
Provides
None
Conflicts
None
Replaces
None
- dev-main
- 1.2.21
- 1.2.20
- 1.2.19
- 1.2.18
- 1.2.17
- 1.2.16
- 1.2.15
- 1.2.14
- 1.2.13
- 1.2.12
- 1.2.11
- 1.2.10
- 1.2.9
- 1.2.8
- 1.2.7
- 1.2.6
- 1.2.5
- 1.2.4
- 1.2.3
- 1.2.2
- 1.2.1
- 1.2.0
- 1.1.03
- 1.1.02
- 1.1.01
- 1.1.0
- 1.0.25
- 1.0.24
- 1.0.23
- 1.0.22
- 1.0.021
- 1.0.20
- 1.0.19
- 1.0.18
- 1.0.17
- 1.0.16
- 1.0.15
- 1.0.14
- 1.0.13
- 1.0.12
- 1.0.11
- 1.0.10
- 1.0.09
- 1.0.08
- 1.0.07
- 1.0.06
- 1.0.05
- 1.0.04
- 1.0.03
- 1.0.02
- 1.0.01
- 1.0.0
This package is auto-updated.
Last update: 2026-09-24 07:58:01 UTC
README
Built-in Swoole http server, background jobs (Task), scheduled task (Cron) worker are available. Failed jobs are saved in the database to be retried. Each server has built-in background task worker. Scheduled tasks run simultaneously on all servers. It is not possible for tasks to run at the same time as locking is used.
Install
Required Symfony 8
composer req cesurapp/swoole-bundle
Edit: public/index.php
... require_once dirname(__DIR__).'/vendor/cesurapp/swoole-bundle/src/Runtime/entrypoint.php'; require_once dirname(__DIR__).'/vendor/autoload_runtime.php'; ...
Configuration:
# config/packages/swoole.yaml swoole: entrypoint: public/index.php watch_dir: /config,/src,/templates watch_extension: '*.php,*.yaml,*.yml,*.twig' replace_http_client: true # Replace Symfony HTTP Client to Swoole Client (checks certificates only with verify_peer: true) cron_worker: true # Enable Cron Worker Service (FailedTaskCron runs while task_worker is on, even without it) task_worker: true # Enable Task Worker Service -> Default false task_sync_mode: false # Enable SYNC Mode -> Default false process_worker: true # Enable Process Worker Service task_retry: [60, 300, 600, 1800] # Seconds before each retry of a failed task (min 60) -> Default [600] task_redeliver_timeout: 3600 # Seconds before a running stored task counts as lost and runs again (min 60)
Server Environment: .env
# Worker Configuration: turns off a worker swoole.yaml enables, never turns one on #SERVER_WORKER_CRON=true # Run Cron Worker -> Default = 1 #SERVER_WORKER_TASK=true # Run Task Worker -> Default = 1 #SERVER_WORKER_PROCESS=true # Run Process Worker -> Default = 1 # HTTP Server Configuration SERVER_HTTP_HOST=127.0.0.1 # Default = 0.0.0.0 SERVER_HTTP_PORT=9090 # Default = 80 #SERVER_HTTP_MODE=2 # SWOOLE_PROCESS -> Default = 2 #SERVER_HTTP_SOCK_TYPE=1 # SWOOLE_SOCK_TCP -> Default = 1 # HTTP Server Settings #SERVER_HTTP_SETTINGS_WORKER_NUM=2 # Default = CPU Count #SERVER_HTTP_SETTINGS_TASK_WORKER_NUM=1 # Default = CPU Count / 2 #SERVER_HTTP_SETTINGS_ENABLE_STATIC_HANDLER=false # Default = false #SERVER_HTTP_SETTINGS_LOG_LEVEL=4 # Details Openswoole\Constant LOG_LEVEL -> Default = 4 (SWOOLE_LOG_WARNING) #SERVER_HTTP_SETTINGS_MAX_WAIT_TIME=60 # Default = 60 #SERVER_HTTP_SETTINGS_TASK_ENABLE_COROUTINE=true # Default = true #SERVER_HTTP_SETTINGS_TASK_MAX_REQUEST=1000 # Restart task worker after N tasks, 0 = unlimited -> Default = 1000 #SERVER_HTTP_SETTINGS_PACKAGE_MAX_LENGTH=15728640 # 15MB -> Default = 15728640 #SERVER_HTTP_SETTINGS_HTTP_COMPRESSION=true # Default = true #SERVER_HTTP_SETTINGS_MAX_REQUEST=10000 # Default = 10000 #SERVER_HTTP_SETTINGS_HEARTBEAT_CHECK_INTERVAL=5 # A stop waits for the next check -> Default = 5 #SERVER_HTTP_SETTINGS_HEARTBEAT_IDLE_TIME=180 # Default = 180
Server Commands
# Cron Commands bin/console cron:list # List cron jobs bin/console cron:run AcmeCron # Run cron process one time, without locking. # Server Commands bin/console server:start # Start http,cron,queue server bin/console server:stop # Stop http,cron,queue server bin/console server:watch # Start http,cron,queue server for development mode (file watcher enabled) # Task|Job Commands bin/console task:list # List registered tasks bin/console task:failed:clear # Clear all failed task (unfinished durable tasks stay) bin/console task:failed:retry # Give failed tasks their attempts back; FailedTaskCron runs them on its next run bin/console task:failed:view # Lists failed tasks
The running server keeps its master process id in var/swoole.pid. server:stop sends it SIGTERM
and waits while the running requests end (up to max_wait_time), then kills a server still up.
Create Cron Job
You can use cron expression for scheduled tasks, or you can use predefined expressions.
<?php namespace App\Cron; use Cesurapp\SwooleBundle\Cron\AbstractCronJob; /** * Predefined Scheduling * * '@yearly' => '0 0 1 1 *', * '@annually' => '0 0 1 1 *', * '@monthly' => '0 0 1 * *', * '@weekly' => '0 0 * * 0', * '@daily' => '0 0 * * *', * '@hourly' => '0 * * * *', * '@EveryMinute' => '* * * * *', * '@EveryMinute5' => '*/5 * * * *', * '@EveryMinute10' => '*/10 * * * *', * '@EveryMinute15' => '*/15 * * * *', * '@EveryMinute30' => '*/30 * * * *', */ class ExampleCron extends AbstractCronJob { public string $TIME = '@EveryMinute10'; public bool $ENABLE = true; public int $TIMEOUT = 1200; // Seconds a run may take before it is stopped public function __invoke(): void { // Cron job logic here } }
Notes:
- One scheduler process starts the jobs on time and gives every run a process of its own: a slow or blocking job (a long query, say) holds up only its own run, never the other jobs
- A run's process lives as long as the run and opens its own connections; the job runs in a coroutine, like in any worker
- A job whose previous run is still going is not started again; that run is skipped
TIMEOUT(default 1200 seconds) stops a run that takes longer; raise it for a longer job. The run's lock lastsTIMEOUTplus a minute, so no other server starts the job while it runs- Stopping the server stops the runs in progress
- The job's constructor runs in the scheduler: open connections in
__invoke(), never earlier
Create Task (Background Job or Queue)
Data passed to tasks must be serializable (string, int, bool, array). Objects cannot be serialized directly.
Create Task:
<?php namespace App\Task; use Cesurapp\SwooleBundle\Task\TaskInterface; class ExampleTask implements TaskInterface { public function __invoke(string $data): mixed { $payload = unserialize($data); var_dump( $payload['name'], $payload['invoke'] ); return 'Task completed'; } }
Dispatch Task:
<?php namespace App\Controller; use App\Task\ExampleTask; use Cesurapp\SwooleBundle\Task\TaskHandler; use Symfony\Component\HttpFoundation\Response; class ExampleController { public function __construct( private readonly TaskHandler $taskHandler ) {} public function hello(): Response { $this->taskHandler->dispatch(ExampleTask::class, [ 'name' => 'Test', 'invoke' => 'Data' ]); return new Response('Task dispatched'); } }
Durable Task:
Swoole keeps its task queue in memory, so a task still queued or running when the server stops is lost.
Pass durable: true for work that must survive a deploy or a crash. The task is written to the
failed_task store before it is queued, and its row is deleted only once the task succeeds.
FailedTaskCron runs it again after a failure (on the task_retry schedule) and after
task_redeliver_timeout if its worker died.
$this->taskHandler->dispatch(TranscribeTask::class, ['call_id' => $id], durable: true);
- A durable task runs at least once, so make it idempotent.
- A durable task never runs inline in the caller. When it can't be queued right away it waits for
FailedTaskCron. That happens when Swoole refuses it, when there is no server (a console command), or when the dispatch is inside an open database transaction, where a worker could not yet see its row. - In sync mode (
task_sync_mode, tests) it runs inline like any other task and writes no row.
Create Process Worker
Process Worker allows you to create continuously running tasks in a separate process when the server starts. It's ideal for Redis LISTEN, Postgres LISTEN, or similar continuous listening commands.
Features:
- Each process runs as a separate, server-managed Swoole Process (
Server::addProcess) - Automatic restart support when the process completes
- Configurable restart delay
- Enable/Disable support
- One running copy across instances (lock), with the other instances on standby as failover
- Can dispatch tasks like any worker
Configuration:
# config/packages/swoole.yaml swoole: process_worker: true # Default: true
Or via environment variable:
SERVER_WORKER_PROCESS=1 # Enable SERVER_WORKER_PROCESS=0 # Disable
Create Process Job:
Use ProcessInterface or extend AbstractProcessJob:
<?php namespace App\Process; use Cesurapp\SwooleBundle\Process\AbstractProcessJob; class RedisListenerProcess extends AbstractProcessJob { // Is process active? public bool $ENABLE = true; // Restart when process completes public bool $RESTART = true; // Wait time before restart (seconds) public int $RESTART_DELAY = 5; public function __construct( private readonly RedisClient $redis, private readonly LoggerInterface $logger ) { } public function __invoke(): void { $this->logger->info('Redis listener started'); // Redis SUBSCRIBE command $this->redis->subscribe(['channel1', 'channel2'], function ($redis, $channel, $message) { $this->logger->info("Received message from {$channel}: {$message}"); // Process here }); } }
Postgres LISTEN Example:
<?php namespace App\Process; use Cesurapp\SwooleBundle\Process\AbstractProcessJob; use Doctrine\DBAL\Connection; class PostgresListenerProcess extends AbstractProcessJob { public bool $ENABLE = true; public bool $RESTART = true; public int $RESTART_DELAY = 3; public function __construct( private readonly Connection $connection, private readonly LoggerInterface $logger ) { } public function __invoke(): void { $this->logger->info('Postgres listener started'); // LISTEN command $this->connection->executeStatement('LISTEN my_channel'); while (true) { // Wait for notification $notification = pg_get_notify($this->connection->getNativeConnection()); if ($notification) { $this->logger->info('Received notification', [ 'channel' => $notification['message'], 'payload' => $notification['payload'] ]); // Process here } usleep(100000); // Wait 100ms } } }
One-Time Process (Without Restart):
<?php namespace App\Process; use Cesurapp\SwooleBundle\Process\AbstractProcessJob; class OneTimeProcess extends AbstractProcessJob { public bool $ENABLE = true; public bool $RESTART = false; // Restart disabled public function __invoke(): void { // One-time operation $this->doSomething(); // The job is done; the process stays parked (see the notes below) } }
Notes:
- Each process runs as a separate Swoole Process, isolated from each other
- Processes are registered with
Server::addProcess: they start with the server, the server restarts one that exits or crashes, andTaskHandler::dispatch()works inside them - One copy runs per job across all instances (the
process_server_<FQCN>lock). The other instances' copies wait on standby and take over when that lock is released - Use a PostgreSQL advisory lock for it:
LOCK_DSN=postgresql+advisory://..., connected directly (PgBouncer's transaction pooling hands the lock's session to other clients, so two copies could run). It never expires, however long the job holds up its process, and drops with the process. With a store whose locks expire (Redis, a plainpostgresql://table) the lock lasts 60 seconds past its last refresh, so a job that blocks its process longer lets a standby copy start - A stop releases the lock at once. Each copy checks its lock every 10 seconds; one that lost it (e.g. its database session dropped) stops and the server restarts it
- A copy that takes the lock over from another waits 15 seconds before starting the job, so the other has noticed a lost lock and stopped
- When
RESTART=true, the job runs again afterRESTART_DELAYseconds upon completion (an exception or error counts as completion) - When
RESTART=false, the finished process stays parked while the server runs: exiting would only have the server restart it and run the job again - The job's constructor runs in the master process (to read
ENABLE): open connections in__invoke(), never earlier - Processes must implement
ProcessInterface(or extendAbstractProcessJob) - Automatically registered in Symfony DI container with lazy loading support
Requirements
- PHP >= 8.4
- Symfony 8+
- Swoole Extension
- POSIX Extension
- PCNTL Extension
License
MIT