Skip to content

gRPC plugin contract - #1

Draft
roxblnfk wants to merge 5 commits into
masterfrom
grpc
Draft

gRPC plugin contract#1
roxblnfk wants to merge 5 commits into
masterfrom
grpc

Conversation

@roxblnfk

@roxblnfk roxblnfk commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

The second dispatcher surface. Host side is ConnectRPC: one registration serves native gRPC, gRPC-Web and Connect (proto + JSON). Whatever the client spoke, PHP sees canonical binary protobuf — framing, compression, grpc-timeout, per-protocol error encoding never cross the boundary.

Worker loop

$grpc = \Rapira\get_dispatcher();
assert($grpc instanceof \Rapira\Grpc\GrpcDispatcher);

foreach ($grpc->getServices() as $service) {
    // ServiceInfo → MethodInfo{name, inputType, outputType, MethodKind}, resolved at boot
    $router->register($service, $container->get($service->name));
}

try {
    while (true) {
        $call = $grpc->receive();            // Call&Responder — reading half + answering half
        try {
            $router->dispatch($call);
        } catch (\Rapira\Grpc\Exception\GrpcException $e) {
            $call->fail($e->status);         // google.rpc.Status triple; host encodes per protocol
        }
    }
} catch (\Rapira\Exception\ClosedException) {
    // drained
}

Dispatch: two instanceof, not four kinds

Request and response shapes are independent facts of the .proto, so each is its own interface pair. MethodKind projects onto the same axes at boot via isStreamingRequest() / isStreamingResponse().

$in = $call instanceof Call\StreamingRequest
    ? $call->getMessages()                   // MessageStream: one forward pass, ends at half-close
    : $call->getMessage();                   // string, binary protobuf

$call instanceof Responder\StreamingResponse
    ? $call->respond($stream($in))           // \Generator, drained inside the call
    : $call->respond($out->serializeToString());

Streaming

// bidi = composition: the generator reads the inbound stream between yields
$call->respond((static function () use ($call, $service): \Generator {
    foreach ($call->getMessages() as $bytes) {
        yield $service->answer($bytes)->serializeToString();
    }
})());
  • respond(\Generator) returns when the stream terminates; backpressure = generator not resumed.
  • Inbound end = client half-close = end of iteration, never an exception. Not pulling = flow control.
  • Client gone → generator destroyed, finally runs, respond() returns normally. No cancellation token API.
  • GrpcException escaping mid-stream becomes the terminal status; anything else is a bug → sanitized INTERNAL, worker recycled.

Context and metadata

$ctx = $call->getContext();                  // method, Metadata, ?deadline, remote, Protocol, receivedAt

$rm = $call->getResponseMetadata();          // mutable accumulator, the only home
$rm->addHeader('x-request-cost', '2.7');     // commits at first yield
$rm->addTrailer('x-cache', 'hit');           // commits at termination
$rm->addBinaryHeader('x-token-bin', $bytes); // `-bin` discipline enforced by method, not by caller care

Call / Responder split is a privilege ladder: Context reads data, Call adds Work facts, Responder answers without reading; receive() hands out the intersection. Namespaces follow one rule — the directory answers who gives you the object: Call\{Context, Protocol, MessageStream}, Responder\ResponseMetadata; what both sides hand out (Metadata) or the user constructs (Status, descriptors) lives at the root.

Full rationale, including everything deliberately left out: README § gRPC.

Assisted-By: Claude Fable 5 <noreply@anthropic.com>
Assisted-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 10, 2026

Copy link
Copy Markdown

Review Change Stack

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 4dfe220c-deca-4fcc-89bb-b446ab63876d

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
📝 Walkthrough

Summary by CodeRabbit

  • New Features

    • Added comprehensive gRPC support for unary, server-streaming, client-streaming, and bidirectional calls.
    • Added call context details, request and response metadata, service discovery, method descriptions, and protocol information.
    • Added structured status handling, error details, cancellation, deadlines, and gRPC-specific exceptions.
  • Documentation

    • Expanded contract documentation covering gRPC, Connect, streaming behavior, metadata, worker suspension, errors, and HTTP deadlines.
    • Clarified shared address semantics and remote endpoint information.

Walkthrough

The PR adds a public gRPC contract with descriptor-based dispatch, unary and streaming call APIs, immutable context, metadata handling, response lifecycle controls, status values, and gRPC-specific exceptions. The README documents these contracts and related suspension and address rules.

