Skip to content

[#470] Enforce response-returning route dispatch#475

Merged
armanist merged 1 commit intosoftberg:masterfrom
armanist:issue/470-fluent-response-dispatcher
Apr 24, 2026
Merged

[#470] Enforce response-returning route dispatch#475
armanist merged 1 commit intosoftberg:masterfrom
armanist:issue/470-fluent-response-dispatcher

Conversation

@armanist
Copy link
Copy Markdown
Member

@armanist armanist commented Apr 24, 2026

Closes #470

Summary by CodeRabbit

  • New Features

    • Response methods now support fluent method chaining, allowing cleaner and more concise code when building responses.
  • Bug Fixes

    • Route handlers now enforce returning a Response object, providing clearer error messages when invalid handler responses are provided.

@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Apr 24, 2026

📝 Walkthrough

Walkthrough

This PR introduces a fluent Response API by refactoring response trait methods to return self instead of void, enabling method chaining. Concurrently, the route dispatcher is enhanced to require handlers return a Response object, with validation that throws an exception for invalid return types. The changes include exception definitions, dispatcher logic updates, and corresponding test updates.

Changes

Cohort / File(s) Summary
Fluent Response API
src/Http/Traits/Response/Body.php, src/Http/Traits/Response/Header.php, src/Http/Traits/Response/Status.php
Updated set, json, jsonp, xml, html, setHeader, setContentType, and setStatusCode methods to return self instead of void for fluent chaining.
Dispatcher Response Validation
src/Router/Enums/ExceptionMessages.php, src/Router/Exceptions/RouteException.php, src/Router/RouteDispatcher.php
Added INVALID_HANDLER_RESPONSE enum constant and corresponding factory method; refactored dispatch to validate handler returns a Response and throw exception for invalid types.
Response Trait Tests
tests/Unit/Http/Traits/Response/HttpResponseBodyTest.php, tests/Unit/Http/Traits/Response/HttpResponseHeaderTest.php, tests/Unit/Http/Traits/Response/HttpResponseStatusTest.php
Updated assertions to verify fluent interface return values using assertSame.
Dispatcher & Controller Tests
tests/Unit/Router/RouteDispatcherTest.php, tests/_root/modules/Test/Controllers/TestController.php
Added negative-path tests for invalid handler returns; updated closure and controller tests to accept Response parameter and return Response; refactored test controller action signature.

Sequence Diagram(s)

sequenceDiagram
    actor Client
    participant Dispatcher as RouteDispatcher
    participant Handler as Handler (Closure/<br/>Controller)
    participant Response
    
    Client->>Dispatcher: dispatch(matched, request)
    Dispatcher->>Handler: invoke handler
    Handler->>Response: json(...) / html(...)
    Response-->>Handler: returns self (fluent)
    Handler->>Handler: return Response
    Handler-->>Dispatcher: returns Response
    alt Response valid
        Dispatcher-->>Client: return Response
    else Response invalid
        Dispatcher->>Dispatcher: throw<br/>RouteException
        Dispatcher-->>Client: exception
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Suggested labels

enhancement

Suggested reviewers

  • andrey-smaelov

Poem

🐰 A hop through responses so fluent and fine,
Chaining methods together, a beautiful line,
The dispatcher now strict, with Response in hand,
No invalid returns in our code-garden land! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 31.43% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title '[#470] Enforce response-returning route dispatch' accurately summarizes the main change: converting the route dispatcher to enforce Response returns and enabling a fluent response API.
Linked Issues check ✅ Passed All required API changes are implemented: json(), html(), xml(), jsonp(), setHeader(), setStatusCode(), set() now return self; RouteDispatcher enforces Response returns; dispatcher handles both closures and controllers; exception thrown for invalid returns.
Out of Scope Changes check ✅ Passed All changes are within scope: Response trait methods updated to return self, RouteDispatcher modified to require and return Response, ExceptionMessages and RouteException added for validation, tests updated to verify new behavior, and TestController refactored to comply with contract.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.97%. Comparing base (0a3c037) to head (7cd8ab1).
⚠️ Report is 2 commits behind head on master.

Additional details and impacted files
@@             Coverage Diff              @@
##             master     #475      +/-   ##
============================================
+ Coverage     82.48%   82.97%   +0.48%     
- Complexity     2902     2905       +3     
============================================
  Files           251      251              
  Lines          7814     7663     -151     
============================================
- Hits           6445     6358      -87     
+ Misses         1369     1305      -64     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (6)
tests/Unit/Http/Traits/Response/HttpResponseHeaderTest.php (1)

23-25: Consider also asserting setContentType returns self.

src/Http/Traits/Response/Header.php now also makes setContentType() return self, but there is no test asserting the fluent contract for that method. A one-line assertSame($response, $response->setContentType(ContentType::JSON)) would close the gap.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Http/Traits/Response/HttpResponseHeaderTest.php` around lines 23 -
25, Add a one-line fluent-interface assertion in HttpResponseHeaderTest to
ensure setContentType returns self: in the same test that asserts setHeader's
fluency, call setContentType with ContentType::JSON (or another ContentType) and
assertSame($response, $response->setContentType(...)); this verifies the
Header::setContentType() change in src/Http/Traits/Response/Header.php and
ensures the method returns the response instance.
tests/Unit/Router/RouteDispatcherTest.php (2)

65-65: Dead Mockery::mock(Response::class) call.

Since the CSRF test was updated to use Di::set for proper dependency injection, leftover Mockery::mock(Response::class); lines in this test (and Line 201) no longer serve a purpose — they create an unused mock that isn't wired into anything. Consider removing them for consistency with the new injection approach.

🧹 Proposed cleanup
         $request = Mockery::mock(Request::class);
-        Mockery::mock(Response::class);

         $dispatcher->dispatch($matched, $request);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Router/RouteDispatcherTest.php` at line 65, Remove the unused
Mockery::mock(Response::class) calls left in the CSRF-related test in
RouteDispatcherTest: they no longer serve any purpose since the test now injects
the Response via Di::set; delete the standalone Mockery::mock(Response::class)
invocations (including the one around line 201) so the test uses only the
DI-provided Response and doesn't create dead mocks.

151-184: Solid negative-path coverage.

Both new tests (testDispatchThrowsWhenClosureRouteDoesNotReturnResponse and testDispatchThrowsWhenControllerActionDoesNotReturnResponse) correctly exercise the new requireResponse guard for closure and controller paths respectively, satisfying the "fail fast" acceptance criterion.

One optional improvement: consider asserting the exception message contains the handler identifier ('closure route' or ControllerClass::post) so regressions in RouteException::invalidHandlerResponse(...) formatting are caught here as well.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@tests/Unit/Router/RouteDispatcherTest.php` around lines 151 - 184, Update the
two tests to also assert the RouteException message contains the handler
identifier so formatting regressions are caught: in
testDispatchThrowsWhenClosureRouteDoesNotReturnResponse assert the thrown
exception message includes "closure route" (or the exact phrase produced by
RouteException::invalidHandlerResponse for closures), and in
testDispatchThrowsWhenControllerActionDoesNotReturnResponse assert the message
contains the controller method identifier (e.g., "ClassName::post" or the exact
string produced by RouteException::invalidHandlerResponse for controller
actions); keep the existing expectException(RouteException::class) and add an
assertion on the exception message after capturing the exception from
RouteDispatcher::dispatch for MatchedRoute and Request mocks.
src/Http/Traits/Response/Body.php (2)

87-92: Optional: make delete() fluent for API consistency.

For full fluent consistency with set(), json(), xml(), jsonp(), and html(), delete() could also return self. Not required by the PR objectives, but would round out the builder API.

♻️ Proposed refinement
-    public function delete(string $key): void
+    public function delete(string $key): self
     {
         if ($this->has($key)) {
             unset($this->__response[$key]);
         }
+        return $this;
     }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Traits/Response/Body.php` around lines 87 - 92, The delete method in
the Body trait currently returns void; make it fluent by changing the signature
of Body::delete(string $key) to return self and have it return $this at the end
(while retaining the existing unset logic that uses $this->__response and the
has($key) check) so it matches the fluent API used by set(), json(), xml(),
jsonp(), and html().

69-73: Fluent API updates look good — consider static over self for extensibility.

Converting set, json, jsonp, xml, and html to return $this with a self return type satisfies the fluent API goal. Minor optional refinement: since this is a trait, using static rather than self would preserve late static binding if/when subclasses override the return contract. Given Response is the primary consumer and likely final, self is fine — flagging only for awareness.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Http/Traits/Response/Body.php` around lines 69 - 73, The trait Body
currently types its fluent methods to return self; change the return type
declarations to static for extensibility so late static binding is preserved —
update the methods set, json, jsonp, xml, and html in the Body trait to return
static instead of self, ensuring each method signature and any
PHPDoc/annotations referencing the return type are adjusted accordingly.
src/Router/RouteDispatcher.php (1)

64-71: Consider validating Response before running __after.

Currently __after runs between requireResponse(...) and the return $response;. If __after mutates response state (e.g., via $response->json(...) again) or throws, the validated response can be altered or lost. This matches pre-existing behavior, so flagging as optional — but if the fluent API encourages hook handlers to also manipulate $response, the ordering may warrant an explicit design choice (e.g., document that __after runs with the validated response already locked in).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/Router/RouteDispatcher.php` around lines 64 - 71, The code calls
requireResponse($this->invoke(...)) to validate the controller result and then
calls $this->callHook($controller, '__after', $params) which may mutate or
replace the already-validated $response; to prevent losing validation, revise
the sequence so requireResponse(...) assigns to $response and then callHook is
invoked in a way that cannot alter the validated response (either call the hook
with a clone/copy of $response or run callHook before returning but after
locking $response), e.g., ensure requireResponse, assign to $response, then
invoke callHook('__after') in a non-mutating way (or explicitly document and
enforce that __after handlers must not modify $response) referencing
requireResponse, invoke, callHook and the __after hook for where to change
behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/Http/Traits/Response/Body.php`:
- Around line 87-92: The delete method in the Body trait currently returns void;
make it fluent by changing the signature of Body::delete(string $key) to return
self and have it return $this at the end (while retaining the existing unset
logic that uses $this->__response and the has($key) check) so it matches the
fluent API used by set(), json(), xml(), jsonp(), and html().
- Around line 69-73: The trait Body currently types its fluent methods to return
self; change the return type declarations to static for extensibility so late
static binding is preserved — update the methods set, json, jsonp, xml, and html
in the Body trait to return static instead of self, ensuring each method
signature and any PHPDoc/annotations referencing the return type are adjusted
accordingly.

In `@src/Router/RouteDispatcher.php`:
- Around line 64-71: The code calls requireResponse($this->invoke(...)) to
validate the controller result and then calls $this->callHook($controller,
'__after', $params) which may mutate or replace the already-validated $response;
to prevent losing validation, revise the sequence so requireResponse(...)
assigns to $response and then callHook is invoked in a way that cannot alter the
validated response (either call the hook with a clone/copy of $response or run
callHook before returning but after locking $response), e.g., ensure
requireResponse, assign to $response, then invoke callHook('__after') in a
non-mutating way (or explicitly document and enforce that __after handlers must
not modify $response) referencing requireResponse, invoke, callHook and the
__after hook for where to change behavior.

In `@tests/Unit/Http/Traits/Response/HttpResponseHeaderTest.php`:
- Around line 23-25: Add a one-line fluent-interface assertion in
HttpResponseHeaderTest to ensure setContentType returns self: in the same test
that asserts setHeader's fluency, call setContentType with ContentType::JSON (or
another ContentType) and assertSame($response, $response->setContentType(...));
this verifies the Header::setContentType() change in
src/Http/Traits/Response/Header.php and ensures the method returns the response
instance.

In `@tests/Unit/Router/RouteDispatcherTest.php`:
- Line 65: Remove the unused Mockery::mock(Response::class) calls left in the
CSRF-related test in RouteDispatcherTest: they no longer serve any purpose since
the test now injects the Response via Di::set; delete the standalone
Mockery::mock(Response::class) invocations (including the one around line 201)
so the test uses only the DI-provided Response and doesn't create dead mocks.
- Around line 151-184: Update the two tests to also assert the RouteException
message contains the handler identifier so formatting regressions are caught: in
testDispatchThrowsWhenClosureRouteDoesNotReturnResponse assert the thrown
exception message includes "closure route" (or the exact phrase produced by
RouteException::invalidHandlerResponse for closures), and in
testDispatchThrowsWhenControllerActionDoesNotReturnResponse assert the message
contains the controller method identifier (e.g., "ClassName::post" or the exact
string produced by RouteException::invalidHandlerResponse for controller
actions); keep the existing expectException(RouteException::class) and add an
assertion on the exception message after capturing the exception from
RouteDispatcher::dispatch for MatchedRoute and Request mocks.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 2abdbbf4-d64f-46f4-be29-b67cd63bbc71

📥 Commits

Reviewing files that changed from the base of the PR and between 0a3c037 and 7cd8ab1.

📒 Files selected for processing (11)
  • src/Http/Traits/Response/Body.php
  • src/Http/Traits/Response/Header.php
  • src/Http/Traits/Response/Status.php
  • src/Router/Enums/ExceptionMessages.php
  • src/Router/Exceptions/RouteException.php
  • src/Router/RouteDispatcher.php
  • tests/Unit/Http/Traits/Response/HttpResponseBodyTest.php
  • tests/Unit/Http/Traits/Response/HttpResponseHeaderTest.php
  • tests/Unit/Http/Traits/Response/HttpResponseStatusTest.php
  • tests/Unit/Router/RouteDispatcherTest.php
  • tests/_root/modules/Test/Controllers/TestController.php

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 7cd8ab1211

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/Router/RouteDispatcher.php
@armanist armanist requested a review from andrey-smaelov April 24, 2026 12:05
@armanist armanist added the enhancement New feature or request label Apr 24, 2026
@armanist armanist added this to the 3.0.0 milestone Apr 24, 2026
@armanist armanist merged commit 454dc43 into softberg:master Apr 24, 2026
7 checks passed
@armanist armanist deleted the issue/470-fluent-response-dispatcher branch April 24, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Introduce fluent Response API and strict dispatcher return contract

2 participants