Very often you may need to interact with external APIs. If the request is simple, you can use built-in file_get_contents() function, or curl extension. But Laravel already uses a rich featured package for interacting with any kinds of APIs. You can use it too.
In this article we will explain how to request to an external API in Laravel using the guzzlehttp/guzzle package. This package has most of the necessary features to work with any API.
Table of Contents
How to Make Requests Using Guzzle Client
In this article I will use The Solar System OpenData API as an example. This is an open API, that does not require registration and authorization and is very simple. You can see its swagger documentation here. I also assume that you have installed Laravel framework. If it is not, see this article.
1. Make Client
The guzzlehttp/guzzle package is already installed as a Laravel dependency, so you don’t need to install it manually. Just import Guzzle Client in your class:
use GuzzleHttp\Client;
Next, create a new instance:
$client = new Client();
In the constructor set client settings. Here are several options that you can use:
- base_uri – url prefix that will be used in each request;
- timeout – time to wait for a response from a server;
- handler – extended client configuration.
For example:
$client = new Client([
"base_uri" => "https://api.le-systeme-solaire.net/rest/",
]);
2. Perform request
You can use this client to perform a request. For example you can get a list of all bodies in the Solar System using this GET request:
$response = $client->get("bodies");
Note, that you can specify the URL with a starting slash (/bodies) or without it (bodies). In the first case, the client took only the domain from base_uri. You will get a wrong URL:
https://api.le-systeme-solaire.net/bodies
So, in this case you should use the URL without a starting slash, and you will get:
https://api.le-systeme-solaire.net/rest/bodies
You can use not just the get() method. Other available request methods include post(), put(), delete(), patch() and general method request() that accepts the request type in the first parameter.
3. Decode Response
The Guzzle client will return a response, but you can use it as is because it is a stream. You need to extract plain text from it using the getContents() method:
$data = $response->getBody()->getContents();
Now, you can convert this data to JSON:
$json = \json_decode($data, true);
4. Catch Exceptions
Guzzle Client can throw a few exceptions: RequestException, ClientException, ConnectException. All of them extend TrasferException. So if you want to catch all Guzzle exceptions, you can do this:
try {
//Do request
} catch (TransferException $e) {
//Do something
}
5. Send Query and Other Params
Very often you might need to send query parameters, form parameters, JSON body or headers with an API request. You can send all these parameters as an array in the second parameter of the request method. In this example, it is get() but it can be post(), delete() or something else. For example, to request only Solar System body IDs set a query parameter data=id:
$response = $client->get("bodies", [
RequestOptions::QUERY => [
"data" => "id",
],
]);
If you want to send form parameters or headers, just use another RequestOptions class constant. Here are some usable constants:
- RequestOptions::QUERY – query parameters;
- RequestOptions::FORM_PARAMS – form parameters;
- RequestOptions::BODY – raw body content;
- RequestOptions::JSON – JSON body content;
- RequestOptions::HEADERS – request headers.
Here it is full code example of this request:
$client = new Client([
"base_uri" => "https://api.le-systeme-solaire.net/rest/",
]);
$response = $client->get("bodies", [
RequestOptions::QUERY => [
"data" => "id",
],
]);
$data = $response->getBody()->getContents();
dd(json_decode($data, true));
As you see, it’s pretty simple. You can find more information about Guzzle in official documentation.
6. Send JSON
If you need to send complicated JSON objects to API, you can fill all data in data transfer objects and use the jms/serializer package to get JSON out of them. For example we have an API that needs this JSON object:
{
"first_name": "John",
"last_name": "Doe",
"data": {
"age": 30,
"date_of_birth": "05/03/2002"
}
}
You can create two DTO:
class User
{
public string $first_name;
public string $last_name;
public UserData $data;
}
class UserData
{
public int $age;
public DateTime $date_of_birth;
}
Now, create objects for classes and fill them with data:
$user = new User();
$user->first_name = "John";
$user->last_name = "Doe";
$userData = new UserData();
$userData->age = 30;
$userData->date_of_birth = new DateTime("05/03/2002");
$user->data = $userData;
Now, you can serialize the $user object to JSON using jms/serializer:
$serializer = JMS\Serializer\SerializerBuilder::create()->build();
$jsonContent = $serializer->serialize($data, "json");
Now, you can use this JSON content in the Guzzle client:
$response = $client->get("some/route", [
RequestOptions::JSON => $jsonContent,
]);
Certainly, you can use standard json_encode() instead of the one shown in this simple example, but when things become more complicated jms/serializer will be helpful. You can find more examples in the official documentation of this package. Now let’s see the application structure that will help you make requests more efficiently.
How to Work with External API in Laravel
1. Application structure
If you want to make requests to some API, you’d better create a dedicated Client class for it and create methods for each API route. It makes code more structured and will help you test your code in the future.
As mentioned above, the Guzzle client can throw different exceptions. It’s a bad idea to let them walk in your application. They can break the integrity of data in your application if you forget to catch them. Also it’s not the best idea to return an array from the client class. You will forget what the API returns after several months, and the IDE will not be able suggest to you anything. So, you can catch all exceptions in the client and return a data transfer object with parsed response data and request status field.
Let’s create a client for the Solar System API that can get a list of objects in the Solar System and get info about them. We will need three classes:
- SolarSystemClient – client;
- BodiesList – data transfer object for a list of body IDs;
- BodyInfo – data transfer object for information about the body.
2. Create DataTransfers
As I wrote before, it is more convenient to work with objects instead of associative arrays. IDE will help you type variable names. But these are not all benefits. If we return only data, you lose data about errors and metadata. In a data transfer object you can store data fields and all metadata fields that you want. For example an exception, a raw response, status of the request, status code, etc. Let’s create DTO for a list of bodies request:
namespace App\DataTransfers;
class BodiesList
{
public array $bodies = [];
public bool $success;
public ?string $response = null;
public ?\Throwable $exception = null;
}
Here there are only the $bodies variable for data, and a few metadata variables. You can save the raw response for fast debugging. If data parsing fails, you will see input data and find where the issue is. Let’s look at DTO to getting information about the body:
namespace App\DataTransfers;
class BodyInfo
{
public bool $success;
public ?string $response = null;
public ?\Throwable $e = null;
public string $id;
public string $name;
public bool $isPlanet;
public int $density;
public int $gravity;
public string $bodyType;
}
Here you can see several data variables and the same metadata.
3. Create Client
Now, you can create a Client class. It should contain a Guzzle client, make requests, parse responses and place them in the data transfer objects. Here is a constructor:
private Client $client;
public function __construct()
{
$this->client = new Client([
'base_uri' => 'https://api.le-systeme-solaire.net/rest/'
]);
}
You can create a Guzzle client here, if it has a simple logic, or you can have it provided by service provider. Next, let’s see at getBodies() method:
public function getBodies(): BodiesList
{
try {
$response = $this->client->get("bodies", [
RequestOptions::QUERY => [
"data" => "id",
],
]);
$result = new BodiesList();
$result->success = true;
$result->response = $response->getBody()->getContents();
try {
$data = json_decode(
$result->response,
true,
512,
JSON_THROW_ON_ERROR
);
foreach ($data["bodies"] as $body) {
$result->bodies[] = $body["id"];
}
} catch (\JsonException $e) {
$result->success = false;
$result->exception = $e;
}
} catch (RequestException $e) {
$result = new BodiesList();
$result->response = $e
->getResponse()
->getBody()
->getContents();
$result->success = false;
$result->exception = $e;
} catch (TransferException $e) {
$result = new BodiesList();
$result->success = false;
$result->exception = $e;
}
return $result;
}
This code catches all exceptions that can be thrown when the program works normally. First it ensures that the remote server responds with a success status code. If not Guzzle will throw a RequestException. This exception have a link to the response and you can parse it. For example, you can extract an error message. Next it makes sure that Guzzle can connect to the remote server. It will throw one of the inheritors of the TransferException class. Here we have only an exception.
Furthermore, the server can respond with a status code 200 and empty result or invalid JSON. So we make sure that JSON has decoded normally, and if not, change success status in DTO to false and save an exception. Only after that we can parse response data.
Here is the code that allows getting information about the Solar System body. It acts same way:
public function getBodyInfo(string $id): BodyInfo
{
try {
$response = $this->client->get("bodies/$id");
$result = new BodyInfo();
$result->success = true;
$result->response = $response->getBody()->getContents();
try {
$data = json_decode(
$result->response,
true,
512,
JSON_THROW_ON_ERROR
);
$result->id = $data["id"];
$result->name = $data["name"];
$result->isPlanet = $data["isPlanet"];
$result->density = $data["density"];
$result->gravity = $data["gravity"];
$result->bodyType = $data["bodyType"];
} catch (\JsonException $e) {
$result->success = false;
$result->exception = $e;
}
} catch (RequestException $e) {
$result = new BodiesList();
$result->response = $e
->getResponse()
->getBody()
->getContents();
$result->success = false;
$result->exception = $e;
} catch (TransferException $e) {
$result = new BodiesList();
$result->success = false;
$result->exception = $e;
}
return $result;
}
Of course, these methods can be optimized. You can separate JSON decoding and parsing responses in dedicated functions. But I decided to keep everything together for better readability. Now you can create a command, run it and look at responses.
php artisan make:command TestRequestsCommand
And paste this code for the handle() method:
public function handle(SolarSystemClient $client)
{
$response = $client->getBodies();
dd($response);
return self::SUCCESS;
}
Here is sample list response:
Or body info response:
4. Log Requests
If you saved a raw response in the data transfer objects, you can write it in the database. But this is not the best way. Guzzle client supports middleware that will be executed for each request. You can use an existing middleware package, for example gmponos/guzzle_logger, or create your own middleware that will save all your requests to a database or special software, for example ClickHouse.
First, create a middleware class. It should be invokable:
use GuzzleHttp\Exception\RequestException;
use GuzzleHttp\Promise\Create;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
class LoggerMiddleware
{
public function __invoke(callable $handler)
{
return function (
RequestInterface $request,
array $options
) use (&$handler) {
return $handler($request, $options)->then(
function (ResponseInterface $response) use (
$request,
$options
) {
//When request success
$requestData = $request->getBody()->getContents();
$responseData = $response->getBody()->getContents();
//...
$request->getBody()->rewind();
$response->getBody()->rewind();
return $response;
},
function ($reason) use ($request, $options) {
//When request fails
$requestData = $request->getBody()->getContents();
$request->getBody()->rewind();
if ($reason instanceof RequestException) {
$responseData = $reason
->getResponse()
->getBody()
->getContents();
$reason
->getResponse()
->getBody()
->rewind();
} else {
$responseData = null;
}
return Create::rejectionFor($reason);
}
);
};
}
}
Now, you can do what you need with $requestData and $responseData variables. Note, that after extracting data from the request or response rewind them back to start using the rewind() method. If you don’t do this, whoever tries to get their contents after you, will get an empty result.
Next create a handler and push the middleware object into it:
use GuzzleHttp\HandlerStack;
$stack = HandlerStack::create();
$stack->push(new LoggerMiddleware());
$this->client = new Client([
"handler" => $stack,
"base_uri" => "https://api.le-systeme-solaire.net/rest/",
]);
Now the client will execute your middleware for all requests, and they will be logged.
5. Test the Client
If you want to write tests for your application this approach has benefits, too. Often you might not want to call external service and use hardcoded responses. With this client you don’t need to have full responses from the external service. You can just fill the data transfer object and use it in your program.
For example, let’s create a test that will mock the getBodies() method from SolarSystemClient:
php artisan make:test SolarSystemTest
Here is a sample code:
class SolarSystemTest extends TestCase
{
public function test_solar_system()
{
$this->mock(SolarSystemClient::class, function (MockInterface $mock) {
$mock->shouldReceive("getBodies")->andReturnUsing(function () {
$bodies = new BodiesList();
$bodies->success = true;
$bodies->bodies[] = "test";
return $bodies;
});
});
//Now, you can test app logic, for example:
$this->artisan("test:request");
}
}
This approach allows testing the application logic regardless of external services. Run the test, and your command will show you filled in data:
php artisan test --filter SolarSystemTest
Wrapping Up
In this article we have explained how to request to an external API in Laravel. The most popular tool for performing API requests is the guzzlehttp library. And now you know how to use it efficiently. What libraries and patterns do you use for API requests in Laravel? Tell us using the comment form below!