From bead2f46c7d64438929d8aa9e191730098eb5dd6 Mon Sep 17 00:00:00 2001 From: Gerd Katzenbeisser Date: Sat, 29 Feb 2020 15:33:15 +0100 Subject: [PATCH] Use events instead of model callbacks --- README.md | 46 +++++----- src/Model/Table/PayPalPaymentsTable.php | 25 ++++-- ...leTest.php => PayPalPaymentsTableTest.php} | 90 ++++++++++++++++--- 3 files changed, 116 insertions(+), 45 deletions(-) rename tests/TestCase/Model/Table/{PayPalPaymentTableTest.php => PayPalPaymentsTableTest.php} (71%) diff --git a/README.md b/README.md index 4ddf473..7cc0acb 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ CREATE TABLE IF NOT EXISTS `PayPalPayments` ( Configuration ------------- -You can find a sample configuration in Config/bootstrap.php. Just override the settings in your own bootstrap.php. +You can find a sample configuration in tests/config/PayPal.php. Just override the settings in your own bootstrap.php. Usage ----- @@ -85,32 +85,34 @@ class OrdersController extends AppController { } ``` -To receive the payment notifications in your app the Plugin needs 3 functions available in your AppModel.php +To receive the payment notifications in your app the Plugin expects 3 event handlers ```php - public function beforePayPalPaymentExecution($orderId) - { - // Will be called just after PayPal redirects the customer - // back to your site. (You could begin a transaction here) - // True is always expected as return value, otherwise the plugin - // will throw an exception - return true; - } - - public function cancelPayPalPaymentExecution($orderId) - { - // Will be called when the REST api call fails or - // the saleState != 'completed' or paymentState != 'approved' - // (You could rollback a transaction here) - } + $eventManager = TableRegistry::getTableLocator()->get('PayPal.PayPalPayments')->getEventManager(); + $eventManager->setEventList(new EventList()); - public function afterPayPalPaymentExecution($orderId) + // Will be called just after PayPal redirects the customer + // back to your site. (You could start a transaction here) + $eventManager->on('PayPal.Model.PayPalPayments.BeforePaymentExecution', + function($event, $remittanceIdentifier, &$handled) { - // Will be called after the REST api call - // and only if the saleState == 'completed' and paymentState == 'approved' - // (You could commit a transaction here) - } + // Handled is expected to be set to TRUE, otherwise the plugin + // will throw an exception + $handled = true; + }); + + // Will be called when the REST api call fails or + // the saleState != 'completed' or paymentState != 'approved' + // (You could rollback a transaction here) + $eventManager->on('PayPal.Model.PayPalPayments.CancelPaymentExecution', + function($event, $remittanceIdentifier) {}); + + // Will be called after the REST api call + // and only if the saleState == 'completed' and paymentState == 'approved' + // (You could commit a transaction here) + $eventManager->on('PayPal.Model.PayPalPayments.AfterPaymentExecution', + function($event, $remittanceIdentifier) {}); ``` diff --git a/src/Model/Table/PayPalPaymentsTable.php b/src/Model/Table/PayPalPaymentsTable.php index 5ca3520..67cf5f1 100644 --- a/src/Model/Table/PayPalPaymentsTable.php +++ b/src/Model/Table/PayPalPaymentsTable.php @@ -3,6 +3,7 @@ namespace PayPal\Model\Table; use Cake\Core\Configure; +use Cake\Event\Event; use Cake\ORM\Table; use Cake\Routing\Router; @@ -117,9 +118,14 @@ public function execute($id) $execution = new PaymentExecution(); $execution->setPayerId($_GET['PayerID']); - if (!$this->beforePayPalPaymentExecution($remittanceIdentifier)) + $handled = false; + $event = new Event('PayPal.Model.PayPalPayments.BeforePaymentExecution', + $this, ['RemittanceIdentifier' => $remittanceIdentifier, 'Handled' => &$handled] ); + $this->getEventManager()->dispatch($event); + if (!$handled) throw new PayPalCallbackException('beforePayPalPaymentExecution did not return true'); + $event = null; try { $ppRes = $ppReq->execute($execution); @@ -128,17 +134,18 @@ public function execute($id) $relatedResources = $this->getRelatedResources($transactions); $sale = $relatedResources->getSale(); $saleState = $sale->getState(); + if ($saleState == 'completed' && $paymentState == 'approved') + $event = new Event('PayPal.Model.PayPalPayments.AfterPaymentExecution', + $this, ['RemittanceIdentifier' => $remittanceIdentifier]); } - catch (\Exception $e) + finally { - $this->cancelPayPalPaymentExecution($remittanceIdentifier); - throw $e; - } + if ($event == null) + $event = new Event('PayPal.Model.PayPalPayments.CancelPaymentExecution', + $this, ['RemittanceIdentifier' => $remittanceIdentifier]); - if ($saleState == 'completed' && $paymentState == 'approved') - $this->afterPayPalPaymentExecution($remittanceIdentifier); - else - $this->cancelPayPalPaymentExecution($remittanceIdentifier); + $this->getEventManager()->dispatch($event); + } $this->savePayment($ppRes); } diff --git a/tests/TestCase/Model/Table/PayPalPaymentTableTest.php b/tests/TestCase/Model/Table/PayPalPaymentsTableTest.php similarity index 71% rename from tests/TestCase/Model/Table/PayPalPaymentTableTest.php rename to tests/TestCase/Model/Table/PayPalPaymentsTableTest.php index 243ab62..e7e73ab 100644 --- a/tests/TestCase/Model/Table/PayPalPaymentTableTest.php +++ b/tests/TestCase/Model/Table/PayPalPaymentsTableTest.php @@ -3,6 +3,7 @@ namespace PayPal\Test\TestCase\Model\Table; use Cake\Core\Configure; +use Cake\Event\EventList; use Cake\ORM\TableRegistry; use Cake\TestSuite\TestCase; @@ -14,7 +15,7 @@ /** * @var \PayPal\Model\Table\PayPalPaymentTable PayPalPayment */ -class PayPalPaymentTableTest extends TestCase +class PayPalPaymentsTableTest extends TestCase { public $fixtures = ['plugin.PayPal.PayPalPayments']; @@ -22,6 +23,7 @@ public function setUp() { parent::setUp(); $this->PayPalPayments = TableRegistry::getTableLocator()->get('PayPal.PayPalPayments'); + $this->PayPalPayments->getEventManager()->setEventList(new EventList()); } public function testGetApiContext() @@ -169,27 +171,27 @@ function() use ($p) { $p->setId('foo'); return $p; })); $this->assertTextContains('0/cancel', $redirectUrls->getCancelUrl()); } - public function testExecuteCompletedAndApproved() + private function prepareExecuteTest($saleState = 'completed', $paymentState = 'approved') { $model = $this->getMockForModel('PayPal.PayPalPayments', [ 'ApiGet', 'savePayment', 'getRelatedResources', - 'beforePayPalPaymentExecution', - 'afterPayPalPaymentExecution' ]); + $model->getEventManager()->setEventList(new EventList()); + $p = $this->getMockBuilder(Payment::class) ->setMethods(['execute']) ->getMock(); - $p->setState('approved'); + $p->setState($paymentState); $rr = new RelatedResources(); $s = new Sale(); $rr->setSale($s); - $s->setState('completed'); + $s->setState($saleState); $model->expects($this->once()) ->method('ApiGet') @@ -200,20 +202,80 @@ public function testExecuteCompletedAndApproved() ->method('getRelatedResources') ->willReturn($rr); - $model->expects($this->once()) - ->method('beforePayPalPaymentExecution') - ->with('ri') - ->willReturn(true); - - $model->expects($this->once()) - ->method('afterPayPalPaymentExecution') - ->with('ri'); + $model->getEventManager()->on('PayPal.Model.PayPalPayments.BeforePaymentExecution', + function($event, $remittanceIdentifier, &$handled) + { + $handled = true; + }); $p->expects($this->once()) ->method('execute') ->willReturn($p); + return $model; + } + + public function testExecuteCompletedAndApproved() + { + $model = $this->prepareExecuteTest(); + + $model->execute(1); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.BeforePaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.AfterPaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + } + + public function testExecuteCancelOnInvalidSaleState() + { + $model = $this->prepareExecuteTest('invalid'); + + $model->execute(1); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.BeforePaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.CancelPaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + } + + public function testExecuteCancelOnInvalidPaymentState() + { + $model = $this->prepareExecuteTest('completed', 'invalid'); + $model->execute(1); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.BeforePaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.CancelPaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + } + + public function testExecuteCancelOnException() + { + $model = $this->getMockForModel('PayPal.PayPalPayments', + [ + 'ApiGet' + ]); + + $model->getEventManager()->setEventList(new EventList()); + + $p = $this->getMockBuilder(Payment::class) + ->setMethods(['execute']) + ->getMock(); + + $p->expects($this->once()) + ->method('execute') + ->will($this->throwException(new \Exception('dummy'))); + + $model->expects($this->once()) + ->method('ApiGet') + ->with('PayPalId') + ->willReturn($p); + $model->getEventManager()->on('PayPal.Model.PayPalPayments.BeforePaymentExecution', + function($event, $remittanceIdentifier, &$handled) + { + $handled = true; + }); + + try { + $model->execute(1); + } catch (\Exception $th) { + $this->assertEquals('dummy', $th->getMessage()); + } + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.BeforePaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); + $this->assertEventFiredWith('PayPal.Model.PayPalPayments.CancelPaymentExecution', 'RemittanceIdentifier', 'ri', $model->getEventManager()); } public function testGetRelatedResources()