PHP Roundtable #37

Hoszt:

Naményi Dávid (@NamenyiDavid)

Sági-Kazár Márk (@sagikazarmark)

Kocsis Máté (@kocsismate90)

Támogatónk:

  • Domain-Driven Design in a Nutshell II.

  • REST isn't the best

Mai témák

Domain-Driven Design in a Nutshell II.

- (more) advanced patterns and techniques

There is a first part:

The missing topics:

  • Command pattern

  • Aggregates

  • CQRS

  • Domain Events

  • Event Sourcing

Command pattern

HTTP

Command

Domain Object

SQL

Validation?

<?php

class PasswordChange extends AbstractMessage
{
    private string $password;

    private string $passwordConfirmation;

    private string $email;

    private string $username;

    private PersonName $name;

    public function __construct(
        string $password,
        string $passwordConfirmation,
        string $email,
        string $username,
        PersonName $name,
        ContextInterface $context
    ) {
        $this->password = $password;
        $this->passwordConfirmation = $passwordConfirmation;
        $this->email = $email;
        $this->username = $username;
        $this->name = $name;

        parent::__construct($context);
        $this->validate(new Validator());
    }

    public function getPassword(): string
    {
        return $this->password;
    }

    protected function validate(Validator $validator): void
    {
        $validator->validate(
            [
                new PasswordFormatRule($this->password),
                new PasswordConfirmationRule(
                    $this->password,
                    $this->passwordConfirmation
                ),
                new PasswordStrengthRule(
                    $this->password,
                    $this->email,
                    $this->username,
                    $this->name
                ),
            ]
        );
    }
}
<?php

abstract class AbstractMessage
{
    /**
     * @var ContextInterface
     */
    protected $context;

    public function __construct(ContextInterface $context)
    {
        $this->context = $context;
    }

    public function getContext(): ContextInterface
    {
        return $this->context;
    }

    protected function normalize(string $string): string
    {
        return Normalizer::normalize(trim($string), Normalizer::FORM_C);
    }
}
interface ContextInterface
{
    public function hasCurrentUser(): bool;

    /**
     * @throws UserNotFound
     */
    public function getCurrentUser(): CurrentUser;

    public function getPlan(): PlanInterface;

    public function getLanguage(): LanguageInterface;

    public function getServerTime(): DateTimeImmutable;

    public function getUserTime(): DateTimeImmutable;

    public function getClientIp(): string;
}
<?php

class WebContext implements ContextInterface { /* ... */ }

class ApiContext implements ContextInterface { /* ... */ }

Command Handler

~ Reusable Controller

Which layer do handlers belong to?

Aggregates

"A collection of related objects that we wish to treat as a unit."

Protects its invariants

Consistency Boundary

Transaction Boundary

Make it as small as possible*

Typical example

Less synthethic example

  • User has: profile, email addresses

  • Their gender can be either "male", "female", "other"

  • They have exactly one primary email address

  • They can have up to 10 email addresses

Design a Domain Model

My solution*

<?php

class ProfileAggregate() { /* ... */ }

class EmailAddressesAggregate() { /* ... */ }

My solution*

<?php

class UpdateProfile
{
    // ...

    public function execute(ServerRequestInterface $request, ResponseInterface $response): ResponseInterface
    {
        $body = $request->getParsedBody();    
    
        $message = new ProfileUpdate(
            body["first_name"],
            body["last_name"],
            $this->genderRepository->getGenderByCode($body["gender_code"]),
            $this->getContext()
        );

        $this->handler->handle($message);

        return $this->success($response);
    }
}

My solution*

<?php

class ProfileUpdateHandler
{
    // ...

    public function handle(ProfileUpdate $message): void
    {
        $aggregate = $this->repository->getProfileAggregateById(
            $message->getContext()->getCurrentUserId()
        );

        $aggregate->updateProfile($message);

        $this->repository->saveProfileAggregate($aggregate);
    }
}

My solution*

<?php

class ProfileAggregate
{
    // ...

    public function updateProfile(ProfileUpdate $message): void
    {
        $this->profile = $this->profile->updateProfile($message);
    }
}

My solution*

<?php

class Profile
{
    // ...

    public function update(ProfileUpdate $message): self
    {
        $self = clone $this;

        $self->name = $message->getName();
        $self->gender = $message->getGender();
        $self->birth = $message->getBirth();
        $self->bio = $message->getBio();
        $self->location = $message->getLocation();

        return $self;
    }
}

My solution*

<?php

class EmailAddresses
{
    // ...

    public function addEmailAddress(NewEmailAddress $message): self
    {
        if (isset($this->emailAddresses[$message->getEmailAddress()->toString()])) {
            throw new EmailAddressAlreadyExists();
        }

        if (count($this->emailAddresses) >= 10) {
            throw new TooManyEmailAddresses();
        }

        $self = clone $this;

        return $this->addEmailAddressFromValueObject($self, $message->getEmailAddress());
    }
}

My solution*

<?php

class EmailAddresses
{
    // ...

    public function removeEmailAddress(EmailAddress $emailAddress): self
    {
        if (isset($this->emailAddresses[$emailAddress->toString()]) === false) {
            throw new EmailAddressNotFound();
        }

        if (count($this->emailAddresses) <= 1) {
            throw new TooFewEmailAddresses();
        }

        if ($emailAddress->isPrimary()) {
            throw new EmailAddressCantBeRemoved();
        }

        $self = clone $this;

        unset($self->emailAddresses[$emailAddress->toString()]);

        return $self;
    }
}

Advantages/Disadvantages?

CQRS

Command-Query Responsibility Segregation

CQS on the model-level

Different  models during reading and writing

Less coupling

You can read from MongoDB

and write to MySQL

You can hydrate data into array

and still persist Domain Objects

Domain Events

Commands -> Events

Looser coupling

Less cohesion

<?php

class RegistrationHandler_Final_V1
{
    // ...

    public function signUp(Registration $message): Uuid
    {
        // Get the Subscription ID

        // Check rate limit

        // Sign up user

        // Increment rate limit

        // Send email verification email

        // Subscribe user to newsletter

        // Update statistics

        return $aggregate->getUserId();
    }
}
<?php

class UserSignedUp
{
    private string $userId;

    public function __construct($userId)
    {
        $this->userId = $userId;
    }

    public function getUserId()
    {
        return $this->userId;
    }
}
<?php

class RegistrationHandler_Copy_Final_V2
{
    // ...

    public function signUp(Registration $message): Uuid
    {
        // Get the Subscription ID

        // Check rate limit

        // Sign up user

        // Increment rate limit

        // Dispatch events

        return $aggregate->getUserId();
    }
}

Event Sourcing

Version control at the model-level

CQRS + Event Sourcing

Eventual consistent reads

Are these practices worth all the fuss?

Further reading:

Thanks!

Domain-Driven Design in a Nutshell II.

By Máté Kocsis

Domain-Driven Design in a Nutshell II.

  • 681