All processes — including the built-in HTTP process — are defined in
config/process.php. After changing this file you must restart (reload is
not enough).
// config/process.php
return [
// ... default webman (HTTP), monitor, etc. ...
// Resident worker with no listen socket
'my-task' => [
'handler' => process\MyTask::class,
'count' => 1, // process count
'constructor' => ['param1' => 'xx'], // constructor args
],
// WebSocket service
'ws' => [
'handler' => process\Ws::class,
'listen' => 'websocket://0.0.0.0:8888',
'count' => 4,
'reusePort' => true,
],
// TCP service (text protocol, framed by \n)
'tcp' => [
'handler' => process\Tcp::class,
'listen' => 'text://0.0.0.0:9000',
'count' => 2,
],
];Process classes receive workerman callbacks via onXxx methods:
<?php
declare(strict_types=1);
namespace process;
use Workerman\Worker;
use Workerman\Connection\TcpConnection;
use Workerman\Protocols\Http\Request;
class Ws
{
public function onWorkerStart(Worker $worker): void
{
// Process start: init connections, load data, register timers
}
public function onConnect(TcpConnection $connection): void {}
public function onWebSocketConnect(TcpConnection $connection, Request $request): void
{
// Auth from GET params during handshake
$token = $request->get('token');
if (!$token) {
$connection->close();
}
}
public function onMessage(TcpConnection $connection, $data): void
{
$connection->send('received: ' . $data);
}
public function onClose(TcpConnection $connection): void {}
}use Workerman\Timer;
public function onWorkerStart(Worker $worker): void
{
// Every 10 seconds
Timer::add(10, function () {
// Always catch exceptions or the timer may stop
try {
OrderService::closeExpired();
} catch (\Throwable $e) {
\support\Log::error('timer error: ' . $e->getMessage());
}
});
// One-shot delayed task
Timer::add(5, fn () => something(), [], false);
}Rules:
- Timer process
countmust be1, otherwise every process runs the job (duplicates). - Do not
Timer::addinside controllers/middleware — each request leaks a timer.
composer require workerman/crontab<?php
declare(strict_types=1);
namespace process;
use Workerman\Crontab\Crontab;
class CronTask
{
public function onWorkerStart(): void
{
// second minute hour day month weekday (6 fields, second-level)
new Crontab('0 */5 * * * *', function () { // every 5 minutes
ReportService::flush();
});
new Crontab('0 30 2 * * *', function () { // daily 02:30
CleanupService::run();
});
}
}Also register in config/process.php with count => 1. On multi-host
deployments, use a Redis lock so only one machine runs the job.
HTTP and WebSocket run in different processes; connection objects are not directly shared. Push options:
- webman/push: pusher-protocol plugin, private channels, JS SDK; trigger from HTTP via API.
- Redis pub/sub or queues: HTTP publishes; WebSocket process subscribes and pushes.
- workerman/channel: lightweight cross-process broadcast.
// webman/push example from an HTTP controller
$api = new \Webman\Push\Api(
'http://127.0.0.1:3232',
config('plugin.webman.push.app.app_key'),
config('plugin.webman.push.app.app_secret')
);
$api->trigger('user-1', 'message', ['content' => 'hello']);composer require webman/redis-queue// Produce (from a controller)
use Webman\RedisQueue\Client;
Client::send('send-mail', ['to' => 'a@b.c'], delay: 60);
// Consumer: app/queue/redis/SendMail.php (auto-registered)
namespace app\queue\redis;
use Webman\RedisQueue\Consumer;
class SendMail implements Consumer
{
public $queue = 'send-mail';
public $connection = 'default';
public function consume($data): void
{
Mailer::send($data['to']);
// Throwing retries per config (retry_seconds, max_attempts)
}
}