Skip to content

Commit

Permalink
Add a transactional command bus for league/tactician
Browse files Browse the repository at this point in the history
  • Loading branch information
remi-san committed Aug 29, 2016
1 parent 8e332fb commit 781518f
Show file tree
Hide file tree
Showing 4 changed files with 222 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
6 changes: 4 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
128 changes: 128 additions & 0 deletions src/Command/TransactionalCommandBus.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
<?php

namespace RemiSan\TransactionManager\Command;

use League\Tactician\CommandBus;
use League\Tactician\Exception\InvalidCommandException;
use RemiSan\TransactionManager\Exception\BeginException;
use RemiSan\TransactionManager\Exception\CommitException;
use RemiSan\TransactionManager\Exception\NoRunningTransactionException;
use RemiSan\TransactionManager\Transactional;

class TransactionalCommandBus extends CommandBus implements Transactional
{
/**
* @var CommandBus
*/
private $commandBus;

/**
* @var array
*/
private $commandsToCommit;

/**
* @var boolean
*/
private $transactionRunning;

/**
* TransactionalCommandBus constructor.
*
* @param CommandBus $commandBus
*/
public function __construct(CommandBus $commandBus)
{
parent::__construct([]); // Build it to prevent warnings but never use it

$this->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;
}
}
89 changes: 89 additions & 0 deletions tests/spec/Command/TransactionalCommandBusSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
<?php

namespace spec\RemiSan\TransactionManager\Command;

use League\Tactician\CommandBus;
use League\Tactician\Exception\InvalidCommandException;
use PhpSpec\Exception\Exception;
use PhpSpec\ObjectBehavior;
use Prophecy\Argument;
use RemiSan\TransactionManager\Exception\BeginException;
use RemiSan\TransactionManager\Exception\CommitException;
use RemiSan\TransactionManager\Exception\NoRunningTransactionException;

class TransactionalCommandBusSpec extends ObjectBehavior
{
function let(CommandBus $commandBus)
{
$this->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();
}
}

0 comments on commit 781518f

Please sign in to comment.