diff --git a/README.md b/README.md index 7d1d486..43fddb0 100644 --- a/README.md +++ b/README.md @@ -30,3 +30,4 @@ Some `Transactional` implementations are provided: - `DoctrineDbalTransactionManager` to deal with [`Doctrine DBAL`](https://github.com/doctrine/dbal) transactions - `DoctrineEntityManager` to deal with [`Doctrine ORM`](https://github.com/doctrine/doctrine2) transactions - `TransactionalEmitter` to emit `Events` with the [`PHP League` lib](https://github.com/thephpleague/event) in a transaction + - `TransactionalCommandBus` to handle `Commands` with the [`PHP League` lib](https://github.com/thephpleague/tactician) in a transaction diff --git a/composer.json b/composer.json index 9ad5dfd..368a948 100644 --- a/composer.json +++ b/composer.json @@ -19,13 +19,15 @@ "doctrine/dbal": "^2.5", "doctrine/orm": "^2.5", "evaneos/burrow": "^3.0", - "league/event": "^2.1" + "league/event": "^2.1", + "league/tactician": "^1.0" }, "suggest": { "doctrine/dbal": "To manage Doctrine DBAL transactions", "doctrine/orm": "To manage Doctrine EntityManager transactions", "evaneos/burrow": "To manage AMQP message broking transactionally", - "league/event": "To manage event emitting transactionally" + "league/event": "To manage event emitting transactionally", + "league/tactician": "To manage command emitting transactionally" }, "autoload": { "psr-4": { diff --git a/src/Command/TransactionalCommandBus.php b/src/Command/TransactionalCommandBus.php new file mode 100644 index 0000000..17a94b3 --- /dev/null +++ b/src/Command/TransactionalCommandBus.php @@ -0,0 +1,128 @@ +commandBus = $commandBus; + + $this->reset(); + } + + /** + * @inheritDoc + */ + public function handle($command) + { + if (!is_object($command)) { + throw InvalidCommandException::forUnknownValue($command); + } + + $this->checkTransactionIsRunning(); + + $this->commandsToCommit[] = $command; + + return true; + } + + /** + * @inheritDoc + */ + public function beginTransaction() + { + if ($this->isTransactionRunning()) { + throw new BeginException(); + } + + $this->set(); + } + + /** + * @inheritDoc + */ + public function commit() + { + $this->checkTransactionIsRunning(); + + foreach ($this->commandsToCommit as $command) { + try { + $this->commandBus->handle($command); + } catch (\Exception $e) { + throw new CommitException($e->getMessage(), $e->getCode(), $e); + } + } + + $this->reset(); + } + + /** + * @inheritDoc + */ + public function rollback() + { + $this->checkTransactionIsRunning(); + + $this->reset(); + } + + /** + * @return bool + */ + private function isTransactionRunning() + { + return (boolean) $this->transactionRunning; + } + + /** + * @throws NoRunningTransactionException + */ + private function checkTransactionIsRunning() + { + if (! $this->isTransactionRunning()) { + throw new NoRunningTransactionException(); + } + } + + private function set() + { + $this->commandsToCommit = []; + $this->transactionRunning = true; + } + + private function reset() + { + $this->commandsToCommit = null; + $this->transactionRunning = false; + } +} diff --git a/tests/spec/Command/TransactionalCommandBusSpec.php b/tests/spec/Command/TransactionalCommandBusSpec.php new file mode 100644 index 0000000..932685c --- /dev/null +++ b/tests/spec/Command/TransactionalCommandBusSpec.php @@ -0,0 +1,89 @@ +beConstructedWith($commandBus); + } + + function it_is_initializable() + { + $this->shouldHaveType('RemiSan\TransactionManager\Command\TransactionalCommandBus'); + } + + function it_should_handle_command_when_committing_if_transaction_is_running( + CommandBus $commandBus, + \stdClass $command + ) { + $this->beginTransaction(); + $this->handle($command); + + $commandBus->handle($command)->shouldBeCalled(); + + $this->commit(); + } + + function it_should_not_handle_command_when_rollbacking(CommandBus $commandBus, \stdClass $command) + { + $this->beginTransaction(); + $this->handle($command); + + $commandBus->handle($command)->shouldNotBeCalled(); + + $this->rollback(); + } + + function it_should_throw_an_exception_if_command_is_invalid() + { + $this->beginTransaction(); + $this->shouldThrow(InvalidCommandException::class) + ->duringHandle(''); + } + + function it_should_throw_an_exception_committing_outside_a_transaction(\stdClass $command) + { + $this->shouldThrow(NoRunningTransactionException::class) + ->duringHandle($command); + } + + function it_should_throw_an_exception_if_sub_handle_fails(CommandBus $commandBus, \stdClass $command) + { + $this->beginTransaction(); + + $this->handle($command); + + $commandBus->handle($command)->willThrow(new Exception()); + + $this->shouldThrow(CommitException::class) + ->duringCommit(); + } + + function it_should_throw_an_exception_if_committing_outside_a_transaction() { + $this->shouldThrow(NoRunningTransactionException::class) + ->duringCommit(); + } + + function it_should_throw_an_exception_if_rollbacking_outside_a_transaction() { + $this->shouldThrow(NoRunningTransactionException::class) + ->duringRollback(); + } + + function it_should_not_be_possible_to_start_a_transaction_more_than_once() + { + $this->beginTransaction(); + $this->shouldThrow(BeginException::class) + ->duringBeginTransaction(); + } +}