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
:ref:`StockChangeListFactoryService::createPickingStockChangesFromBinLocationMappings() <erp:Shopware\\Plugins\\ViisonPickwareERP\\Components\\StockLedger\\StockChangeList\\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:

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:

.. code-block:: php

    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:

.. code-block:: php

    $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 <https://developers.shopware.com/developers-guide/shopware-5-core-service-extensions/#decorating-an-existing-service>`_
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
:ref:`StockChangeListFactoryService::createPickingStockChangesFromBinLocationMappings() <erp:Shopware\\Plugins\\ViisonPickwareERP\\Components\\StockLedger\\StockChangeList\\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
:ref:`StockChangeListFactoryService::createStockChangeList() <erp:Shopware\\Plugins\\ViisonPickwareERP\\Components\\StockLedger\\StockChangeList\\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:

.. code-block:: php

    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 <https://github.com/shopware/shopware/pull/1501>`_ 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.
