领域驱动设计PHP辅助类(应用服务、事务、领域事件等)
Requires
- jms/serializer: ~1.1
- react/event-loop: ^0.4.1
Requires (Dev)
- phpunit/phpunit: 4.*
Suggests
- doctrine/orm: It allows Transactional Application Services and/or Domain Events persisted using Doctrine
- ext/amqp: It allows the usage of asynchronous messaging producers and message listeners
- videlalvaro/php-amqplib: It allows Domain Events notification using RabbitMQ
README
此库将帮助您处理典型的领域驱动设计场景,目前
- 应用服务接口
- 使用Doctrine和ADODb的事务性应用服务
- 数据转换器接口
- 无转换器的数据转换器
- 领域事件接口
- 事件存储接口
- 事件存储Doctrine实现
- 领域事件发布服务
- 消息生产者接口
- RabbitMQ实现的消息生产者
示例项目
有一些项目是使用carlosbuenosvinos/ddd库开发的。检查一些以了解如何使用它
- 最后的愿望:在您遇到任何事情时运行的行动,例如推文、发送电子邮件等。
应用服务
应用服务接口
考虑一个注册新用户到您应用程序的应用服务。
$signInUserService = new SignInUserService(
$em->getRepository('MyBC\Domain\Model\User\User')
);
$response = $signInUserService->execute(
new SignInUserRequest(
'carlos.buenosvinos@gmail.com',
'thisisnotasecretpassword'
)
);
$newUserCreated = $response->getUser();
//...
我们需要在构造函数中传入所有依赖项。在这种情况下,用户存储库。根据领域驱动设计,Doctrine存储库正在实现用户存储库的通用接口。
<?php
namespace MyBC\Application\Service\User;
use MyBC\Domain\Model\User\User;
use MyBC\Domain\Model\User\UserAlreadyExistsException;
use MyBC\Domain\Model\User\UserRepository;
use Ddd\Application\Service\ApplicationService;
/**
* Class SignInUserService
* @package MyBC\Application\Service\User
*/
class SignInUserService implements ApplicationService
{
/**
* @var UserRepository
*/
private $userRepository;
/**
* @param UserRepository $userRepository
*/
public function __construct(UserRepository $userRepository)
{
$this->userRepository = $userRepository;
}
/**
* @param SignInUserRequest $request
* @return SignInUserResponse
* @throws UserAlreadyExistsException
*/
public function execute($request = null)
{
$email = $request->email();
$password = $request->password();
$user = $this->userRepository->userOfEmail($email);
if (null !== $user) {
throw new UserAlreadyExistsException();
}
$user = new User(
$this->userRepository->nextIdentity(),
$email,
$password
);
$this->userRepository->persist($user);
return new SignInUserResponse($user);
}
}
我建议让您的应用服务实现以下接口,遵循命令模式。
/**
* Interface ApplicationService
* @package Ddd\Application\Service
*/
interface ApplicationService
{
/**
* @param $request
* @return mixed
*/
public function execute($request = null);
}
事务
应用服务在处理数据库持久化策略时应该管理事务。为了干净地管理它,我提供了一个应用服务装饰器,该装饰器包装应用服务并在事务边界内执行它。
装饰器是 Ddd\Application\Service\TransactionalApplicationService
类。为了创建一个,您需要一个非事务性应用服务和一个事务性会话。我们提供不同类型的事务性会话。看看如何使用Doctrine。
Doctrine事务性应用服务
对于Doctrine事务性会话,传递EntityManager实例。
/** @var EntityManager $em */
$txSignInUserService = new TransactionalApplicationService(
new SignInUserService(
$em->getRepository('MyBC\Domain\Model\User\User')
),
new DoctrineSession($em)
);
$response = $txSignInUserService->execute(
new SignInUserRequest(
'carlos.buenosvinos@gmail.com',
'thisisnotasecretpassword'
)
);
$newUserCreated = $response->getUser();
//...
如您所见,用例的创建和执行与非事务性相同,唯一的区别是用事务性应用服务进行装饰。
作为附带好处,Doctrine会话内部管理 flush
方法,因此您不需要在领域或基础设施中添加 flush
。
异步AMQP监听器
此库能够支持异步消息传递,以便使边界上下文能够以高效的方式监听其他边界上下文的事件。这是基于amqp和react的事件循环。此外,为了支持更高效的事件循环,我们建议安装以下扩展之一
任何这些扩展的使用都由ReactPHP的事件循环以完全透明的方式进行管理。
示例
假设我们需要监听由另一个边界上下文通过消息触发的事件Acme\Billing\DomainModel\Order\OrderWasCreated
。以下是一个监听Acme\Billing\DomainModel\Order\OrderWasCreated
事件的AMQP交换监听器的示例。
<?php namespace Acme\Inventory\Infrastructure\Messaging\Amqp; use stdClass; use AMQPQueue; use JMS\Serializer\Serializer; use League\Tactician\CommandBus; use React\EventLoop\LoopInterface; use Ddd\Infrastructure\Application\Notification\AmqpExchangeListener; class OrderWasCreatedListener extends AmqpExchangeListener { private $commandBus; public function __construct(AMQPQueue $queue, LoopInterface $loop, Serializer $serializer, CommandBus $commandBus) { $this->commandBus = $commandBus; parent::construct($queue, $loop, $serializer); } /** * This method will be responsible to decide whether this listener listens to an specific * event type or not, given an event type name * * @param string $typeName * * @return bool */ protected function listensTo($typeName) { return 'Acme\Billing\DomainModel\Order\OrderWasCreated' === $typeName; } /** * The action to perform * * @param stdClass $event * * @return void */ protected function handle($event) { $this->commandBus->handle(new CreateOrder( $event->order_id, // ... )); } }
以下是一个创建AMQP工作者的可能命令
<?php namespace AppBundle\Command; use AMQPConnection; use AMQPChannel; use React\EventLoop\Factory; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use JMS\Serializer\Serializer; use League\Tactician\CommandBus; class OrderWasCreatedWorkerCommand extends Command { private $serializer; private $commandBus; public function __construct(Serializer $serializer, CommandBus $commandBus) { $this->serializer = $serializer; $this->commandBus = $commandBus; parent::__construct(); } public function execute(InputInterface $input, OutputInterface $output) { $connection = new AMQPConnection([ 'host' => 'example.host', 'vhost' => '/', 'port' => 5763, 'login' => 'user', 'password' => 'password' ]); $connection->connect(); $queue = new AMQPQueue(new AMQPChannel($connection)); $queue->setName('events'); $queue->setFlags(AMQP_NOPARAM); $queue->declareQueue(); $loop = Factory::create(); $listener = new OrderWasCreatedListener( $queue, $loop, $serializer, $this->commandBus ); $loop->run(); } }
AMQP消息生产者
AMQP消息生产者的目的是在其他类中组合。以下是一个使用AMQP消息生产者的示例。
<?php use Doctrine\ORM\Tools\Setup; use Doctrine\ORM\EntityManager; use Ddd\Infrastructure\Application\Notification\AmqpMessageProducer; use Ddd\Application\Notification\NotificationService; $connection = new AMQPConnection([ 'host' => 'example.host', 'vhost' => '/', 'port' => 5763, 'login' => 'user', 'password' => 'password' ]); $connection->connect(); $exchange = new AMQPExchange(new AMQPChannel($connection)); $exchange->setName('events'); $exchange->declare(); $config = Setup::createYAMLMetadataConfiguration([__DIR__."/src/Infrastructure/Application/Persistence/Doctrine/Config"], false); $entityManager = EntityManager::create(['driver' => 'pdo_sqlite', 'path' => __DIR__ . '/db.sqlite'], $config); $eventStore = $entityManager->getRepository('Ddd\Domain\Event\StoredEvent'); $publishedMessageTracker = $entityManager->getRepository('Ddd\Domain\Event\PublishedMessage'); $messageProducer = new AmqpMessageProducer($exchange); $notificationService = new NotificationService( $eventStore, $publishedMessageTracker, $messageProducer ); $notificationService->publish(/** ... **/);