webdevilopers / php-ddd Goto Github PK
View Code? Open in Web Editor NEWPHP Symfony Doctrine Domain-driven Design
PHP Symfony Doctrine Domain-driven Design
When reading my first DDD Java examples I saw a lot of List
s:
class Order
{
private Integer ID;
private Customer owner;
private List<Product> ordered;
A List
is an interface like Set
and has ArrayList
as stated by @iamzero
Collection
as stated by @brendanlongUnfortunately PHP does not have a true equivalent as @giorgiosironi knows:
But we have @doctrine Collections:
And alternatives like Ardent
by @morrisonlevi:
The docs say:
An ordered collection (also known as a sequence). The user of this interface has precise control over where in the list each element is inserted. The user can access elements by their integer index (position in the list), and search for elements in the list.
When using @doctrine ArrayCollection
s you can use the indexBy
mapping to make your Collection
indexed (ordered):
Doctrine 2 collections are modelled after PHPs native arrays. PHP arrays are an ordered hashmap, but in the first version of Doctrine keys retrieved from the database were always numerical unless INDEX BY was used.
You have to manage both the key and field if you want to change the index by field value.
But you have to be careful when using accessors as @MacDada warns:
Still it seems to be a matter of taste if you use ArrayCollection
at all in your Domain Models:
Don't be alarmed by the use of ArrayCollections. Notice that is in the Doctrine/Common namespace. It's just a little utility array wrapper with no particular ties to the Doctrine persistence layer. You could easily replace it with another array class.
@tPl0ch warns:
The problem with Doctrine collections and the ORM is that you cannot use custom Collection implementations since you will always receive a PersistenCollection in your proxies.
What are your thoughts? Any use cases? Any comparison to the Java DDD approaches using List
?
Though I'm personally not using it here is a blog about the interface implementation by @dericcain for Eloquent and Laravel:
For further adapting to Domain-Driven Design please refer to #13.
Came from:
do you have any example of the process manager pattern in PHP?
/cc @iosifch
Example coming soon.
When deserializing messages (e.g. Domain Events inside the same context) that hold value objects, do you still validate the given data or do you simply trust the incoming data?
Currently my objects offer a toArray() and fromArray() (see fromArray_v2) method. But the latter could theoretically be used by a developer to create "invalid state".
Especially when value objects do some custom logic e.g. price calculation it gets tricky. Maybe the calculation has changed when replaying the Domain Event.
Alternatively I could go through the constructor again. But then the calculation is again executed though it may has changed.
Which version do you prefer? Or are there any additional checks or other factory method suggestions?
final class CustomPrice
{
/** @var int $quantity */
private $quantity;
/** @var float $subtotal */
private $subtotal;
/** @var float $total */
private $total;
/**
* CustomPrice constructor.
* @param int $quantity
* @param float $subtotal
*/
public function __construct(int $quantity, float $subtotal)
{
$this->quantity = $quantity;
$this->subtotal = $subtotal;
$this->total = $quantity*$subtotal;
}
/**
* @param array $array
* @return CustomPrice
*/
public static function fromArray(array $array): CustomPrice
{
Assert::keyExists($array, 'quantity');
Assert::integer($array['quantity']);
Assert::keyExists($array, 'subtotal');
Assert::float($array['quantity']);
return new self($array['quantity'], $array['subtotal']);
}
public static function fromArray_v2(array $array): CustomPrice
{
Assert::keyExists($array, 'quantity');
Assert::integer($array['quantity']);
Assert::keyExists($array, 'subtotal');
Assert::float($array['quantity']);
Assert::keyExists($array, 'total');
Assert::float($array['total']);
$self = new self();
$self->quantity = $array['quantity'];
$self->subtotal = $array['subtotal'];
$self->total = $array['total'];
return $self;
}
/**
* @return array
*/
public function toArray(): array
{
return [
'quantity' => $this->quantity,
'subtotal' => $this->subtotal,
'total' => $this->total
];
}
Original discussion started here:
P.S.:
The value object shown here is stored inside the payload of a Domain Event. The payload uses primitives only. In order to use on a event handler method on the aggregate Root it has to be re-constructed from those primitives.
Came from:
Example:
/cc @AntonStoeckl
Our FirstName
value object has changed over time. It started with no constraints (FirstName_V1). That's whay event payload like the following was possible:
{"firstName":"Max 12345"}
Later constraints e.g. no numbers allowed
were added (FirstName_V2). The names were fixed. Now the event history that has to applied to the Person
aggregate root includes this:
{"firstName":"John 12345"},
{"firstName":"John Doe"},
When creating the event and serializing the data this will not cause any problem.
$event = self::occur(
$personId->toString(),
[
'personId' => $personId->toString(),
'oldFirstName' => $oldFirstName->toString(),
'newFirstName' => $newFirstName->toString(),
]
);
But when applying it...
protected function apply(AggregateChanged $event): void
{
switch (get_class($event)) {
case NameChanged::class:
/** @var NameChanged $event */
$this->firstName = $event->newFirstName();
...it will break and throw the NameContainsIllegalCharacters
exception.
private function __construct(string $aName)
{
$name = NameNormalizer::withString($aName);
if (!NamePolicy::isSatisfiedBy($name)) {
throw new NameContainsIllegalCharacters();
}
$this->name = $name;
}
public static function fromString(string $name): FirstName
{
return new self($name);
}
There are two solutions I can imagine so far:
(1) Somehow catch the date of the BC for the payload and convert the old breaking payload to the new constraints. Similar to "event upcasting".
(2) Add a named constructor e.g. fromPayload
that skips the validation. Similar to value objects based on multiple properties that have an fromArray
and toArray
e.g.:
abstract class DomainMessage implements Message
{
public static function fromArray(array $messageData): DomainMessage
{
MessageDataAssertion::assert($messageData);
$messageRef = new \ReflectionClass(\get_called_class());
/** @var $message DomainMessage */
$message = $messageRef->newInstanceWithoutConstructor();
$message->uuid = Uuid::fromString($messageData['uuid']);
$message->messageName = $messageData['message_name'];
$message->metadata = $messageData['metadata'];
$message->createdAt = $messageData['created_at'];
$message->setPayload($messageData['payload']);
return $message;
}
}
Source: https://github.com/prooph/common/blob/master/src/Messaging/DomainMessage.php#L49-L65
This example even uses reflection in order to skip the constructor.
Concrete solution:
/**
* Character e.g. numbers were forbidden.
*/
final class FirstName_V2
{
/** @var string $name */
private $name;
private function __construct()
{
}
public static function fromString(string $aName): FirstName
{
$name = NameNormalizer::withString($aName);
if (!NamePolicy::isSatisfiedBy($name)) {
throw new NameContainsIllegalCharacters();
}
$self = new self();
$self->name = $name;
return $self;
}
public static function fromPayload(string $name): FirstName
{
$self = new self();
$self->name = $name;
return $self;
}
public function toString(): string
{
return $this->name;
}
}
Disadvantage: the validation is no longer inside the constructor. This is fine as long there is only a single named constructor that requires it. If there are multiple methods I guess you would have to put them into an extra validate
method.
Please note:
Normally a first name is not a critical property that needs to be stored inside an aggregate root since it is not relevant when protecting the invariants when changing state. But let's assume it is in example. That's why I added the old first name to the event.
I will post my Symfony example soon.
Until then: how should the generic subdomain be named? Most examples suggest IdentityAccess
. Others demand using the ubiquitous language
.
Here is poll on twitter - feel free to vote and comment here:
For instance @symfony uses the Interface
Suffix e.g. FooRepositoryInterface
:
I've seen C# and @dotnet examples prefixing Interfaces with an I
e.g. IFooRepository
:
Why the prefix in C# but not in Java?
The difference between Java and C# is that Java allows you to easily distinguish whether you implement an interface or extend a class since it has the corresponding keywords implements and extends.
As C# only has the : to express either an implementation or extension, I recommend following the standard and put an I before an interface's name.
Or:
Because of that, I don't think conventions are nearly as important in java for interfaces, since there is an explicit difference between inheritance and interface implementation.
Further links for discussion:
Since PHP offers the implements
keyword I think it is best practice to NOT USE any prefix or suffix.
At least inside the Domain Model
namespace:
While working with Symfony you should adapt their naming conventions.
Personally and since I started to put all my Interfaces into my Application or Infrastructure namespace I don't use any prefix or suffix at all anymore.
Related:
Hello @juliendufresne!
We recognized you already create a great organization here:
https://github.com/php-ddd
And you already added some @symfony bundle integrations:
Are you interested in joining us resp. adding us to your organization? Feel free to ask me or @yvoyer about our plans. Just comment here or send us a DM on twitter. Thanks!
I know that DDD is good with a Task-Based UI, but I'm refactoring a legacy app, where I have an Anemic Domain Model (many setters without the business logic).
One of the first steps was to make it reach model and add Domain Events. While adding events for creating (TaskCreated
in constructor) and removing (TaskRemoved
) the model is an easy process, I'm struggling with updating the model.
We have a RESTful API with PUT /tasks/{id}
endpoint. Under the hood the framework maps the body of the response to Task
object and then calls setters one by one:
$task->setText('new text');
$task->setStartDate($newStartDate);
// and so on
I want to listen some event when the task is updated and update it in e.g. Google Calendar.
As you can imaging, if I record events in each setter
(TextChanged
, StartDateChanged
) and listen all of them, I will end up with many API calls to the Google API, which is not what I want.
Question is: how should I work with Update operation in the right way? Should I replace all those setters
calls with one ->update($newData)
call and dispatch only one domain event there? How to make only one API call to google calendar after the task is updated?
PS: I've read tons of posts but didn't find useful information about Updating domain model.
The real example I found in the dddinphp
repository in UpdatePostHandler
class, where they call updaters (setters) one by one: https://github.com/dddinphp/blog-cqrs/blob/master/src/CQRSBlog/BlogEngine/Command/UpdatePostHandler.php#L27
// UpdatePostHandler.php
$aPost->changeTitle($anUpdatePostCommand->getTitle());
$aPost->changeContent($anUpdatePostCommand->getContent());
// Post.php
public function changeTitle($aNewTitle)
{
$this->applyAndRecordThat(
new PostTitleWasChanged($this->postId, $aNewTitle)
);
}
If anywhere in your application e.g. in your Handler or DomainModel
an Exception / DomainException
is thrown you can catch and convert the message to use it in your Symfony form:
$fooCommand = FooCommand::fromBar($bar);
$form = $this->createForm(FooForm::class, $fooCommand);
$form->handleRequest($this->getRequest());
if ($form->isValid()) {
$fooHandler = $this->get('acme.handler.foo');
try {
$fooHandler->handle($fooCommand);
return $this->redirectToRoute('acme_foo_list');
} catch (\Exception $e) {
$form->addError(new FormError($e->getMessage()));
}
}
And then inside your TWIG template:
{{ form_errors(form) }}
@beberlei shows a good example on how to use this approach here:
http://whitewashing.de/2012/08/22/building_an_object_model__no_setters_allowed.html
With the following code you can add the EditPostCommand
to the data_class
of the EditPostType
form:
<?php
class EditPostCommand
{
public $id;
public $headline;
public $text;
public $tags = array();
}
<?php
class PostController
{
public function editAction(Request $request)
{
$post = $this->findPostViewModel($request->get('id'));
// This could need some more automation/generic code
$editPostCommand = new EditPostCommand();
$editPostCommand->id = $request->get('id');
$editPostCommand->headline = $post->headline;
$editPostCommand->text = $post->text;
$editPostCommand->tags = $post->tags;
// here be the form framework handling...
$form = $this->createForm(new EditPostType(), $editPostCommand);
$form->bind($request);
if (!$form->isValid()) {
// invalid, show errors
}
// here we invoke the model, finally, through the service layer
$this->postService->edit($editPostCommand);
}
}
But it seems like Commands should be regarded immutable too which means the properties should not be public. Of course Symfony Form needs at least these to populate the Command.
@matthiasnoback has an interesting solution for this Symfony Form Use Case:
https://github.com/matthiasnoback/convenient-immutability
A Groovy example by @bodiam:
http://www.jworks.nl/2011/08/26/friday-repost-groovys-immutable-pitfalls/
General discussion:
https://groups.google.com/forum/#!msg/dddinphp/a5ielXU3EZg/qEoi5ppWAgAJ
Is there any approach how we could use the Command with Symfony Form if the properties were private
?
Maybe using the empty_data
callback on the Command / DTO and a static method as suggested by @webmozart:
http://whitewashing.de/2012/08/22/building_an_object_model__no_setters_allowed.html#comment-1091314235
Came from:
/cc @matthiasnoback @ashishkpoudel @cherifGsoul @stemmlerjs
Just finished reading your "Web Application Architecture" book @matthiasnoback . I finally understood the difference between READ and VIEW models. In your chapter about layers you suggest to put VIEW models into the application layer. But where to put the READ models?
Since in #CQRS for instance a read model represents the state of the write model, some people state it should belong to the domain layer. I agree VIEW models are optimized for presentation and should live elsewhere. E.g. application or as we like to add a "presentation" layer.
@matthiasnoback states:
I'm not sure if the distinction between read and view model is a very common one. For a view model is used to expose data to actors (e.g. users). A read model exposes data to the system itself (i.e. it's for internal use). It makes sense to put that one in the domain layer.
Related:
Here is what I thought could represent a good use case to start an example of ddd with all types of relations (one to many, many to many), while being not too complex.
I will make a draft with tests to start a domain model (without symfony, doctrine or command bus). From there, we'll be able to start doing infrastructure, and application context.
Domain requirements:
Mr. Smith is the proud owner of the restaurant Smith's Pizzeria. He wish to have a site to manage it's company and promote it's products on the Web. Here are the requirement he wishes you to implement.
@webdevilopers Any other cases you would need that could represent the domain?
I initially startet a post here focussing on where to put @symfony bundles:
Recently I found an example for @silexphp by @danitome24:
He was also inspired by the @dddinphp book.
Allthough he does not use the Domain\Model
directory. Instead he is separating Entity
from ValueObject
.
I prefer the approach to share the same folder for Domain Models that belong to an Aggregate.
Instead of Domain\Entity\User
and Domain\ValueObject\UserId
it should be Domain\Model\User\User
, Domain\Model\User\UserId
etc..
I'm not sure about the Application layer though. Though it is not the Domain Layer I prefer the same approach in order to quickly identify which Commands, Handlers etc. relate to an aggregate:
Application\Service\User\RegisterUserCommand
Application\Service\User\RegisterUserCommandHandler
What are your thoughts on this @danitome24?
The discussion originally started on Twitter:
The question was triggered by an older post by @shijuvar:
Later @gnugat and @mauroservienti joined the discussion and @SelrahcD opened a gist here:
Voting results:
In the end I pretty much agree with @SelrahcD
I guess a service and an abstract command are quite the same in the end, so yes, an abstract command would work. I won't use the same command yet and keep the two "real" commands separated.
Currently we have a service A that publishes messages to the outside world (e.g Service B) whenever 1 of about 20 stage changes happen in our context.
final class StaffMember extends AggregateRoot
{
public function assignToJobFunction(JobFunctionId $jobFunctionId, string $jobFunctionName): void
{
$this->recordThat(AssignedToJobFunction::with($this->staffMemberId, $jobFunctionId, $jobFunctionName));
}
// 19 more state changes
}
final class StaffPublisher extends Producer implements MessageSubscriberInterface
{
/** @var ProducerInterface */
protected $producer;
public function whenAssignedToJobFunction(AssignedToJobFunction $event): void
{
$this->publish($event->serialize(), 'acme.staff.staff.member_assigned_to_job_function');
}
}
The consumer does not require to know all the details leading to the actual state change. And we do not want to add a message broker routing key all the time.
After reading the following article by @mathiasverraes we decided to go for a "Summary Event":
You can use a Projection to generate the Summary Event. A Projection is an event listener that persists state from events it listens to, and either exposes that state by responding to queries, or by emitting new events.
The problem with our current implementation:
When the event reaches our Publisher ("Event Listener") only some state data is in-memory of the event. We could try to query the read model. But there is no guarantee that is has already been updated before.
One way (1) to solve that is to force a delay. Or use a cronjob (2 that sends summaries every 2 minutes. The latter solution would require some kind of publishing log to know which summaries were already published.
Another hack (3) would be to query the read model which may not be up-to-date yet. But then overwrite its data only with the properties that changed taken from the event e.g. "jobFunctionId" + Name.
A different approach (4) we are thinking about is adding an event e.g. "StaffMemberModified" or "StaffMemberDetailsSummarized". This could be fired in addition to all the other domain events and include the entire state.
final class StaffMember extends AggregateRoot
{
/** @var StaffMemberId */
private $staffMemberId;
/** @var JobFunctionId */
private $jobFunctionId;
/** @var string */
private $jobFunctionName;
// 19+x more properties holding the state only for the extra event, currently not required for protecting invariants
public function assignToJobFunction(JobFunctionId $jobFunctionId, string $jobFunctionName): void
{
$this->recordThat(AssignedToJobFunction::with($this->staffMemberId, $jobFunctionId, $jobFunctionName));
$this->recordThat(StaffMemberDetailsSummarized::with($this->staffMemberId, $jobFunctionId, $jobFunctionName, // entire state...));
}
// 19 more state changes
}
This solves the publisher problem. But it adds the entire state on the event-sourced aggregate-root. This has no technical disadvantages though. It's just more code.
Do you have any suggestions?
Thanks in advance.
Came from:
What really interests me is defining the actual aggregate root from the Order POV.
In my current company I have to processes:
Regarding the Offer and Order it is the Employee with the active part. In the red book there is this example "Table 1.5 Analyzing the best model for the business.":
http://www.informit.com/articles/article.aspx?p=1944876&seqNum=3
Now I started thinking that it should be something like:
or at least:
instead of:
This really confused me! ;) My feeling as a developer tells me to use the last approach.
What do you think @yvoyer ?
We enrich our Events when we reference another aggregate by ID e.g. employerId
by adding a name employerName
.
This allows us to directly update the read model.
A+ES:
final class EmploymentContractSigned extends AggregateChanged
{
/** @var EmploymentContractId $contractId */
private $contractId;
/** @var EmployerId */
private $employerId;
}
Projector:
final class EmploymentContractDetailsProjection implements ReadModelProjection
{
public function project(ReadModelProjector $projector): ReadModelProjector
{
$projector->fromStream('employment_contract_stream')
->when([
EmploymentContractSigned::class => function ($state, EmploymentContractSigned $event) {
/** @var EmploymentContractDetailsReadModel $readModel */
$readModel = $this->readModel();
$readModel->stack('sign', [
'contract_id' => $event->contractId()->toString(),
'employer_id' => $event->employerId()->toString(),
'employer_name' => $event->employerName(),
'created_at' => $event->createdAt()->format('Y-m-d H:i:s'),
'created_by_user_id' => $event->metadata()['created_by_user_id'],
'created_by_username' => $event->metadata()['created_by_username'],
'created_by_full_name' => $event->metadata()['created_by_full_name']
]);
},
]);
return $projector;
}
}
Sometimes an Employer name can change. Normally this is not relevant to historical data. But for active Contracts the read model should always have the current Employer name.
Normally you could simply update a table:
UPDATE `employment_contract_details` SET employer_name = 'New Acme' where employer_id = '...';
But of course this data will get lost as soon as we reply the event stream. The original Employer name will be set and the UPDATE is lost.
One possibility to to keep the change is to fire an event on the aggregate:
final class EmploymentContractSigned extends AggregateChanged
{
public function recognizeEmployerChange($newEmployerName): void
{
$this->recordThat(EmployerChangeRecognized::with($this->contractId, $this->employerId, $newEmployerName));
}
"If an external change from another context is relevant to the state of the current context then the state change should be recorded."
But in this case we are not changing the actual state. That would require the Employer (ID) itself to change e.g.:
final class EmploymentContractSigned extends AggregateChanged
{
public function transferToNewEmployer($newEmployerId, $newEmployerName): void
{
$this->recordThat(TransferredToNewEmployer::with($this->contractId, $newEmployerId, $newEmployerName));
}
But in our case we are simply changing a property that was already enriched for read-model purposes.
It is convenient to have a Projector fill a single table for the read model. But updating the read model data by going through the aggregate feels strange.
How do you guys handle these external changes that do no seem relevant to the state of the aggregate?
Return of the Join
This is our current READ Model repository implementation:
final class EmploymentContractDetailsRepository extends DbalReadModelRepository implements DetailsRepository
{
public function ofId(string $id): Details
{
$stmt = $this->getConnection()->prepare(sprintf(
"SELECT * FROM `%s` WHERE contract_id = :id",
Table::EMPLOYMENT_CONTRACT_DETAILS
));
$stmt->bindValue('id', $id);
$stmt->execute();
$result = $stmt->fetch();
if (false === $result) {
throw new \Exception('Contract not found');
}
return Details::fromArray($result);
}
}
Is this a valid use case to use a JOIN on a local copy of the data that has to be up-to-date?
final class EmploymentContractDetailsRepository extends DbalReadModelRepository implements DetailsRepository
{
public function ofId(string $id): Details
{
$stmt = $this->getConnection()->prepare(sprintf(
"SELECT * FROM `%s` JOIN `employers` USING (employer_id) WHERE contract_id = :id",
Table::EMPLOYMENT_CONTRACT_DETAILS
));
$stmt->bindValue('id', $id);
$stmt->execute();
$result = $stmt->fetch();
if (false === $result) {
throw new \Exception('Contract not found');
}
return Details::fromArray($result);
}
}
Advantage:
Whether to use the current or the old Employer name can still be handled per READ (VIEW) model (UI Use Case).
Hi,
I have an API and use command bus (the @thephpleague's Tactician) to handle commands.
The question is how to return new created entity after POST
request in the body?
Controller's code (simplified):
public function postAction(Request $request)
{
$command = new CreateTaskCommand($request->get('name'), $request->get('startDate'));
$this->get('command_bus')->handle($command);
return $this->createView(???); // here I want to return new created entity as a response body with 201 CREATED status code
}
Keeping in mind that command bus can't return anything, what is the good way to get the created entity?
I found two recommendations:
For the second point the code might look like:
public function postAction(Request $request)
{
$uuid = $request->get('uuid'); // or new TaskId();
$command = new CreateTaskCommand($uuid, $request->get('name'), $request->get('startDate'));
$this->get('command_bus')->handle($command);
// here we fetch the new entity from the storage
$entity = $someService->find($uuid);
return $this->createView($entity);
}
How do you guys solve this case? Maybe creators of command buses (@matthiasnoback, @rosstuck) know something about this?
Thanks in advance.
We have a Contract
Domain Model holding a huge collection of Timesheets
. I just added some simple Symfony code snippets using Doctrine Entities and Annotations.
All Entities are later moved to Acme\Contract\Domain\Model namespace and XML Mapping inside Acme\Contract\Infrastructure\Persistence\Doctrine etc..
use Acme\Contract\ContractBundle;
/**
* @ORM\Entity
*/
class Contract
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
public $id;
/**
* @ORM\Column(type="string", name="status")
*/
protected $status;
/**
* @ORM\OneToMany(targetEntity="Timesheet", mappedBy="contract", cascade={"persist"})
* @todo Use LAZY loading?
*/
private $timesheets;
public function addTimesheet(Timesheet $timesheet) {
// Policy
if ($this->isClosed()) {
throw new ContractAlreadyClosedException();
}
$this->timesheets[] = $timesheet;
}
}
use Acme\Contract\ContractBundle;
/**
* @ORM\Entity
*/
class Timesheet
{
/**
* @ORM\Id
* @ORM\Column(type="integer")
* @ORM\GeneratedValue(strategy="AUTO")
*/
private $id;
/**
* @ORM\ManyToOne(targetEntity="Contract", inversedBy="timesheets")
* @ORM\JoinColumn(name="contract_id", referencedColumnName="id")
*/
private $contract;
}
Currently we are using CQRS and a single Symfony app. Using Contract as the only AR forces our policy on the invariant:
use Acme\Contract\Application\Service;
class WriteTimeSheetHandler
{
public function handle($command)
{
$contract = $this->contractRepository->find($command->contractId());
$timesheet = new Timesheet($command->start(), $command->end());
$contract->addTimesheet($timesheet); // Check invariant
}
}
There can be many related timesheet aggregates. This gives us the hint that Timesheet itself should be handled as an Aggregate Root (AR):
Another solution we thought about is creating a new Contract Domain Model inside a different Bounded Context where the Timesheet will be living in. This would lead us to Eventual Consistency but it would be possible with our current infrastructure where bounded contexts are shared across the app.
As suggested by @mgonzalezbaile:
use Acme\Contract\Application\Service;
class WriteTimesheetHandler
{
public function handle($command)
{
$timesheet = new Timesheet($command->start(), $command->end());
$this->timesheetRepository->save($timesheet);
$this->raiseEvent(TimesheetCreated($contractId));
}
}
class TimesheetCreatedListener
{
public function __constuct($event)
{
$contract = $this->contractRepository->find($event->contractId());
$timesheet = $this->timesheetRepository->find($event->timesheetId());
$contract->addTimesheet($timesheet); // Check invariant
$this->contractRepository->save($contract);
}
}
But this way the Contract still has the relations to existing timesheets and all of them would be loaded.
Other thought on large aggregates by @voroninp:
Instead of loading all the timesheets we thought about removing them completely from the Contract. The only connection would be on the Timesheet aggregate via reference (ContractId):
class Timesheet
{
private $contractId;
public function __construct(Contract $contract, $start, $end)
{
// Policy - move to domain service using repository?
if ($contract->isClosed()) {
throw new ContractAlreadyClosedException();
}
$this->contractId = ContractId::create($contract->id()); // UUID value object soon
$this->start = $start;
$this->end = $end;
}
}
Still we would load the Contract Aggregate and pass it to the Timesheet aggregate in order to check the invariant.
But passing the Contract AR to the Timesheet constructor doesn't feel good.
Another variation is moving the policy out of the Timesheet into a Domain Service. This Domain Service CanOnlyAddTimesheetToPendingContract could receive the contract repository. I think this is absolutely legal in DDD.
We would only have to pass the ContractId to the constructor instead of the full domain model.
Though I like this approach @VoiceofUnreason states here for a similar example:
Which is to say, if the rules for modifying a Timesheet depend on the current state of the Employee entity, then Employee and Timesheet definitely need to be part of the same aggregate, and the aggregate as a whole is responsible for ensuring that the rules are followed.
An aggregate has one root entity; identifying it is part of the puzzle. If an Employee has more than one Timesheet, and they are both part of the same aggregate, then Timesheet is definitely not the root. Which means that the application cannot directly modify or dispatch commands to the timesheet - they need to be dispatched to the root object (presumably the Employee), which can delegate some of the responsibility.
Related:
Coming from:
I never got it working on the Document side - don't know if the XML mapping is complete anyway:
http://atlantic18.github.io/DoctrineExtensions/schemas/orm/doctrine-extensions-3.0.xsd
namespace Acme\AppBundle\Entity;
class Customer
{
/**
* @Gedmo\ReferenceMany(type="document", class="Acme\DomainModel\Order", mappedBy="customer")
*/
private $orders;
}
<field fieldName="customerId" type="integer" />
<gedmo:reference type="entity"
class="Acme\AppBundle\Entity\Customer"
inversed-by="orders" identifier="customerId" />
namespace Acme\DomainModel;
class Order
{
private $customer;
private $customerId;
}
Code examples:
Doctrine MongoDB offers a Default GridFS Repository to upload files and map them to a MongoDB Document. This document has default fields e.g. "uploadDate" that get populated when the document was successfully added to the bucket / collections.
In addition custom data can be stored to a Metadata document.
In our Domain Models we do not want to care about MongoDBs way of storing their native ObjectIds in a "_id" field. That's why we try to hide this "Surrogate Key" by extending the
SurrogateIdEntity. We then map the auto-generated MongoDB ObjectId to a column named
surrogateId`.
Internally we generated our own UUIDs. Since we want this as our primary key we map the id()
method to the UUID mapped to "metadata" since this is the only place we can put it.
These are the first compromises between integrating the infrastructure and keeping the domain "clean".
Next: Object creation
When uploading the file to GridFS it directly adds it to the bucket AND returns a proxy of the created object.
There is no way to create the object and then store it separately.
In order to offer an API without caring about the GridFS implementation we added a factory for the InvoiceDocument
.
The factory / domain service takes the required arguments (should be value objects BTW).
Then it passes it to the repository implementation. There is an interface in the domain layer that defines the required method.
This method then uploads the file and returns it to the factory. Done.
What we don't like about this approach:
We also thought about moving the repository method to the factory. But then it would mix up persistence
implementation and object creation.
Are there better approaches? Are these acceptable comprimises due to infrastructure choices?
Thank you for your feedback!
I see everyone everywhere writing about DDD, CQRS, ES but no concrete examples especially when it comes to Symfony or Doctrine. So I am very puzzled about this becuase my requirements are very low:
class Customer {
protected $customerId; // CustomerId Value Object - uuid5 using the username
protected $username;
}
class Wallet {
protected $uuid; // uuid5 with customer uuid as namespace and something random.
protected $balance;
protected $customer;
// __construct($customer, $balance = 0)
}
What made me some headaches yesterday was:
This issue might come down to some "best practice". But there are really some points I see with advantages and disadvantages.
I could do it while creating a new object in __construct
(DDD-Layer). But in the app-layer I need some setter for doctrine when it loads the entity from the database. I surely don't want a setter for UUIDs - they shall never change.
This could also be done via a factory. Compatible with Doctrine when using a prePersist hook / generate uuid on the fly. But static methods are just bad solutions.
Speaking about on-the-fly I could also do it in the getter getUuid
which checks if a uuid is already present and if not it generates a new one.
As mentioned by @Skahrz here:
injecting a Policy / Sepcification seems to be just fine e.g.:
Maybe it also has the advantage of making an implicit concept explicit?
General difference:
My structure is inspired by this highly recommended book written by @carlosbuenosvinos, @theUniC and @keyvanakbary:
src/Acme/QualityManagement\Application\Services\ (All my Commands, Queries and Handlers go here)
src/Acme/QualityManagement\DomainModel\Order\Order.php (and OrderId, OrderRepository etc.)
src/Acme/QualityManagement\Infrastructure\Persistence\Doctrine\OrderRepository.php (ORM and ODM Repositories implementing the DomainModel Interfaces)
The UserInterface
aka PresentationLayer
is currently missing. Maybe there won't be a UserInterface
in this src
folder after all when a frontend developer creates a separate AngularJS app for instance.
Currently I'm using a Symfony Bundle for this:
src/Acme/QualityManagementBundle
It holds the Doctrine XML Mapping for ORM and ODM, Forms, Controllers and Tests.
Translations and templates were moved to the app/Resources/QualityManagementBundle
folder.
I ask myself if this "Bundle" actually is some kinde of Presentation
or even Application
Layer.
Should I move it to:
src/Acme/QualityManagement\Application\QualityManagementBundle
Should I try to include the Bundle into the Application folder?
src/Acme/QualityManagement\Application\Controller (formerly QualityManagementBundle\Controller)
src/Acme/QualityManagement\Application\Form (formerly QualityManagementBundle\Form)
Or is the Bundle the Presentation Layer? Should I keep the QualityManagementBundle
name and move it to the Domain:
src/Acme/QualityManagement\UserInterface\QualityManagementBundle
or
src/Acme/QualityManagement\Presentation\QualityManagementBundle
or try to override the Symfony internals to use the correct layer naming instead of the Domain name QualityManagement
with the the Bundle suffix?
src/Acme/QualityManagement\UserInterface (formerly QualityManagementBundle)
or
src/Acme/QualityManagement\Presentation (formerly QualityManagementBundle)
Looking forward to thoughts from @leopro and @willdurand!
Here is an example suggested by @simensen:
Related:
My CalculateDormerHandler
needs to pass data from a CalculateDormerCommand
to a DormerCalculator
Domain Service.
Currently the Handler is passing the complete DTO (Command including a lot of args) and some Infrastructure concerns to the Domain Service:
class CalculateDormerHandler extends AbstractCalculateDormerHandler
{
public function handle(CalculateDormerCommand $command)
{
// Pass to domain service calculator
$dormerCalculation = $this->dormerCalculator->calculate(
PartnerId::create($this->getTokenStorage()->getToken()->getUser()->getId()),
$command
);
$this->dormerCalculationRepository->save($dormerCalculation);
}
}
ATM the Domain Service then transforms some of the args from the DTO into Domain Models:
class DormerCalculator
{
public function calculate(
PartnerId $partnerId,
$command
) {
$this->partner = $this->partnerRepository->ofId($partnerId);
$this->gutter = $this->calculateGutter($command->gutter());
}
private function calculateGutter($gutterData) : Gutter
{
if (true !== $gutterData['add']) {
return;
}
return new Gutter(
GutterMaterial::create($gutterData['materialId']),
GutterColor::create($gutterData['colorId'])
);
}
}
I'm thinking about refactoring the Domain Service and move the creation of the required Domain Model
s to the Handler
.
class CalculateDormerHandler extends AbstractCalculateDormerHandler
{
public function handle(CalculateDormerCommand $command)
{
if (true !== $command->gutter()['add']) {
$gutter = new Gutter(
GutterMaterial::create($gutterData['materialId']),
GutterColor::create($gutterData['colorId'])
);
}
// Pass to domain service calculator
$dormerCalculation = $this->dormerCalculator->calculate(
PartnerId::create($this->getTokenStorage()->getToken()->getUser()->getId()),
$gutter
// ...
);
}
}
class DormerCalculator
{
public function calculate(
PartnerId $partnerId,
Gutter $gutter = null,
// ...
) {
$this->partner = $this->partnerRepository->ofId($partnerId);
$this->gutter = $this->gutter;
}
}
This way the Domain
receives Models
that it understands. In addtion every other Application
or even Domain Service
could use the DormerCalculator
Domain Service
.
As I mentioned my command holds a lot(!) of arguments. Should the Handler
still carry out the task as refactored?
Came from:
Recently I started a symfony project w/ @prooph components. Looking at the bluebrints I recognized the creation of events has changed.
class User extends AggregateRoot
{
/**
* @var Uuid
*/
private $uuid;
/**
* @var string
*/
private $name;
/**
* ARs should be created via static factory methods
*/
public static function nameNew(string $username): User
{
//Perform assertions before raising a event
Assertion::notEmpty($username);
$uuid = Uuid::uuid4();
//AggregateRoot::__construct is defined as protected so it can be called in a static factory of
//an extending class
$instance = new self();
//Use AggregateRoot::recordThat method to apply a new Event
$instance->recordThat(UserWasCreated::occur($uuid->toString(), ['name' => $username]));
return $instance;
}
/**
* Every AR needs a hidden method that returns the identifier of the AR as a string
*/
protected function aggregateId(): string
{
return $this->uuid->toString();
}
protected function apply(AggregateChanged $event): void
{
switch (\get_class($event)) {
case UserWasCreated::class:
//Simply assign the event payload to the appropriate properties
$this->uuid = Uuid::fromString($event->aggregateId());
$this->name = $event->username();
break;
case UserWasRenamed::class:
$this->name = $event->newName();
break;
}
}
}
/**
* ProophEventSourcing domain events are of the type AggregateChanged
*/
class UserWasCreated extends AggregateChanged
{
public function username(): string
{
return $this->payload['name'];
}
}
Before there was a lot of boilerplate going on e.g. from an earlier project:
final class DormerElementCreated extends AggregateChanged
{
/** @var DormerElementId */
private $dormerElementId;
/** @var VariableName */
private $variableName;
/** @var string */
private $variableDescription;
/** @var DormerPart */
private $dormerPart;
/** @var MeasurementUnit */
private $measurementUnit;
/** @var float */
private $workingHours;
/** @var int */
private $surcharge;
public static function with(
DormerElementId $dormerElementId, VariableName $variableName, string $variableDescription,
DormerPart $part, MeasurementUnit $unit, float $workingHours, int $surcharge
): DormerElementCreated
{
$event = self::occur(
$dormerElementId->toString(),
[
'id' => $dormerElementId->toString(),
'variableName' => $variableName->toString(),
'variableDescription' => $variableDescription,
'dormerPart' => $part->toString(),
'measurementUnit' => $unit->toString(),
'workingHours' => $workingHours,
'surcharge' => $surcharge,
]
);
$event->dormerElementId = $dormerElementId;
$event->variableName = $variableName;
$event->variableDescription = $variableDescription;
$event->dormerPart = $part;
$event->measurementUnit = $unit;
$event->workingHours = $workingHours;
$event->surcharge = $surcharge;
return $event;
}
public function dormerElementId(): DormerElementId
{
if (null === $this->dormerElementId) {
$this->dormerElementId = DormerElementId::fromString($this->aggregateId());
}
return $this->dormerElementId;
}
public function variableName(): VariableName
{
if (null === $this->variableName) {
$this->variableName = VariableName::fromString($this->payload['variableName']);
}
return $this->variableName;
}
}
Most of the getters were creating value objects from the payload. This was really annoying when dealing with arrays. Most of the time you had to implement toArray
and fromArray
methods and loop them through array_map
methods.
This change made me think. Then I read this post by @heynickc:
And followed some other discussions:
@prooph for instance keeps the symfony structure for convenience too:
I'm really beginning to like this approach. What do you guys think?
See also:
Came from:
/cc @JulianMay
An EmploymentContract
is an event-sourced Aggregate Root (A+ES). It holds a reference to a A+ES Person
.
<?php
namespace AcmersonnelManagement\Domain\Model\EmploymentContract;
final class EmploymentContract extends AggregateRoot
{
/** @var EmploymentContractId $employmentContractId */
private $id;
/** @var PersonId $personId */
private $personId;
/** @var EmploymentPeriod */
private $employmentPeriod;
public static function sign(
EmploymentContractId $anId, PersonId $aPersonId, PersonalData $aPersonalData, ContractType $aContractType,
DateTimeImmutable $aStartDate, ?DateTimeImmutable $anEndDate, ?DateTimeImmutable $aProbationEndDate,
WorkerCategory $aWorkerCategory, WageType $aWageType, bool $aWorkingTimeAccount,
WorkweekDays $aWorkWeekDays, WeeklyWorkingHours $aWeeklyWorkingHours,
HolidayEntitlement $aHolidayEntitlement, AdditionalLeave $additionalLeave,
JobFunctionId $aJobFunctionId, string $aJobFunctionName,
EmployerId $anEmployerId, string $anEmployerName, WorkplaceId $aWorkplaceId, string $aWorkplaceName
): EmploymentContract
{
$employmentPeriod = EmploymentPeriod::withType($aContractType, $aStartDate, $anEndDate);
$self = new self();
$self->recordThat(EmploymentContractSigned::withData(
$anId, $aPersonId, $aPersonalData, $aContractType, $employmentPeriod, $aPbationaryPeriod,
$aWorkerCategory, $aWageType, $aWorkingTimeAccount,
$aWorkWeekDays, $aWeeklyWorkingHours,
$aHolidayEntitlement, $additionalLeave,
$aJobFunctionId, $aJobFunctionName,
$anEmployerId, $anEmployerName, $aWorkplaceId, $aWorkplaceName,
new DateTimeImmutable()
));
return $self;
}
protected function apply(AggregateChanged $event): void
{
switch (get_class($event)) {
/** @var EmploymentContractSigned $event */
case EmploymentContractSigned::class:
$this->id = $event->contractId();
$this->personId = $event->personId();
$this->employmentPeriod = $event->employmentPeriod();
break;
}
}
public function aggregateId(): string
{
return $this->id->toString();
}
}
Employment periods of contracts for a person must NOT overlap. This is ensured by a OverlappingEmploymentContractPolicy
that currently is called inside the command handler. The handler has to get the existing contracts of a Person from a read model repository.
<?php
namespace Acme\PersonnelManagement\Application\Service\EmploymentContract;
final class SignEmploymentContractHandler
{
/** @var EmploymentContractEventStoreRepository */
private $contractCollection;
/** @var PersonDetailsRepository */
private $personDetailsRepository;
/** @var ContractDetailsRepository */
private $contractsDetailsRepository;
public function __construct(
EmploymentContractEventStoreRepository $contractCollection,
PersonDetailsRepository $personDetailsRepository,
ContractDetailsRepository $contractsDetailsRepository
)
{
$this->contractCollection = $contractCollection;
$this->personDetailsRepository = $personDetailsRepository;
$this->contractsDetailsRepository = $contractsDetailsRepository;
}
public function __invoke(SignEmploymentContract $command): void
{
$person = $this->personDetailsRepository->ofPersonId($command->personId()->toString());
$enteredContracts = $this->contractsDetailsRepository->ofPersonId($person->personId());
if (!OverlappingEmploymentContractPolicy::isSatisfiedBy(
$command->contractId(), $command->contractType(),
$command->employmentPeriod(), $command->employerId(), $enteredContracts
)) {
throw new EmploymentPeriodOverlapsException();
}
$contract = EmploymentContract::sign(
$command->contractId(), $command->personId(),
new PersonalData(...),
$command->contractType(),
$command->startDate(), $command->endDate(), $command->probationEndDate(),
$command->workerCategory(), $command->wageType(),
$command->workingTimeAccount(), $command->workweekDays(),
$command->weeklyWorkingHours(), $command->holidayEntitlement(), $command->additionalLeave(),
$command->jobFunctionId(), $jobFunction->name(),
$command->employerId(), $employer->name(),
$command->workplaceId(), $workplace->name()
);
$this->contractCollection->save($contract);
}
}
The idea is to move the creation of the contract to the Read Model for the Person as demonstrated in PersonReadModel.
Based on this article by @udidahan:
Or the example from "Implementing Domain-Driven Design" by @VaughnVernon:
public class Forum extends Entity {
...
public Discussion startDiscussion(
DiscussionId aDiscussionId, Author anAuthor, String aSubject) {
if (this.isClosed()) {
throw new IllegalStateException("Forum is closed.");
}
Discussion discussion = new Discussion(
this.tenant(), this.forumId(), aDiscussionId, anAuthor, aSubject);
DomainEventPublisher.instance().publish(new DiscussionStarted(...));
return discussion;
}
as mentioned by @sofiaguyang:
The new handler would then look like this:
public function __invoke(SignEmploymentContract $command): void
{
$person = $this->personDetailsRepository->ofPersonId($command->personId()->toString());
$contract = $person->signEmploymentContract(...);
$this->contractCollection->save($contract);
}
<?php
namespace Acme\PersonnelManagement\Domain\Model\Person;
final class PersonReadModel
{
private $personId;
private $personalData;
private $employmentContracts;
public function signEmploymentContract(
EmploymentContractId $contractId, ContractType $contractType,
DateTimeImmutable $startDate, ?DateTimeImmutable $endDate, ?DateTimeImmutable $aProbationEndDate,
...
): EmploymentContract {
$employmentPeriod = EmploymentPeriod::withType($contractType, $startDate, $endDate);
if (!OverlappingEmploymentContractPolicy::isSatisfiedBy(
$contractId, $contractType, $employmentPeriod, ..., $this->employmentContracts
)) {
throw new EmploymentPeriodOverlapsException();
}
return EmploymentContract::sign(
$contractId, $this->personId, $this->personalData,
$contractType, $startDate(), $endDate(), $probationEndDate(),
...
);
}
}
This scenario is made for an application that lives in a single microservice. But even if Person and Contracts were dedicated services the Contracts service could consume PersonHired
events and create a "local copy" of persons and use them as a read model. Or you would move the logic back to the command handler.
But THEN I would indeed recommend to make everything event-driven and create separate events that may result in a ContractCancelledDueToOverlapping
event.
Currently I'm using PHP Arrays for single READ models. Actually it was a performance driven decision since I use them for exporting 100s of rows into a generated PDF.
But looks like this myth has been busted or at least is no longer regarded "relevant" since PHP_7_:
Looking at some DDD repository examples Java and DotNET write results coming from a persistence storage into a collection of objects.
Mostly these objects are then serializable to deliver primitive types like Command DTOs.
Currently I'm using the Doctrine Array hydration
to handle a more complex query:
$qb->select(array(
'b.id',
'b.date',
'b.shift',
'b.shippingnumber AS shipping_number',
'GroupConcat(DISTINCT b.comment) AS comment',
'SUM(b.customFieldNum1) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS custom_field_num_1',
'SUM(b.customFieldNum2) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS custom_field_num_2',
'SUM(b.customFieldNum3) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS custom_field_num_3',
'SUM(b.customFieldNum4) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS custom_field_num_4',
'SUM(b.customFieldNum5) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS custom_field_num_5',
'b.customFieldStr1 AS custom_field_str_1',
'b.customFieldStr2 AS custom_field_str_2',
'b.customFieldStr3 AS custom_field_str_3',
'b.customFieldStr4 AS custom_field_str_4',
'b.customFieldStr5 AS custom_field_str_5',
'p.id AS parttype_id',
'p.integratorNumber AS part_number',
'l.name AS location',
'SUM(bps_ok.quantity) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS ok_total',
'SUM(bps_ok.quantity+bps_nok_processed.quantity+bps_nok_blocked.quantity) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS total',
'SUM(bps_nok_processed.quantity+bps_nok_blocked.quantity) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS nok_total',
'SUM(bps_nok_processed.quantity) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS nok_processed',
'SUM(bps_nok_blocked.quantity) / CASE WHEN COUNT(bps_ok.id) = 0 THEN 1 ELSE COUNT(bps_ok.id) / COUNT(DISTINCT bps_ok.id) AS nok_blocked',
))
Since Doctrine 2.4 it is possible to directly write a result into a DTO:
<?php
class CustomerDetails
{
public function __construct($name, $email, $city, $value = null)
{
// Bind values to the object properties.
}
}
<?php
$query = $em->createQuery('SELECT NEW CustomerDetails(c.name, e.email, a.city) FROM Customer c JOIN c.email e JOIN c.address a');
$users = $query->getResult(); // array of CustomerDetails
This works for any PHP class / POPO. While using ResultSetMapping
only seems to work with Doctrine Entities:
What are your experiences / favorited strategies?
Related:
Came from:
I have a service that extracts the MIN and MAX date of a period. This is the original INSIDE approach #1
Domain Service
<?php
namespace Acme\PersonnelManagement\Domain\Service\EmploymentContract;
use Acme\PersonnelManagement\Domain\Model\EmploymentContract\EmploymentContractId;
use Acme\PersonnelManagement\Domain\Model\EmploymentContract\EmploymentPeriod;
use Acme\PersonnelManagement\Presentation\Model\EmploymentContract\TermRepository;
use Webmozart\Assert\Assert;
final class EmploymentPeriodExtractor
{
/** @var TermRepository */
private $termRepository;
public function __construct(TermRepository $termRepository)
{
$this->termRepository = $termRepository;
}
/**
* @param EmploymentContractId[] $contractIds
* @return EmploymentPeriod
*/
public function fromContractIds(array $contractIds): EmploymentPeriod
{
Assert::allIsInstanceOf($contractIds, EmploymentContractId::class);
Assert::minCount($contractIds, 1);
$terms = $this->termRepository->ofContractIds(array_map(function (EmploymentContractId $contractId) {
return $contractId->toString();
}, $contractIds));
$employmentPeriods = [];
foreach ($terms as $term) {
$employmentPeriods[] = new EmploymentPeriod(
$term->startDate(), $term->endDate()
);
}
return EmploymentPeriodMerger::merge($employmentPeriods);
}
}
Application Service (Command Handler)
<?php
namespace Acme\PersonnelManagement\Application\Service\Person;
use Acme\PersonnelManagement\Domain\Service\EmploymentContract\EmploymentPeriodExtractor;
final class ExtractEmploymentPeriodHandler
{
/** @var EmploymentPeriodExtractor */
private $extractor;
public function __construct(EmploymentPeriodExtractor $extractor)
{
$this->extractor = $extractor;
}
public function __invoke(ExtractEmploymentPeriod $command): void
{
$newPeriod = $this->extractor->fromContractIds($command->contractIds());
// Save aggregate...
}
}
The domain layer always holds an interface for the TermRepository
:
<?php
namespace Acme\PersonnelManagement\Presentation\Model\EmploymentContract;
use Acme\PersonnelManagement\Domain\Model\EmploymentContract\EmploymentContractId;
interface TermRepository
{
public function ofPersonId(string $personId): array;
/**
* @param string[] $contractIds
* @return Term[]
*/
public function ofContractIds(array $contractIds): array;
}
The implementation lives inside the infrastructure layer.
Since the Extractor Service only gets the interface type hinted I think it is valid to see it as a Domain Service.
Unfort. this is quite hard to unit test. It would require mocking or a InMemoryTermRepository
with same fake data.
It also looks like it is violating the single-responsibility principle (SRP).
This is my OUTSIDE approach #2:
Domain Service
<?php
namespace Acme\PersonnelManagement\Domain\Service\EmploymentContract;
use Acme\PersonnelManagement\Domain\Model\EmploymentContract\EmploymentContractId;
use Acme\PersonnelManagement\Domain\Model\EmploymentContract\EmploymentPeriod;
use Acme\PersonnelManagement\Presentation\Model\EmploymentContract\TermRepository;
use Webmozart\Assert\Assert;
final class EmploymentPeriodExtractor
{
/**
* @param Term[] $terms
* @return EmploymentPeriod
*/
public function fromTerms(array $terms): EmploymentPeriod
{
Assert::allIsInstanceOf($terms, Term::class);
Assert::minCount($terms, 1);
$employmentPeriods = [];
foreach ($terms as $term) {
$employmentPeriods[] = new EmploymentPeriod(
$term->startDate(), $term->endDate()
);
}
return EmploymentPeriodMerger::merge($employmentPeriods);
}
}
Application Service (Command Handler)
<?php
namespace Acme\PersonnelManagement\Application\Service\Person;
use Acme\PersonnelManagement\Domain\Service\EmploymentContract\EmploymentPeriodExtractor;
final class ExtractEmploymentPeriodHandler
{
/** @var TermRepository */
private $termRepository;
/** @var EmploymentPeriodExtractor */
private $extractor;
public function __construct(TermRepository $termRepository, EmploymentPeriodExtractor $extractor)
{
$this->termRepository = $termRepository;
$this->extractor = $extractor;
}
public function __invoke(ExtractEmploymentPeriod $command): void
{
$terms = $this->termRepository->ofContractIds(array_map(function (EmploymentContractId $contractId) {
return $contractId->toString();
}, $command->contractIds()));
$newPeriod = $this->extractor->fromTerms(terms);
// Save aggregate...
}
}
This is much easier to test. Though a developer could easily use this code to manipulate the Extractor result by freely passing any Terms
as argument. But I guess the developer should not be "the enemy".
Which approach do you prefer? Any exceptions to this or improvements?
Thank you for your feedback.
Came from:
Given a Rule spanning aggregates: existing contract periods should not overlap new period. How much โlogicโ in repository? 1) query all contracts and service class checks overlap 2) query and check overlap by passing new period (logic in SQL)
/cc @AntonStoeckl @plalx @BrunoRommens
After the discussion I moved my entire logic from an extra service and repository methods to the aggregate.
A few things to consider: - Logic in infrastructure implies integration tests verification. - Checking in service leaks BL outside the model -- I'd start trying: 1.
contract.changePeriod(period, otherPeriods)
2.contract.changePeriod(period, overlapDetector)
. -- Refactor...
As suggested by @plalx.
Event-sourced aggregate root (A+ES)
final class ServiceContract extends AggregateRoot
{
public static function enter(
ServiceContractId $anId, PersonId $anPersonId, PersonalData $aPersonalData,
AssignmentPeriod $anAssignmentPeriod, WorkweekDays $aWorkweekDays, WeeklyWorkingHours $aWeeklyWorkingHours,
JobFunctionId $aJobFunctionId, string $aJobFunctionName,
AgencyId $anAgencyId, string $anAgencyName,
WorkplaceId $aWorkplaceId, string $aWorkplaceName,
array $enteredContracts
): ServiceContract
{
Assert::notEmpty($aJobFunctionName);
Assert::notEmpty($anAgencyName);
Assert::notEmpty($aWorkplaceName);
$self = new self();
$self->detectConflicts($anId, $enteredContracts, $anAssignmentPeriod);
$self->recordThat(ServiceContractEntered::withData(
$anId, $anPersonId, $aPersonalData,
$anAssignmentPeriod, $aWorkweekDays, $aWeeklyWorkingHours,
$aJobFunctionId, $aJobFunctionName, $anAgencyId, $anAgencyName,
$aWorkplaceId, $aWorkplaceName, new DateTimeImmutable()
));
return $self;
}
private function detectConflicts(ServiceContractId $id, array $enteredContracts, AssignmentPeriod $period): void
{
Assert::allIsInstanceOf($enteredContracts, Term::class);
foreach ($enteredContracts as $enteredContract) {
if ($id->sameValueAs(ServiceContractId::fromString($enteredContract->contractId()))) {
continue; // Used only when editing the assignment period, not for actual creation of the contract
}
if ($period->overlapsWith($enteredContract->assignmentPeriod())) {
throw new AssignmentPeriodOverlapsException();
}
}
}
}
Application Service Command Handler
final class EnterServiceContractHandler
{
public function __invoke(EnterServiceContract $command): void
{
$person = $this->personDetailsRepository->ofPersonId($command->personId()->toString());
$enteredContracts = $this->contractTermRepository->ofPersonId($command->personId()->toString());
$contract = ServiceContract::enter(
$command->contractId(), $command->personId(),
... personal data from the person etc. ...
$enteredContracts
);
$this->contractCollection->save($contract);
}
}
As @BrunoRommens suggested the enteredContracts
are no full details read models. They are just value objects holding the actual assignment periods of the other contracts entered by the person.
The "Available periods" aggregate contains only "Period" value objects. Maybe the tradeoff (against nature in classical DDD), is to implement this type of aggregate as an interface in the domain with an implementation in infrastructure handling available periods finding operation
Unit test
final class ServiceContractTest extends EventSourcedAggregateRootTestCase
{
/** @test */
public function signingDetectsConflictWhenExistingPrecedingContractIsStillActive(): void
{
$this->expectException(AssignmentPeriodOverlapsException::class);
$enteredContracts = [
Term::fromArray([
'contract_id' => 'a391c713-2cf7-4500-b997-e37ea24c5889',
'person_id' => '71efb6b7-6eb1-4b17-b1d5-a3a47bac78f1',
'start_date' => '2019-02-01',
'end_date' => '2020-01-31',
])
];
ServiceContract::enter(
ServiceContractId::fromString('b88f676e-221d-4807-97c9-8f549b13425a'),
PersonId::fromString('71efb6b7-6eb1-4b17-b1d5-a3a47bac78f1'),
$this->getPersonalData(),
AssignmentPeriod::with(
new DateTimeImmutable('2020-01-01'),
new DateTimeImmutable('2020-12-31'),
),
WorkweekDays::fromArray(["monday","tuesday","wednesday","thursday","friday"]),
WeeklyWorkingHours::fromFloat(40),
JobFunctionId::fromInteger(4), 'Qualitรคtsprรผfer/in',
AgencyId::fromString('904c0436-4226-4725-afbe-14526906afdb'), 'Office People',
WorkplaceId::fromString('1e522e02-b28d-44d2-a3fb-4efbdacec462'), 'Bratislava',
$enteredContracts
);
}
}
When I started decoupling from Symfony Form and the EntityType
I switched to primitive types only that would then be used on the data_class
which is the Command / DTO.
In conlusion my command only uses integers (for IDs) and strings (also UUID) or arrays of these types.
I'm sometimes asked if objects e.g. value objects should be used. As I mentioned I don't use objects in a DTO at all. As @jeremyb states in his slides:
Tips #3: Command & Value Object
Il est recommandรฉ d'utiliser des types primitifs si la Command doit รชtre traitรฉe en asynchrone (ร cause de la serialization)
Passer des Values Objects ร une Command permet de rรฉutiliser un cadre et les validations dรฉfinies
Most examples I see follow these "guidelines":
https://github.com/SimpleBus/MessageBus/blob/master/doc/command_bus.md#using-the-command-bus-an-example @matthiasnoback
Sometimes a DTO property transfers a string coming from a constant:
class ContractState
{
const STATE_ACCEPTED = 'contract_accepted';
const STATE_APPROVED = 'contract_approved';
const STATE_QUOTE_SUBMITTED = 'quote_submitted';
const STATE_IN_PROGRESS = 'in_progress';
const STATE_PENDING = 'pending';
const STATE_CLOSED = 'closed';
}
People ask how to ensure (validate) that the string $contractState
of the DTO is a correct string from the constants.
If you are using Symfony Form you already set the choices and a user cannot select a different choice.
When the input comes directly from the Request I guess you could add a custom Constraint to the Command that could check if the strings exists:
Since I'm using Symfony Form I prefer validating the input inside the Value Object. The handler will try to create an object from String:
ContractState::fromString($command->contractState());
The fromString
method then will throw an Exception if the string was invalid.
What are your thoughts on this?
Related issues:
There are some nice tutorials out there:
For Zend Framework too by @UFOMelkor:
Another implementation by @mbrevda:
And @maximecolin:
But there is also a implementation for Doctrine by @Happyr @Nyholm @cakper:
Anybody else with other implementations? What are your experiences? Any best practices?
Thanks for the feedback!
The example by @yvoyer features a custom implementation of the Event Bus e.g..
But it will also show how to integrate with Symfony components.
Another custom example by @juliendufresne:
https://github.com/php-ddd/php-ddd-bundle/tree/master/src
Of course there is also the @SimpleBus MessageBus by @matthiasnoback::
https://github.com/SimpleBus/MessageBus
I currently use it in my Symfony projects:
https://github.com/SimpleBus/SymfonyBridge
And not to forget @thephpleague Tactician by @rosstuck:
https://github.com/thephpleague/tactician
Another one is @PHPMessageBus by @nilportugues:
https://github.com/PHPMessageBus/messagebus
Opinions?
Originally posted by @gquemener :
Let's define domain related validation once and for all within well-defined VO and expose those errors to the world. > https://gist.github.com/gquemener/09b2bc303e63dfc3123f7d540e9891a0
It prevents to add extra constraint metadata!
Is this too much magic?
@matthiasnoback and @AntonStoeckl joined the discussion.
@AntonStoeckl also blogged about a solution in GO:
On our case:
We decided to work w/ the #symfonymessenger usind the mentioned validation middleware based on symfony constraints.
VOs are created from command getters. Every domain exception is caught by an listener and converted into human-readable messages.
Example:
# config/packages/messenger.yaml
framework:
messenger:
default_bus: messenger.bus.commands
buses:
messenger.bus.commands:
middleware:
- validation
# config/services.yaml
services:
Acme\Common\Infrastructure\Symfony\EventListener\ExceptionListener:
tags:
- { name: kernel.event_listener, event: kernel.exception }
arguments: ['@translator.default']
<?php
namespace Acme\Common\Infrastructure\Symfony\EventListener;
use DomainException;
use Prooph\EventStore\Exception\ConcurrencyException;
use Acme\Common\Presentation\Model\NotFoundException;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpKernel\Event\ExceptionEvent;
use Symfony\Component\Messenger\Exception\HandlerFailedException;
use Symfony\Component\Messenger\Exception\RuntimeException;
use Symfony\Component\Messenger\Exception\ValidationFailedException;
use Symfony\Component\Validator\ConstraintViolation;
use Symfony\Contracts\Translation\TranslatorInterface;
/**
* Handles bad requests and returns a 400 status with human-readable errors to the client.
* Returns a 500 status otherwise.
*/
final class ExceptionListener
{
/** @var TranslatorInterface */
private $translator;
private $translationDomain = 'exception';
private $locale = 'de';
public function __construct(TranslatorInterface $translator)
{
$this->translator = $translator;
}
public function onKernelException(ExceptionEvent $event)
{
// You get the exception object from the received event
$exception = $event->getThrowable();
if (!$exception instanceof RuntimeException) {
return;
}
$response = new JsonResponse();
$response->setStatusCode(Response::HTTP_BAD_REQUEST);
if ($exception instanceof ValidationFailedException) {
// Handle domain specific exceptions - public to the client
$errors = [];
/** @var ConstraintViolation $violation */
foreach ($exception->getViolations() as $violation) {
$errors[] = [
'message' => $violation->getMessage(),
'path' => $violation->getPropertyPath()
];
}
$response->setContent(json_encode(['errors' => $errors]));
$event->setResponse($response);
return;
}
if ($exception instanceof HandlerFailedException) {
// Handle presentation and domain specific exceptions - public to the client
$errors = [];
if ($exception->getPrevious() instanceof NotFoundException) {
$errors = [
'message' => $exception->getMessage(),
'path' => null
];
$response->setContent(json_encode(['errors' => $errors]));
$response->setStatusCode(Response::HTTP_NOT_FOUND);
$event->setResponse($response);
return;
}
if ($exception->getPrevious() instanceof DomainException) {
$errors[] = [
'message' => $this->translator->trans(
$exception->getPrevious()->getMessage(), [], $this->translationDomain, $this->locale
),
'path' => null
];
$response->setContent(json_encode(['errors' => $errors]));
$event->setResponse($response);
return;
}
// Handle individual server errors if relevant to the client
switch (get_class($exception->getPrevious())) {
case ConcurrencyException::class:
$errors = [
'message' => 'Duplicate entry',
'path' => null
];
$response->setContent(json_encode(['errors' => $errors]));
$event->setResponse($response);
return;
break;
}
}
}
}
The listener can be expanded for individual exceptions. Exception messages are simply translated.
If it is a Symfony Constraint error message it normally was already translated. In addtion the path
will be added.
The JSON result normally looks like this:
errors: {
message: "Some error in the command DTO"
path: "firstName"
}
errors: {
message: "Some error in the domain e.g. thrown inside value object"
path: null
}
AFAIK Symfony Messenger can be used without Symfony. You can use it as a standalone service bus.
The Validation middleware is included too. Putting the "logic" of the listener elsewhere should be no problem.
WDYT?
Older possibly related issues:
The following implementation uses a CQRS Login Query through the service bus.
The authentication currently is based on Symfony Security and the UserPasswordEncoderInterface
.
How would you test this scenario?
First of all, would you move the logic away from the handler e.g. to a "LoginService"?
Would you even consider loading the WRITE model (event-sourced aggregate-root) though you don't want to change state?
Though you could add events e.g. "LoginFailed" to fill some audit log. But this actually is a query that should return a valid token.
Login Controller
final class LoginController
{
private MessageBusInterface $queryBus;
public function __construct(MessageBusInterface $queryBus)
{
$this->queryBus = $queryBus;
}
public function __invoke(Request $request): Response
{
$query = new LoginHost(json_decode($request->getContent(), true));
$envelope = $this->queryBus->dispatch($query);
$handledStamp = $envelope->last(HandledStamp::class);
return new JsonResponse($handledStamp->getResult(), Response::HTTP_OK);
}
}
Read Model
final class HostReadModel
{
private HostId $hostId;
private EmailAddress $emailAddress;
private EncodedPassword $password;
private bool $verified = false;
private string $locale = 'de';
public static function fromArray(array $data): HostReadModel
{
$self = new self();
$self->hostId = HostId::fromString($data['hostId']);
$self->emailAddress = EmailAddress::fromString($data['emailAddress']);
$self->password = EncodedPassword::fromString($data['password']);
$self->verified = $data['verified'];
$self->locale = 'de';
return $self;
}
public function isVerified(): bool
{
return $this->verified;
}
}
Command handler
final class LoginHostHandler
{
private HostDetailsFinder $hostDetailsFinder;
private UserPasswordEncoderInterface $passwordEncoder;
private JWTEncoderInterface $jwtEncoder;
public function __construct(
HostDetailsFinder $hostDetailsFinder,
UserPasswordEncoderInterface $passwordEncoder,
JWTEncoderInterface $jwtEncoder
)
{
$this->hostDetailsFinder = $hostDetailsFinder;
$this->passwordEncoder = $passwordEncoder;
$this->jwtEncoder = $jwtEncoder;
}
public function __invoke(LoginHost $query): array
{
/** @var HostReadModel $hostDetails */
$hostDetails = $this->hostDetailsFinder->ofEmailAddress($query->emailAddress()); // CQRS Read Model
if (!$hostDetails->isVerified()) {
throw new HostNotVerifiedException();
}
if (!$this->passwordEncoder->isPasswordValid($hostDetails, $query->password()->toString())) {
throw new InvalidCredentialsException();
}
return ['token' => $this->jwtEncoder->encode($hostDetails->toArray())];
}
}
Currently there is a issue with the Symfony Form Text Type and the empty_data
option.
<?php
class ChangeInspectionDetails extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('problemNumber', TextType::class, [
'empty_data' => '' // no effect
]);
}
}
Though emtpy_data
is set to an empty string the field will return null
.
This can be a problem if you use PHP7 type hinting on your Domain Model:
<?php
class Contract
{
public $problemNumber = ''; // no effect
public function changeInspectionDetails(
string $problemNumber = ''
) {
$this->problemNumber = $problemNumber;
}
}
You will get a Expected argument of type "string", "NULL" given.
There are some workarounds using Data Transformers until this issue eventually can be fixed.
A custom TextType Extension suggested by @webmozart:
The fix "simply" is to explicitly type cast the value to a string before passing it to the method.
Most of the time I'm using Commands as data_class
in my Forms:
<?php
class ChangeInspectionDetails extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('problemNumber', TextType::class, [
'empty_data' => '' // no effect
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => ChangeInspectionDetailsCommand::class
]);
}
}
This allows us to type cast in two places.
The Command:
<?php
class ChangeInspectionDetailsCommand
{
public $problemNumber = ''; // no effect
public function problemNumber()
{
return (string) $this->problemNumber; // workaround 1
}
}
Or the Handler:
<?php
class ChangeInspectionDetailsHandler
{
public function handle(ChangeInspectionDetailsCommand $command)
{
// ...
$contract->changeInspectionDetails(
#$command->problemNumber() // no effect
(string) $command->problemNumber() // workaround 2
);
}
}
Hope this helps!
Gist:
Came from:
Related issues:
Here is a question by @chrisguitarguy that came to mind before when I was using the @SimpleBus by @matthiasnoback:
Do you hide your event emiting code behind an interface you own in your #php apps? Why or why not?
By that, I mean instead of using Symfony's event dispatcher directly you create an interface and a Symfony adapater implenting it.
The @SimpleBus has a SmyfonyBridge:
And for the RabbitMQBundle
too:
I remember trying out the tutorial:
But I think I didn't figure out why the events did not actually fire.
Does anybody have a working example or code on github?
In my Entity EmploymentContract
I have the value objects ContractType
- "fixed_term" or "permanent" and EmploymentPeriod
with "startDate" and "endDate".
When the ContractType
is "permanent" then the "endDate" should be "null".
My current implementation:
final class EmploymentContract extends AggregateRoot
{
public static function sign(
EmploymentContractId $anId, ContractType $aContractType,
DateTimeImmutable $aStartDate, ?DateTimeImmutable $anEndDate
) {
if ($aContractType->isFixedTerm() && null === $anEndDate) {
throw new FixedTermContractMustHaveAnEndDateException();
}
$employmentPeriod = new EmploymentPeriod($aStartDate, $anEndDate);
}
}
In this solution the entity (event-sourced aggregate root A+ES) groups the two value objects and protects the invariants / business rules.
In issue #39 we talked about creating Entities through Aggregate roots. No I wonder if this works for value objects too.
Here a two different approaches:
final class EmploymentContract extends AggregateRoot
{
public static function sign(
EmploymentContractId $anId, ContractType $aContractType, EmploymentPeriod $aPeriod
) {
// no further validation, the passed value objects are expected too be valid
}
}
final class EmploymentPeriod
{
public function __construct(DateTimeImmutable $startDate, ?DateTimeImmutable $endDate)
{
if (null !== $endDate) {
if ($endDate < $startDate) {
throw new EndDateMustBeGreaterThanStartDateException();
}
}
$this->startDate = $startDate;
$this->endDate = $endDate;
}
public static function withType(
ContractType $type, DateTimeImmutable $startDate, ?DateTimeImmutable $endDate
): EmploymentPeriod
{
if ($type->isFixed()) {
if (null === $endDate) {
throw new FixedTermContractMustHaveAnEndDateException();
}
}
if ($type->isPermanent()) {
if (null !== $endDate) {
throw new PermanentContractMustNotHaveAnEndDateException();
}
}
return new self($startDate, $endDate);
}
}
Or:
final class EmploymentContract extends AggregateRoot
{
public static function sign(
EmploymentContractId $anId, ContractType $aContractType, EmploymentPeriod $aPeriod
) {
// no further validation, the passed value objects are expected too be valid
}
}
final class ContractType
{
public function specifyEmploymentPeriod($startDate, $endDate); EmploymentPeriod
{
if ($this->isFixed()) {
if (null === $endDate) {
throw new FixedTermContractMustHaveAnEndDateException();
}
}
if ($this->isPermanent()) {
if (null !== $endDate) {
throw new PermanentContractMustNotHaveAnEndDateException();
}
}
return new EmploymentPeriod($startDate, $endDate);
}
}
Or should ContractType
and EmploymentPeriod
event be grouped into a single value object?
Thoughts?
Came from:
Given a
Token
#valueobject that internally uses current time to generateexpiresAt
date.
For unit testing expiration I would prefer reflection and set date - X days.
I would not like to refactor the constructor to receive aexpirationStartsAt
instead. Other suggestions?
/cc @plalx @BrunoRommens @AntonStoeckl @kapitancho @iosifch @tomjvdberg @swyrazik @dkarlovi @mannion @gilbertotcc @SelrahcD
The original idea was to NOT create an extra service but move all the logic into a value object.
The aggregate root
final class Host extends AggregateRoot
{
private HostId $hostId;
private EmailAddress $emailAddress;
private VerificationToken $verificationToken;
private bool $verified = false;
public static function register(HostId $hostId, EmailAddress $emailAddress): Host
{
$emailAddressVerificationToken = VerificationToken::generate($hostId, $emailAddress);
$self = new self();
$self->recordThat(HostRegistered::with($hostId, $emailAddress, $emailAddressVerificationToken));
return $self;
}
public function verify(string $token): void
{
if ($this->isVerified()) {
return;
}
if ($this->verificationToken->token() !== $token) {
throw new InvalidVerificationTokenException();
}
if ($this->verificationToken->hasExpired()) {
throw new VerificationTokenExpiredException();
}
$this->recordThat(HostVerified::with($this->hostId));
}
Value object
final class VerificationToken
{
public const LIFETIME = 345600;
private string $token;
private DateTimeImmutable $expiresAt;
private function __construct()
{
}
public static function generate(HostId $hostId, EmailAddress $emailAddress): VerificationToken
{
$encodedData = json_encode([$hostId->toString(), $emailAddress->toString()]);
$signingKey = 'some_secret';
$token = base64_encode(hash_hmac('sha256', $encodedData, $signingKey, true));
$expiryTimestamp = time() + VerificationToken::LIFETIME;
$expiresAt = (new DateTimeImmutable())->setTimestamp($expiryTimestamp);
$self = new self();
$self->token = $token;
$self->expiresAt = $expiresAt;
return $self;
}
public function token(): string
{
return $this->token;
}
public function expiresAt(): DateTimeImmutable
{
return $this->expiresAt;
}
public function hasExpired(): bool
{
return time() > $this->expiresAt->getTimestamp();
}
In order to this it would require Relfection. One approach to make it more testable - WITHOUT moving it to an extra service - would be passing the expiration date:
final class Host extends AggregateRoot
{
public static function register(HostId $hostId, EmailAddress $emailAddress): Host
{
$expiryTimestamp = time() + VerificationToken::LIFETIME;
$expiresAt = (new DateTimeImmutable())->setTimestamp($expiryTimestamp);
$emailAddressVerificationToken = VerificationToken::generate($hostId, $emailAddress, $expiresAt);
That makes the VerificationToken
value object easier to test. But it's still hard to check if the aggregate throws an exception without again using Reflection:
final class Host extends AggregateRoot
{
public function verify(string $token): void
{
if ($this->isVerified()) {
return;
}
if ($this->verificationToken->token() !== $token) {
throw new InvalidVerificationTokenException();
}
if ($this->verificationToken->hasExpired()) {
throw new VerificationTokenExpiredException();
}
This could be changed by also expanding the aggregate root by passing a registeredAt
date:
final class Host extends AggregateRoot
{
public static function register(HostId $hostId, EmailAddress $emailAddress, DateTimeImmutable $registeredAt): Host
{
$expiryTimestamp = $registeredAt + VerificationToken::LIFETIME;
$expiresAt = (new DateTimeImmutable())->setTimestamp($expiryTimestamp);
$emailAddressVerificationToken = VerificationToken::generate($hostId, $emailAddress, $expiresAt);
Though the code looks fine I'm just worried about adding "behaviour & data" that is only required for testing and not originally in the domain logic.
The main idea was to NOT write an extra service and keep the logic inside the value object.
If you want to use a service there a some good examples using a Clock Interface and implementation:
When trying to use Doctrine ORM Entities with ODM Documents (e.g. MongoDB) most solutions adding extra fields:
For me this feels like "polluting" the Domain Model with extra properties to fit the infrastructure.
So I am currently looking for a way to keep the Domain Models clean:
A possible workaround could be a custom mapper:
A different solution could be treating referenced entities as Value Objects which at the same time could serve as a historical "query database".
For instance:
Instead of linking an ODM Order
Document (still an "Entity" in DDD though) with an ORM Customer
you introduce a Customer
Value Object that keeps Id and full name. Maybe the delivery and / or invoice address too.
Remember kids:
Implementation issues (such as persistence) are not dealt with in the model, but they must be in the design.โ - @ericevans0
https://twitter.com/fromddd/status/725604978564308992
I recently started implementing RabbitMQ for some memory and time expensive operations e.g. mailing or generating reports.
Before I had a WRITE model that raised an event with all required data.
This event was then consumed by an event bus (@SimpleBus with DoctrineORMBridge) and mailing for instance was handled synchronous.
My first step was to simply serialize the event and publish it via RabbitMQ producer instead of the event bus.
Is this simple replacement fine?
Do you prefer using asynch messages in general or keep simple tasks (updating READ model) synch?
Related:
Came from: https://twitter.com/webdevilopers/status/829703225452457988
Inviting @michaelperrin @Sander-Toonen @mablae @tPl0ch @cakper @ChristianRiesen
Given is an event-sourced EmploymentContract
holding an EmploymentPeriod
with a start and end date.
It is then possible to "deploy employees of a contract to other work places".
But the DeploymentPeriod
must be inside the EmploymentPeriod
.
We want to keep our aggregates small and decided to make Deployment
a separated event-sourced aggregate root (A+ES) too.
In a future solution we would create a separate microservice and make them event-driven. Then the could suggest a deployment, check the business rule and eventually fire a `DeploymentRejected" event or similar.
For now we are using CQRS and a deployEmployeeToWorkplace
is handled by the command handler application service.
Currently we are thinking of three approaches ("Deployment" and "Solution" 1-3):
Currently we prefer solution 1 which creates the Deployment
thought a factory method on the EmploymentContract
which already will hold the EmploymentPeriod
.
All rules - actually they are not invariants since this does not concern the "transaction" of the Deployment
A+ES - can be validated here and return the Deployment
.
The only thing we ask ourselves:
Could and should we prevent other team developers from skipping the factory method and calling the named constructor on Deployment
directly?
final class Deployment1
{
public static function with(DeploymentId $anId, EmploymentContractId $anEmploymentContractId,
DeploymentPeriod $aPeriod): Deployment
{
// No period range validation, final period was already validated and passed
}
}
Should this be done in your code base in general or should these constructors just be available as is and the rules explained in pair-programming. What are your thoughts?
Final thoughts:
Whatever is the best solution, we are thinking of moving the responsibility away from the handler into a domain factory.
Similar topics:
Discussion started here:
Interesting reads:
My original post and poll started here:
https://twitter.com/webdevilopers/status/1100102866583339008
This was the original gist:
Use case:
I need to calculate a Dormer
based on specific DormerType
(Entity).
Business rule: Some DormerType
s can not have a Gutter
attached.
Dormer
in the end will just be a value object stored inside an DormerCalculation
Entity. / Aggregate Root that could throw the DormerCalculated
Domain Event.
This means that the DormerTypeId
should only be a reference.
I currently think of three approaches.
Version 1:
The full Entity is passed to the value object. This feels ugly. Allthough it is now impossible for a developer to create an inconsistent object. E.g. by attaching a gutter to a dormer with a type that does not allow gutters.
<?php
final class Dormer
{
/** @var DormerType $type */
private $type;
public static function construct(DormerType $type)
{
$this->type = $type;
}
public function addGutter(Gutter $gutter)
{
if (!$this->type->canHaveGutterInstalled()) {
throw new DormerTypeCannotHaveGutterInstalledException();
}
$this->gutter = $gutter;
}
}
final class DormerCalculationFactory extends AbstractCalculator
{
private $dormerTypeRepository;
public function createWithDto(CalculateDormerCommand $dto)
{
$dormerType = $this->dormerTypeRepository->ofId(DormerTypeId::fromInteger($dto->dormerTypeId));
/** @var Dormer $dormer */
$dormer = Dormer::construct($dormerType);
$dormer->addGutter(Gutter::fromDto($dto->gutter));
}
}
Version 2:
Again the business rule is ensured. The value object is created through the dormer type Entity which sounds logical. But in the end it is not an Aggregate Root that e.g. could throw a "DormerCalculated" event.
In addition the factory method calculateDormer
and the factory method on the dormer value object would look too identical.
<?php
class DormerType()
{
private $id;
public function calculateDormer(Gutter $gutter)
{
if (!$this->canHaveGutterInstalled()) {
throw new DormerTypeCannotHaveGutterInstalledException();
}
return new Dormer($this->id(), $gutter);
}
}
Version 3:
All creation logic is inside the factory. It protects the invariants and creates the Dormer only with the DormerTypeId
.
But a developer could create an inconsistend value object by skipping the factory. Ouch!?
final class DormerCalculationFactory extends AbstractCalculator
{
private $dormerTypeRepository;
public function createWithDto(CalculateDormerCommand $dto)
{
$dormerType = $this->dormerTypeRepository->ofId(DormerTypeId::fromInteger($dto->dormerTypeId));
/** @var Dormer $dormer */
$dormer = Dormer::construct($dormerType->id());
if (null !== $dto->gutter && $dormerType->canHaveGutterInstalled()) {
$dormer->addGutter(Gutter::fromDto($dto->gutter));
}
}
}
After reading a post on @culttt b @philipbrown version 3 still feels like the best way though.
Due to the coupled nature of the Factory and the object, it is usually fine to allow the Factory to protect the invariants of the object.
What do you guys think?
For naming conventions please see:
Injecting EntityManager
only and the Law of Demeter
:
Extend the Doctrine EntityRepository as suggested by @MacDada?
Blending ORM and ODM:
Symfony 4/5 create an App
namespace by default refering to alle files inside the src
folder.
Initially a Controller
folder may be created.
When adding the doctrine
recipe it will add Entity
, Repository
and Migration
.
Prooph Event Store will add a Command
namespace for instance.
Currently we remove the unused folders e.g. "Entity" since we only use the DBAL not the ORM.
Required folders e.g. "Controller" are moved to src\Acme\BoundedContext\Infrastructure\Symfony
. It could also be Framework
.
But after creating some new projects moving the files and reconfiguring the Symfony autowiring we are thinking about skipping this task.
We only want to focus on adding our bounded context and (sub-)domains to the organization folder e.g. Acme
and keep the Symfony App
as it is for a better quickstart.
What do you guys prefer?
Also see:
All events of multiple aggregates have to be selected, changed in some domain relevant ways and be merged into a new aggregate.
I decided to select the events from the default repository using the metadata matcher.
I did not create a new aggregate by calling the factory methods with the new data. Since the data passed was already valid at the time to original events took place and were applied.
Instead I decided to use reflection to pass the "recorded" events.
This was inspired by @prooph code:
namespace Prooph\Common\Messaging;
abstract class DomainMessage implements Message
{
public static function fromArray(array $messageData): DomainMessage
{
MessageDataAssertion::assert($messageData);
$messageRef = new \ReflectionClass(\get_called_class());
/** @var $message DomainMessage */
$message = $messageRef->newInstanceWithoutConstructor();
$message->uuid = Uuid::fromString($messageData['uuid']);
$message->messageName = $messageData['message_name'];
$message->metadata = $messageData['metadata'];
$message->createdAt = $messageData['created_at'];
$message->setPayload($messageData['payload']);
return $message;
}
}
At the bottom line a lot of prooph-inspired code was used. At the bottom line I think there is not too much coupling.
This example will have a unit test with an in-memory solution. Will add it soon.
Just like any other use case the new aggregate is stored to the repository. The event publisher publishes all events and the projector create the read models and process manager eventually publish some messages to the outside world.
Another process manager will catch the final MergedWithStaffMembers
event and fire some "removeMergedStaffMember" commands.
Would love to have your feedback on this approach @Ocramius, @prolic.
The factory:
<?php
namespace Acme\Staff\Domain\Service;
use DomainException;
use Prooph\EventSourcing\AggregateChanged;
use Ramsey\Uuid\Uuid;
use ReflectionClass;
use ReflectionProperty;
use Acme\Staff\Domain\Model\StaffMember\ContractId;
use Acme\Staff\Domain\Model\StaffMember\EmploymentPeriod;
use Acme\Staff\Domain\Model\StaffMember\Event\MergedWithStaffMembers;
use Acme\Staff\Domain\Model\StaffMember\Event\StaffMemberAdded;
use Acme\Staff\Domain\Model\StaffMember\Event\StaffMemberContractModified;
use Acme\Staff\Domain\Model\StaffMember\StaffMember;
use Acme\Staff\Domain\Model\StaffMember\StaffMemberId;
use Acme\Staff\Domain\Model\StaffMember\StaffMemberRepository;
final class MergedStaffMember
{
/** @var StaffMemberRepository */
private $staffMemberRepository;
/**
* Current version
*
* @var int
*/
private $version = 0;
/**
* List of events that are not committed to the EventStore
*
* @var AggregateChanged[]
*/
private $recordedEvents = [];
/** @var StaffMemberId */
private $newStaffMemberId;
/** @var StaffMemberId[] */
private $mergeWithStaffMemberIds = [];
/** @var ContractId */
private $newContractId;
/** @var EmploymentPeriod */
private $newEmploymentPeriod;
public function __construct(StaffMemberRepository $staffMemberRepository)
{
$this->staffMemberRepository = $staffMemberRepository;
}
public function fromMergedHistory(
StaffMemberId $newStaffMemberId, array $mergeWithStaffMemberIds,
ContractId $newContractId, EmploymentPeriod $newEmploymentPeriod
): StaffMember
{
if (0 === count($mergeWithStaffMemberIds)) {
throw new DomainException('Missing staff members to merge');
}
$this->newStaffMemberId = $newStaffMemberId;
$this->mergeWithStaffMemberIds = $mergeWithStaffMemberIds;
$this->newContractId = $newContractId;
$this->newEmploymentPeriod = $newEmploymentPeriod;
$this->buildHistoryFromMergedStaffMembers();
$this->finalizeNewStaffMemberHistory();
$newStaffMemberRef = new ReflectionClass(StaffMember::class);
/** @var StaffMember $newStaffMember */
$newStaffMember = $newStaffMemberRef->newInstanceWithoutConstructor();
$newStaffMemberRecordedEventsRef = new ReflectionProperty($newStaffMember, 'recordedEvents');
$newStaffMemberRecordedEventsRef->setAccessible(true);
$newStaffMemberRecordedEventsRef->setValue($newStaffMember, $this->recordedEvents);
$newStaffMemberStaffMemberIdRef = new ReflectionProperty($newStaffMember, 'staffMemberId');
$newStaffMemberStaffMemberIdRef->setAccessible(true);
$newStaffMemberStaffMemberIdRef->setValue($newStaffMember, $newStaffMemberId);
return $newStaffMember;
}
private function buildHistoryFromMergedStaffMembers(): void
{
$oldEvents = $this->staffMemberRepository->ofStaffMemberIds($this->mergeWithStaffMemberIds);
// Ensure chronological order
uasort($oldEvents, function(AggregateChanged $a, AggregateChanged $b) {
return $a->createdAt() <=> $b->createdAt();
});
$initialStaffMemberId = StaffMemberId::fromString(reset($oldEvents)->aggregateId());
/** @var AggregateChanged[] $oldEvent */
foreach ($oldEvents as $oldEvent) {
$newMessageData = $oldEvent->toArray();
// The new event needs an own unique ID.
$newMessageData['uuid'] = Uuid::uuid4()->toString();
// Set the new staff member ID instead of the merged one.
$newMessageData['metadata']['_aggregate_id'] = $this->newStaffMemberId->toString();
// This will be automatically reset correctly.
unset($newMessageData['metadata']['_position']);
if ($oldEvent instanceof StaffMemberAdded) {
/** @var StaffMemberAdded $oldEvent */
if (!$oldEvent->staffMemberId()->sameValueAs($initialStaffMemberId)) {
// Only the initial event can add a staff member.
// All other events can only be modifications of the contract.
$newMessageData['message_name'] = StaffMemberContractModified::class;
}
}
if ($oldEvent instanceof StaffMemberAdded || $oldEvent instanceof StaffMemberContractModified) {
$newMessageData['payload']['contractId'] = $this->newContractId->toString();
// Set new employment period to satisfy all time-period relevant policies.
$newMessageData['payload']['employmentPeriod'] = $this->newEmploymentPeriod->toArray();
}
$eventClassName = $newMessageData['message_name'];
$newEvent = $eventClassName::fromArray($newMessageData);
$this->recordThat($newEvent);
}
}
private function finalizeNewStaffMemberHistory(): void
{
// Create final event
$mergedWithStaffMembers = MergedWithStaffMembers::with(
$this->newStaffMemberId, $this->mergeWithStaffMemberIds,
$this->newContractId, $this->newEmploymentPeriod
);
$mergedWithStaffMembers = $mergedWithStaffMembers
->withAddedMetadata('_aggregate_type', StaffMember::class)
;
$this->recordThat($mergedWithStaffMembers);
}
/**
* Record an aggregate changed event
*/
protected function recordThat(AggregateChanged $event): void
{
$this->version += 1;
$this->recordedEvents[] = $event->withVersion($this->version);
}
}
The command handler:
<?php
namespace Acme\Staff\Application\Service\StaffMember;
use Acme\Staff\Domain\Model\StaffMember\StaffMemberRepository;
use Acme\Staff\Domain\Service\MergedStaffMember;
final class MergeStaffMembersHandler
{
/** @var MergedStaffMember */
private $mergedStaffMember;
/** @var StaffMemberRepository */
private $staffMemberRepository;
public function __construct(MergedStaffMember $mergedStaffMember, StaffMemberRepository $staffMemberRepository)
{
$this->mergedStaffMember = $mergedStaffMember;
$this->staffMemberRepository = $staffMemberRepository;
}
public function __invoke(MergeStaffMembers $command): void
{
$newStaffMember = $this->mergedStaffMember->fromMergedHistory(
$command->newStaffMemberId(),
$command->mergeWithStaffMemberIds(),
$command->newContractId(),
$command->newEmploymentPeriod()
);
$this->staffMemberRepository->save($newStaffMember);
}
}
Just some framework - Symfony and YAML for Marco ;) - config:
services:
Rewotec\Staff\Domain\Service\MergedStaffMember:
arguments:
- '@staff_member_collection'
Rewotec\Staff\Application\Service\StaffMember\MergeStaffMembersHandler:
public: true
tags: [messenger.message_handler]
arguments:
- '@Rewotec\Staff\Domain\Service\MergedStaffMember'
- '@staff_member_collection'
function (ActionEvent $event) use ($eventStore): void {
$recordedEvents = $event->getParam('streamEvents', new \ArrayIterator());
if (! $this->inTransaction($eventStore)) {
if ($event->getParam('streamNotFound', false)
|| $event->getParam('concurrencyException', false)
) {
return;
}
foreach ($recordedEvents as $recordedEvent) {
$this->eventBus->dispatch($recordedEvent);
}
} else {
$this->cachedEventStreams[] = $recordedEvents;
}
}
Regardless of what event-bus to use - at this point of the code the events have already been written to the stream, correct? What if dispatching fails here for some reason? How can an app recover from the domain event not being published?
For instance a console command that re-dispatches / publishes the events? Or am I thinking "too carefully"?!
Discussion on Twitter:
I have an Inspection
Domain Model with a lot of properties, some of them nested.
A new Inspection is handled by a Command Handler. The Inspection AR then raises an ResultRecorded
Event.
Currently this event only holds the AR InspectionId. Not all of the properties I mentioned.
Then an Event Handler catches the event. Currently the Handler lives inside the same Bounded Context "Inspection".
The Handler fetches the (WRITE) Domain Model from its repository by the InspectionId
. It then transforms ("projects"?) the WRITE Model to the READ Model by passing the data to a "fromInspection" constructor. This constructor then has to do some heavy projection because of the nested Aggregates.
Finally the Event Handler stores the READ Model.
If I get the term correctly the Event Handler can be regarded as the "Projector"?
https://abdullin.com/post/event-sourcing-projections/
Then I asked myself what the responsibilities of a Projector is. I think it should do what every Application (Handler) should do:
Receive a message, transform it into Domain language if required (e.g. convert primitive types to Value Objects) and then pass it to the repository and return nothing.
So far this seems to be covered by my example. Though I didn't feel sure if the "transformation" inside the constructor should be moved to the Event Handler.
Then I read about "enriching" Events by @lavinski:
https://www.lavinski.me/generating-read-models-with-event-sourcing/
Also mentioned as "Fat Events" by @mathiasverraes:
https://speakerdeck.com/mathiasverraes/practical-event-sourcing?slide=41
This made me rethink the process of the Event Handler / Projector. What if the Projector was living in a different Bounded Context?
Conclusion: NO! NO! And NO!
The Projector - different bounded context or not - should not receive the Identifier of the original WRITE model to fetch it.
Instead it should receive all data it needs - even it is a lot of data.
Optionally the event could be extended or the data could be devided into multiple Events the Subscriber can listen to(o):
I would move the transformation inside the READ model constructor to place where the Event is raised - the (WRITE) Domain Model.
This felt strange at first! Why?
It felt like setting up a data structure the the Domain Model should not know about BUT the Subscriber e.g. the Projector or the READ Model in the end.
But I guess this type of thinking is simply wrong! We are NOT ASKING what somebody needs - WE ARE TELLING!
What do you think? Code examples will follow!
A declarative, efficient, and flexible JavaScript library for building user interfaces.
๐ Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. ๐๐๐
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google โค๏ธ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.