Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Gateway Worker Laravel Package

workerman/gateway-worker

GatewayWorker is a PHP distributed framework built on Workerman for long-lived TCP, UDP, and WebSocket connections. It separates gateway, business worker, and register processes to scale real-time apps across servers, with process management and messaging for chat, IoT, push, and game backends.

View on GitHub
Deep Wiki
Context7

Getting Started

workerman/gateway-worker is not a Laravel package with a service provider — it is a runnable scaffold you deploy as a separate daemon alongside your Laravel app. The most important mental shift: Laravel handles HTTP and enqueues work; GatewayWorker holds TCP/WebSocket connections and pushes.

Install and inspect

# In a service directory, NOT inside your Laravel app
composer create-project workerman/gateway-worker gateway
cd gateway

Look at these files first, in this order:

  1. start_register.php — service discovery registry (0.0.0.0:1236).
  2. start_gateway.php — the public WebSocket/TCP listener (0.0.0.0:8282). Set $gateway->count here to scale connections.
  3. start_businessworker.php — where your logic runs. Set $worker->count here to scale CPU.
  4. Applications/YourApp/Events.php — your actual code: onConnect, onMessage, onClose, onWorkerStart.

All three start_*.php files must point at the same register address:

$gateway->registerAddress       = '127.0.0.1:1236';
$businessworker->registerAddress = '127.0.0.1:1236';

Run it

php start.php start        # foreground, prints logs, use during dev
php start.php start -d     # daemonized
php start.php status       # live process/connection counts
php start.php stop

First use case — an authenticated chat channel

On the client, connect to ws://your-host:8282. In Events.php:

public static function onMessage($clientId, $message)
{
    $data = json_decode($message, true);
    if ($data['type'] === 'login') {
        $uid = /* validate $data['token'] via Laravel/Redis/JWT */;
        Gateway::bindUid($clientId, $uid);             // stable identity
        Gateway::joinGroup($clientId, "user:$uid");     // personal channel
    }
}

From Laravel, push to that user with the separate client package:

composer require workerman/gatewayclient
use GatewayClient\Gateway;
Gateway::$registerAddress = config('services.gateway.register'); // 127.0.0.1:1236

Gateway::sendToUid($user->id, json_encode(['event' => 'ping']));

That round trip — Laravel request writes → GatewayClient → Gateway process → socket — is the core day-to-day loop.

Implementation Patterns

1. Three-tier topology is the design, not an accident. Register is a lightweight service registry. Gateway does nothing but hold sockets and speak the internal protocol. BusinessWorker runs Events.php. Scale Gateway->count for connection volume (10k+ per process is normal), scale BusinessWorker->count for message throughput. All three can be on separate hosts as long as registerAddress is reachable from Gateway and BusinessWorker.

2. Auth in the BusinessWorker, not the Gateway. The Gateway process should never touch your database. Accept the socket on connect, then require a login frame within N seconds and close unauth sockets from a Timer. Rejecting at the WebSocket handshake is possible (onWebSocketConnect), but you can't cheaply hit Eloquent there.

3. Bootstrap Laravel inside onWorkerStart so Events.php can use Eloquent, Config, Redis.

public static function onWorkerStart($worker)
{
    require __DIR__.'/../../../laravel/vendor/autoload.php';
    $app = require __DIR__.'/../../../laravel/bootstrap/app.php';
    $app->make(Illuminate\Contracts\Console\Kernel::class)->bootstrap();
    // now config(), DB, Cache, Redis are available
}

Once bootstrapped, Events.php can validate tokens (Auth::guard('api')->setToken(...)->user()), write to models, dispatch jobs — but see the gotchas about connection staleness.

4. Use bindUid for identity, joinGroup for channels. Client IDs change on every reconnect; never persist them. bindUid($clientId, $uid) gives you sendToUid, isUidOnline, getClientIdByUid. Groups are the natural abstraction for rooms/subscriptions (joinGroup, leaveGroup, sendToGroup($group, $data, $excludeClientId)).

5. Wire GatewayClient into Laravel broadcasting. Implement a custom broadcaster so broadcast(new MessageSent($m)) reaches the socket:

Broadcast::extend('gateway', fn ($app, $config) => new GatewayBroadcaster(
    $config['register'], $config['app_key'] // if you use Gateway::$appKey scoping
));

Or, simpler: a queued event listener that calls Gateway::sendToUid() in handle(). Keep sends in queue workers so HTTP latency never depends on the socket layer.

6. Push from the "other direction" with AsyncTcpConnection. BusinessWorker is itself a Workerman client. When a client sends a message, you can either enqueue it to Laravel (via Redis list) or open a non-blocking AsyncTcpConnection to a Laravel internal endpoint. Never use blocking file_get_contents/curl in onMessage.

7. Periodic work via Timer. Heartbeats, presence sweeps, draining a Redis queue of outbound messages:

Timer::add(2, function () {
    while ($job = Redis::lpop('outbound')) {
        [$uid, $payload] = unserialize($job);
        Gateway::sendToUid($uid, $payload);
    }
});

