anandpilania / php-node-bridge
Bidirectional bridge between PHP and Node.js — call Node.js handlers from PHP and vice versa. Framework agnostic with Laravel and Symfony integrations.
Requires
- php: ^8.1
Requires (Dev)
- laravel/framework: ^10.0|^11.0|^12.0|^13.0
- symfony/config: ^6.0|^7.0
- symfony/console: ^6.0|^7.0
- symfony/dependency-injection: ^6.0|^7.0
- symfony/http-kernel: ^6.0|^7.0
README
Bidirectional bridge between PHP and Node.js — call Node.js handlers from PHP and PHP handlers from Node.js. Framework agnostic with first-class Laravel and Symfony support.
Works with: Laravel, Symfony, Slim, Zend/Laminas, vanilla PHP ↔ Express, Next.js, Nuxt.js, Fastify, NestJS.
How It Works
Your PHP App Your Node App
(Laravel / Symfony / Slim) (Express / Next / Nuxt)
PhpBridge ◄──── HTTP ────► NodeBridge
:5556 :5555
register() ← Node can call register() ← PHP can call
call() → calls Node call() → calls PHP
Both sides register named handlers and communicate over local HTTP. JSON payloads, shared-secret auth, timeout handling, middleware support.
Installation
composer require anandpilania/php-node-bridge
Quick Start
Vanilla PHP
<?php require 'vendor/autoload.php'; use PhpNodeBridge\Bridge\PhpBridge; $bridge = new PhpBridge([ 'port' => 5556, 'nodePort' => 5555, 'secret' => 'my-secret', ]); // Register handlers Node.js can call $bridge ->register('invoice.create', function (array $payload): array { // your DB logic here return ['id' => 123, 'total' => 99.99]; }) ->register('user.find', function (array $payload): array { return ['id' => $payload['id'], 'name' => 'Alice']; }); // Call Node.js (e.g. generate a PDF) $pdf = $bridge->call('pdf.generate', [ 'template' => 'invoice', 'data' => ['invoiceId' => 123], ]); // Start listener (blocking — run as a separate daemon) $bridge->listen();
Laravel Integration
1. Install & auto-discover
The service provider is auto-discovered via composer.json. Just install and publish config:
composer require anandpilania/php-node-bridge php artisan vendor:publish --tag=bridge-config
2. Configure .env
BRIDGE_PORT=5556 NODE_BRIDGE_PORT=5555 NODE_BRIDGE_HOST=127.0.0.1 BRIDGE_SECRET=your-super-secret-key BRIDGE_TIMEOUT=10
3. Register handlers
// app/Providers/AppServiceProvider.php public function boot(PhpBridge $bridge): void { $bridge->register('invoice.create', function (array $p): array { return Invoice::create($p)->toArray(); }); }
4. Use in controllers
use PhpNodeBridge\Laravel\Bridge; // Facade class DocumentController extends Controller { public function sign(Request $request) { $result = Bridge::call('esign.sign', $request->all()); return response()->json($result); } public function sendEmail(Request $request) { Bridge::fire('email.send', $request->all()); // fire-and-forget return response()->json(['queued' => true]); } }
5. Start the bridge daemon
php artisan bridge:listen
Symfony Integration
1. Register bundle
// config/bundles.php return [ PhpNodeBridge\Symfony\PhpNodeBridgeBundle::class => ['all' => true], ];
2. Configure
# config/packages/php_node_bridge.yaml php_node_bridge: port: '%env(int:BRIDGE_PORT)%' node_port: '%env(int:NODE_BRIDGE_PORT)%' node_host: '%env(NODE_BRIDGE_HOST)%' secret: '%env(BRIDGE_SECRET)%' timeout: 10 log_level: 'info'
3. Inject and use
use PhpNodeBridge\Bridge\PhpBridge; class DocumentController extends AbstractController { public function __construct(private PhpBridge $bridge) {} #[Route('/api/pdf', methods: ['POST'])] public function pdf(Request $request): JsonResponse { $result = $this->bridge->call('pdf.generate', json_decode($request->getContent(), true)); return $this->json($result); } }
4. Start daemon
php bin/console bridge:listen
API Reference
new PhpBridge(config)
| Option | Type | Default | Description |
|---|---|---|---|
port |
int | 5556 |
Port this PHP bridge listens on |
nodePort |
int | 5555 |
Port the Node bridge listens on |
nodeHost |
string | 127.0.0.1 |
Node bridge host |
secret |
string | null |
Shared secret for auth (recommended) |
timeout |
int | 10 |
Default call timeout in seconds |
logLevel |
string | 'info' |
silent/error/warn/info/debug |
$bridge->register(string $name, callable $fn): static
Register a handler Node.js can call.
$bridge->registerAll(array $handlers): static
Register multiple handlers: ['name' => fn, ...].
$bridge->unregister(string $name): static
Remove a handler.
$bridge->call(string $handler, array $payload, ?int $timeout): mixed
Call a handler on the Node.js side.
$bridge->fire(string $handler, array $payload): static
Fire-and-forget call to Node.js.
$bridge->use(callable $fn): static
Add middleware: function(string $name, array $payload): void. Throw to reject.
$bridge->listen(): void
Start listening (blocking). Run in a separate daemon process.
$bridge->listHandlers(): string[]
List all registered handler names.
Exceptions
| Class | When |
|---|---|
BridgeException |
Base — connection failure, generic errors |
HandlerNotFoundException |
Requested handler not registered |
TimeoutException |
Call exceeded timeout |
AuthException |
Wrong or missing shared secret |
Running as a Daemon
Supervisor (recommended)
; /etc/supervisor/conf.d/php-bridge.conf [program:php-node-bridge] command=php /var/www/html/artisan bridge:listen directory=/var/www/html autostart=true autorestart=true startretries=3 user=www-data stdout_logfile=/var/log/supervisor/bridge.log stderr_logfile=/var/log/supervisor/bridge-err.log
systemd
[Unit] Description=PHP Node Bridge After=network.target [Service] User=www-data WorkingDirectory=/var/www/html ExecStart=/usr/bin/php artisan bridge:listen Restart=always [Install] WantedBy=multi-user.target
Docker Compose
services: php: build: ./php-app environment: NODE_BRIDGE_HOST: node BRIDGE_SECRET: ${BRIDGE_SECRET} ports: - "8000:8000" - "5556:5556" node: build: ./node-app environment: PHP_BRIDGE_HOST: php BRIDGE_SECRET: ${BRIDGE_SECRET} ports: - "3000:3000" - "5555:5555"
Testing
php tests/run.php
License
MIT