#
Response Decoder
HTTP response is a crucial aspect of web development, and it is essential to decode the response body to extract the necessary information. However, the response body is typically returned in a string format as JSON or XML, which can be challenging to work with. This is where a response decoder comes in handy. A response decoder can convert the HTTP response body from a string format to a more manageable format, such as an array. This conversion enables developers to extract the relevant information from the response quickly.
#
Configuring
The decoder should be configured as per-request basis. By default Fansipan\Request
uses ChainDecoder
to decode the response body. Essentially, it iterates over a list of JsonDecoder
and XmlDecoder
and attempts to read the Content-Type
header to determine which one to use for decoding the body.
#
Creating Custom Decoder
To create a custom decoder, you need to implement DecoderInterface
which defines the structure that a decoder must have. The contract contains only one method: decode
where you can implement your own logic to decode the response body. Then you can start using it in your request.
use Fansipan\Contracts\DecoderInterface;
use Fansipan\Request;
class MyRequest extends Request
{
public function decoder(): DecoderInterface
{
return new MyCustomDecoder();
}
}
#
Mapping Response Body To Object
When dealing with the response body of an HTTP request, utilizing Data Transfer Objects (DTOs) or Value Objects (VOs) can greatly enhance the way data is represented, processed, and transferred.
Standardized Data Structure: DTOs and VOs provide a standardized and consistent structure for representing the response body. This ensures that the data is organized in a predictable manner, making it easier for developers to understand and work with the response data. With a well-defined structure, it becomes simpler to parse, validate, and manipulate the response data.
Versioning and Compatibility: DTOs and VOs can help manage versioning and compatibility issues when dealing with APIs. If the structure of the response body changes over time, having DTOs/VOs in place allows for seamless handling of different versions. New fields can be added or deprecated fields can be removed in a controlled manner, without affecting the consuming codebase. This decoupling between the API and the consuming code helps to maintain backward compatibility and avoids breaking changes.
Validation and Data Integrity: DTOs and VOs can incorporate data validation rules, ensuring that the response body adheres to specific constraints or business rules. By validating the data at the point of parsing or deserialization into DTOs/VOs, potential errors or inconsistencies can be detected early on. This helps to maintain data integrity and prevents the propagation of invalid or inconsistent data throughout the application.
Serialization and Deserialization: DTOs and VOs can simplify the process of serializing and deserializing the response data. Many frameworks and libraries provide built-in mechanisms for converting objects to and from various formats such as JSON or XML. By utilizing DTOs/VOs, the serialization and deserialization process becomes more straightforward, reducing the amount of boilerplate code required.
Testability: DTOs and VOs can greatly enhance the testability of the code that handles HTTP requests. By representing the response body with DTOs/VOs, it becomes easier to write unit tests that verify the behavior of the code in isolation. Mocking or stubbing the response data becomes simpler, as the expected structure is well-defined. This improves the overall test coverage and makes it easier to catch bugs and regressions.
#
Creating Mapper
To create a mapper, your decoder must also implements MapperInterface
and include additional logic in the map
method to map the response body to an object.
use Fansipan\Contracts\DecoderInterface;
use Fansipan\Contracts\MapperInterface;
final class MyUserMapperDecoder implements DecoderInterface, MapperInterface
{
public function decode(ResponseInterface $response): iterable
{
return \json_decode((string) $response->getBody(), true);
}
public function map(ResponseInterface $response): ?object
{
// this is a very basic piece of code,
// you should also handle the mapping in case the response fails.
$decoded = $this->decode($response);
return new User(
$decoded['id'] ?? 0,
$decoded['name'] ?? '',
);
}
}
use Fansipan\Contracts\DecoderInterface;
use Fansipan\Request;
final class GetUserRequest extends Request
{
public function decoder(): DecoderInterface
{
return new MyUserMapperDecoder();
}
}
final class User
{
public int $id;
public string $name;
}
You can also utilize a (de)serializer/mapper in your decoder to automatically create the object. Here is an example using cuyz/valinor
:
The flowing examples use PHP 8.1+ syntax.
use CuyZ\Valinor\Mapper\TreeMapper;
use CuyZ\Valinor\MapperBuilder;
use Fansipan\Contracts\DecoderInterface;
use Fansipan\Contracts\MapperInterface;
use Fansipan\Decoder\ChainDecoder;
use Psr\Http\Message\ResponseInterface;
/**
* @template T of object
* @implements MapperInterface<T>
*/
final class CustomDecoder implements DecoderInterface, MapperInterface
{
private TreeMapper $mapper;
private DecoderInterface $decoder;
/**
* @param string|class-string<T> $signature
*/
public function __construct(
private readonly string $signature,
?TreeMapper $mapper = null,
?DecoderInterface $decoder = null
) {
$this->mapper = $mapper ?? (new MapperBuilder())
->allowSuperfluousKeys()
->mapper();
$this->decoder = $decoder ?? ChainDecoder::default();
}
public function map(ResponseInterface $response): ?object
{
$status = $response->getStatusCode();
$decoded = $this->decode($response);
if ($status >= 200 && $status < 300) {
return $this->mapper->map($this->signature, $decoded);
} else {
return $this->mapper->map(ErrorResponse::class, $decoded); // "Error" response object
}
}
public function decode(ResponseInterface $response): iterable
{
return $this->decoder->decode($response);
}
}
It is entirely up to the SDK developer to choose the (de)serializer/mapper to work with. Some notable mentions are:
cuyz/valinor
symfony/serializer
eventsauce/object-hydrator
crell/serde
spatie/laravel-data
jms/serializer
netresearch/jsonmapper
json-mapper/json-mapper
brick/json-mapper
#
Using Your Object
Then, you can retrieve your response body as an object:
$response = $connector->send(new GetUserRequest());
/** @var User $user */
$user = $response->object();
#
Typed object with generics support
Additionally, you can define the type your DTO/VO via generics annotations:
use Fansipan\Request;
/**
* @extends Request<User>
*/
final class GetUserRequest extends Request
{
}
$response = $connector->send(new GetUserRequest());
$user = $response->object();
// Your IDE recognizes that $user is an instance of User without the need for an @var annotation.
echo $user->id;
The @extends
annotation allows your IDE to understand which type your response body, and therefore allows for better static code analysis and code completion.
#
Working With List of Resources
Dealing with a list of resources is common when working with API endpoints, such as fetching a list of users. In typical scenarios, your code would have a method that returns something like this:
/**
* @return User[]
*/
public function fetchUsers(): array
{
//
}
However the $response->object()
method's return type is ?object
, which is not suitable. Fortunately, you can utilize a collection library like ramsey/collection
or Laravel Collection to represent your list of users as an object. If you prefer not to install additional dependencies, PHP's built-in ArrayIterator
is a solid choice for handling the list of resources.