Skip to content

Instantly share code, notes, and snippets.

@makasim
Last active March 14, 2023 16:34
Show Gist options
  • Save makasim/5670217 to your computer and use it in GitHub Desktop.
Save makasim/5670217 to your computer and use it in GitHub Desktop.
Payum details

The architecture

Note: The code snippets presented below are only for demonstration purposes (a pseudo code). Its goal is to illustrate general approach to various tasks. To see real live examples follow the links provided when appropriate.

Note: If you'd like to see real world examples we have a sandbox: online, code.

In general, you have to create a request and have to have an action which knows what to do with such request. A payment contains sets of actions and can execute a request. So, payment is the place where a request and an action meet each other.

<?php
$payment = new Payment;
$payment->addAction(new CaptureAction));

//CaptureAction does its job.
$payment->execute($capture = new CaptureRequest(array(
    'amount' => 100,
    'currency' => 'USD'
));

var_export($capture->getModel());
<?php
class CaptureAction implements ActionInterface
{
    public function execute($request)
    {
       $model = $request->getModel();
       
       //capture payment logic here
       
       $model['status'] = 'success';
       $model['transaction_id'] = 'an_id';
    }
    
    public function supports($request)
    {
        return $request instanceof CaptureRequest;
    }
}

Here's a real world example.

That's a big picture. Now let's talk about the details:

  • An action does not want to do all the job alone, so it delegates some responsibilities to other actions. In order to achieve this the action must be a payment aware action. Only then, it can create a sub request and pass it to the payment.

    <?php
    class FooAction extends PaymentAwareAction
    {
        public function execute($request)
        {
            //do its jobs
    
            // delegate some job to bar action.
            $this->payment->execute(new BarRequest);
        }
    }

    See paypal capture action.

  • What about redirects? Some payments like paypal requires authorization on their side. The payum can handle such cases and for that we use something called interactive requests. It is a special request object, which extends an exception. You can throw interactive redirect request at any time and catch it at a top level.

    <?php
    class FooAction implements ActionInterface
    {
        public function execute($request)
        {
            throw new RedirectUrlInteractiveRequest('http://example.com/auth');
        }
    }
    <?php
    try {
        $payment->addAction(new FooAction);
    
        $payment->execute(new FooRequest);
    } catch (RedirectUrlInteractiveRequest $redirectUrlInteractiveRequest) {
        header( 'Location: '.$redirectUrlInteractiveRequest->getUrl());
        exit;
    }

    See paypal authorize token action.

  • Good status handling is very important. Statuses must not be hard coded and should be easy to reuse, hence we use an interface to hanle this. Status request is provided by default by our library, however you are free to use your own and you can do so by implementing status interface.

    <?php
    class FooAction implements ActionInterface
    {
        public function execute($request)
        {
            if ('success condition') {
               $request->markSuccess();
            } else if ('pending condition') {
               $request->markPending();
            } else {
               $request->markUnknown();
            }
        }
    
        public function supports($request)
        {
            return $request instanceof StatusRequestInterface;
        }
    }
    <?php
    
    $payment->addAction(new FooAction);
    
    $payment->execute($status = new BinaryMaskStatusRequest);
    
    $status->isSuccess();
    $status->isPending();
    
    // or
    
    $status->getStatus();

    The status logic could be a bit complicated or pretty simple.

  • There must be a way to extend the payment with custom logic. Extension to the rescue. Let's look at the example below. Imagine you want to check permissions before user can capture the payment:

    <?php
    class PermissionExtension implements ExtensionInterface
    {
        public function onPreExecute($request)
        {
            if (false == in_array('ROLE_CUSTOMER', $request->getModel()->getRoles())) {
                throw new Exception('The user does not have required roles.');
            }
    
            // congrats, user have enough rights.
        }
    }

    <?php $payment->addExtension(new PermissionExtension);

// here the place the exception may be thrown.
$payment->execute(new FooRequest);
```

The [storage extension][storage-extension-interface] may be a good extension example.
  • Before you are redirected to a gateway side, you may want to store data somewhere, right? We take care of that too. This is handled by storage and its storage extension for payment. The extension can solve two tasks. First it can save a model after the request is processed. Second, it could find a model by its id before request is processed. Currently Doctrine and filesystem (use it for tests only!) storages are supported.

    <?php
    $storage = new FooStorage;
    
    $payment = new Payment;
    $payment->addExtension(new StorageExtension($storage));
  • The payment API can have different versions? No problem! A payment may contain a set of APIs. When API aware action is added to a payment it tries to set an API one by one to the action until the action accepts one.

    <?php
    class FooAction implements ActionInterface, ApiAwareInterface
    {
        public function setApi($api)
        {
            if (false == $api instanceof FirstApi) {
                throw new UnsupportedApiException('Not supported.');
            }
        
            $this->api = $api;
        }
    }
    
    class BarAction implements ActionInterface, ApiAwareInterface
    {
        public function setApi($api)
        {
            if (false == $api instanceof SecondApi) {
                throw new UnsupportedApiException('Not supported.');
            }
        
            $this->api = $api;
        }
    }
    <?php
    $payment = new Payment;
    $payment->addApi(new FirstApi);
    $payment->addApi(new SecondApi);
    
    //here the ApiVersionOne will be injected to FooAction
    $payment->addAction(new FooAction);
    
    //here the ApiVersionTwo will be injected to BarAction
    $payment->addAction(new BarAction);

    See authorize.net capture action.

As a result of such architecture we have decoupled, easy to extend and reusable library. For example, you can add your domain specific actions or a logger extension. Thanks to its flexibility any task could be achieved.

The bundle architecture

Note: There is a doc on how to setup and use payum bundle with supported payment gateways.

The bundle allows you easy configure payments, add storages, custom actions/extensions/apis. Nothing is hardcoded: all payments and storages are added via factories (payment factories, storage factories) in the bundle build method. You can add your payment this way too!

Also, it provides a nice secured capture controller. It's extremely reusable. Check the sandbox (code) for more details.

The bundle supports omnipay gateways (up to 25) out of the box. They could be configured the same way as native payments. The capture controller works here too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment