Skip to content

Commit

Permalink
Execute delete and submit actions in database transaction
Browse files Browse the repository at this point in the history
  • Loading branch information
Dominic Tubach committed Jul 30, 2024
1 parent fef1cef commit d0df501
Show file tree
Hide file tree
Showing 6 changed files with 285 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php
declare(strict_types = 1);

namespace Civi\RemoteTools\ActionHandler;

use Civi\RemoteTools\Api4\Action\RemoteDeleteAction;
use Civi\RemoteTools\Api4\Action\RemoteSubmitCreateFormAction;
use Civi\RemoteTools\Api4\Action\RemoteSubmitUpdateFormAction;
use Civi\RemoteTools\Database\TransactionFactory;

final class RemoteActionsHandlerTransactionDecorator extends AbstractRemoteEntityActionsHandlerDecorator {

private TransactionFactory $transactionFactory;

public function __construct(RemoteEntityActionsHandlerInterface $handler, TransactionFactory $transactionFactory) {
parent::__construct($handler);
$this->transactionFactory = $transactionFactory;
}

public function delete(RemoteDeleteAction $action): array {
$transaction = $this->transactionFactory->createTransaction();
try {
return parent::delete($action);
}
// @phpstan-ignore-next-line Dead catch clause.
catch (\Throwable $e) {
$transaction->rollback();

throw $e;
}
finally {
$transaction->commit();
}
}

public function submitCreateForm(RemoteSubmitCreateFormAction $action): array {
$transaction = $this->transactionFactory->createTransaction();
try {
return parent::submitCreateForm($action);
}
// @phpstan-ignore-next-line Dead catch clause.
catch (\Throwable $e) {
$transaction->rollback();

throw $e;
}
finally {
$transaction->commit();
}
}

public function submitUpdateForm(RemoteSubmitUpdateFormAction $action): array {
$transaction = $this->transactionFactory->createTransaction();
try {
return parent::submitUpdateForm($action);
}
// @phpstan-ignore-next-line Dead catch clause.
catch (\Throwable $e) {
$transaction->rollback();

throw $e;
}
finally {
$transaction->commit();
}
}

}
31 changes: 31 additions & 0 deletions Civi/RemoteTools/Database/TransactionFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
<?php
/*
* Copyright (C) 2022 SYSTOPIA GmbH
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation in version 3.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/

declare(strict_types = 1);

namespace Civi\RemoteTools\Database;

/**
* @codeCoverageIgnore
*/
class TransactionFactory {

public function createTransaction(): \CRM_Core_Transaction {
return \CRM_Core_Transaction::create();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
use Civi\Api4\Generic\AbstractAction;
use Civi\RemoteTools\ActionHandler\ActionHandlerInterface;
use Civi\RemoteTools\ActionHandler\ActionHandlerProvider;
use Civi\RemoteTools\ActionHandler\RemoteActionsHandlerTransactionDecorator;
use Civi\RemoteTools\DependencyInjection\Compiler\Traits\DecorateServiceTrait;
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
use Symfony\Component\DependencyInjection\Compiler\ServiceLocatorTagPass;
use Symfony\Component\DependencyInjection\ContainerBuilder;
Expand All @@ -33,6 +35,8 @@
*/
final class ActionHandlerPass implements CompilerPassInterface {

use DecorateServiceTrait;

public static function buildHandlerKey(string $entityName, string $actionName, ?string $profileName): string {
if (NULL === $profileName) {
return $entityName . '.' . $actionName;
Expand Down Expand Up @@ -79,6 +83,15 @@ public function process(ContainerBuilder $container): void {
}
}

foreach ($handlerServices as $handlerKey => $handlerService) {
$this->decorateService(
$container,
$handlerService->__toString(),
RemoteActionsHandlerTransactionDecorator::class,
$handlerKey
);
}

$container->getDefinition(ActionHandlerProvider::class)
->addArgument(ServiceLocatorTagPass::register($container, $handlerServices));
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php
declare(strict_types = 1);

namespace Civi\RemoteTools\DependencyInjection\Compiler\Traits;

use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Reference;

trait DecorateServiceTrait {

/**
* @phpstan-param class-string $decoratorClass
* @param int|string $argumentKey
*/
protected function decorateService(
ContainerBuilder $container,
string $id,
string $decoratorClass,
string $serviceIdPostfix,
$argumentKey = 0
): void {
$decoratorId = $decoratorClass . ':' . $serviceIdPostfix;
$container->autowire($decoratorId, $decoratorClass)
->setDecoratedService($id)
->setArgument($argumentKey, new Reference($decoratorId . '.inner'));
}

}
3 changes: 3 additions & 0 deletions services/remote-tools.php
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
use Civi\RemoteTools\Contact\RemoteContactIdResolverInterface;
use Civi\RemoteTools\Contact\RemoteContactIdResolverProvider;
use Civi\RemoteTools\Contact\RemoteContactIdResolverProviderInterface;
use Civi\RemoteTools\Database\TransactionFactory;
use Civi\RemoteTools\DependencyInjection\Compiler\ActionHandlerPass;
use Civi\RemoteTools\DependencyInjection\Compiler\RemoteEntityProfilePass;
use Civi\RemoteTools\EntityProfile\Helper\ProfileEntityDeleter;
Expand All @@ -58,6 +59,8 @@
$container->autowire(RequestContextInterface::class, RequestContext::class)
->setPublic(TRUE);

$container->autowire(TransactionFactory::class);

$container->autowire(ActionHandlerProviderInterface::class, ActionHandlerProviderCollection::class)
->addArgument(new TaggedIteratorArgument(ActionHandlerProviderInterface::SERVICE_TAG));

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
<?php
declare(strict_types = 1);

namespace Civi\RemoteTools\ActionHandler;

use Civi\RemoteTools\Api4\Action\RemoteDeleteAction;
use Civi\RemoteTools\Api4\Action\RemoteSubmitCreateFormAction;
use Civi\RemoteTools\Api4\Action\RemoteSubmitUpdateFormAction;
use Civi\RemoteTools\Database\TransactionFactory;
use Civi\RemoteTools\PHPUnit\Traits\CreateMockTrait;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;

/**
* @covers \Civi\RemoteTools\ActionHandler\RemoteActionsHandlerTransactionDecorator
*/
final class RemoteActionsHandlerTransactionDecoratorTest extends TestCase {

use CreateMockTrait;

private RemoteActionsHandlerTransactionDecorator $decorator;

/**
* @var \Civi\RemoteTools\ActionHandler\RemoteEntityActionsHandlerInterface&\PHPUnit\Framework\MockObject\MockObject
*/
private MockObject $handlerMock;

/**
* @var \Civi\RemoteTools\Database\TransactionFactory&\PHPUnit\Framework\MockObject\MockObject
*/
private MockObject $transactionFactoryMock;

protected function setUp(): void {
parent::setUp();
$this->handlerMock = $this->createMock(RemoteEntityActionsHandlerInterface::class);
$this->transactionFactoryMock = $this->createMock(TransactionFactory::class);
$this->decorator = new RemoteActionsHandlerTransactionDecorator(
$this->handlerMock,
$this->transactionFactoryMock
);
}

public function testDelete(): void {
$action = $this->createApi4ActionMock(RemoteDeleteAction::class, 'RemoteEntity', 'delete');

$transactionMock = $this->createMock(\CRM_Core_Transaction::class);
$this->transactionFactoryMock->method('createTransaction')
->willReturn($transactionMock);

$transactionMock->expects(static::once())->method('commit');
$this->handlerMock->method('delete')
->with($action)
->willReturn(['foo' => 'bar']);

static::assertSame(['foo' => 'bar'], $this->decorator->delete($action));
}

public function testDeleteRollback(): void {
$action = $this->createApi4ActionMock(RemoteDeleteAction::class, 'RemoteEntity', 'delete');

$transactionMock = $this->createMock(\CRM_Core_Transaction::class);
$this->transactionFactoryMock->method('createTransaction')
->willReturn($transactionMock);

$exception = new \Exception('test');
$transactionMock->expects(static::once())->method('rollback');
$transactionMock->expects(static::once())->method('commit');
$this->handlerMock->method('delete')
->with($action)
->willThrowException($exception);

static::expectExceptionObject($exception);
$this->decorator->delete($action);
}

public function testSubmitCreateForm(): void {
$action = $this->createApi4ActionMock(RemoteSubmitCreateFormAction::class, 'RemoteEntity', 'submitCreateForm');

$transactionMock = $this->createMock(\CRM_Core_Transaction::class);
$this->transactionFactoryMock->method('createTransaction')
->willReturn($transactionMock);

$transactionMock->expects(static::once())->method('commit');
$this->handlerMock->method('submitCreateForm')
->with($action)
->willReturn(['foo' => 'bar']);

static::assertSame(['foo' => 'bar'], $this->decorator->submitCreateForm($action));
}

public function testSubmitCreateFormRollback(): void {
$action = $this->createApi4ActionMock(RemoteSubmitCreateFormAction::class, 'RemoteEntity', 'submitCreateForm');

$transactionMock = $this->createMock(\CRM_Core_Transaction::class);
$this->transactionFactoryMock->method('createTransaction')
->willReturn($transactionMock);

$exception = new \Exception('test');
$transactionMock->expects(static::once())->method('rollback');
$transactionMock->expects(static::once())->method('commit');
$this->handlerMock->method('submitCreateForm')
->with($action)
->willThrowException($exception);

static::expectExceptionObject($exception);
$this->decorator->submitCreateForm($action);
}

public function testSubmitUpdateForm(): void {
$action = $this->createApi4ActionMock(RemoteSubmitUpdateFormAction::class, 'RemoteEntity', 'submitUpdateForm');

$transactionMock = $this->createMock(\CRM_Core_Transaction::class);
$this->transactionFactoryMock->method('createTransaction')
->willReturn($transactionMock);

$transactionMock->expects(static::once())->method('commit');
$this->handlerMock->method('submitUpdateForm')
->with($action)
->willReturn(['foo' => 'bar']);

static::assertSame(['foo' => 'bar'], $this->decorator->submitUpdateForm($action));
}

public function testSubmitUpdateFormRollback(): void {
$action = $this->createApi4ActionMock(RemoteSubmitUpdateFormAction::class, 'RemoteEntity', 'submitUpdateForm');

$transactionMock = $this->createMock(\CRM_Core_Transaction::class);
$this->transactionFactoryMock->method('createTransaction')
->willReturn($transactionMock);

$exception = new \Exception('test');
$transactionMock->expects(static::once())->method('rollback');
$transactionMock->expects(static::once())->method('commit');
$this->handlerMock->method('submitUpdateForm')
->with($action)
->willThrowException($exception);

static::expectExceptionObject($exception);
$this->decorator->submitUpdateForm($action);
}

}

0 comments on commit d0df501

Please sign in to comment.