领域驱动设计PHP辅助类(应用服务、事务、领域事件等)

1.6.0 2016-07-22 09:51 UTC

This package is not auto-updated.

Last update: 2024-09-11 13:15:12 UTC


README

Build Status

此库将帮助您处理典型的领域驱动设计场景,目前

  • 应用服务接口
  • 使用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监听器

此库能够支持异步消息传递,以便使边界上下文能够以高效的方式监听其他边界上下文的事件。这是基于amqpreact的事件循环。此外,为了支持更高效的事件循环,我们建议安装以下扩展之一

任何这些扩展的使用都由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(/** ... **/);