chrome-php/wrench
Simple PHP WebSocket library with server and client support. Create a BasicServer, register multiple apps per path, handle incoming data via interfaces, and send responses back to clients. Install via Composer; supports PHP 7.4–8.5.
composer require chrome-php/wrench
app/Console/Commands/WebSocketServer.php):
use Wrench\BasicServer;
use Wrench\Application\DataHandlerInterface;
use Illuminate\Console\Command;
class WebSocketServer extends Command implements DataHandlerInterface
{
protected $signature = 'ws:server';
protected $description = 'Start the WebSocket server';
public function handle()
{
$server = new BasicServer('ws://localhost:8000', [
'allowed_origins' => ['*'], // Adjust for production
]);
$server->registerApplication('chat', $this);
$server->run();
}
public function onData(string $data, \Wrench\Connection $connection): void
{
$this->info("Received: {$data}");
$connection->send("Echo: {$data}");
}
}
app/Console/Kernel.php:
protected $commands = [
Commands\WebSocketServer::class,
];
php artisan ws:server
const socket = new WebSocket('ws://localhost:8000/chat');
socket.onopen = () => socket.send('Hello');
socket.onmessage = (e) => console.log(e.data);
chat application endpoint to handle messages.DataHandlerInterface to implement custom logic (e.g., broadcast to all connected clients).Server-Side Routing:
$server->registerApplication('notifications', new NotificationHandler());
$server->registerApplication('live-updates', new LiveUpdateHandler());
$server->registerApplication('auth', app(AuthWebSocketHandler::class));
Client Management:
Connection object:
public function onData(string $data, \Wrench\Connection $connection): void
{
$userId = $connection->getExtraData('user_id');
// Store connection in Redis for later broadcasting
}
Connection::getExtraData() to attach metadata (e.g., user IDs, session tokens).Broadcasting:
// In a Laravel event listener
event(new MessageSent($message));
// In a queue job
public function handle()
{
$connections = Redis::smembers('ws:connections');
foreach ($connections as $connectionId) {
$client = new \Wrench\Client('ws://localhost:8000/chat');
$client->connect();
$client->sendData($this->message->data);
$client->disconnect();
}
}
Hybrid HTTP/WebSocket:
Route::get('/ws-proxy', function () {
$server = new \Wrench\Client('ws://localhost:8000/chat');
$server->connect();
return response()->json(['status' => 'connected']);
});
$server->setLogger(app(\Psr\Log\LoggerInterface::class));
try {
$server->run();
} catch (\Exception $e) {
report($e);
$this->error("WebSocket server crashed: " . $e->getMessage());
}
config/websocket.php:
return [
'port' => env('WEBSOCKET_PORT', 8000),
'origins' => explode(',', env('WEBSOCKET_ORIGINS', '*')),
];
Then load it in your server command:
$server = new BasicServer("ws://localhost:{$config['port']}", [
'allowed_origins' => $config['origins'],
]);
Blocking Calls:
onData(). Use queues or async tasks:
public function onData(string $data, \Wrench\Connection $connection): void
{
dispatch(new ProcessWebSocketData($data, $connection));
}
Connection Leaks:
$client->disconnect(); // Critical for resource cleanup
registerShutdownFunction to ensure cleanup on crashes.Origin Restrictions:
allowed_origins will block WebSocket connections. Test with:
'allowed_origins' => ['http://localhost:3000', 'https://yourdomain.com'],
PHP 8.5 Quirks:
random_bytes() is available (it is in PHP 8.5, but test if using custom PHP builds).DataHandlerInterface may require adjustments if extending classes.Large Payloads:
if (strlen($data) > 65536) {
$chunks = str_split($data, 65500);
foreach ($chunks as $chunk) {
$connection->send($chunk);
}
}
Handshake Failures:
Sec-WebSocket-Key headers or malformed responses. Enable verbose logging:
$server->setLogger(new \Monolog\Logger('wrench', [
new \Monolog\Handler\StreamHandler(storage_path('logs/wrench.log'), \Monolog\Logger::DEBUG),
]));
Connection Timeouts:
max_execution_time or use a process manager (e.g., Supervisor) to restart crashed servers.Memory Leaks:
memory_get_usage(). Close idle connections:
$connection->closeIfIdle(30); // Close after 30 seconds of inactivity
Custom Handshake:
\Wrench\Server to validate tokens or enforce auth:
class AuthServer extends \Wrench\BasicServer
{
protected function onHandshake(\Wrench\Connection $connection): void
{
$token = $connection->getHeader('Authorization');
if (!auth()->validateToken($token)) {
$connection->close();
return;
}
parent::onHandshake($connection);
}
}
STOMP Support:
$stompServer = new \Wrench\StompServer('ws://localhost:8000/stomp');
$stompServer->registerApplication('queue', new StompHandler());
Laravel Service Provider:
class WebSocketServiceProvider extends ServiceProvider
{
public function register()
{
$this->app->singleton(WebSocketServer::class, function () {
$server = new BasicServer(config('websocket.url'), config('websocket.options'));
return $server;
});
}
}
Metrics:
$server->onConnection(function (\Wrench\Connection $connection) {
app(\App\Metrics\WebSocketMetrics::class)->incrementConnections();
});
Use reactphp for Async:
use React\EventLoop\Factory;
$loop = Factory::create();
$server = new \Wrench\BasicServer('ws://localhost:8000', [], $loop);
$loop->run();
Load Testing:
websocat or autobahn-testsuite to simulate traffic:
websocat ws://localhost:8000/chat
Docker Integration:
How can I help you explore Laravel packages today?