Skip to content

Instantly share code, notes, and snippets.

@icanhazstring
Last active October 17, 2022 20:12
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save icanhazstring/c0a819eef032a9e8cf3e0c96b14be405 to your computer and use it in GitHub Desktop.
Save icanhazstring/c0a819eef032a9e8cf3e0c96b14be405 to your computer and use it in GitHub Desktop.
Working with optional fields in api responses

Based on @ocramius Handling optional input parameters in PHP with vimeo/psalm and azjezz/psl I tried to use the same approach for out API implementation also with shared SDK DTOs.

Using the same OptionalField given from ocramius, we can leverage that to add optional fields into the json serialized response. This way we also add the ability to work with

  • absent values
  • adding/updating values
  • or removal of values

Given an example

A Delivery has a DeliveryId and a possible Tracking DTO. This Tracking DTO holds a Carrier. But not all deliveries might have a Tracking available for them. To avoid handling null values on receiver and api provider side we can leverage the use of OptionalField.

How it works with json_encode

To be able to call json_encode on Delivery we need to make sure to only include the field with OptionalValue if it even has a value. So if OptionalValue::hasValue returns true.

Otherwise we would again include the field with an absent value into the response having it null, which is not correct. So we need to to give the OptionalField::apply() method a call back which adds the data into the return value for jsonSerializer().

public function jsonSerialize(): mixed
{
    $data = new stdClass();
    $valueExtractor = static fn(string $name, OptionalField $value): callable => static fn(mixed $value): mixed => $data->$name = $value;


    foreach (get_object_vars($this) as $name => $value) {
        if ($value instanceof OptionalField) {
            // This only adds $data->$name = $value if OptionalField::hasValue === true
            $value->apply($valueExtractor($name, $value));
            continue;
        }

        $data->$name = $value;
    }

    return $data;
}

We need to also be aware that if we want the Tracking to be empty, we need to make sure, that we can create it using Tracking::fromArray(null) which just returns null on creation for the OptionalField.

<?php
class Carrier
{
public function __construct(public readonly string $value)
{
}
public function jsonSerialize(): string
{
return $this->value;
}
}
class Tracking
{
public function __construct(public readonly Carrier $carrier)
{
}
public static function fromArray(?array $array): ?self
{
if ($array === null) {
return null;
}
\Webmozart\Assert\Assert::keyExists($array, 'carrier');
return new self(new Carrier($array['carrier']));
}
public function jsonSerialize(): array
{
return [
'carrier' => $this->carrier
];
}
}
class DeliveryId implements JsonSerializable
{
public function __construct(
public readonly string $id,
) {
}
public function jsonSerialize(): mixed
{
return $this->id;
}
}
class Delivery implements JsonSerializable
{
public function __construct(
public readonly DeliveryId $id,
public readonly OptionalField $tracking
) {
}
public static function fromArray(array $array): self
{
\Webmozart\Assert\Assert::keyExists($array, 'id');
return new self(
new DeliveryId($array['id']),
OptionalField::forPossiblyMissingArrayKey($array, 'tracking', \Psl\Type\mixed())
->map([Tracking::class, 'fromArray'])
);
}
public function jsonSerialize(): mixed
{
$data = new stdClass();
$valueExtractor = static fn(string $name, OptionalField $value): callable => static fn(mixed $value
): mixed => $data->$name = $value;
foreach (get_object_vars($this) as $name => $value) {
if ($value instanceof OptionalField) {
$value->apply($valueExtractor($name, $value));
continue;
}
$data->$name = $value;
}
return $data;
}
}
$absentTracking = [
'id' => '1cb4c033-939c-467c-a88a-ab01333492aa',
];
$withTracking = [
'id' => '1cb4c033-939c-467c-a88a-ab01333492aa',
'tracking' => [
'carrier' => 'dhl'
]
];
$removeTracking = [
'id' => '1cb4c033-939c-467c-a88a-ab01333492aa',
'tracking' => null
];
echo 'Absent Tracking' . PHP_EOL;
$d1 = Delivery::fromArray($absentTracking);
$d1->tracking->apply('var_dump');
var_dump(json_encode($d1));
echo PHP_EOL . PHP_EOL;
echo 'With Tracking' . PHP_EOL;
$d2 = Delivery::fromArray($withTracking);
$d2->tracking->apply('var_dump');
var_dump(json_encode($d2));
echo PHP_EOL . PHP_EOL;
echo 'Remove Tracking' . PHP_EOL;
$d3 = Delivery::fromArray($removeTracking);
$d3->tracking->apply('var_dump');
var_dump(json_encode($d3));
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment