The payload-interop
project defines an interoperable interface for reading
the domain result and domain status from a Domain Payload Object, in a
way that is independent of any particular user interface.
The Domain Payload Object pattern was first described by Vaughn Vernon at https://vaughnvernon.co/?page_id=40
Whereas a Data Transfer Object provides properties found on domain objects, a Domain Payload Object provides entire domain objects. Its purpose is to transport those domain objects across the architectural boundary from the domain layer to the user interface layer.
The Domain Payload Object is not for intra-domain use. It is only for transporting a domain result across the domain layer boundary back to the calling code, typically the user interface layer.
Futher, the Domain Payload Object is independent of any particular user interface. As part of the domain layer, it should have no knowledge of HTTP or CLI contexts.
This project defines only a reading interface, so that any user interface code can know how to get both the result and the status out of the Domain Payload Object.
This project does not define a writing or mutation interface. Creation and manipulation of Domain Payload Objects are core application concerns, not user interface concerns. The domain-specific nature places it outside the scope of an interoperability specification.
Research on Packagist and elsewhere revealed a number
of existing Domain Payload Object implementations.
The commonalities discovered by this research are codified in the
DomainPayload
interface and the
DomainStatus
constants.
All of the reference implementations provided functionality to get the domain results or domain data for presentation. It was noted that these results included not only domain objects, but also the input as received by the domain, error or validation messages, exception objects, and so on.
As such, this project expands the semantics of the Domain Payload Object definition to include any data produced by the domain layer that might be needed for presentation by the user interface.
This commonality is reflected in the DomainPayload::getResult() : array
method.
Having found that commonality, this project encourages implementors to provide additional reading methods typhinted to their respective domain objects, to enable better static analysis and automated refactoring. In doing so, implementors may find themselves with multiple Domain Payload Object classes, one for each domain interaction (or family of related interactions).
The reference implementations also include functionality to discover the "status" of the attempted domain activity. That is, whether the activity was successful or not, and what kind of success or failure was encountered.
There was a wide range of status values, occasionally integers but more frequently strings, defined as constants. Some of these values obviously mimicked HTTP user interface response codes. Less often, statuses were indicated by separate classes, rather than by a constant value.
After normalizing and collating the different statuses across the reference implementations, it turned out there were several that were both user-interface agnostic and common to most or all implementations. These were most frequently string valued constants.
This commonality is reflected in the DomainPayload::getStatus() : string
method, along with the DomainStatus
constants:
ACCEPTED
CREATED
DELETED
ERROR
FOUND
INVALID
NOT_FOUND
PROCESSING
SUCCESS
UNAUTHORIZED
UPDATED
Implementors may add other status values as appropriate for their domain.
A basic, bare-bones implementation might look something like this:
namespace Application;
use PayloadInterop\DomainPayload;
class Payload implements DomainPayload
{
protected $status;
protected $result;
public function __construct(string $status, array $result)
{
$this->status = $status;
$this->result = $result;
}
public function getStatus() : string
{
return $this->status;
}
public function getResult() : array
{
return $this->result;
}
}
A domain layer Application Service, Transaction Script, or Use Case might use it like so:
namespace Application;
use Domain\Exception\InvalidInput;
use Domain\Forum\ForumRepository;
use Exception;
use PayloadInterop\DomainStatus;
class CreateForumTopic
{
protected $forumRepository;
public function __construct(ForumRepository $forumRepository)
{
$this->forumRepository = $forumRepository;
}
public function __invoke(string $title, string $body) : Payload
{
try {
$topic = $this->forumRepository->newTopic($title, $body);
$this->forumRepository->save($topic);
return new Payload(DomainStatus::CREATED, [
'topic' => $topic
]);
} catch (InvalidInput $e) {
return new Payload(DomainStatus::INVALID, [
'input' => [
'title' => $title,
'body' => $body,
]],
'messages' => $e->getMessages()
);
} catch (Exception $e) {
return new Payload(DomainStatus::ERROR, [
'exception' => $e
]);
}
}
}