splash / bridge-builder
Splash Bridge Builder - Package Connectors as Isolated PHAR Workers
Requires
- php: ^8.1
- ext-fileinfo: *
- splash/php-bundle: ^2.0|^3.0
Requires (Dev)
- badpixxel/php-sdk: ^3.0
- symfony/config: ^5.4|^6.4|^7.0
- symfony/console: ^5.4|^6.4|^7.0
- symfony/dependency-injection: ^5.4|^6.4|^7.0
- symfony/finder: ^5.4|^6.4|^7.0
- symfony/flex: ^1.0|^2.0|^7.0
- symfony/form: ^5.4|^6.4|^7.0
- symfony/framework-bundle: ^5.4|^6.4|^7.0
- symfony/http-kernel: ^5.4|^6.4|^7.0
- symfony/monolog-bundle: ^2.0|^3.0
- symfony/twig-bundle: ^5.4|^6.4|^7.0
- symfony/yaml: ^5.4|^6.4|^7.0
This package is auto-updated.
Last update: 2026-04-21 00:16:47 UTC
README
The Bridge Builder packages any Splash connector into an isolated, standalone worker that communicates via JSON-RPC over stdin/stdout. This is the build-time companion to the Bridge Bundle, which consumes these workers at runtime.
Why?
Splash connectors (Shopify, Faker, Mailchimp, etc.) are Symfony bundles with their own dependencies. When migrating to a new stack version, dependency conflicts make it impossible to run legacy connectors in the same process. The Bridge solves this by running each connector in its own isolated PHP process with its own vendor directory.
The Builder takes a connector's source code, wraps it in a minimal Symfony micro-kernel with a JSON-RPC worker loop, installs its dependencies independently, and packages everything into a single self-contained .splx binary.
The result: any app can spawn ./faker.splx and communicate with the connector via stdin/stdout, with zero dependency conflicts, zero PHP installation required on the target, and sub-millisecond runtime latency.
Output formats
| Format | Command | Output | Host requires | Use case |
|---|---|---|---|---|
.splx (default) | vendor/bin/bridge-builder | dist/{name}.splx | Nothing | Production deployment |
.phar (legacy) | vendor/bin/bridge-builder --phar | dist/{name}.phar | PHP | Legacy systems |
| Build dir (dev) | vendor/bin/bridge-builder --dev | dist/{name}/ | PHP | Debug / development |
Only one format is kept in dist/ at a time — switching modes cleans up the previous output.
.splx format
The .splx is a single executable binary that embeds:
- A static PHP interpreter (
micro.sfxfrom static-php-cli) - The connector's isolated Symfony micro-app (compressed)
- The JSON-RPC worker loop
Boot time: ~280ms cold start, then sub-millisecond per RPC call. Binary size: ~17-25 MB (depends on connector dependencies).
Install
composer require --dev splash/bridge-builder
Configure
All configuration lives in your project's composer.json under extra.splash-bridge:
{
"extra": {
"splash-bridge": {
// REQUIRED: unique name for this connector build
// Used for: build dir (dist/{name}/), splx file (dist/{name}.splx),
// and the connector's bridge ID ({version}@{name})
"name": "faker",
// REQUIRED for .splx: exact PHP version to embed in the binary
// Must match an available build on https://dl.static-php.dev/
// Not needed for --phar or --dev modes
"php-version": "8.3.30",
// REQUIRED: the connector class (FQCN) or service ID
// This is the class that implements ConnectorInterface
// The BridgeWorkerBundle auto-detects it via the splash.connector tag
// and wires it into the WorkerService at container warmup
"connector": "Splash\\Connectors\\Faker\\Connectors\\FakeConnector",
// OPTIONAL: override the connector version
// If omitted, uses the version from the connector's getProfile()
// Empty string or --native flag = native mode (no version@ prefix)
"version": "test",
// REQUIRED: files and directories to copy into the build
// Keys = target path in the build dir
// Values = source path relative to the project root
"sources": {
// The connector's composer.json — used to read its "require" section
// so the builder knows which dependencies the connector needs
"composer.json": "vendor/splash/faker/composer.json",
// The connector's source code
// IMPORTANT: the target path must match the PSR-4 namespace structure
// so the Kernel can auto-discover the *Bundle.php file
// Example: Splash\Connectors\Faker\ lives in connector/Faker/
"connector/Faker": "vendor/splash/faker/src",
// OPTIONAL: Symfony config files the connector needs
// (doctrine, twig, splash config, extra bundles, etc.)
// These override the default stubs config
"config/packages/doctrine.yaml": "vendor/splash/faker/tests/config/packages/doctrine.yaml",
"config/packages/splash.yaml": "vendor/splash/faker/tests/config/packages/splash.yaml",
// OPTIONAL: extra bundles the connector requires
// Classes that don't exist in the build are automatically filtered out
"config/bundles.php": "vendor/splash/faker/tests/config/bundles.php"
},
// OPTIONAL: extra dependencies needed for the connector to run standalone
// These are NOT in the connector's own require (they come from the host app)
// They go into require-dev of the generated composer.json
"require": {
// Symfony components (the connector relies on the host app for these)
"symfony/console": "^6.4",
"symfony/http-kernel": "^6.4",
"symfony/framework-bundle": "^6.4",
"symfony/twig-bundle": "^6.4",
"symfony/translation": "^6.4",
"symfony/routing": "^6.4",
"symfony/form": "^6.4",
"symfony/yaml": "^6.4",
"symfony/runtime": "^6.4",
"symfony/monolog-bundle": "*",
// Doctrine (if the connector uses entities)
"doctrine/orm": "^2.6",
"doctrine/doctrine-bundle": "^1.9|^2.0",
// Logging
"monolog/monolog": "^2.0|^3.0",
// PHP extensions required at runtime
"ext-pdo_sqlite": "*"
}
}
},
"scripts": {
// Commands to run in the build directory after composer install
// The build dir is a full Symfony project with bin/console available
"post-build-cmd": [
"php bin/console cache:clear --env=prod --no-debug",
"php bin/console doctrine:schema:update --env=prod --force --complete --no-interaction --no-debug",
"php bin/console assets:install --env=prod"
]
}
}
Command-line options
| Flag | Effect |
|---|---|
| (none) | Build .splx binary (default, recommended) |
--phar | Build .phar archive instead (legacy, requires PHP on target) |
--dev | Keep the build dir, skip packaging (for debugging) |
--native | Native mode — no version@ prefix, connector registers with its real name |
--publish | Upload built files to the GitLab Generic Package Registry (CI only) |
The --native and --publish modifiers can combine with any mode (e.g. --native --publish).
All .splx builds require extra.splash-bridge.php-version to be set.
Build
# Default: produces dist/faker.splx (autonomous binary)
php -d phar.readonly=0 vendor/bin/bridge-builder
# Legacy: produces dist/faker.phar (requires PHP on target)
php -d phar.readonly=0 vendor/bin/bridge-builder --phar
# Dev: keeps the build dir for debugging
vendor/bin/bridge-builder --dev
-d phar.readonly=0is only needed for.phar/.splxmodes (PHP needs permission to write PHAR archives).
Build pipeline
========================================
Splash Bridge Builder
========================================
Name: faker
Connector: Splash\Connectors\Faker\Connectors\FakeConnector
Format: splx
[1/8] Creating project structure <- Copies stubs + connector sources
[2/8] Composer install <- Generates composer.json, installs deps
[3/8] Configuring worker <- Injects connector FQCN into services.yaml
[4/8] Post-build scripts <- Runs post-build-cmd (cache, DB, assets)
[5/8] Generating manifest <- Queries the worker for profile (5 locales)
[6/8] Verifying build <- Sends a ping to verify everything works
[7/8] Packaging .phar <- Builds the .phar (splx + phar modes)
[8/8] Building .splx binary <- Concatenates micro.sfx + phar (splx only)
========================================
Build complete!
========================================
Step counts differ per mode:
--dev: 6 steps (no packaging)--phar: 7 steps (no splx)- default: 8 steps (full pipeline)
Output layout
dist/
faker.splx <- Default: single autonomous binary (~17 MB)
faker.phar <- With --phar: PHAR archive (~6 MB, needs PHP)
faker/ <- With --dev: debuggable build dir
worker.php <- Entry point: stdin/stdout JSON-RPC loop
manifest.json <- Connector metadata with multi-locale title/description
composer.json <- Generated composer.json
bin/console <- Symfony console (for debugging)
vendor/ <- Isolated dependencies
bridge/ <- BridgeWorker runtime
connector/ <- Connector sources (auto-discovered by Kernel)
config/ <- Symfony config
var/ <- Cache + database (after post-build scripts)
.cache/ <- Downloaded micro.sfx for .splx builds (reusable)
Testing the worker manually
# .splx binary (no PHP needed)
echo '{"id":"1","method":"ping","params":[]}' | ./dist/faker.splx
# .phar archive
echo '{"id":"1","method":"ping","params":[]}' | php dist/faker.phar
# Build dir (dev mode)
echo '{"id":"1","method":"ping","params":[]}' | php dist/faker/worker.php
# Full session
printf '{"id":"1","method":"ping","params":[]}\n{"id":"2","method":"getAvailableObjects","params":[]}\n{"id":"99","method":"__shutdown__","params":[]}\n' | ./dist/faker.splx
Expected output: one JSON line per request, plus a ready signal line first:
{"jsonrpc":"2.0","id":"__ready__","result":{"name":"faker","version":"test"},...}
{"jsonrpc":"2.0","id":"1","result":true,...}
{"jsonrpc":"2.0","id":"2","result":["short","simple",...],...}
Base64-encoded input
For payloads containing binary data, control characters, newlines, or UTF-8 that may conflict with the line-delimited JSON protocol, the worker supports base64 encoding on both input and output.
Prefix a request line with base64: followed by the base64-encoded JSON:
# Regular JSON-RPC line
{"id":"1","method":"ping","params":[]}
# Same request, base64-encoded
base64:eyJpZCI6IjEiLCJtZXRob2QiOiJwaW5nIiwicGFyYW1zIjpbXX0=
Example:
REQUEST='{"id":"1","method":"ping","params":[]}'
ENCODED=$(echo -n "$REQUEST" | base64 -w0)
echo "base64:$ENCODED" | ./dist/faker.splx
Symmetry guarantee: The worker replies in the same format as the request. A base64:-prefixed request gets a base64:-prefixed response, a raw JSON request gets a raw JSON response. This lets clients choose per-call which encoding they want.
The ready signal at worker startup is always raw JSON (the worker doesn't know yet which encoding the client will use).
The Bridge Bundle uses base64 encoding by default for every RPC call to avoid any escaping issues with large payloads or binary data.
Publishing to GitLab Package Registry
Add --publish to the build command to upload the built artifacts directly to the current project's GitLab Generic Package Registry at the end of the build. No manual curl in .gitlab-ci.yml needed.
# .gitlab-ci.yml
script:
- php -d phar.readonly=0 vendor/bin/bridge-builder --native --publish
- php -d phar.readonly=0 vendor/bin/bridge-builder --publish
Upload target
Every *.splx and *.phar in dist/ is pushed to:
{CI_API_V4_URL}/projects/{CI_PROJECT_ID}/packages/generic/connector/{VERSION}/{FILENAME}
Where {VERSION} is CI_COMMIT_TAG if set, otherwise CI_COMMIT_REF_NAME.
Skip conditions
The publish step skips cleanly (build stays successful) when:
- Running outside GitLab CI (missing env vars) — allows local
--publishwithout failing CI_PIPELINE_SOURCE=schedule— scheduled pipelines don't re-upload- Branch is not protected AND not a tag — prevents accidental publishes from feature branches
Only tags and protected branches (main, release/*, etc.) actually publish.
Error handling
Each file upload's HTTP status is checked:
- 2xx → logged with ✔
- 4xx / 5xx / transport error → logged with ✘, the publish step exits with non-zero so the CI job fails
How it works
- You declare what connector to package in your
composer.json(extra.splash-bridge) - The builder creates a mini Symfony project with the connector sources, a micro-kernel, and a JSON-RPC worker loop
- Composer installs the connector's dependencies in isolation (separate vendor directory)
- Post-build scripts run to warm up the cache, initialize the database, etc.
- The manifest is generated by querying the worker in 5 locales (en/fr/it/es/de) so translations are embedded
- The worker is verified with a configure ping to make sure everything boots correctly
- The build is packaged as a
.phar, then concatenated withmicro.sfxinto a.splxbinary
The Bridge Bundle then spawns this worker via proc_open, sends JSON-RPC requests on stdin, and reads responses from stdout. From the ConnectorsManager's perspective, it's just another connector.
Reverse callbacks
During an RPC call, the worker can send callbacks back to the bridge on stdout:
getFile— fetch a file from the bridge hostevent.commit/event.id_changed/event.config_update— dispatch events synchronouslyping— POC callback
The bridge detects these interleaved messages, dispatches them to handlers, and writes the response back to the worker's stdin. The worker blocks until it receives the response, then continues.
This allows the isolated worker to request resources (files, config) from the main app without breaking isolation.
Architecture
Php-BridgeBuilder/
src/ <- Splash\BridgeBuilder namespace
Collectors/
ConfigCollector.php <- Reads extra.splash-bridge from composer.json
Builders/
ProjectBuilder.php <- Step 1: copies stubs + sources
ComposerBuilder.php <- Step 2: generates composer.json + install
WorkerBuilder.php <- Step 3: injects connector FQCN into services.yaml
ScriptsBuilder.php <- Step 4: runs post-build-cmd
ManifestBuilder.php <- Step 5: generates manifest.json (multi-locale)
VerifyBuilder.php <- Step 6: ping verification
PharBuilder.php <- Step 7: packages into .phar (GZ compressed)
SplxBuilder.php <- Step 8: downloads micro.sfx + concatenates into .splx
stubs/ <- Template files copied into every build
bridge/ <- Splash\BridgeWorker namespace (runtime code)
Kernel.php <- MicroKernel with 3-step bundle loading
BridgeWorkerBundle.php <- CompilerPass wires WorkerService to connector
Services/
WorkerService.php <- Public entry point (connector injected via DI)
WorkerRunner.php <- stdin/stdout JSON-RPC loop
RpcResponseBuilder.php<- Encodes responses + Splash logs
CallbackService.php <- Sends reverse callbacks to the bridge
LocaleContext.php <- Holds current locale for translations
TransportService.php <- stdin/stdout I/O with idle timeout
Dispatcher/
ProfileDispatcher.php <- Translates title/label + encodes ico as base64
FormDispatcher.php <- Describes config form for the bridge
TemplateDispatcher.php <- Renders connector templates
ConfigDispatcher.php <- Handles configure RPC + locale
ActionDispatcher.php <- Executes public/secured actions
Listener/
FileCallbackListener.php <- Intercepts ObjectFileEvent → getFile callback
CommitCallbackListener.php <- Intercepts ObjectsCommitEvent → event.commit callback
IdChangedCallbackListener.php <- Intercepts ObjectsIdChangedEvent
ConfigUpdateCallbackListener.php <- Intercepts UpdateConfigurationEvent
Dictionary/
ProtocolMessages.php <- JSON-RPC + callback protocol constants
DependencyInjection/
Compiler/
WorkerConnectorPass.php <- Auto-detects splash.connector service
Resources/config/
services.yaml <- Autowires Worker classes
worker.php <- Bootstrap: boots Kernel → WorkerService::run()
bin/bridge-builder <- CLI entry point
License
MIT - See LICENSE