DEV Community

Cover image for Explore the Advantages of Data Transfer Objects (DTOs) and How PHP 8.2 Readonly Classes Can Elevate Your Laravel Code
Indunil Peramuna
Indunil Peramuna

Posted on

Explore the Advantages of Data Transfer Objects (DTOs) and How PHP 8.2 Readonly Classes Can Elevate Your Laravel Code

In modern web application development, efficiently and securely managing and transferring data is crucial. One design pattern that significantly aids in this process is the Data Transfer Object (DTO). This post will delve into the advantages of using DTOs, particularly in a Laravel application, and show how PHP 8.2 readonly classes can further enhance their benefits.

What is a Data Transfer Object (DTO)?

A Data Transfer Object (DTO) is a simple object designed to carry data between processes or systems. Unlike typical models or entities, DTOs are free from business logic. They encapsulate data, providing a clear and structured way to transfer information between different layers of an application or between various systems.

DTO Pattern

The DTO pattern is utilized to transfer data across different subsystems within a software application. The main objectives of using DTOs are to minimize the number of method calls, aggregate the necessary data, and offer a structured approach to managing data transformations and validations.

Benefits of Using DTOs

  • Separation of Concerns: DTOs isolate business logic from data representation, resulting in cleaner, more maintainable code that’s easier to understand.

  • Data Validation: DTOs allow for validation of data before it's processed by other application layers, ensuring that only valid data is used.

  • Consistency: By providing a consistent structure for data transfer, DTOs simplify the management and processing of data from various sources.

  • Security: DTOs can safeguard your application against unauthorized data manipulation by controlling which data is accessible and modifiable.

  • Testing: Since DTOs are straightforward objects without embedded business logic, they are simpler to mock and test.

  • Transformation: DTOs facilitate the transformation of data into formats required by different application layers.

  • Immutability: DTOs often promote immutability, meaning once created, their state cannot change. This feature brings several advantages:

    • Predictability: Immutable objects are predictable and easier to reason about, as their state remains constant post-creation.
    • Thread-Safety: Immutability inherently supports thread-safety, simplifying concurrent processing.
    • Debugging: Debugging is easier with immutable objects because their state is guaranteed to remain unchanged throughout their lifecycle.

PHP 8.2 and Readonly Classes

With PHP 8.2, the introduction of readonly classes enhances the use of DTOs. Readonly classes eliminate the need to explicitly define properties as readonly, simplifying your DTO implementations. Here’s how PHP 8.2’s readonly classes improve DTOs:

  • Simplified Code: Readonly classes automatically make properties immutable, reducing boilerplate code and improving clarity.
  • Enhanced Security: By ensuring properties cannot be modified once set, readonly classes enhance data integrity and security.
  • Improved Maintainability: The use of readonly classes leads to cleaner, more maintainable code, as the immutability of data is enforced by the language itself.

Example: Using DTOs in a Property Management System

Let's consider a property management system where properties can come from various sources such as an API and CSV imports. We can use DTOs to create the Property model, Subscriptions, Assets, etc., ensuring that the data is consistent and validated across the application.

Defining the PropertyDTO

First, let's define a PropertyDTO class:

app/DTO/PropertyDTO.php

namespace App\DTO;

/**
 * Class PropertyDTO
 *
 * Represents a Data Transfer Object for property data.
 */
readonly class PropertyDTO extends AbstractDTO
{
    /**
     * The name of the property.
     *
     * @var string
     */
    public string $name;

    /**
     * The address of the property.
     *
     * @var string
     */
    public string $address;

    /**
     * The price of the property.
     *
     * @var float
     */
    public float $price;

    /**
     * The subscription status of the property, if applicable.
     *
     * @var string|null
     */
    public ?string $subscription;

    /**
     * The list of assets associated with the property.
     *
     * @var array|null
     */
    public ?array $assets;

    /**
     * Set the properties from a model instance.
     *
     * @param $model The model instance.
     * @return $this
     */
    public function setFromModel($model): self
    {
        $this->name = $model->name;
        $this->address = $model->address;
        $this->price = $model->price;
        $this->subscription = $model->subscription;
        $this->assets = $model->assets;

        return $this;
    }

    /**
     * Set the properties from API data.
     *
     * @param array $data The API data.
     * @return $this
     */
    public function setFromAPI(array $data): self
    {
        $this->name = $data['property_name'];
        $this->address = $data['property_address'];
        $this->price = $data['property_price'];
        $this->subscription = $data['subscription'] ?? null;
        $this->assets = $data['assets'] ?? null;

        return $this;
    }

    /**
     * Set the properties from CSV data.
     *
     * @param array $data The CSV data.
     * @return $this
     */
    public function setFromCSV(array $data): self
    {
        $this->name = $data[0];
        $this->address = $data[1];
        $this->price = (float) $data[2];
        $this->subscription = $data[3] ?? null;
        $this->assets = explode(',', $data[4] ?? '');

        return $this;
    }
}
Enter fullscreen mode Exit fullscreen mode
Using the PropertyDTO

