Derafu Backbone

Write an operation once. Call it from HTTP, the command line and Python.

Your library, callable by anyone

The methods of a Backbone library are ordinary, typed PHP methods: great for a PHP caller, and of no use to anything else.

Backbone Dispatcher adds one generic layer on top. An operation is called by its id, with plain parameters, and it always answers with a result that any caller can read. The HTTP API, the console and the Python bridge are doors on that layer, so you write no controller and no command per operation.

How it works

Four steps, and only the first two are code you write.

Structure your library

Packages hold components, and components hold workers. PHP attributes tell Backbone where each one is.

Mark the operations

Put #[Operation] on the methods that are meant to be called from outside. Nothing else is exposed.

Call it by its id

An id such as billing.invoice.builder::build and a plain array of parameters. They are checked against the real method, and the call never throws.

Open the doors you need

HTTP with an OpenAPI 3.1 description, a console command for each operation, and a call from Python.

One operation, every door

The same operation, called three ways.

// A normal class with a normal method. In a package called "billing", a component
// called "invoice" and a worker called "builder", this is the operation
// billing.invoice.builder::build, and #[Operation] is all it takes to expose it.
class InvoiceBuilderWorker extends AbstractWorker implements WorkerInterface
{
    #[Operation]
    public function build(string $number, int $amount): array
    {
        // Your business logic.
    }
}

// Only the methods marked with #[Operation] can be called from outside.
// You can also allow a list of ids instead, with wildcards: "billing.invoice.*".
# One generic route: the path says which operation to call.
# /api/:package/:component/:worker/:operation
POST /api/billing/invoice/builder/build
Content-Type: application/json

{ "parameters": { "number": "F-001", "amount": 15000 } }

# The answer:
{ "meta": { "timestamp": 1755500000.123456, "data_type": "array" },
  "data": { "id": "DR-001" } }

# The API describes itself:
GET /api/openapi-docs.json       the OpenAPI 3.1 description of every exposed operation
GET /api/billing/invoice         the workers of a component, as linked resources (HATEOAS)
# Every operation is a command. The id billing.invoice.builder::build becomes:
bin/console billing:invoice:builder:build request.json

# The request is a file or STDIN, in JSON, YAML or XML:
echo '{"parameters": {"number": "F-001", "amount": 15000}}' \
    | bin/console billing:invoice:builder:build

# The answer goes to STDOUT in the same format, or to a file:
bin/console billing:invoice:builder:build request.json --output=result.yaml

# A command is built only when it is needed, never all of them upfront,
# so a library can have hundreds of operations.
bin/console help billing:invoice:builder:build
from derafu_backbone_bridge import GenericDispatcher

# The PHP class of yours that boots your library, and where its autoloader is.
dispatcher = GenericDispatcher(
    'App\\Bootstrap',
    autoload_path='/path/to/vendor/autoload.php',
)

# The same id, with the parameters as keyword arguments.
result = dispatcher.dispatch(
    'billing.invoice.builder::build',
    number='F-001',
    amount=15000,
)

result.value      # {'id': 'DR-001'}, as plain Python data.
result.data_type  # 'array'
result.metadata   # Time, memory and process of the call.
# A call never ends in a raw PHP exception. A failure is a problem description
# (shaped after RFC 7807) that each door shows in its own way.

{ "type": "about:blank",
  "title": "MissingParameterException",
  "detail": "...",
  "instance": "billing.invoice.builder::build",
  "extensions": { "...": "..." } }

HTTP     status 403 if the operation is not allowed, 422 if a parameter is missing
         or has the wrong type, 500 for anything else. The body is the description.
CLI      exit code 0 on success; 10 to 16 for the generic errors of the dispatcher;
         65 if the request cannot be parsed; 66 if its file does not exist.
Python   an exception, chosen from the PHP class of the failure.

Frequently asked questions

What it does, and what it leaves to you.

Yes. The operations are the methods of Backbone workers, found through the registry of your packages, and the dispatcher, the API, the console and the Python bridge all work on that. In return, a library organized this way needs no more code to be reachable.

No. By default, only the methods marked with #[Operation] can be called, or the ids that you list instead. A public method that has no mark is not reachable, even if it exists.

The safe dispatcher never throws. A failure becomes a problem description, and each door turns it into its own signal: an HTTP status, a console exit code or a Python exception. You can add your own exit codes for your business exceptions in the console. The HTTP status mapping is fixed for now.

Python 3.14 or later, and the phpy extension of Swoole, which needs a PHP built with --enable-embed and installed in the system: it is not installed with pip. It works on Linux and macOS, not on Windows.

No. It brings the controller and the routing logic, and you mount the controller in your own application, with the HTTP response factory you already use. Every operation is a POST: there is no distinction between reading and writing.

PHP 8.5 or later. The Derafu packages are published as dev-main for now.

Where it fits

Other good tools exist. Choose by how you work.

Approach Examples Choose it when Backbone instead
Command buses Tactician, SimpleBus You need to dispatch messages inside your own application. The call is made from outside: by id, from HTTP, the console or another language.
One class, several entry points Laravel Actions You work in Laravel and want a class to be a controller, a job and a command. It is not tied to a framework, and the routes and the commands are not declared one by one.
Resource-driven APIs API Platform Your API is a set of resources with their own state and rules. The API is the business operations you mark, and it is also a command and a Python call.

Install it

Start with the base and the dispatcher, then add the doors you need.

# The Derafu packages are published as dev-main for now, so your composer.json needs:
#   "minimum-stability": "dev", "prefer-stable": true
composer require derafu/backbone derafu/backbone-dispatcher

# The doors:
composer require derafu/backbone-api        # HTTP
composer require derafu/backbone-console    # command line
pip install derafu-backbone-bridge          # Python

Next: the documentation explains the structure, and each door has its own page.

Ready to write an operation once?

Start with the structure of a Backbone library, then open the doors one at a time.