PHP Roundtable #37
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