splash/bridge-builder

Splash Bridge Builder - Package Connectors as Isolated PHAR Workers

Maintainers

Package info

gitlab.com/SplashTools/bridge-builder

Issues

pkg:composer/splash/bridge-builder

Statistics

Installs: 244

Dependents: 7

Suggesters: 0

Stars: 0

3.0.x-dev 2026-04-21 02:16 UTC

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

FormatCommandOutputHost requiresUse case
.splx (default)vendor/bin/bridge-builderdist/{name}.splxNothingProduction deployment
.phar (legacy)vendor/bin/bridge-builder --phardist/{name}.pharPHPLegacy systems
Build dir (dev)vendor/bin/bridge-builder --devdist/{name}/PHPDebug / 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.sfx from 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

FlagEffect
(none)Build .splx binary (default, recommended)
--pharBuild .phar archive instead (legacy, requires PHP on target)
--devKeep the build dir, skip packaging (for debugging)
--nativeNative mode — no version@ prefix, connector registers with its real name
--publishUpload 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=0 is only needed for .phar / .splx modes (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 --publish without 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

  1. You declare what connector to package in your composer.json (extra.splash-bridge)
  2. The builder creates a mini Symfony project with the connector sources, a micro-kernel, and a JSON-RPC worker loop
  3. Composer installs the connector's dependencies in isolation (separate vendor directory)
  4. Post-build scripts run to warm up the cache, initialize the database, etc.
  5. The manifest is generated by querying the worker in 5 locales (en/fr/it/es/de) so translations are embedded
  6. The worker is verified with a configure ping to make sure everything boots correctly
  7. The build is packaged as a .phar, then concatenated with micro.sfx into a .splx binary

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 host
  • event.commit / event.id_changed / event.config_update — dispatch events synchronously
  • ping — 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