Making internal requests with Laravel
Published by Matthew Daly at 2nd September 2017 1:45 pm
Recently I've been working on a Phonegap app that needs to work offline. The nature of relational databases can often make this tricky if you're dealing with related objects and you're trying to retrofit it to something that wasn't built with this use case in mind.
Originally my plan was to push each request that would have been made to a queue in WebSQL, and then on reconnect, make every request individually. It quickly became apparent, however, that this approach had a few problems:
- If one request failed, the remaining requests had to be stopped from executing
- It didn't allow for storing the failed transactions in a way that made them easy to retrieve
Instead, I decided to create a single sync
endpoint for the API that would accept an object containing all the requests that would be made, and then step through each one. If it failed, it would get the failed request and all subsequent ones in the object, and store them in the database. That way, even if the data didn't sync correctly, it wasn't lost, and if necessary it could be resolved manually.
Since the necessary API endpoints already existed, and were thoroughly tested, it was not a good idea to start duplicating that functionality. Instead, I implemented the functionality to carry out internal requests, and I thought I'd share how you can do this.
For any service you may build for your Laravel applications, it's a good idea to create an interface for it first:
1<?php23namespace App\Contracts;45interface MakesInternalRequests6{7 /**8 * Make an internal request9 *10 * @param string $action The HTTP verb to use.11 * @param string $resource The API resource to look up.12 * @param array $data The request body.13 * @return \Illuminate\Http\Response14 */15 public function request(string $action, string $resource, array $data = []);16}
That way you can resolve the service using dependency injection, making it trivial to replace it with a mock when testing.
Now, actually making an internal request is pretty easy. You get the app instance (you can do so by resolving it using dependency injection as I do below, or call the app()
helper). Then you put together the request you want to make and pass it as an argument to the app's handle()
method:
1<?php23namespace App\Services;45use Illuminate\Http\Request;6use App\Contracts\MakesInternalRequests;7use Illuminate\Foundation\Application;8use App\Exceptions\FailedInternalRequestException;910/**11 * Internal request service12 */13class InternalRequest implements MakesInternalRequests14{15 /**16 * The app instance17 *18 * @var $app19 */20 protected $app;2122 /**23 * Constructor24 *25 * @param Application $app The app instance.26 * @return void27 */28 public function __construct(Application $app)29 {30 $this->app = $app;31 }3233 /**34 * Make an internal request35 *36 * @param string $action The HTTP verb to use.37 * @param string $resource The API resource to look up.38 * @param array $data The request body.39 * @throws FailedInternalRequestException Request could not be synced.40 * @return \Illuminate\Http\Response41 */42 public function request(string $action, string $resource, array $data = [])43 {44 // Create request45 $request = Request::create('/api/' . $resource, $action, $data, [], [], [46 'HTTP_Accept' => 'application/json',47 ]);4849 // Get response50 $response = $this->app->handle($request);51 if ($response->getStatusCode() >= 400) {52 throw new FailedInternalRequestException($request, $response);53 }5455 // Dispatch the request56 return $response;57 }58}
Also note that I've created a custom exception, called FailedInternalRequestException
. This is fired if the status code returned from the internal requests is greater than or equal to 400 (thus denoting an error):
1<?php23namespace App\Exceptions;45use Illuminate\Http\Request;6use Illuminate\Http\Response;78/**9 * Exception for when a bulk sync job fails10 */11class FailedInternalRequestException extends \Exception12{13 /**14 * Request instance15 *16 * @var $request17 */18 protected $request;1920 /**21 * Response instance22 *23 * @var $response24 */25 protected $response;2627 /**28 * Constructor29 *30 * @param Request $request The request object.31 * @param Response $response The response object.32 * @return void33 */34 public function __construct(Request $request, Response $response)35 {36 parent::__construct();37 $this->request = $request;38 $this->response = $response;39 }4041 /**42 * Get request object43 *44 * @return Request45 */46 public function getRequest()47 {48 return $this->request;49 }5051 /**52 * Get response object53 *54 * @return Response55 */56 public function getResponse()57 {58 return $this->response;59 }60}
You can catch this exception in an appropriate place and handle it as you wish. Now, if you import the internal request class as $dispatcher
, you can just call $dispatcher->request($action, $resource, $data)
, where $action
is the HTTP verb, $resource
is the API resource to send to, and $data
is the data to send.
It's actually quite rare to have to do this. In this case, because this was a REST API and every request made to it was changing the state of the application (there were no GET requests, only POST, PUT, PATCH and DELETE), it made sense to just break down the request body and do internal requests against the existing API, since otherwise I'd have to duplicate the existing functionality. I would not recommend this approach for something like fetching data to render a page on the server side, as there are more efficient ways of accomplishing it. In all honesty I can't think of any other scenario where this would genuinely be the best option. However, it worked well for my use case and allowed me to implement this functionality quickly and simply.