Reference - SteelzZ/OxyBase GitHub Wiki
Oxy_Domain
Oxy_Domain package provides a lightweight and flexible framework to create domain by applying Domain Driven Design (DDD) principles. DDD states that your domain model should not be tied up to any framework, but we know that it's not always possible or perhaps I should say that it's not that bad if you can reuse something that would speed up your development process.
Oxy_Domain contains the following sub packages:
- AggregateRoot
- Entity
- Repository
- ValueObject
AggregateRoot
In DDD world Aggregate Root is an entity that has an unique identity and is responsible for other entities, called child entities. Child entities can not be accessed directly from outside, everything is controlled by AR. You are allowed to pass child entity to other AR but it should not not rely on given reference. Other important notice is that once you store your AR you store child entities along with it. All this leads to a general AR interface like this:
/**
* @category Oxy
* @package Oxy_Domain
* @subpackage Entity
* @author Tomas Bartkus <[email protected]>
*/
interface Oxy_Domain_AggregateRoot_AggregateRootInterface
extends Oxy_Domain_EntityInterface
{
/**
* @return Oxy_Domain_AggregateRoot_ChildEntitiesCollection
*/
public function getChildEntities();
}
Yes, return child entities.
Entity
Entity in DDD is "something" that has unique identifier. It's more complicated than this of course but you should read more about it if you want to get better understanding.
Now AR is also entity at the same time, because it has unique identifier and the main difference between AR and Entity is that AR has child entities to care about and Entity does not. So let's define our entities interface:
/**
* Entity interface
*
* @category Oxy
* @package Oxy_Domain
* @author Tomas Bartkus <[email protected]>
*/
interface Oxy_Domain_EntityInterface
{
/**
* Returns unique identifier
*
* @return Oxy_Guid
*/
public function getGuid();
}
Entity interface defines single method that returns identity and that is main characteristic of entity.
If we would talk about child entities difference between entity would be that child entity has unique identifier but that identifier is unique only under AR. Entity on the other hand has a global unique identifier, like email address. So our child entity extends normal entity interface by adding one more method to return AR that it belongs to.
/**
* @category Oxy
* @package Oxy_Domain
* @subpackage Entity
* @author Tomas Bartkus <[email protected]>
*/
interface Oxy_Domain_AggregateRoot_ChildEntityInterface
extends Oxy_Domain_EntityInterface
{
/**
* @return Oxy_Domain_AggregateRoot_AggregateRootInterface
*/
public function getAggregateRoot();
}
This package also has a strongly typed collection that accepts only Oxy_Domain_AggregateRoot_ChildEntityInterface this type of instances. It is useful small thing that will help you to manage child entities in AR (we will see later how).
/**
* Entities collection
*
* @category Oxy
* @package Oxy_Domain
* @subpackage Entity
* @author Tomas Bartkus <[email protected]>
**/
class Oxy_Domain_AggregateRoot_ChildEntitiesCollection extends Oxy_Collection
{
/**
* @param array $collectionItems
*/
public function __construct(array $collectionItems = array())
{
parent::__construct(
'Oxy_Domain_AggregateRoot_ChildEntityInterface',
$collectionItems
);
}
/**
* @param Oxy_Collection $collection $collection
* @throws InvalidArgumentException when wrong type
*/
public function addCollection(Oxy_Collection $collection)
{
foreach($collection as $value){
$this->set(
(string)$value->getGuid(),
$value
);
}
}
}
So that basically it. These interfaces is all you need to start working in DDD way. But it's nothing basically, right?
So let's move to more interesting stuff - "Event Sourcing".
Because at the moment I am focusing on this type of AR this is way it's the only one implementation.
Event Sourced AR
So, if you want to create AR that would implement "Event Sourcing" pattern, your AR should extend the following class:
/**
* Event sourced Aggregate Root
* Base class
*
* @category Oxy
* @package Oxy_Domain
* @subpackage AggregateRoot
* @author Tomas Bartkus <[email protected]>
*/
abstract class Oxy_Domain_AggregateRoot_EventSourcedAbstract
extends Oxy_Domain_Entity_EventSourcedAbstract
implements Oxy_Domain_AggregateRoot_AggregateRootInterface
{
/**
* @var Oxy_Domain_AggregateRoot_ChildEntitiesCollection
*/
protected $_childEntities;
/**
* @return Oxy_Domain_AggregateRoot_ChildEntitiesCollection
*/
public function getChildEntities()
{
return $this->_childEntities;
}
/**
* Initialize aggregate root
*
* @param Oxy_Guid $guid
* @param string $realIdentifier
*/
public function __construct(
Oxy_Guid $guid,
$realIdentifier
)
{
parent::__construct($guid, $realIdentifier);
$this->_childEntities = new Oxy_Domain_AggregateRoot_ChildEntitiesCollection();
}
/**
* Register child entity event
*
* @param Oxy_Domain_AggregateRoot_ChildEntityInterface $childEntity
* @param Oxy_EventStore_Event_EventInterface $event
*
* @return void
*/
public function registerChildEntityEvent(
Oxy_Domain_AggregateRoot_ChildEntityInterface $childEntity,
Oxy_EventStore_Event_EventInterface $event
)
{
$this->_childEntities->set((string)$childEntity->getGuid(), $childEntity);
$this->_appliedEvents->addEvent(
new Oxy_EventStore_Event_StorableEvent(
$childEntity->getGuid(),
$event
)
);
}
/**
* @param Oxy_EventStore_Event_EventInterface $event
*
* @return void
*/
protected function _handleEvent(Oxy_EventStore_Event_EventInterface $event)
{
// This should not be called when loading from history
// because if event was applied and we are loading from history
// just load it do not add it to applied events collection
// Add event to to applied collection
// those will be persisted
$this->_appliedEvents->addEvent(
new Oxy_EventStore_Event_StorableEvent(
$this->_guid,
$event
)
);
// Apply event - change state
$this->_apply($event);
}
/**
* Load events
*
* @param Oxy_EventStore_Event_StorableEventsCollectionInterface $domainEvents
*/
public function loadEvents(Oxy_EventStore_Event_StorableEventsCollectionInterface $domainEvents)
{
foreach ($domainEvents as $index => $storableEvent) {
$eventGuid = (string)$storableEvent->getProviderGuid();
if ($eventGuid === (string)$this->_guid) {
$this->_apply($storableEvent->getEvent());
} else if ($this->_childEntities->exists($eventGuid)) {
$childEntity = $this->_childEntities->get($eventGuid);
if($childEntity instanceof Oxy_EventStore_EventProvider_EventProviderInterface){
$childEntity->loadEvents(
new Oxy_EventStore_Event_StorableEventsCollection(
array(
$storableEvent->getProviderGuid() => $storableEvent->getEvent()
)
)
);
} else {
throw new Oxy_Domain_Exception(
sprintf(
'Child entity must implement %s interface',
'Oxy_EventStore_EventProvider_EventProviderInterface'
)
);
}
} else {
throw new Oxy_Domain_Exception(
sprintf('Child entity with guid %s does not exists', $storableEvent->getProviderGuid())
);
}
}
}
}
This class handles all magic that is required to enable event sourcing. If we would go in details about the magic, it's pretty simple. In your AR you will have behaviors, these behaviors will be implementing all your business rules. Once those rules will be applied your behavior will come to the point that you will have to trigger appropriate event. That event will be expressed in past tense and will state a fact that something has just happened. So when you will come to this point you will use base class method - _handleEvent() like this:
$this->_handleEvent(
new Account_Domain_Account_AggregateRoot_Account_Event_NewAccountCreated(
array(
'accountGuid' => (string)$this->_guid,
'primaryEmail' => (string)$primaryEmailAddress,
'password' => (string)$password,
'passwordAgain' => (string)$passwordAgain,
'isAutoGenerated' => (boolean)$password->isAutoGenerated(),
'encodedPassword' => $encodedPassword,
'personalInformation' => $ownerPersonalInformation->toArray(),
'deliveryInformation' => $ownerDeliveryInformation->toArray(),
'settings' => $settings->getProperties(),
'state' => Account_Domain_Account_ValueObject_State::ACCOUNT_STATE_INITIALIZED,
'emailActivationKey' => (string)$emailActivationKey,
'date' => date('Y-m-d H:i:s'),
'loginState' => (string)$this->_loginState,
'generatedPassword' => $generatedPassword,
)
)
);
This method will register event in AR's applied events collection.
So if you will be triggering more than one event, those will be stored safely.
When you will come to the point that your AR has finished processing command (aka executing behavior) you will pass your AR to repository which will handle persistence of AR. In this case it will persist events somewhere, I say somewhere because it depends on you where you will decide to save them - MongoDB, MySQL, MSSQL etc.
Let's take our "Account" BC example, and see how it looks in real world. This fragment shows just events creation and registration phase, saving will be explained later.
class Account_Domain_Account_AggregateRoot_Account
extends Oxy_Domain_AggregateRoot_EventSourcedAbstract
{
/**
* Setup new account
*
* @param Account_Domain_Account_ValueObject_EmailAddress $primaryEmailAddress
* @param Account_Domain_Account_ValueObject_Password $password
* @param Account_Domain_Account_ValueObject_Password $passwordAgain
* @param Account_Domain_Account_ValueObject_PersonalInformation $ownerPersonalInformation
* @param Account_Domain_Account_ValueObject_DeliveryInformation $ownerDeliveryInformation
* @param Account_Domain_Account_ValueObject_Properties $settings
*/
public function setup(
Account_Domain_Account_ValueObject_EmailAddress $primaryEmailAddress,
Account_Domain_Account_ValueObject_Password $password,
Account_Domain_Account_ValueObject_Password $passwordAgain,
Account_Domain_Account_ValueObject_PersonalInformation $ownerPersonalInformation,
Account_Domain_Account_ValueObject_DeliveryInformation $ownerDeliveryInformation,
Account_Domain_Account_ValueObject_Properties $settings
)
{
if($this->_isNew()){
if($this->_comparePasswords($password, $passwordAgain)){
if($this->_checkPasswordStrength($password)){
$emailActivationKey = new Oxy_Guid();
$encodedPassword = (string)$password->getEncoded();
$generatedPassword = $password->isAutoGenerated() == true ? (string)$password : '';
$this->_handleEvent(
new Account_Domain_Account_AggregateRoot_Account_Event_NewAccountCreated(
array(
'accountGuid' => (string)$this->_guid,
'primaryEmail' => (string)$primaryEmailAddress,
'password' => (string)$password,
'passwordAgain' => (string)$passwordAgain,
'isAutoGenerated' => (boolean)$password->isAutoGenerated(),
'encodedPassword' => $encodedPassword,
'personalInformation' => $ownerPersonalInformation->toArray(),
'deliveryInformation' => $ownerDeliveryInformation->toArray(),
'settings' => $settings->getProperties(),
'state' => Account_Domain_Account_ValueObject_State::ACCOUNT_STATE_INITIALIZED,
'emailActivationKey' => (string)$emailActivationKey,
'date' => date('Y-m-d H:i:s'),
'loginState' => (string)$this->_loginState,
'generatedPassword' => $generatedPassword,
)
)
);
} else {
$this->_handleEvent(
new Account_Domain_Account_AggregateRoot_Account_Event_PasswordTooWeakExceptionThrown(
array(
'accountGuid' => (string)$this->_guid,
'message' => 'account.error.weak.password',
'date' => date('Y-m-d H:i:s'),
'additional' => sprintf(
'Password [%s] way to weak',
(string)$password
)
)
)
);
}
} else {
$this->_handleEvent(
new Account_Domain_Account_AggregateRoot_Account_Event_PasswordsDidNotMatchExceptionThrown(
array(
'accountGuid' => (string)$this->_guid,
'message' => 'account.error.passwords.didnt.match',
'date' => date('Y-m-d H:i:s'),
'additional' => sprintf(
'[%s:%s] not equals [%s:%s]',
(string)$password,
(string)$password->getEncoded(),
(string)$passwordAgain,
(string)$passwordAgain->getEncoded()
)
)
)
);
}
} else if($this->_isDeactivated()){
$this->resurect(
$primaryEmailAddress,
$password,
$passwordAgain,
$ownerPersonalInformation,
$ownerDeliveryInformation,
$settings
);
}else {
$this->_handleEvent(
new Account_Domain_Account_AggregateRoot_Account_Event_AccountAlreadyExistsExceptionThrown(
array(
'accountGuid' => (string)$this->_guid,
'message' => 'account.error.account.already.exists',
'date' => date('Y-m-d H:i:s'),
'additional' => sprintf(
'This email [%s] was trying to setup more than once',
(string)$primaryEmailAddress
)
)
)
);
}
}
/**
* @param Account_Domain_Account_AggregateRoot_Account_Event_NewAccountCreated $event
*/
protected function onNewAccountCreated(
Account_Domain_Account_AggregateRoot_Account_Event_NewAccountCreated $event
)
{
$this->_primaryEmail = new Account_Domain_Account_ValueObject_EmailAddress(
$event->getPrimaryEmail()
);
$this->_state = new Account_Domain_Account_ValueObject_State(
$event->getState()
);
$this->_currentPassword = new Account_Domain_Account_ValueObject_Password(
$event->getEncodedPassword(),
true,
$event->getIsAutoGenerated()
);
$this->_activationKey = new Oxy_Guid($event->getEmailActivationKey());
$this->_settings = new Account_Domain_Account_ValueObject_Properties($event->getSettings());
}
/**
* @param Account_Domain_Account_AggregateRoot_Account_Event_AccountAlreadyExistsExceptionThrown $event
*/
protected function onAccountAlreadyExistsExceptionThrown(
Account_Domain_Account_AggregateRoot_Account_Event_AccountAlreadyExistsExceptionThrown $event
)
{
}
/**
* @param Account_Domain_Account_AggregateRoot_Account_Event_PasswordsDidNotMatchExceptionThrown $event
*/
protected function onPasswordsDidNotMatchExceptionThrown(
Account_Domain_Account_AggregateRoot_Account_Event_PasswordsDidNotMatchExceptionThrown $event
)
{
}
/**
* @param Account_Domain_Account_AggregateRoot_Account_Event_PasswordTooWeakExceptionThrown $event
*/
protected function onPasswordTooWeakExceptionThrown(
Account_Domain_Account_AggregateRoot_Account_Event_PasswordTooWeakExceptionThrown $event
)
{
}
}
As you see, once you have processed data, performed state checks and you are ready to trigger one or other event, all you have to do is just call "_handleEvent" method which will register event in base class.
With child entities is the same, base class handles everything (we will see later).
One thing in addition to event registering is that you have to define an internal handler for each of your event. These handlers will be methods which actually will change the state of Entity/AR. You are not allowed to change state in behavior, it's forbidden. Because when you will be retrieving AR from repository those events will be applied to your AR. So it means that only the code in handler will be executed. All this means two very important things, very important:
- NEVER CHANGE STATE IN BEHAVIOR
- IN INTERNAL HANDLER CAN NOT BE ANY LOGIC
First one is pretty clear I guess. Second one might be tricky in the beginning, but as a rule of thumb, you can think like this - if there is anything else then just direct property state change statement - it's wrong. It has to be pure state changes statements, like:
/**
* @param Account_Domain_Account_AggregateRoot_Account_Event_NewAccountCreated $event
*/
protected function onNewAccountCreated(
Account_Domain_Account_AggregateRoot_Account_Event_NewAccountCreated $event
)
{
$this->_primaryEmail = new Account_Domain_Account_ValueObject_EmailAddress(
$event->getPrimaryEmail()
);
$this->_state = new Account_Domain_Account_ValueObject_State(
$event->getState()
);
$this->_currentPassword = new Account_Domain_Account_ValueObject_Password(
$event->getEncodedPassword(),
true,
$event->getIsAutoGenerated()
);
$this->_activationKey = new Oxy_Guid($event->getEmailActivationKey());
$this->_settings = new Account_Domain_Account_ValueObject_Properties($event->getSettings());
}
There is no "if something" there is no "new date('Ymdhis')" statements everything is taken from event instance and then applied to entity properties.
And this is how all you business logic is implemented. Now if you are not sure if isn't it to complex as for simple method like "login", well it depends what you are looking for. If you are having data centric application then perhaps i would say, no you shouldn't be using it, but if you have more like behavior driven application then keep reading and you will understand all benefits by yourself.
Now if we would continue by talking about child entities, there is nothing special about them. Child entities are same entities as ARs and all those rules applies to it. The only thing worth mentioning is that AR base class is handling registered child entity events and is responsible to "know" which event to which entity belongs. But all this is handled by base classes so you don't need to worry about it. All child entities has to extend this base class:
/**
* Event sourced child entity
* Base class
*
* @category Oxy
* @package Oxy_Domain
* @subpackage Entity
* @author Tomas Bartkus <[email protected]>
*/
abstract class Oxy_Domain_AggregateRoot_EventSourcedChildEntityAbstract
extends Oxy_Domain_Entity_EventSourcedAbstract
implements Oxy_Domain_AggregateRoot_ChildEntityInterface
{
/**
* @var Oxy_Domain_AggregateRoot_AggregateRootInterface
*/
protected $_aggregateRoot;
/**
* @return Oxy_Guid
*/
public function getGuid()
{
return $this->_guid;
}
/**
* @return Oxy_Domain_AggregateRoot_AggregateRootInterface
*/
public function getAggregateRoot()
{
return $this->_aggregateRoot;
}
/**
* @param Oxy_Guid $guid
* @param string $guid
* @param Oxy_Domain_AggregateRoot_AggregateRootInterface $aggregateRoot
*/
public function __construct(
Oxy_Guid $guid,
$realIdentifier,
Oxy_Domain_AggregateRoot_AggregateRootInterface $aggregateRoot = null
)
{
parent::__construct($guid, $realIdentifier);
$this->_aggregateRoot = $aggregateRoot;
}
/**
* @param Oxy_Domain_EventInterface $event
* @return void
*/
protected function _handleEvent(Oxy_EventStore_Event_EventInterface $event)
{
// This should not be called when loading from history
// because if event was applied and we are loading from history
// just load it do not add it to applied events collection
// Add event to to applied collection
// those will be persisted
$this->_aggregateRoot->registerChildEntityEvent($this, $event);
// Apply event - change state
$this->_apply($event);
}
/**
* @return string
*/
public function __toString()
{
return (string)$this->_guid;
}
}
As you see child entity class is the same entity just it has reference to AR and overrides _handleEvent method.
One last thing about AR and child entity is that both extends the following class - Oxy_EventStore_EventProvider_EventProviderInterface. This interface is required by Oxy_EventStore, which will be used to store our entities (we will talk more about it in Oxy_EventStore chapter).
/**
* Event sourced entity
* Base class
*
* @category Oxy
* @package Oxy_Domain
* @subpackage Entity
* @author Tomas Bartkus <[email protected]>
*/
abstract class Oxy_Domain_Entity_EventSourcedAbstract
implements Oxy_Domain_EntityInterface,
Oxy_EventStore_EventProvider_EventProviderInterface
{
/**
* @var Oxy_Guid
*/
protected $_guid;
/**
* @var integer
*/
protected $_version;
/**
* @var Oxy_EventStore_Event_StorableEventsCollection
*/
protected $_appliedEvents;
/**
* @var string
*/
protected $_realIdentifier;
/**
* @return Oxy_EventStore_Event_StorableEventsCollection
*/
public function getChanges()
{
return $this->_appliedEvents;
}
/**
* @return integer $version
*/
public function getVersion()
{
return $this->_version;
}
/**
* @return Oxy_Guid
*/
public function getGuid()
{
return $this->_guid;
}
/**
* @param Oxy_Guid $guid
* @param string $realIdentifier
*/
public function __construct(
Oxy_Guid $guid,
$realIdentifier
)
{
$this->_appliedEvents = new Oxy_EventStore_Event_StorableEventsCollection();
$this->_guid = $guid;
$this->_realIdentifier = $realIdentifier;
}
/**
* @see Oxy_EventStore_EventProvider_EventProviderInterface::getRealIdentifier()
*/
public function getRealIdentifier()
{
return $this->_realIdentifier;
}
/**
* @see Oxy_EventStore_EventProvider_EventProviderInterface::getName()
*/
public function getName()
{
return (string)get_class($this);
}
/**
* @param integer $version
*/
public function updateVersion($version)
{
$this->_version = $version;
}
/**
* @param Oxy_EventStore_Event_StorableEventsCollectionInterface $domainEvents
*/
public function loadEvents(Oxy_EventStore_Event_StorableEventsCollectionInterface $domainEvents)
{
foreach ($domainEvents as $index => $storableEvent) {
if ((string)$storableEvent->getProviderGuid() === (string)$this->_guid) {
$this->_apply($storableEvent->getEvent());
} else {
throw new Oxy_Domain_Exception(
sprintf(
'Given event does not belong to this entity - %s [%s]',
(string)$storableEvent->getProviderGuid(),
(string)$this->_guid
)
);
}
}
}
/**
* @param Oxy_EventStore_Event_EventInterface $event
* @return void
*/
protected function _handleEvent(Oxy_EventStore_Event_EventInterface $event)
{
// This should not be called when loading from history
// because if event was applied and we are loading from history
// just load it do not add it to applied events collection
// Add event to to applied collection
// those will be persisted
$this->_appliedEvents->addEvent(
new Oxy_EventStore_Event_StorableEvent(
$this->_guid,
$event
)
);
// Apply event - change state
$this->_apply($event);
}
/**
* @param Oxy_Domain_EventInterface $event
* @return void
*/
protected function _apply(Oxy_EventStore_Event_EventInterface $event)
{
$eventHandlerName = 'on' . $event->getEventName();
if(method_exists($this, $eventHandlerName)){
call_user_func_array(array($this, $eventHandlerName), array($event));
} else {
$this->_onEventHandlerNotFound($event);
}
}
/**
* Child classes can override this one and have their own logic
* when event handler for given event does not exists anymore
*
* @param Oxy_EventStore_Event_Interface $event
* @throws Oxy_Domain_Exception
*/
protected function _onEventHandlerNotFound(Oxy_EventStore_Event_Interface $event)
{
throw new Oxy_Domain_Exception(
sprintf('Event handler for %s does not exists', $event->getEventName())
);
}
/**
* @param string $where
* @throws Oxy_EventStore_Event_WrongStateException
*/
protected function _throwWrongStateException($where, $state)
{
throw new Oxy_EventStore_Event_WrongStateException(
sprintf('Can not execute [%s] behaviour in current state [%s]! [%s]', $where, (string)$state, (string)$this->_guid)
);
}
/**
* @return string
*/
public function __toString()
{
return (string)$this->_guid;
}
}
Repository
DDD also adopts Repository pattern. This pattern basically introduces persistence ignorance. You are working with it like some kind of Facade, because it hides complex things under simple interface. Now, what we currently have is this general Repository interface:
/**
* Domain repository interface
*
* @category Oxy
* @package Oxy_Domain
* @subpackage Repository
* @author Tomas Bartkus <[email protected]>
*/
interface Oxy_Domain_Repository_RepositoryInterface
{
/**
* Get Aggregate root by GUID
*
* @param string $aggregateRootClassName
* @param Oxy_Guid $aggregateRootGuid
* @param string $realIdentifier
*
* @return Oxy_Domain_AggregateRoot_AggregateRootInterface
*/
public function getById($aggregateRootClassName, Oxy_Guid $aggregateRootGuid, $realIdentifier);
/**
* Save aggregate root
*
* @param Oxy_Domain_AggregateRoot_AggregateRootInterface $aggregateRoot
*
* @return void
*/
public function add(Oxy_Domain_AggregateRoot_AggregateRootInterface $aggregateRoot);
}
Which is able to save somewhere AR and to get it back for you.
Now I have defined another interface, which is more suited to event based AR:
/**
* Domain repository interface
*
* @category Oxy
* @package Oxy_Domain
* @subpackage Repository
* @author Tomas Bartkus <[email protected]>
*/
interface Oxy_Domain_Repository_EventStoreInterface
{
/**
* Get Aggregate root by GUID
*
* @param string $aggregateRootClassName
* @param Oxy_Guid $aggregateRootGuid
* @param string $realIdentifier
*
* @return Oxy_Domain_AggregateRoot_AggregateRootInterface
*/
public function getById($aggregateRootClassName, Oxy_Guid $aggregateRootGuid, $realIdentifier);
/**
* Save aggregate root
*
* @param Oxy_EventStore_EventProvider_EventProviderInterface $aggregateRoot
*
* @return void
*/
public function add(Oxy_EventStore_EventProvider_EventProviderInterface $aggregateRoot);
}
As you see it's the same just add method requires different type of instance. I did like this because I think we are still ignoring the actual persistence and all we do is just explicitly defining different interface that accepts different type of AR. Of course we could hide under the same interface method that accepts general AR, but I think that if you will be changing your domain so drastically it won't hurt much to change repository type. And in this way it's more clear and less magical.
So once we have defined interfaces let's see implementations. This is base class for repositories that that will handle your domain AR persistence. You actually need only one instance of this class to handle your all same bounded context ARs.
/**
* Event store domain repository
*
* @category Oxy
* @package Oxy_Domain
* @subpackage Repository
* @author Tomas Bartkus <[email protected]>
*/
abstract class Oxy_Domain_Repository_EventStoreAbstract
implements Oxy_Domain_Repository_EventStoreInterface
{
/**
* @var Oxy_EventStore_EventStoreInterface
*/
protected $_eventStore;
/**
* @var Oxy_EventStore_EventPublisher_EventPublisherInterface
*/
protected $_eventsPublisher;
/**
* Initialize repository
*
* @param Oxy_EventStore_EventStoreInterface $eventStore
* @param Oxy_EventStore_EventPublisher_EventPublisherInterface $eventsPublisher
*/
public function __construct(
Oxy_EventStore_EventStoreInterface $eventStore,
Oxy_EventStore_EventPublisher_EventPublisherInterface $eventsPublisher
)
{
$this->_eventStore = $eventStore;
$this->_eventsPublisher = $eventsPublisher;
}
/**
* @see Oxy_Domain_Repository_Interface::add()
*/
public function add(Oxy_EventStore_EventProvider_EventProviderInterface $aggregateRoot)
{
$this->_eventStore->add($aggregateRoot);
$this->_eventStore->commit();
$this->_eventsPublisher->notifyListeners($aggregateRoot->getChanges());
}
/**
* @see Oxy_Domain_Repository_Interface::getById()
*/
public function getById($aggregateRootClassName, Oxy_Guid $aggregateRootGuid, $realIdentifer)
{
try{
// State will be loaded on this object
$aggregateRoot = new $aggregateRootClassName(
$aggregateRootGuid,
$realIdentifer
);
} catch(Exception $ex) {
throw new Oxy_Domain_Repository_Exception(
sprintf('Class of this entity was not found - %s', $aggregateRootClassName)
);
}
try{
$this->_eventStore->getById(
$aggregateRootGuid,
$aggregateRoot
);
} catch(Exception $ex){
throw new Oxy_Domain_Repository_Exception(
sprintf('Could not load events on this entity - %s', $aggregateRootClassName)
);
}
// OK return
return $aggregateRoot;
}
}
For example in command handlers (we will talk about them bit later) you will be injecting one and the same instance of repository and then inside handler you will use it like this:
/**
* Base command handler class
*
* @category Oxy
* @package Oxy_Cqrs
* @subpackage Command
* @author Tomas Bartkus <[email protected]>
*/
abstract class Oxy_Cqrs_Command_Handler_EventStoreHandlerAbstract
implements Oxy_Cqrs_Command_Handler_HandlerInterface
{
/**
* @var Oxy_Domain_Repository_EventStoreInterface
*/
protected $_eventStoreRepository;
public function __construct(Oxy_Domain_Repository_EventStoreInterface $repository)
{
$this->_eventStoreRepository = $repository;
}
}
/**
* @category Account
* @package Account_Account
*/
class Account_Lib_Command_Handler_DoSetupAccount
extends Oxy_Cqrs_Command_Handler_EventStoreHandlerAbstract
{
/**
* @param Oxy_Cqrs_Command_CommandInterface $command
*/
public function execute(Oxy_Cqrs_Command_CommandInterface $command)
{
$account = $this->_eventStoreRepository->getById(
'Account_Domain_Account_AggregateRoot_Account',
$command->getGuid(),
$command->getRealIdentifier()
);
$primaryEmailAddress = new Account_Domain_Account_ValueObject_EmailAddress(
$command->getEmailAddress()
);
$password = new Account_Domain_Account_ValueObject_Password(
$command->getPassword(),
false,
(boolean)$command->getPasswordAutoGenerated()
);
$passwordAgain = new Account_Domain_Account_ValueObject_Password(
$command->getPasswordAgain(),
false,
(boolean)$command->getPasswordAutoGenerated()
);
$ownerPersonalInformation = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_PersonalInformation',
$command->getPersonalInformation()
);
$ownerDeliveryInformation = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_DeliveryInformation',
$command->getDeliveryInformation()
);
$settings = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_Properties',
$command->getSettings()
);
$account->setup(
$primaryEmailAddress,
$password,
$passwordAgain,
$ownerPersonalInformation,
$ownerDeliveryInformation,
$settings
);
$this->_eventStoreRepository->add($account);
}
}
Simple as that - get and add, everything else is done by Oxy_EventStore.
Value Object
Value object in DDD world is something that describes something, like color for example. But again you should read about it more in details.
What Oxy_Domain package has regarding value objects is only this abstract class:
abstract class Oxy_Domain_ValueObject_ArrayableAbstract
{
/**
* Convert object to array
*
* @return array
*/
public function toArray()
{
$properties = get_class_vars(get_class($this));
$vars = array();
foreach ($properties as $name => $defaultVal) {
$vars[str_replace('_', '', $name)] = $this->$name;
}
return $vars;
}
/**
* @param string $className
* @param array $params
*
* @return Oxy_Domain_ValueObject_ArrayableAbstract
*/
public static function createFromArray($className, $params)
{
try{
$reflectedClass = new ReflectionClass($className);
$classInstance = $reflectedClass->newInstanceArgs($params);
return $classInstance;
} catch (Exception $ex){
throw new Oxy_Domain_Exception(
sprintf(
'Could not create [%s] class instance! Exact message was [%s]',
$className,
$ex->getMessage()
)
);
}
}
}
Now what it does is that it enables toArray() functionality of your VOs and also offers a factory method to create VOs. It is usefull little thing:
$ownerPersonalInformation = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_PersonalInformation',
$command->getPersonalInformation()
);
It's easier to change constructor later if needed. And it saves time as well. Because when i need to do that I just change constructor and client will get fatal error while it will try to push me old format command, because it will be rejected as invalid, because handler will not be able to build all required components for behavior execution.
Oxy_Cqrs
This package has few lightweight components that will help you to enable few other CQRS pieces - commands, command handlers, and commands consumers.
This package has the following sub-packages:
- Command
- Query
Command
Commands in CQRS environment is the way how you are handling write operations. Command is immutable instance with a bunch of properties that are required to build Value Objects, which later will be passed to AR behavior for processing. This is a command example:
/**
* @category Account
* @package Account_Lib
* @subpackage Command
*/
class Account_Lib_Command_DoSetupAccount extends Oxy_Cqrs_Command_CommandAbstract
{
/**
* @var string
*/
private $_emailAddress;
/**
* @var string
*/
private $_password;
/**
* @var string
*/
private $_passwordAgain;
/**
* @var string
*/
private $_passwordAutoGenerated;
/**
* @var array
*/
private $_personalInformation;
/**
* @var array
*/
private $_deliveryInformation;
/**
* @var array
*/
private $_settings;
/**
* @param string $commandName
* @param string $guid
* @param string $emailAddress
* @param string $password
* @param string $passwordAgain
* @param string $passwordAutoGenerated
* @param array $personalInformation
* @param array $deliveryInformation
* @param array $settings
*/
public function __construct(
$commandName,
$guid,
$realIdentifier,
$emailAddress,
$password,
$passwordAgain,
$passwordAutoGenerated,
$personalInformation,
$deliveryInformation,
$settings
)
{
parent::__construct($commandName, $guid, $realIdentifier);
$this->_emailAddress = $emailAddress;
$this->_password = $password;
$this->_passwordAgain = $passwordAgain;
$this->_passwordAutoGenerated = $passwordAutoGenerated;
$this->_personalInformation = $personalInformation;
$this->_deliveryInformation = $deliveryInformation;
$this->_settings = $settings;
}
/**
* @return string
*/
public function getPasswordAutoGenerated()
{
return $this->_passwordAutoGenerated;
}
/**
* @return string
*/
public function getEmailAddress()
{
return $this->_emailAddress;
}
/**
* @return string
*/
public function getPassword()
{
return $this->_password;
}
/**
* @return string
*/
public function getPasswordAgain()
{
return $this->_passwordAgain;
}
/**
* @return array
*/
public function getPersonalInformation()
{
return $this->_personalInformation;
}
/**
* @return array
*/
public function getDeliveryInformation()
{
return $this->_deliveryInformation;
}
/**
* @return array
*/
public function getSettings()
{
return $this->_settings;
}
}
And this is how it is created and published to the queue somewhere by some client:
$command = Oxy_Cqrs_Command_CommandAbstract::factory(
'Account_Lib_Command_DoSetupAccount',
array(
$accountGuid,
$email,
$email,
$password,
$passwordAgain,
$passwordAutoGenerated,
$ownerInformation,
$deliveryInformation,
array($settings)
)
);
$globalQueue->addCommand($command);
All commands should extend this base class:
/**
* Base command class
*
* @category Oxy
* @package Oxy_Cqrs
* @subpackage Command
* @author Tomas Bartkus <[email protected]>
*/
abstract class Oxy_Cqrs_Command_CommandAbstract implements Oxy_Cqrs_Command_CommandInterface
{
/**
* @var String
*/
protected $_commandName;
/**
* @var Oxy_Guid
*/
protected $_guid;
/**
* @var string
*/
protected $_realIdentifier;
/**
* Initialize command
*
* @param string $commandName
* @param Oxy_Guid $guid
* @param string $realIdentifier
*
* @return void
*/
public function __construct($commandName, Oxy_Guid $guid, $realIdentifier)
{
$this->_commandName = $commandName;
$this->_guid = $guid;
$this->_realIdentifier = $realIdentifier;
}
/**
* Return command name
*
* @return string
*/
public function getCommandName()
{
return $this->_commandName;
}
/**
* Return GUID
*
* @return Oxy_Guid
*/
public function getGuid()
{
return $this->_guid;
}
/**
* Return real identifier
*
* @return Oxy_Guid
*/
public function getRealIdentifier()
{
return $this->_realIdentifier;
}
/**
* Create command
*
* @param string $targetClass
* @param array $params
*/
public static function factory($targetClass, $params)
{
$reflectedClass = new ReflectionClass($targetClass);
if ($reflectedClass->isSubclassOf(__CLASS__)){
$guid = array_shift($params);
if(!($guid instanceof Oxy_Guid)){
$guid = new Oxy_Guid((string)$guid);
}
array_unshift($params, $guid);
array_unshift($params, $targetClass);
$commandInstance = $reflectedClass->newInstanceArgs($params);
return $commandInstance;
} else {
throw new Oxy_Cqrs_Command_Exception("The command '$targetClass' doesn not exists.");
}
}
}
Now command handlers. Each command should have one command handler. Command handler can not handle more than one command. You can design your handlers as you want, one per file or more then one per file, but one command handler can handle only one command. It's 1:1 relation. Interface for command handler would be like this:
/**
* Command handler interface
*
* @category Oxy
* @package Oxy_Cqrs
* @subpackage Command
* @author Tomas Bartkus <[email protected]>
*/
interface Oxy_Cqrs_Command_Handler_HandlerInterface
{
/**
* @param Oxy_Cqrs_Command_CommandInterface $command
*/
public function execute(Oxy_Cqrs_Command_CommandInterface $command);
}
And actual command handler:
/**
* @category Account
* @package Account_Account
*/
class Account_Lib_Command_Handler_DoSetupAccount
extends Oxy_Cqrs_Command_Handler_EventStoreHandlerAbstract
{
/**
* @param Oxy_Cqrs_Command_CommandInterface $command
*/
public function execute(Oxy_Cqrs_Command_CommandInterface $command)
{
$account = $this->_eventStoreRepository->getById(
'Account_Domain_Account_AggregateRoot_Account',
$command->getGuid(),
$command->getRealIdentifier()
);
$primaryEmailAddress = new Account_Domain_Account_ValueObject_EmailAddress(
$command->getEmailAddress()
);
$password = new Account_Domain_Account_ValueObject_Password(
$command->getPassword(),
false,
(boolean)$command->getPasswordAutoGenerated()
);
$passwordAgain = new Account_Domain_Account_ValueObject_Password(
$command->getPasswordAgain(),
false,
(boolean)$command->getPasswordAutoGenerated()
);
$ownerPersonalInformation = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_PersonalInformation',
$command->getPersonalInformation()
);
$ownerDeliveryInformation = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_DeliveryInformation',
$command->getDeliveryInformation()
);
$settings = Oxy_Domain_ValueObject_ArrayableAbstract::createFromArray(
'Account_Domain_Account_ValueObject_Properties',
$command->getSettings()
);
// Executing actual behavior
$account->setup(
$primaryEmailAddress,
$password,
$passwordAgain,
$ownerPersonalInformation,
$ownerDeliveryInformation,
$settings
);
// Save to repository
$this->_eventStoreRepository->add($account);
}
}