Here's how you can use the PropertyDTO to handle properties from different sources:

// From a Model
$model = Property::find(1);
$propertyDTO = (new PropertyDTO([]))->setFromModel($model);

// From an API
$apiData = [
    'property_name' => 'Beautiful House',
    'property_address' => '1234 Elm Street',
    'property_price' => 450000,
    'subscription' => 'Premium',
    'assets' => ['pool', 'garden']
];
$propertyDTO = (new PropertyDTO([]))->setFromAPI($apiData);

// From a CSV
$csvData = ['Beautiful House', '1234 Elm Street', 450000, 'Premium', 'pool,garden'];
$propertyDTO = (new PropertyDTO([]))->setFromCSV($csvData);

// Convert to Array
$arrayData = $propertyDTO->toArray();

// Convert to JSON
$jsonData = $propertyDTO->toJson();
Enter fullscreen mode Exit fullscreen mode

Summary

Data Transfer Objects (DTOs) offer numerous advantages in Laravel applications by ensuring data consistency, validation, and separation of concerns. By implementing DTOs, you can make your application more maintainable, secure, and easier to test. In a property management system, DTOs help in handling data from various sources like APIs and CSV imports efficiently, ensuring that your business logic remains clean and focused on processing validated data.

Moreover, embracing immutability within DTOs enhances predictability, thread-safety, and simplifies debugging.

Extending DTOs with Abstract Classes for Consistency

To streamline the creation of DTOs and promote code reuse, we can use an abstract class or base class. This approach allows us to define common methods and properties in the abstract class and extend it for specific data sources.

Defining the AbstractDTO

app/DTO/AbstractDTO.php

namespace App\DTO;

/**
 * AbstractDTO
 *
 * An abstract base class for Data Transfer Objects (DTOs).
 * Provides common methods and properties for DTO implementations.
 */
abstract class AbstractDTO
{
    /**
     * AbstractDTO constructor.
     *
     * Initialises the DTO with data from an associative array.
     *
     * @param array $data The data array to initialize the DTO.
     */
    public function __construct(array $data)
    {
        $this->setFromArray($data);
    }

    /**
     * Set the properties of the DTO from a model instance.
     *
     * @param $model The model instance from which to populate the DTO.
     * @return $this
     */
    abstract public function setFromModel($model): self;

    /**
     * Set the properties of the DTO from API data.
     *
     * @param array $data The data array from the API.
     * @return $this
     */
    abstract public function setFromAPI(array $data): self;

    /**
     * Set the properties of the DTO from CSV data.
     *
     * @param array $data The data array from the CSV.
     * @return $this
     */
    abstract public function setFromCSV(array $data): self;

    /**
     * Convert the DTO to an associative array.
     *
     * @return array The DTO data as an associative array.
     */
    public function toArray(): array
    {
        $properties = get_object_vars($this);
        return array_filter($properties, function ($property) {
            return $property !== null;
        });
    }

    /**
     * Convert the DTO to a JSON string.
     *
     * @return string The DTO data as a JSON string.
     */
    public function toJson(): string
    {
        return json_encode($this->toArray());
    }

    /**
     * Set the properties of the DTO from an associative array.
     *
     * @param array $data The data array to populate the DTO.
     */
    protected function setFromArray(array $data): void
    {
        foreach ($data as $key => $value) {
            if (property_exists($this, $key)) {
                $this->$key = $value;
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Final Thoughts

Using an abstract or base class for DTOs not only ensures consistency across different DTO implementations but also promotes code reuse and maintainability. By defining common methods and properties in an abstract class, you can create a structured and efficient way to manage data transfer within your application. This approach aligns well with the principles of clean code and helps in building scalable and robust applications.

Here’s a revised phrase that includes a call to action:

"By leveraging DTOs and abstract classes together, you can refine your Laravel application's design, improving how data is managed and ensuring a more organised and efficient data flow. If you want to further encapsulate and enhance your DTOs with traits and interfaces, explore our guide on Enhancing Object-Oriented Design with Traits, Interfaces, and Abstract Classes."

Top comments (1)

Collapse
 
cnastasi profile image
Christian Nastasi

Hi @indunilperamuna, It's an interesting article, but it smells like AI-Generated to me.

You talk about immutability (that's good) and readonly classes, but you can't set a class property outside the __construct method.

So, this means that the setter method will not work as you expressed.

To make it work, you need to change it in this way:

public static function setFromAPI(array $data): static
{
    return new static(
        name: $data['property_name'],
        address: $data['property_address'],
        price: $data['property_price'],
        subscription: $data['subscription'] ?? null,
        assets: $data['assets'] ?? null
    );
}
Enter fullscreen mode Exit fullscreen mode

Another point is that you talked about validation, but I can't see any examples in your code. And if you put validation into a DTO, you have a Value Object.