Sunday, November 17, 2024

Data Transfer Objects

Data Transfer Objects (DTOs) decouple the internal structure of your models, entities, or database schema from the data exposed to the external world (e.g., APIs, front-end). They serve as a contract for the data being transferred, which helps ensure that changes in your data layer don't inadvertently affect the consumer.

Another benefit of DTOs is to enforce structured data and eliminate errors from missing [string] keys. By using a constructor, you control how the raw data is mapped. If a key is missing, you can provide a default value or handle the absence gracefully or you can add validation logic centrally in constructor. Using DTOs allows tools like PhpStorm or VSCode to identify missing properties during development. When working with arrays, typos in keys can go unnoticed until runtime, with DTOs you are using object fields which don’t have this weakness. Example without DTO (note the usage of error prone string keys):

private function prepareProductDetails(array $product): array {
        $product['photos'] = $this->processProductPhotos($product['photos']??[]);
        $product['tr_currency_formatted'] = isset($product['deposit']) 
                ?Currency::formatTRY($product['deposit'])
        return $product;
    }

With DTO:

class ProductDetailsDTO {
    public array $photos;
    public ?float $deposit = null;
    public ?string $tr_currency_formatted = null;
    public function __construct(array $data) {
        $this->photos = $data['photos'] ?? [];
        $this->deposit = $data['deposit'] ?? null;
    }
    public function setFormattedCurrency(callable $formatter): void {
        if ($this->deposit !== null) {
            $this->tr_currency_formatted = $formatter($this->deposit);
        }
    }
}

Note the usage of object fields instead of string keys:

private function prepareProductDetails(array $product): ProductDetailsDTO {
    $productDTO = new ProductDetailsDTO($product);
    $productDTO->photos = $this->processProductPhotos($productDTO->photos);
    $productDTO->setFormattedCurrency(fn($amount) =>
        Currency::formatTRY($amount));
    return $productDTO;
}