2. Domain Design

Radar for PHP

2. Domain Design

Radar concentrates exclusively the HTTP request/response cycle. This means that, for Radar to be useful, you need to build your Domain outside of, and probably in parallel with, your Radar wrapper around that Domain.

With that in mind, this is a minimalist primer on building a Domain service. For more information, please consult Domain Driven Design and similar works.

2.1. Application Service

All Radar cares about is the outermost (or topmost) entry point into the Domain layer. This entry point is likely to be something like an ApplicationService.

Your ADR Action will pass user input into the ApplicationService. The ApplicationService will initiate and coordinate all the underlying activity in the Domain, and return a Payload back to the Action for the Responder to use.

The ApplicationService should never access anything directly in the HTTP or CLI layer. Everything it needs should be injected from the outside, either at construction time or through a method call. For example, no superglobal should ever appear in an ApplicationService (or anywhere else in the Domain either). This is to make sure the ApplicationService, and by extension the Domain as a whole, is independent from any particular user interface system.

Each ApplicationService should be as narrowly-purposed as possible, handling either a single activity, or a limited set of related activities.

2.2. Class Structure

In a todo system, for example, there might be a single TodoApplicationService with methods for browse, read, edit, add, and delete:

namespace Domain\Todo;

class TodoApplicationService
{
    // fetch a list of todo items
    public function getList(array $input) { ... }

    // edit a todo item
    public function editItem(array $input) { ... }

    // mark a todo item as done or not
    public function markItem(array $input) { ... }

    // add a new todo item
    public function addItem(array $input) { ... }

    // delete a todo item
    public function deleteItem(array $input) { ... }
}

Alternatively, and perhaps preferably, there might be a series of single-purpose Todo application services:

namespace Domain\Todo\ApplicationService;

class GetList
{
    public function __invoke(array $input) { ... }
}

class EditItem
{
    public function __invoke(array $input) { ... }
}

class AddItem
{
    public function __invoke(array $input) { ... }
}

class DeleteItem
{
    public function __invoke(array $input) { ... }
}

2.2.1. Domain Logic

The logic inside the ApplicationService is entirely up to you. You can use anything from a plain-old database connection to a formal DDD approach. As long as the ApplicationService returns a Payload, the internals of the ApplicationService and its interactions do not matter to Radar.

Here is a naive bit of logic for a Fetch service in our todo application. It guards against several error conditions (anonymous user, invalid input, user attempting to edit a todo item they do not own, and database update failures). It returns a Payload that describes exactly what happened inside the Domain. Also notice how it is completely independent from HTTP or CLI elements; this makes it easier to test in isolation, and to reuse in different interfaces.

namespace Domain\Todo\ApplicationService;

use Aura\Payload\Payload;
use Aura\Payload_Interface\PayloadStatus;
use Exception;
use Todo\User;
use Todo\Mapper;

class EditItem
{
    public function __construct(
        User $user,
        Mapper $mapper,
        Payload $payload
    ) {
        $this->user = $user;
        $this->mapper = $mapper;
        $this->payload = $payload;
    }

    public function __invoke(array $input)
    {
        if (! $this->user->isAuthenticated()) {
            return $this->payload
                ->setStatus(PayloadStatus::NOT_AUTHENTICATED);
        }

        if (empty($input['id'])) {
            return $this->payload
                ->setStatus(PayloadStatus::NOT_VALID)
                ->setInput($input)
                ->setMessages([
                    'id' => 'Todo ID not set.'
                ]);
        }

        $todo = $this->mapper->fetchById($input['id']);
        if (! $todo) {
            return $this->payload
                ->setStatus(PayloadStatus::NOT_FOUND)
                ->setInput($input);
        }

        if ($this->user->userId !== $todo->userId) {
            return $this->payload
                ->setStatus(PayloadStatus::NOT_AUTHORIZED)
                ->setInput($input);
        }

        try {
            $todo->description = $input['description'];
            $this->mapper->update($todo);
            return $this->payload
                ->setStatus(PayloadStatus::UPDATED)
                ->setOutput($todo);
        } catch (Exception $e) {
            return $this->payload
                ->setStatus(PayloadStatus::ERROR)
                ->setInput($input)
                ->setOutput($e);
        }
    }
}

2.2.2. Domain Packaging

Although you can place the Domain layer in the Radar src/ directory, it may be wiser to create it as a separate package, and import it via Composer. This will help to enforce the separation between the core application and the Radar user-interface wrapper around it, along with test suites independent from the Radar project.