Sir.Nathan Stassen
Sir.Nathan Stassen

Reputation: 1881

Laravel nesting all json responses

I'm using Laravel to create a JSON REST API, and it has been quite present so far. However I'm needing to wrap my JSON outputs with a bit of meta status JSON created by say a metaDataController (or probably model) and I am curious what a good approach to this might be.

For instance, all responses would take on the following format:

{
    "meta": {
        "status": 200, 
        "notifications": 2
    },
    "response": {
        //JSON from the route's Controller/Model/etc
    }
}

From what I can tell I either need to modify the Laravel Response defaults and delegate to a metaDataController, or create some sort of Route::any that merges the two sections of JSON as mentioned in Returning Multiple Laravel Eloquent Models as JSON. Although I always know metaDataController, the other controller is in flux depending on the route.

I'm thinking there must be a way to declare this structure as a default for all routes or a Route::group.

Thanks!

Upvotes: 2

Views: 2903

Answers (2)

phobos2077
phobos2077

Reputation: 342

I don't think doing json_decode->json_encode cycle is an acceptable solution (as in Chris answer).

Here is another solution

use Illuminate\Http\Response;
use Illuminate\Http\Request;

Route::filter('apisuccess', function($route, Request $request, Response $response = null) {
    $response->setContent(json_encode([
        'data' => $response->original, 
        'meta' => ['somedata': 'value']
    ]));
});

Then I would attach this filter to my REST API routes.

Edit: another solution (more complex).

  1. Create a custom Response class:
use Illuminate\Http\Response;
class ApiResponse extends Response
{
    protected $meta;
    protected $data;

    public function __construct($content = '', $status = 200, $headers = array())
    {
        parent::__construct([], $status, $headers);
        $this->meta = [];
    }

    public function withMeta($property, $value = null)
    {
        if (is_array($property))
            $this->meta = $property;
        else 
            array_set($this->meta, $property, $value);
        return $this;
    }

    public function withData($data)
    {
        $this->data = $data;
        return $this;
    }

    public function sendContent()
    {
        echo json_encode(['success' => true, 'data' => $this->data, 'meta' => $this->meta, 'echo' => ob_get_contents()]);
    }
}
  1. Put it as a singleton in IOC container:

    $this->app->bindShared('ApiResponse', function() {
        return new \Truinject\Http\ApiResponse();
    });
    
  2. Finally, create filter and use it as "before" on your routes:

Route::filter('apiprepare', function(Illuminate\Routing\Route $route, Illuminate\Http\Request $request) {
    $data = $route->run();
    return App::make('ApiResponse')->withData($data);
});

So we are basically overriding default response class with our own, but still calling the appropriate controller with $route->run() to get the data.

To set meta data, in your controller do something like this:

\App::make('ApiResponse')->withMeta($property, $value);

I've added method "meta" in my base API controller class, which encapsulates this.

Upvotes: 2

Chris
Chris

Reputation: 58142

You could use the global after filter in app.php to catch all responses, then reconfigure it however you please:

App::after(function($request, $response)
{
    if(is_a($response, 'Illuminate\Http\JsonResponse')) {
        $response->setContent(json_encode(array(
            'data' => json_decode($response->getContent()),
            'foo' => 'bar',
            'cat' => 'dog'
        )));
    }
});

In the above example, you're taking all the existing json data and putting it in a child data element (this would be "response" in your example) then adding foo and bar. So foo, bar and data would be top level json objects.

If you don't like the global positioning, after is an event sent, so you could also listen to it inside a controller/elsewhere.

Upvotes: 0

Related Questions