Search code examples
phpjsonlaravelresourcesmiddleware

Laravel: cannot access resource collection in middleware


I'm trying to create a middleware which wraps the returned response into a uniform JSON response for my API. This will ensure that all API responses have the same base structure which looks like this:

{
    "success": true,
    "data": {...}
}

Now this is the middleware I am currently using:

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Http\Response;

class ApiResponseWrapper
{
    /**
     * Handle an incoming request.
     *
     * @param Request $request
     *
     * @return mixed
     */
    public function handle($request, Closure $next)
    {
        /** @var Response $response */
        $response = $next($request);
        $original = $response->getOriginalContent();

        $content = [
            'success' => $response->isSuccessful(),
            'data' => $original,
        ];

        $jsonContent = json_encode($content, JsonResponse::DEFAULT_ENCODING_OPTIONS);
        $response->setContent($jsonContent);

        return $response;
    }
}

The two key points of this middleware are:

  1. The $original = $response->getOriginalContent(); line gets the content of the response to wrap.
  2. The content needs to be a string in order to be set as the new content. That's why I json_encode the $content with the default encoding options for a JSON response.

The problem arises when I want to pass a paginated ResourceCollection into the middleware. The controller method looks like this:

public function index(): AnonymousResourceCollection
{
    /** @var User $user */
    $user = auth()->user();

    return PostResource::collection($user->posts->paginate(20));
}

Normally, the ->paginate(...) method does not exist in a Laravel Collection. It is a macro taken from here: gist.github.com/simonhamp/549e8821946e2c40a617c85d2cf5af5e.

If I now hit the endpoint that calls the index() method, the expected output is something like this:

{
    "success": true,
    "data": {
        "data": [
            <PostResource as JSON object>
        ],
        "meta": {...},
        "links": {...},
    }
}

But what I actually get is this:

{
    "success": true,
    "data": [
        <Post Model as array>
    ]
}

I thought I might have to call a method to encode the $original (which is of type Illuminate\Support\Collection) into a paginated resource collection, but I didn't find one.

All other responses are fine. For example on another controller method I simply return an array or a string and they are wrapped as expected.

Does anyone have an idea what is wrong here?


Edit:

I also tried to add this before the $content variable is set:

if (method_exists($original, 'toArray')) {
    $original = $original->toArray();
}

It results in the same output for ResourceCollections.


Solution

  • After investigating the variable types and looking through the Laravel API, I found that the easiest solution was to just check if the response is a JsonResponse and get the rendered pagination data from it:

    public function handle($request, Closure $next)
    {
        /** @var Response $response */
        $response = $next($request);
        $content = [
            'success' => $response->isSuccessful(),
        ];
    
        if ($response instanceof JsonResponse) {
            /** @var JsonResponse $response */
    
            $data = $response->getData(true);
    
            $content = array_merge($content, $data);
        } else {
            $content['data'] = $response->getOriginalContent();
        }
    
        $jsonContent = json_encode($content, JsonResponse::DEFAULT_ENCODING_OPTIONS);
        $response->setContent($jsonContent);
    
        return $response;
    }
    

    This results in the expected responses for my use case:

    {
        "success": true,
        "data": [
            <PostResource as JSON object>
        ],
        "meta": {...},
        "links": {...},
    }
    

    Also note that I originally specified the data, meta and links keys within the wrapping data key. I find the result above a bit prettier because I got around double wrapped data.

    I hope this will help anyone who came across this question!