Changes

gRPC contract

Layer / File(s) Summary
Dispatch and method descriptors
src/Grpc/GrpcDispatcher.php, src/Grpc/GrpcDispatcherInfo.php, src/Grpc/MethodKind.php, src/Grpc/MethodInfo.php, src/Grpc/ServiceInfo.php, README.md
The gRPC dispatcher now defines call reception, service discovery, dispatcher information, and descriptor-based method classification.
Call context and request input
src/Grpc/Call.php, src/Grpc/Call/Context.php, src/Grpc/Call/Protocol.php, src/Grpc/Call/UnaryRequest.php, src/Grpc/Call/StreamingRequest.php, src/Grpc/Call/MessageStream.php, src/Grpc/Metadata.php, README.md
Call APIs now expose immutable context, unary request messages, streaming request messages, protocols, and normalized metadata.
Response and metadata lifecycle
src/Grpc/Responder.php, src/Grpc/Responder/ResponseMetadata.php, src/Grpc/Responder/UnaryResponse.php, src/Grpc/Responder/StreamingResponse.php, README.md
Responder APIs now support unary responses, generator-based streaming responses, response metadata, call failure, backpressure, cancellation, and finalization rules.
Status, errors, and shared documentation
src/Grpc/StatusCode.php, src/Grpc/Status.php, src/Grpc/ErrorDetail.php, src/Grpc/Exception/GrpcException.php, src/Grpc/Exception/HeadersAlreadyCommittedError.php, src/InetAddress.php, README.md
The contract adds gRPC status codes, packed error details, status-carrying exceptions, committed-header errors, and related address and exception documentation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant GrpcDispatcher
  participant Application
  participant Responder
  Client->>GrpcDispatcher: Send RPC
  GrpcDispatcher->>Application: Provide Call and Responder
  Application->>Responder: respond(message) or respond(generator)
  Responder-->>Client: Send response metadata and status
``

</details>

<!-- walkthrough_end -->
<!-- pre_merge_checks_walkthrough_start -->

<details>
<summary>🚥 Pre-merge checks | ✅ 5</summary>

<details>
<summary>✅ Passed checks (5 passed)</summary>

|         Check name         | Status   | Explanation                                                                                                                                            |
| :------------------------: | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------- |
|     Docstring Coverage     | ✅ Passed | Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.                                                                   |
|     Linked Issues check    | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                               |
| Out of Scope Changes check | ✅ Passed | Check skipped because no linked issues were found for this pull request.                                                                               |
|         Title check        | ✅ Passed | The title clearly identifies the pull request's main change: introducing the gRPC plugin contract.                                                     |
|      Description check     | ✅ Passed | The description directly explains the gRPC dispatcher contract, streaming behavior, metadata, context, and error handling introduced by the changeset. |

</details>

</details>

<!-- pre_merge_checks_walkthrough_end -->
<!-- finishing_touch_checkbox_start -->

<details>
<summary>✨ Finishing Touches</summary>

<details>
<summary>📝 Generate docstrings</summary>

- [ ] <!-- {"checkboxId": "7962f53c-55bc-4827-bfbf-6a18da830691"} --> Create stacked PR
- [ ] <!-- {"checkboxId": "3e1879ae-f29b-4d0d-8e06-d12b7ba33d98"} --> Commit on current branch

</details>
<details>
<summary>🧪 Generate unit tests (beta)</summary>

