Modifying Service Behavior

For advanced use cases, it might make sense to modify the behavior of Shopware ERP’s services. This is a technique which is used by Shopware WMS powered by Pickware and Shopware POS powered by Pickware to add their functionality onto Shopware ERP.

All services from the Shopware\Plugins\ViisonPickwareERP\Components namespace which are not marked as final can be extended using hooks or decoration. Nevertheless, extreme care should be taken when doing so, since bugs in these extensions can break existing system functionality and even cause stock inconsistencies and other data quality problems. We do not recommend this approach for developers who have limited experience writing Shopware plugins.

An Example Use-Case

Both Shopware ERP and Shopware WMS call StockChangeListFactoryService::createPickingStockChangesFromBinLocationMappings() with a prioritized list of bin location mappings, which returns a list of BinLocationStockChanges which specify the amount of stock to pick from each of these bin location mappings. A possible use case for modifying service behavior might be to change the order of the bin location mappings to implement a different picking strategy. To keep the focus on the logistics of modifying the service’s behavior, we will simply reverse the picking order instead of implementing a complex picking strategy ourselves.

Using Decoration

One approach to modify the behavior of services is to use the Decorator Pattern. A decorator implements the same interface as the decorated service and delegates to that service for any behavior it does not wish to implement itself. Here is how a decorator which reverses the picking order could look:

use Shopware\CustomModels\ViisonPickwareERP\Warehouse\BinLocation;
use Shopware\Plugins\ViisonPickwareERP\Components\StockLedger\StockChangeList\StockChangeListFactory;
use Shopware\Plugins\ViisonPickwareERP\Components\StockLedger\StockChangeList\StockChangeListFactoryServiceDecoration;

class ModifiedPickingOrderDecorator implements StockChangeListFactory
{
    use StockChangeListFactoryServiceDecoration;

    public function __construct(StockChangeListFactory $decoratedService)
    {
        $this->decoratedService = $decoratedService;
    }

    public function createPickingStockChangesFromBinLocationMappings(
        $quantity,
        array $binLocationMappings,
        BinLocation $fallbackBinLocation
    ) {
        return $this->decoratedService->createPickingStockChangesFromBinLocationMappings(
            $quantity,
            array_reverse($binLocationMappings),
            $fallbackBinLocation
        );
    }
}

Note that this decorator does not implement all methods of the StockChangeListFactory interface. This is possible because every decoratable service in Shopware ERP comes with a ServiceDecoration trait which provides delegating default implementations for all interface methods. This is convenient, but also makes the Decorator Pattern more robust. One of the problems with decoration is that adding a new method to a service interface is a breaking change towards all decorators implementing the interface. Since we do not consider adding new public methods to service interfaces breaking changes with regard to semantic versioning, not using the service decoration traits may cause your decorators to break as the result of updating to a new minor or even patch version. Because of this, we strongly suggest using these traits whenever you decorate a service of Shopware ERP.

To install your decorator, add a subscriber on the appropriate container event from your plugin’s Bootstrap:

$decoratedServiceName = 'pickware.erp.stock_change_list_factory_service';
$decorationEventName = sprintf('Enlight_Bootstrap_AfterInitResource_%s', $decoratedServiceName);
$this->get('events')->registerListener(
    new Enlight_Event_Handler_Default(
        $decorationEventName,
        function (Enlight_Event_EventArgs $eventArgs) use ($decoratedServiceName) {
            $decoratedService = $eventArgs->get('subject')->get($decoratedServiceName);
            $eventArgs->get('subject')->set(
                $decoratedServiceName,
                new ModifiedPickingOrderDecorator($decoratedService)
            );
        }
    )
);

See Shopware’s Developer Guide for an example of how to use decoration with the overhauled plugin system introduced in Shopware 5.2.

Using Hook Events

Decorators have another inherent problem in addition to newly-added public methods being breaking. Namely, if one public method of a service calls another public method on the same service, none of the decorators of that service will be called. The example shown in the previous section exhibits this exact problem: While the decorator will be executed when StockChangeListFactoryService::createPickingStockChangesFromBinLocationMappings() is called from Shopware WMS’ services, the picking order will be reversed as expected. However, when the same method is called transitively from StockChangeListFactoryService::createStockChangeList(), the decorator will not be called. This unexpectedly prevents the picking order from being reversed when an order is marked as shipped from the backend.

Hook events avoid this problem due to the way in which they are implemented. A hook event subscriber for reversing the picking order correctly in all cases could look like this:

use Enlight\\Event\SubscriberInterface;
use Enlight_Hook_HookArgs;
use Shopware\Plugins\ViisonPickwareERP\Components\StockLedger\StockChangeList\StockChangeListFactoryService;

class ModifiedPickingOrderHookSubscriber implements SubscriberInterface
{
    public static function getSubscribedEvents()
    {
        return [
            StockChangeListFactoryService::class . '::createPickingStockChangesFromBinLocationMappings::before' => 'onBeforeCreatePickingStockChangesFromBinLocationMappings',
        ];
    }

    public function onBeforeCreatePickingStockChangesFromBinLocationMappings(Enlight_Hook_HookArgs $hookArgs)
    {
        $binLocationMappings = $hookArgs->get('binLocationMappings');
        $hookArgs->set('binLocationMappings', array_reverse($binLocationMappings));
    }
}

In this case, a simple ::before hook event subscriber is sufficient to modify the picking order. Note that ::replace hooks have suitable and predictable semantics since Shopware 5.5. Because hook events can modify service behavior in all cases and are not as brittle as decorators with regards to changes in the decorated service, we strongly recommend their use instead of relying on decorators.