Developers/Controller/Implement subscription processors

From Aimeos documentation

Other languages:
English 100%

When a customer has bought a product including a subscription interval, you can implement and configure any additional task you need when the subscriptions begins, is renewed or ends. The article below describes how to implement your own subscription processor.

Configure processors

These processors are included in the Aimeos core:

  • Cgroup (add/remove customer groups)

By default, no processor is used by the subscription job controllers.To add the Cgroup processor for example, you have to use this configuration option:

controller/common/subscription/process/processors = ['Cgroup']

This setting requires an array of processor names that should be executed. Please refer to the articles about how to use configuration settings for your framework or application.

The setting must be available when the job controller is executed. Thus, for TYPO3 it must be added to the TS-Config field of the scheduler task.

Adapt existing processors

If you want to modify an existing processor implementation, e.g the Cgroup implementation that adds/removes groups to/from the customer, use the same directory but a different file/class name. The file must be located in your own Aimeos extension:


That class must extend from the existing class and only contain the methods you want to modifiy. For the Cgroup example and a file named Mygroup.php this can look like:

  1. namespace Aimeos\Controller\Common\Subscription\Process\Processor\Cgroup;
  3. class Mygroup extends Standard
  4. 	implements \Aimeos\Controller\Common\Subscription\Process\Processor\Iface
  5. {
  6. 	public function begin( \Aimeos\MShop\Subscription\Item\Iface $subscription )
  7. 	{
  8. 		// Your modifed code from the original method
  9. 	}
  10. }

Afterwards, you have to configure your new Mygroup class so it's used instead of the Standard implementation. Use this configuration for the Cgroup example:

controller/common/subscription/process/processor/cgroup/name = Mygroup

The directory name Cgroup corresponds to the cgroup part of the configuration setting, i.e. the part in the configuration setting must be the lower case name of the directory part.

The setting must be available when the job controller is executed. Thus, for TYPO3 it must be added to the TS-Config field of the scheduler task.

Create a new processor

To create a new subscription processor, the file for the class must be located in your own Aimeos extension in that directory structure:


Please replace the "<type>" place holder with the name of the task your processor handles, e.g. "Ldap" if it connects to an LDAP server to manage authorization information. The example below uses the type Myproc, so the file location in your own Aimeos extension would be:


Your new processor class should be used by the subscription job controllers and the appropriate method should be executed depending on the stage of the subscription. For that, you have to make your class known to the job controllers by adding this configuration option:

controller/common/subscription/process/processors = ['Myproc']

The code below contains a skeleton for the new class of type Myproc:

  1. namespace Aimeos\Controller\Common\Subscription\Process\Processor\Myproc;
  3. class Myproc
  4. 	extends \Aimeos\Controller\Common\Subscription\Process\Processor\Base
  5. 	implements \Aimeos\Controller\Common\Subscription\Process\Processor\Iface
  6. {
  7. 	public function __construct( \Aimeos\MShop\Context\Item\Iface $context )
  8. 	{
  9. 		parent::__construct( $context );
  10. 		// more initialization code
  11. 	}
  13. 	public function begin( \Aimeos\MShop\Subscription\Item\Iface $subscription )
  14. 	{
  15. 		$context = $this->getContext();
  16. 		// Code that is executed at the beginning of the subscription
  17. 	}
  19. 	public function renew( \Aimeos\MShop\Subscription\Item\Iface $subscription, \Aimeos\MShop\Order\Item\Iface $order )
  20. 	{
  21. 		$context = $this->getContext();
  22. 		// Code that is executed each time the subscription is renewed
  23. 	}
  25. 	public function end( \Aimeos\MShop\Subscription\Item\Iface $subscription )
  26. 	{
  27. 		$context = $this->getContext();
  28. 		// Code that is executed at the end of the subscription
  29. 	}
  30. }

You can remove each method you don't want to implement because there are default implementations available in the base class in this case.

Each method receives the subscription item as first parameter. It contains the IDs of the original order and the subscription product. You can load the complete order using:

  1. $manager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
  2. $order = $manager->load( $subscription->getOrderBaseId() );

The renew() method also receives the new order item that is created by job controller. You can use it to load the newly created order too:

  1. $manager = \Aimeos\MShop\Factory::createManager( $context, 'order/base' );
  2. $order = $manager->load( $order->getBaseId() );

Unit tests

Testing processors is an important part of the implementation to ensure that they are working correctly. The implementation of the unit tests cases doesn't differ much from other unit tests and you can use this skeleton for your own tests:

  1. namespace Aimeos\Controller\Common\Subscription\Process\Processor\Cgroup;
  3. class StandardTest extends \PHPUnit\Framework\TestCase
  4. {
  5. 	protected function setUp()
  6. 	{
  7. 		\Aimeos\MShop\Factory::setCache( true );
  8. 	}
  10. 	protected function tearDown()
  11. 	{
  12. 		\Aimeos\MShop\Factory::setCache( false );
  13. 		\Aimeos\MShop\Factory::clear();
  14. 	}
  16. 	public function testBegin()
  17. 	{
  18. 		$context = \TestHelperCntl::getContext();
  19. 		$manager = \Aimeos\MShop\Factory::createManager( $context, 'subscription' );
  20. 		$object = new \Aimeos\Controller\Common\Subscription\Process\Processor\Myproc\Standard( $context );
  21. 		$object->begin( $manager->createItem() );
  22. 	}
  23.  }

If you already know unit tests the implementation is pretty straight forward. The only thing that is special are these lines:

  1. \Aimeos\MShop\Factory::setCache( true );
  2. \Aimeos\MShop\Factory::setCache( false );
  3. \Aimeos\MShop\Factory::clear();

When you use the \Aimeos\MShop\Factory::createManager() method to create manager objects, it caches objects and returns them if its asked for the same kind of object again. In unit tests, this may have undesired side effects and therefore, the lines above enable this caching only for the test cases of this unit test class and clears the object cache afterwards so the next unit test class starts in a defined state.

When testing subscription processor methods, it may be useful to test indirectly if the test case succeeded by testing what the method is doing internally, i.e. if object methods in the processor are called. This is a bit more advanced as you must create mock objects first and let the \Aimeos\MShop\Factory class return them instead of creating a new, real object:

  1. $customerStub = $this->getMockBuilder( '\\Aimeos\\MShop\\Customer\\Manager\\Standard' )
  2. 	->setConstructorArgs( [$context] )
  3. 	->setMethods( ['getItem', 'saveItem'] )
  4. 	->getMock();
  5. \Aimeos\MShop\Factory::injectManager( $context, 'customer', $customerStub );
  7. $customerStub->expects( $this->once() )->method( 'getItem' )
  8. 	->will( $this->returnValue( $customerStub->createItem() ) );
  9. $customerStub->expects( $this->once() )->method( 'saveItem' );

This creates a customer manager as mock object, injects it into the \Aimeos\MShop\Factory class and defines that the getItem() and saveItem() method has to be called at least once before the test is marked as successful.

More information about mocking object is available in the test doubles section of the PHPUnit site.