roadrunner/api-dto
roadrunner/api-dto provides typed Data Transfer Objects for Laravel APIs. Define DTOs to convert request arrays into structured objects, validate and cast attributes, and handle nested data, keeping controllers lean and API payloads consistent and type-safe. Designed to keep API code organized.
This package is the wire contract layer for RoadRunner's gRPC API: it ships the canonical .proto definitions plus pre-generated PHP classes under RoadRunner\Api\V1\*. You reach for it when you're building a RoadRunner plugin, a custom worker that speaks the RR API directly, or anything that needs to encode/decode Payload, Response, KV, Jobs, Metrics, Lock, or Informer messages without hand-rolling protobuf.
Heads-up as of v2.1.0: the package was renamed from roadrunner-php/roadrunner-api-dto to roadrunner/api-dto. The generated namespaces (RoadRunner\Api\V1\*) and the proto/ layout are unchanged — only the Composer package name moved. Install under the new name:
composer require roadrunner/api-dto
If you're already on ^2.0, treat the rename as a lockfile-level change: update the require key in your composer.json (and any replace/provide entries, CI allowlists, or dependency-review configs that reference the old name) and run composer update roadrunner/api-dto. Existing imports keep working.
You need ext-protobuf (and ext-grpc if you'll use the generated *Client stubs, since they extend Grpc\BaseStub). Verify with php -m | grep -E 'protobuf|grpc'.
First things to look at, in order:
proto/ — the source of truth for every message and service. Read the .proto file for your target service before the PHP class; field numbers and oneof groupings are clearer there.src/Api/V1/ (generated) — mirror of the protos in PHP. Class names map 1:1 (Payload, Response, KV\Item, Jobs\Job, etc.).*Client classes for the service you need — e.g. RoadRunner\Api\V1\KV\KVClient.First use case — encode a payload for a service call:
use RoadRunner\Api\V1\Payload;
$payload = new Payload();
$payload->setBody('hello');
$bytes = $payload->serializeToString();
// Or over gRPC:
$client = new \RoadRunner\Api\V1\KV\KVClient(
'unix:///tmp/rr.sock',
['credentials' => \Grpc\ChannelCredentials::createInsecure()]
);
Treat DTOs as dumb wire objects, wrap them. Don't pass Payload/Response around your domain. Convert at the boundary: build a DTO, serialize, send; deserialize, then map to your own value objects. Generated classes have no validation and mutable setters, so leaking them into business logic invites inconsistent state.
Construction and nested messages. Message-typed fields must be assigned whole objects:
$item = new \RoadRunner\Api\V1\KV\Item();
$item->setKey('user:1');
$item->setValue((new Payload())->setBody($json));
Don't try to mutate $item->getValue()->setBody(...) inline unless getValue() returns a non-null instance — with proto3 it returns the default empty message, not null, which is a common source of silent bugs.
Repeated fields. getHeaders()/getItems() return a RepeatedField. Prefer the setter with an array — setHeaders(['a' => 'b']) — over mutating the RepeatedField in place; the setter is clearer and serializes predictably.
Sockets and transport. RoadRunner exposes its API on a Unix socket; the path is typically in RR_RELAY. Use the unix:// scheme for gRPC:
$socket = getenv('RR_RELAY') ?: 'unix:///tmp/rr.sock';
spiral/roadrunner-worker and spiral/goridge handle framing and socket discovery; combine them with these DTOs rather than reinventing the protocol. The DTO is the message, not the frame.
Serving a plugin. If you're implementing a RoadRunner plugin in PHP, prefer spiral/roadrunner-grpc for the server side and use the generated message classes from this package as your handlers' argument/return types. On the client side, the generated *Client stubs here should be used as-is.
Version pinning. These protos track the RoadRunner binary. Pin the package version in lockstep with your RR release (e.g. 2024.x package with 2024.x binary). When you bump RR, bump this package in the same commit — mismatched proto versions manifest as Unexpected wire type decode errors that are painful to trace.
Migration across the rename. If a third-party package (or your own split-out library) still requires roadrunner-php/roadrunner-api-dto, you can end up with two entries for what is now one project. Check with composer why roadrunner-php/roadrunner-api-dto and composer why roadrunner/api-dto; a duplicate means an upstream pin needs bumping, not a replace hack. Keep the rename in its own commit so a lockfile diff is easy to audit.
Testing. Round-trip assertions are cheap and catch field-number mistakes:
$parsed = new Payload();
$parsed->mergeFromString($payload->serializeToString());
$this->assertSame('hello', $parsed->getBody());
composer require snippets still say roadrunner-php/roadrunner-api-dto. On v2.1.0+ prefer roadrunner/api-dto; if you copy a snippet, translate the require line but leave the use statements alone — the namespace did not change.ext-protobuf, serializeToString() throws at runtime, not install time. Add an extension check to CI.ext-grpc is optional but load-bearing. If you don't use the generated clients, you can skip it — but the composer autoloader still resolves the client classes, so any code touching them will fatal.getFoo() on an unset message field returns a default instance, and scalar fields return 0/''/false. Use hasFoo() for message presence and has-less sentinels (e.g. empty string) for scalars. Never assume a returned message means it was set.oneof semantics. Only the last-set member of a oneof survives serialization. If you build a message by setting multiple branches "just in case," you'll silently lose data.composer update. Regenerate from proto/ if you must patch; better yet, vendor the proto and generate into your own namespace.print_r on a DTO dumps internal protobuf plumbing. Use serializeToJsonString() (and mergeFromJsonString()) for human-readable inspection; be aware JSON mapping renames fields to camelCase.bytes fields become base64 and int64 may become strings in JSON. For anything that must be exact, stick to binary..proto files into your repo and generating with protoc-gen-php / protoc-gen-php-grpc into a separate namespace; don't fork this package — and now that the name has changed once, forking is an even worse bet. Wrap the generated classes behind your own interfaces so a future rename or proto change is one adapter away (this release is exactly the scenario that interface insulates you from).Connection refused, Permission denied) are env issues, not DTO issues — verify RR_RELAY, file mode, and that the RR server is actually running before debugging serialization.How can I help you explore Laravel packages today?