skip to content

In Laravel, how do Response::macro() and a class implementing Responsable each let you reuse one response shape across controllers?

level: middleimportance: nice to knowfreq 16%

answer

  1. extend the factory or teach an object
  2. register macros in AppServiceProvider::boot
  3. response()->yourMacro(...) after registration
  4. toResponse($request) returns a response
  5. router calls toResponse() before other rules

basics

~10 s

Response::macro('name', fn) adds a method to the response factory, so response()->name(...) builds the shared shape anywhere. A Responsable class implements toResponse($request), and the router calls it when a controller returns that object.

solid answer

~40 s

The `Response` facade points at the `ResponseFactory` contract, and `Illuminate\Routing\ResponseFactory` uses the `Macroable` trait. Calling `Response::macro('ticketPdf', function ($ticket) { ... })` in `AppServiceProvider::boot()` makes `response()->ticketPdf($ticket)` available everywhere; inside the closure `$this` is the factory, so it can call `$this->json()` or `$this->download()`. A macro suits a small reusable call shape. `Illuminate\Contracts\Support\Responsable` suits a response with its own data and logic: the class implements `toResponse($request)`, the controller returns an instance, and `Router::toResponse()` calls that method before any other conversion. Responsable objects are easy to unit test and can vary by request, for example sending JSON to an API client and HTML to a browser.

code

php · 20 lines
php
<?php

namespace App\Http\Responses;

use App\Models\Ticket;
use Illuminate\Contracts\Support\Responsable;

class TicketResponse implements Responsable
{
    public function __construct(private Ticket $ticket) {}

    public function toResponse($request)
    {
        if ($request->expectsJson()) {
            return response()->json(['id' => $this->ticket->id, 'seat' => $this->ticket->seat]);
        }

        return response()->file(storage_path('app/private/'.$this->ticket->pdf_path));
    }
}

go deeper

for a junior

Recall that Response::macro() registers a reusable response() method in a service provider, and that returning a Responsable object lets it build its own response.

for a middle

Explain that the facade targets the macroable ResponseFactory, that closures are bound to the factory, and that Router::toResponse() checks Responsable first.

for a senior

Choose macros for thin envelopes and Responsable classes for logic that varies by request, register in boot(), and watch for global macro name clashes.

for a principal

Decide where response conventions live so teams share one envelope, and keep them testable rather than scattered across closures.

## The problem both solve In a concert-venue app, several controllers return the same kind of reply: a JSON status block for the door scanners (`{"ok": true, "gate": "B", "at": ...}`), or a PDF ticket that is shown inline to browsers but returned as JSON metadata to the mobile app. Copying the same `response()->json([...], 200, [...])` into each controller drifts over time. Laravel gives you two reuse points. ## Response macros: add a method to the factory `Illuminate\Support\Facades\Response` resolves the `Illuminate\Contracts\Routing\ResponseFactory` contract, whose implementation `Illuminate\Routing\ResponseFactory` uses `Macroable`. Registering a macro adds a named method: ```php use Illuminate\Support\Facades\Response; Response::macro('gateStatus', function (string $gate, bool $ok = true) { return $this->json(['ok' => $ok, 'gate' => $gate, 'at' => now()->toIso8601String()]); }); ``` Key points: - **Where:** in the `boot()` method of a service provider, typically `App\Providers\AppServiceProvider`, so the macro exists before any route runs. - **How it is called:** `response()->gateStatus('B')`, or `Response::gateStatus('B')` through the facade. - **What `$this` is:** `Macroable::__call()` binds the closure to the factory instance, so `$this->json()`, `$this->view()` and `$this->download()` are available. - **Unknown names fail loudly:** calling a macro that was never registered throws `BadMethodCallException` ("Method ... does not exist."). - **Other classes are macroable too:** `Illuminate\Http\Response`, `JsonResponse` and `RedirectResponse` use `Macroable` as well, so a macro registered on one of those classes becomes a chainable method on its instances rather than a factory method. Macros are best for thin wrappers that add a status, headers or a standard envelope. ## Responsable: let an object build its own response `Illuminate\Contracts\Support\Responsable` has one method, `toResponse($request)`, which returns a Symfony response. When a controller returns such an object, `Router::toResponse()` calls it **first**, before the array, model and string rules. That gives you a class with a constructor, dependencies, private helpers and tests: 1. The controller gathers data and returns `new TicketResponse($ticket)`. 2. The router calls `$ticketResponse->toResponse($request)`. 3. The object decides, for instance with `$request->expectsJson()`, whether to return `response()->json(...)` or `response()->file(...)`. API resource classes are the best-known example of this interface; their own features (wrapping, conditional attributes) are a separate subject. ## Choosing between them | | `Response::macro()` | `Responsable` class | |---|---|---| | Unit of reuse | a named factory method | a class with its own data | | Registered | in a provider's `boot()` | nowhere; return an instance | | Holds logic and dependencies | a closure only | constructor, methods, tests | | Varies by request | possible, but awkward | natural, `toResponse()` receives the request | | Static analysis and IDE support | needs docblocks or helper files | a normal class | ## Pitfalls - A macro registered in `register()` or inside a controller can be missing when another code path runs first. Register in `boot()`. - Macros are global names; two packages registering the same name overwrite each other, since the last registration wins. - A `Responsable` that returns a plain array from `toResponse()` breaks the contract; it must return a response object. ## Seeing both in the venue app 1. The door-scanner endpoints all end with `return response()->gateStatus($gate->code);`, so a change to the envelope (say, adding a `version` field) is one edit in `AppServiceProvider`. 2. The ticket endpoints end with `return new TicketResponse($ticket);`. The class checks whether the client wants JSON, and returns either metadata or the PDF through `response()->file()`. 3. Each `TicketResponse` can be tested by constructing it with a ticket and a fake request and asserting on the response it returns, without routing a full HTTP request. The two approaches also combine: a `Responsable` class can call a macro inside `toResponse()` to reuse the shared envelope. ## What interviewers look for Knowing that the facade targets the factory contract, where to register a macro, and that `Responsable` is the hook the router checks first. Choosing the class over the macro once the shape has logic shows design sense.

  • Inside a `Response::macro()` closure, why can you call `$this->json()`?
    `Macroable::__call()` binds the stored closure to the object the macro is called on. For `response()->gateStatus()` that object is the `ResponseFactory`, so `$this` refers to it and all factory methods such as `json()`, `view()` and `download()` are available.
  • What is the difference between `Response::macro()` and `JsonResponse::macro()`?
    The facade version adds a method to the response factory, used as `response()->name()`. `JsonResponse::macro()` adds a method to JSON response instances, so you chain it after building one, as in `response()->json($data)->name()`. One creates responses, the other decorates an existing one.

saying these in an interview costs you the question

  • Response::macro() must be called inside each controller that uses it.
  • A Responsable object is converted to JSON like any other object.
  • Inside a macro closure, $this is the service provider that registered it.
  • A missing macro name silently returns an empty response.