2

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!

2 Answers 2

2

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.

Sign up to request clarification or add additional context in comments.

2 Comments

This is rather interesting, I may refactor using one of these solutions. To my understanding we're talking about moving the encapsulation to the routing layer return/filter. Few questions: -Why before instead of after? -Where is the IOC container that it needs to be place? -How might a controller's method throw a 503 or routing layer throw a 404? Pretty awesome idea. It seems right.
-Why before instead of after? - If I remember correctly, when "after" filter runs the response is already built. Don't get the other questions though. With this approach the code works pretty much the same, so there is no change to controllers code.
0

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.

3 Comments

Ah that makes sense, working great, thanks! I'm using the global positioning since this needs to impacts all of laravel's JSON responses. It didn't like it in app.php, so I ended up placing it in filters.php were I noted an empty App::after waiting to be used.
Cool - I believe app.php should already contain the App::after function which you can drop code into, but filters works too - what ever makes sense to your own layout
Downvoter care to explain? Happy to tweak answer if it can be improved

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.