2

Question

Can a data transfer object (DTO) reference a value object (VO) of the domain model?

Context

In my domain, I've an importer which import aggregates from a collection. The collection is made of DTO build by a collector on which the importer rely. Since both the importer and collector are services (interfaces) of my domain, can the DTO reference domain value objects, or should I stick with primitives and turn them into value objects only when processing the collection (build of aggregates)?

Collector implementation where DTO made of value objects from the domain model are built

<?php
/**
 * i-MSCP Patcher plugin
 *
 * @author        Laurent Declercq <l.declercq@nuxwin.com>
 * @copyright (C) 2019 Laurent Declercq <l.declercq@nuxwin.com>
 * @license       i-MSCP License <https://www.i-mscp.net/license-agreement.html>
 */

/**
 * @noinspection
 * PhpUnhandledExceptionInspection
 * PhpDocMissingThrowsInspection
 */

declare(strict_types=1);

namespace iMSCP\Plugin\Patcher\Infrastructure\Domain\Service\Component\Importer;

use iMSCP\Plugin\Patcher\Domain\Model\Component\ComponentBuild;
use iMSCP\Plugin\Patcher\Domain\Model\Component\ComponentName;
use iMSCP\Plugin\Patcher\Domain\Model\Component\ComponentVersion;
use iMSCP\Plugin\Patcher\Domain\Service\Component\Importer\ComponentCollector;
use iMSCP\Plugin\Patcher\Domain\Service\Component\Importer\DTO\ComponentDTO;
use iMSCP\Plugin\Patcher\Domain\Service\Component\Importer\DTO\ComponentDTOCollection;
use iMSCP_Config_Handler_File as MergedConfig;
use iMSCP_Plugin_Manager as PluginManager;
use RuntimeException;
use Throwable;

/**
 * Class DefaultComponentCollector
 * @package iMSCP\Plugin\Patcher\Infrastructure\Domain\Service\Component\Importer
 */
class DefaultComponentCollector implements ComponentCollector
{
    /**
     * @var MergedConfig
     */
    private $mergedConfig;

    /**
     * @var PluginManager
     */
    private $pluginManager;

    /**
     * DefaultComponentCollector constructor.
     *
     * @param MergedConfig $mergedConfig
     * @param PluginManager $pluginManager
     */
    public function __construct(
        MergedConfig $mergedConfig, PluginManager $pluginManager
    )
    {
        $this->mergedConfig = $mergedConfig;
        $this->pluginManager = $pluginManager;
    }

    /**
     * @inheritDoc
     */
    public function collect(ComponentDTOCollection $collection): void
    {
        try {
            // Core
            $collection->add(new ComponentDTO(
                ComponentName::fromString('core'),
                ComponentVersion::fromString($this->mergedConfig['Version']),
                ComponentBuild::fromString($this->mergedConfig['Build'])
            ));

            // Plugins
            $this->collectComponentsFromCorePluginManager($collection);
        } catch (Throwable $e) {
            throw new RuntimeException(sprintf(
                "Couldn't collect list of components: %s", $e->getMessage()
            ), $e->getCode(), $e);
        }
    }

    /**
     * Collects components from the i-MSCP core plugin manager.
     *
     * @param ComponentDTOCollection $collection
     * @return void
     */
    private function collectComponentsFromCorePluginManager(
        ComponentDTOCollection $collection
    ): void
    {
        $pluginList = $this->pluginManager->pluginGetList('all', false);

        foreach ($pluginList as $pluginName) {
            $pluginInfo = $this->pluginManager
                ->pluginGetInfo($pluginName)
                ->toArray();

            $componentDTO = new ComponentDTO(
                ComponentName::fromString($pluginInfo['name']),
                ComponentVersion::fromString($pluginInfo['version']),
                ComponentBuild::fromString((string)$pluginInfo['build'])
            );

            $collection->add($componentDTO);
        }
    }
}

1 Answers1

4

Can a data transfer object (DTO) reference a value object (VO) of the domain model?

Yes, but you want to be very careful about doing that.

A data transfer object is, at its core, a message. For a message to serve its purpose, both the sender and the receiver must have compatible understandings of its semantics. Making an incompatible change to the DTO schema requires corresponding changes to the receiver.

A value object, in the domain model, is not a message. It is structured information, purely an implementation detail of the current model. If we want to deploy a new version of our model, which uses a completely different arrangement of values, or their underlying data structures, then we can.

So having a DTO (which is supposed to be stable) depend on a value object (which does not promise to be stable) is creating an opportunity for problems down the road.

In cases where your vocabulary of values is stable, then the risks are lower.

VoiceOfUnreason
  • 52,766
  • 5
  • 49
  • 91