The danger of Laravel’s request() helper

The request() helper (or Request facade) is one of the options to get to the Request object in Laravel. Another one is via dependency injection. Lately, the request() helper is quickly gaining momentum and seeing greater usages throughout the Laravel ecosystem. It provides a somehow cleaner and easier access to the Request object. For example $request->input('foo') vs. request('foo') and if you use closures, you don’t need to use the use ($request). But it has some drawbacks and some gotchas which you must be aware of.

Let’s say you are building a blog platform. The platform will provide API endpoints (JSON responses to provide data for, let’s say a mobile app) and you will have a web page.

Another post, with a blog example, yay!

You start by building an API. In your api.php routes file you create a new API resource Route::apiResource(‘posts’, ‘PostsController’);

And then inside the index action:

public function index()
{
return Post::when(request('category'), function($query) {
return $query->where('category_id', request('category'));
})
->paginate(request('per_page',15));
}

Now you need to create a web page. On the front page you want to display 5 posts all with the category_id of 8. You start by creating a route in web.php — Route::get(‘/’, ‘HomeController@home’);

Inside your action, you then make an HTTP request to your endpoint, and get the posts (with ApiResource or Whatan’s Zttp):

public function home()
{
$posts = \Api::get('/api/posts', ['query' => ['per_page' => 5, 'category' => 8]]);
return view('welcome', ['posts' => $posts]);
}

It works as expected. But … now if you want to display paginated posts you need to create a new Pagination instance. You quickly notice, that with this approach, you always get a plain array, and not an Eloquent objects.

OK, because we are using a single Laravel project, for both our API and the web page, we can make a direct call to the API controller action, from our web page controller:

public function home()
{
$posts = app(PostsController::class)->index();
return view('welcome', ['posts' => $posts]);
}

Notice, we don’t make an HTTP call anymore (so there is no overhead of that), we get the Pagination object back and when we loop through the object, notice it contains Eloquent models, whereas we were working with plain arrays before.

But how to pass parameters (per_page, category) to our actions? Notice we don’t accept any parameters in our index() action above, since we use the request() helper. One option is to redirect the user to new URL with query parameters (http://test.dev/?per_page=5&category=8). But we can all agree this is not a solution.

We have to get rid of the request() function helper and use dependency injection to inject the Request object into our action.

So let’s modify our API action:

public function index(Request $request)
{
return Post::when($request->has('category'), function($query) use ($request) {
return $query->where('category_id', $request->input('category'));
})
->paginate($request->input('per_page',15),['*'], 'page', $request->input('page', 1));
}

And our home action:

public function home()
{
$posts = app(PostsController::class)->index(new Request(['per_page' => 5, 'category' => 8]));
return view('welcome', ['posts' => $posts]);
}

Now this works as expected and we get rid of the unnecessary HTTP call.

Another option would be to make repositories and then call them. But the ability to make these sorts of API calls is nice and it moves all logic to the API and makes your web page mostly view layer.

I used this approach in my last project which was built from 4 parts:

  1. API — main app logic, endpoints returning JSON (auto convert response)
  2. Web page — Responsive web page
  3. Admin — SPA app build with Vue.js 2
  4. Mobile — Mobile app for iOS and Android

The API, web page and admin are all one Laravel project (the admin is just one view, as it is a SPA). And all controllers for the web page are only 2 or 3 calls to API action to get data and return a view.