The following article is a short snippet from the "Code Techniques" chapter of my book "Consuming APIs In Laravel".
The book teaches you how to write powerful and robust code to build API integrations in your Laravel apps. It's packed with over 440+ pages of actionable advice, real-life code examples, and tips and tricks.
In the book, I teach you how to write code that can help you sleep easy at night having confidence that your API integration works!
If you enjoy this content, you might want to check out the rest of the book. You can download a FREE sample from the "Webhooks" section that covers "Building Webhook Routes" and "Webhook Security".
๐ Make sure to use the discount code CAIL20 to get 20% off!
Data Transfer Objects (DTOs) in PHP
In PHP, arrays are both a blessing and a curse. They are flexible, and you can put almost anything you want in them, which makes them great for moving data around your application's code.
However, misusing them can make your code more difficult to understand and can make it unclear what data they contain. Additionally, arrays can make it more difficult to fully benefit from features such as PHP's type system.
For these reasons, I like to use classes such as "data transfer objects" (DTOs) instead of arrays where possible. Using these allows you to benefit from PHP's type system, get better autocomplete suggestions from your IDE, and reduce the chance of bugs.
Later in the book, we'll use DTOs to represent the data we're sending (in an HTTP request) and receiving (in an HTTP response) using Saloon. Saloon is a package you can use to interact with external APIs and comes with some convenient features. For this example, you don't need to know the specifics of how Saloon works under the hood โ we'll cover how to build your own API integrations using Saloon later in the book in the "Building an API Integration Using Saloon" chapter.
Let's look at a code example to understand the advantages of using DTOs in our code.
Imagine we have an app/Services/GitHubService
service class that can be used to interact with the GitHub API. The service class may have a getRepo
method that can be used to fetch data about a single GitHub repository. The outline of the method may look something like so:
app/Services/GitHubService.php
1public function getRepo(string $owner, string $repo): array2{3 return $this->client()4 ->send(new GetRepo($owner, $repo))5 ->json();6}
In the example, assume the $this->client()
call returns a Saloon client to send the request and that the GetRepo
class is a Saloon request class used to build the request. You don't need to understand how the request is being sent, but it's important to note that the json
method converts the API response to an array.
Now we have to ask an important question about this method: "What data is inside the array that's being returned?".
Without checking GitHub's API documentation or using something like XDebug, log()
, dd()
, or ray()
to inspect the data, we have no clear indication of what data is available in the array. We also don't know the data types of the values in the array. The API response for fetching a GitHub repository's data is large and contains 95 fields by default (including integers, strings, booleans, and arrays). As a result, it can be difficult to know how to use the data being returned to us.
Another issue that can arise from these types of scenarios is that we may accidentally reference a key that doesn't exist. For example, the API response contains a private
boolean field which indicates whether the GitHub repository is public or private. In our code, we may correctly reference this field like so:
1$repoData = app(GitHubService::class)->getRepo('ash-jc-allen', 'short-url');2 3$isPrivate = $repoData['private'];
However, somewhere else in our code, we may accidentally reference a non-existent is_private
key instead of the private
key:
1$repoData = app(GitHubService::class)->getRepo('ash-jc-allen', 'short-url');2 3$isPrivate = $repoData['is_private'];
As you'd imagine, this would throw a PHP error. Assuming this error was caught during the development process, you would need to either check the API documentation or inspect the API response to find the correct key name. This can be a tedious task and can slow down your development process. In the worst-case scenario, this error might only be spotted in the production environment, which could result in your application crashing for your users.
To reduce the likelihood of these issues occurring, we can use a DTO to represent the data being returned from the API. For example, we could create a GitHubRepoData
class that looks like so:
app/DataTransferObjects/Repo.php
1declare(strict_types=1);23namespace App\DataTransferObjects;45use Carbon\CarbonInterface;67final readonly class Repo8{9 public function __construct(10 public int $id,11 public string $name,12 public string $fullName,13 public bool $isPrivate,14 public string $description,15 public CarbonInterface $createdAt,16 ) {17 //18 }19}
You may have noticed that we only have a subset of the fields (6 out of the 95) returned in the API response. This can help keep your DTOs manageable by only including the fields you need for your application. If you need to add more fields later, you can do so without updating any of the code that uses the DTO. This helps keep your DTOs focused and indicates what data you're currently using in your codebase. However, suppose you're building a package that other developers will use to interact with an API. In that case, you'll likely want to include all the fields returned in the API response because you can't predict what fields other developers may need.
Using constructor property promotion and readonly classes/properties (covered in the next chapter), we can avoid creating "setter" or "getter" methods and don't need to assign the property values inside the constructor. So we can have a simple immutable object that doesn't need to be updated after instantiation.
Now that we have our DTO that represents the data being returned from the API, we can update our getRepo
method to return an instance of the DTO instead of an array.
1public function getRepo(string $owner, string $repo): Repo2{3 return $this->client()4 ->send(new GetRepo($owner, $repo))5 ->dtoOrFail(Repo::class);6}
You might wonder what the dtoOrFail
method is and where it originated. We'll cover this in more detail in a later section, but the dtoOrFail
method is provided by Saloon and converts a JSON response to an instance of a specified class. In our GetRepo
class, a createDtoFromResponse
method would need to be specified that handles mapping the API response to fields in a DTO that we can return like so:
1/** 2 * @param \Saloon\Http\Response $response 3 * @return \App\DataTransferObjects\Repo 4 */ 5public function createDtoFromResponse(Response $response): mixed 6{ 7 $responseData = $response->json(); 8 9 return new Repo(10 id: $responseData['id'],11 name: $responseData['name'],12 fullName: $responseData['full_name'],13 isPrivate: $responseData['private'],14 description: $responseData['description'] ?? '',15 createdAt: Carbon::parse($responseData['created_at']),16 );17}
Now that we've updated our getRepo
method to return an instance of our DTO, we can update our code to use the DTO instead of an array:
1$repoData = app(GitHubService::class)->getRepo('ash-jc-allen', 'short-url');2 3$isPrivate = $repoData->isPrivate;
Making these changes has improved the maintainability of our code because we are decoupling our application's code from the raw API response. For instance, we have been able to convert the created_at
field to a Carbon
object rather than leaving it as a string.
It has also allowed us to use PHP's type system to improve the type safety of our application. By defining the data types of each field in the Repo
DTO, we can have more confidence that we're working with the correct data types in the rest of our codebase.
An additional benefit is that we have also improved the predictability of our code. If you're using an integrated development environment (IDE) such as PhpStorm, you can get autosuggestions for the fields available on the DTO. This reduces the likelihood of referencing a non-existent field and helps speed up your development process by allowing you to write code faster and more confidently without leaving your editor.
Enjoyed This Snippet?
If you enjoyed this snippet, you might want to check out the rest of the "Consuming APIs in Laravel" book.
๐ Make sure to use the discount code CAIL20 to get 20% off!