Derafu Backbone
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
Mark the operations
#[Operation] on the methods that are meant to be called from outside. Nothing else is exposed.
Call it by its id
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
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.
#[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.
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.
POST: there is no distinction between reading and writing.
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.