8. Presence and online lists. Gateway::isUidOnline($uid) and Gateway::getClientIdByUid work across BusinessWorker processes via the Gateway tier. But your own static $clients = [] in Events.php is per-worker — put anything that must be globally visible in Redis (SADD online:$room, SREM on onClose).

9. Scaling out. Run two Registers for HA (Gateway/BusinessWorker round-robin between them). Add Gateway hosts behind a load balancer (sticky not required — GatewayClient and sendToUid route via the register). All hosts share one register address.

Gotchas and Tips

  • Don't require this into Laravel. The package is a project skeleton. composer require workerman/gateway-worker in your Laravel app won't give you a facade — Events.php lives in its own directory, deployed by its own supervisor program.
  • Two packages, easy to confuse: workerman/gateway-worker is the server; workerman/gatewayclient is what Laravel calls. They share GatewayProtocol — keep them roughly in sync when upgrading.
  • $registerAddress must be set on every participant. Gateway, BusinessWorker, and Laravel's GatewayClient each need it. If sendToUid silently returns false, 90% of the time it's a missing/wrong register address or port 1236 blocked.
  • No shared state across BusinessWorker processes. A static map of client IDs in onWorkerStart is only visible in that one worker. If you bindUid on worker A and check getClientIdByUid on worker B, it works because the Gateway tier answers — but your local arrays won't. Use Redis for anything global.
  • The client is unauthenticated until you say otherwise. Track "authenticated at" in a per-worker map and close with Gateway::closeClient($clientId) if a Timer sweep finds it older than N seconds. Otherwise unauthenticated sockets pile up.
  • Memory leaks in long-running processes. PHP won't reclaim cycles in a 24/7 worker the way a request-scoped FPM process does. unset large payloads, cap any in-memory caches, and let supervisor autorestart periodically (or Timer a graceful Worker::stopAll() after a request count).
  • Stale DB connections. A Laravel connection opened at onWorkerStart will time out. On a Timer (say, every 30s while idle) call DB::connection()->reconnect() or wrap message handling in a try/catch that calls DB::purge() and retries once.
  • Exceptions kill the worker. Workerman does not catch them; an uncaught throwable in onMessage terminates the process and supervisor restarts it, dropping all its connections. Wrap handlers and log via Worker::log() or Log::error() after Laravel bootstrap.
  • Blocking I/O is poison. One BusinessWorker process may serve thousands of connections via the event loop. A synchronous curl or sleep(3) stalls everyone on that worker. Use AsyncTcpConnection, Redis, and queues.
  • Broadcast cost. Gateway::sendToAll and sendToUid fan out internally across Gateway workers, but they still traverse each socket at send time. For fan-out to large groups, batch your writes, prefer sendToGroup, and throttle server-side. sendToGroup returns the count of recipients — useful for metrics.
  • SSL/WSS in production. Set $gateway->transport = 'ssl' with ssl_cert_file and ssl_key_file on the Gateway, or terminate TLS at nginx with a WebSocket upgrade proxy (proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade";).
  • Windows dev is limited. Workerman works but lacks full multi-process and -d daemonization. Always run Linux in production.
  • GatewayClient connection behavior. By default each Gateway::sendTo* call opens a socket to the register, sends, and closes. In a hot loop set Gateway::$persistentConnection = true and register a register_shutdown_function that Gateway::close()s — otherwise you'll exhaust PHP sockets under load.
  • Logging goes to stdout by default. php start.php start streams everything to your terminal. For daemon mode configure $worker->logFile per process, and log-rotate it with logrotate or a size cap.
  • Firewall the two internal ports. Gateway's public port (8282) is the only one that should be exposed; Register (1236) and BusinessWorker's internal port must be private. BusinessWorker needs no public port at all.
  • Debug with php start.php status. It shows per-process connection counts, memory, and timers — the fastest way to spot a busy vs. idle worker. php start.php restart is safer than stop + start for zero-downtime-ish reloads.
  • Gateway::$appKey scoping. If you run multiple apps behind one register, set a shared $appKey on Gateway, BusinessWorker, and GatewayClient. Mismatched keys produce "connection not found"-style failures with no obvious error.
  • Framing is internal. The Gateway↔BusinessWorker protocol prepends a 4-byte length packet. Never hand-roll frames; keep business messages as plain JSON strings and let the transport do its job.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
jinas/dhivatar
store-doc/laravel-storage-driver
plus-it/laravel-translation-manager
diephp/perhaps
diephp/telegraph
osmaviation/spreadsheet
microsoft/fluentui-system-icons
mmccook/php-json-canonicalization-scheme
middlewares/firewall
qubiqx/montapacking
nyoncode/laravel-package-console
pnscripts/filament-broadquill-lite
pnscripts/laravel-sentfold-lite
vleap/mx-sdk-laravel-internal
twidpay/utils
htetmyathlaing/scout-elasticsearch-driver
artisan-toolbox/front-awesome
ls/client-assistant
users-au/laravel-client
cgv/localized-resource