- [ ] <!-- {"checkboxId": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Create PR with unit tests
- [ ] <!-- {"checkboxId": "6ba7b810-9dad-11d1-80b4-00c04fd430c8", "radioGroupId": "utg-output-choice-group-unknown_comment_id"} -->   Commit unit tests in branch `grpc`

</details>
<details>
<summary>✨ Simplify code</summary>

- [ ] <!-- {"checkboxId": "f120d606-b0e2-4b7d-8316-181794555b43", "radioGroupId": "simplify-output-choice-group-unknown_comment_id"} -->   Create PR with simplified code
- [ ] <!-- {"checkboxId": "9a4e3077-58f6-4eba-b7ee-62e936ea00ea", "radioGroupId": "simplify-output-choice-group-unknown_comment_id"} -->   Commit simplified code in branch `grpc`

</details>

</details>

<!-- finishing_touch_checkbox_end -->
<!-- tips_start -->

---

Thanks for using [CodeRabbit](https://coderabbit.ai?utm_source=oss&utm_medium=github&utm_campaign=rapira-rs/contract&utm_content=1)! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

<details>
<summary>❤️ Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>


<sub>Comment `@coderabbitai help` to get the list of available commands.</sub>

<!-- tips_end -->
Loading

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/Grpc/Responder/ResponseMetadata.php`:
- Around line 35-88: Implement the documented accumulator behavior in
ResponseMetadata, or replace the final concrete class with an interface that
declares the same methods and provide a concrete implementation used by
Responder::getResponseMetadata(). Ensure all mutators validate names, values,
lifecycle state, and binary suffix rules, accumulate repeated metadata, and make
headers() and trailers() return the stored Metadata instances.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: eaf042e5-951d-4929-8631-1edd86ba76dd

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd692e and fb14849.

📒 Files selected for processing (23)
  • README.md
  • src/Grpc/Call.php
  • src/Grpc/Call/Context.php
  • src/Grpc/Call/MessageStream.php
  • src/Grpc/Call/Protocol.php
  • src/Grpc/Call/StreamingRequest.php
  • src/Grpc/Call/UnaryRequest.php
  • src/Grpc/ErrorDetail.php
  • src/Grpc/Exception/GrpcException.php
  • src/Grpc/Exception/HeadersAlreadyCommittedError.php
  • src/Grpc/GrpcDispatcher.php
  • src/Grpc/GrpcDispatcherInfo.php
  • src/Grpc/Metadata.php
  • src/Grpc/MethodInfo.php
  • src/Grpc/MethodKind.php
  • src/Grpc/Responder.php
  • src/Grpc/Responder/ResponseMetadata.php
  • src/Grpc/Responder/StreamingResponse.php
  • src/Grpc/Responder/UnaryResponse.php
  • src/Grpc/ServiceInfo.php
  • src/Grpc/Status.php
  • src/Grpc/StatusCode.php
  • src/InetAddress.php
📜 Review details
🧰 Additional context used
🪛 PHPMD (2.15.0)
src/Grpc/Responder/ResponseMetadata.php

[warning] 48-48: Avoid unused parameters such as '$name'. (undefined)

(UnusedFormalParameter)


[warning] 48-48: Avoid unused parameters such as '$value'. (undefined)

(UnusedFormalParameter)


[warning] 60-60: Avoid unused parameters such as '$name'. (undefined)

(UnusedFormalParameter)


[warning] 60-60: Avoid unused parameters such as '$bytes'. (undefined)

(UnusedFormalParameter)


[warning] 71-71: Avoid unused parameters such as '$name'. (undefined)

(UnusedFormalParameter)


[warning] 71-71: Avoid unused parameters such as '$value'. (undefined)

(UnusedFormalParameter)


[warning] 81-81: Avoid unused parameters such as '$name'. (undefined)

(UnusedFormalParameter)


[warning] 81-81: Avoid unused parameters such as '$bytes'. (undefined)

(UnusedFormalParameter)

src/Grpc/Metadata.php

[warning] 25-25: Avoid unused parameters such as '$name'. (undefined)

(UnusedFormalParameter)

🪛 PHPStan (2.2.7)
src/Grpc/Call/MessageStream.php

[warning] 34-34: Method Rapira\Grpc\Call\MessageStream::current() should return string but return statement is missing.

(return.missing)


[warning] 37-37: Method Rapira\Grpc\Call\MessageStream::key() should return int<0, max> but return statement is missing.

(return.missing)


[warning] 50-50: Method Rapira\Grpc\Call\MessageStream::valid() has Rapira\Exception\WorkDiscardedException in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 50-50: Method Rapira\Grpc\Call\MessageStream::valid() should return bool but return statement is missing.

(return.missing)


[warning] 57-57: Method Rapira\Grpc\Call\MessageStream::rewind() has Error in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)

src/Grpc/Responder/ResponseMetadata.php

[warning] 48-48: Method Rapira\Grpc\Responder\ResponseMetadata::addHeader() has Rapira\Exception\AlreadyFinalizedError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 48-48: Method Rapira\Grpc\Responder\ResponseMetadata::addHeader() has Rapira\Grpc\Exception\HeadersAlreadyCommittedError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 48-48: Method Rapira\Grpc\Responder\ResponseMetadata::addHeader() has ValueError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 60-60: Method Rapira\Grpc\Responder\ResponseMetadata::addBinaryHeader() has Rapira\Exception\AlreadyFinalizedError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 60-60: Method Rapira\Grpc\Responder\ResponseMetadata::addBinaryHeader() has Rapira\Grpc\Exception\HeadersAlreadyCommittedError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 60-60: Method Rapira\Grpc\Responder\ResponseMetadata::addBinaryHeader() has ValueError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 71-71: Method Rapira\Grpc\Responder\ResponseMetadata::addTrailer() has Rapira\Exception\AlreadyFinalizedError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 71-71: Method Rapira\Grpc\Responder\ResponseMetadata::addTrailer() has ValueError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 81-81: Method Rapira\Grpc\Responder\ResponseMetadata::addBinaryTrailer() has Rapira\Exception\AlreadyFinalizedError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 81-81: Method Rapira\Grpc\Responder\ResponseMetadata::addBinaryTrailer() has ValueError in PHPDoc @throws tag but it's not thrown.

(throws.unusedType)


[warning] 84-84: Method Rapira\Grpc\Responder\ResponseMetadata::headers() should return Rapira\Grpc\Metadata but return statement is missing.

(return.missing)


[warning] 87-87: Method Rapira\Grpc\Responder\ResponseMetadata::trailers() should return Rapira\Grpc\Metadata but return statement is missing.

(return.missing)

src/Grpc/Metadata.php

[warning] 25-25: Method Rapira\Grpc\Metadata::values() should return list but return statement is missing.

(return.missing)


[warning] 32-32: Method Rapira\Grpc\Metadata::all() should return array<string, list> but return statement is missing.

(return.missing)


[warning] 35-35: Method Rapira\Grpc\Metadata::count() should return int<0, max> but return statement is missing.

(return.missing)


[warning] 38-38: Method Rapira\Grpc\Metadata::getIterator() should return Iterator<string, list> but return statement is missing.

(return.missing)

🔇 Additional comments (21)
src/Grpc/Responder/StreamingResponse.php (1)

48-51: 🩺 Stability & Availability

Verify that cancellation closes retained generators.

PHP exposes no Generator::close() operation. A generator runs finally on completion or after all references are removed. Service code can retain $messages, so discarding only the host reference cannot guarantee the documented cleanup. (php.net)

Show that the host injects an uncatchable terminal cancellation path, or weaken this guarantee and document the retained-reference behavior.

src/Grpc/StatusCode.php (1)

7-30: LGTM!

src/Grpc/Status.php (1)

16-27: LGTM!

src/Grpc/ErrorDetail.php (1)

12-21: LGTM!

src/Grpc/Exception/GrpcException.php (1)

28-42: LGTM!

src/Grpc/Exception/HeadersAlreadyCommittedError.php (1)

9-17: LGTM!

src/InetAddress.php (1)

9-11: LGTM!

src/Grpc/Responder.php (1)

28-39: 🎯 Functional Correctness

No change needed.

The interface methods are declared with trailing semicolons and do not define method bodies.

src/Grpc/GrpcDispatcher.php (1)

40-72: LGTM!

src/Grpc/GrpcDispatcherInfo.php (1)

9-15: LGTM!

src/Grpc/MethodKind.php (1)

13-42: LGTM!

src/Grpc/MethodInfo.php (1)

11-24: LGTM!

src/Grpc/Call/StreamingRequest.php (1)

23-29: LGTM!

src/Grpc/Call/MessageStream.php (1)

28-57: 🩺 Stability & Availability

Verify MessageStream registration order.

Rapira\Grpc\Call\MessageStream is declared final with empty typed methods, but this package provides Rapira\Grpc\Call\StreamingRequest::getMessages(): MessageStream. Composer autoloads this source as Rapira\; require evidence that the native host registers that class before Composer can load it. Otherwise implement current(), key(), and valid() here or publish the implemented interface for the host.

src/Grpc/ServiceInfo.php (1)

7-21: LGTM!

README.md (1)

271-448: LGTM!

Also applies to: 477-483, 515-523

src/Grpc/Call.php (1)

10-23: LGTM!

src/Grpc/Call/Context.php (1)

14-53: LGTM!

src/Grpc/Call/Protocol.php (1)

7-16: LGTM!

src/Grpc/Call/UnaryRequest.php (1)

11-26: LGTM!

src/Grpc/Metadata.php (1)

7-38: LGTM!

Comment thread src/Grpc/Responder/ResponseMetadata.php Outdated
@roxblnfk
roxblnfk marked this pull request as draft August 10, 2026 10:57
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant