Extend an existing plugin via another plugin

Introduction

This document will describe how existing plugins can be extended.

Why change a plugin with a plugin? To keep the changes compatible if the plugins gets an update.

The target is to implement an own option type in SwagCustomProducts, which can be used in a template. This new option makes it possible for a customer to upload files with special mime types.

To achieve this, we decorate a service, extend ExtJs with Smarty blocks and implement new ExtJs files and Smarty templates.

On base of the new 5.2 plugin system, we extend the plugin SwagCustomProducts with the following functions.

  • Decoration of the FileTypeWhiteList.php to add other file extensions to the whitelist.
  • Adding a custom option type.

The file and directory structure

Directory and file overview

Create the plugin basics

Create the base structure of the plugin, including plugin.xml, services.xml and the SwagExtendCustomProducts.php.

For more information about the Shopware 5.2 plugin system click here

SwagExtendCustomProducts/plugin.xml

contains the base information about the plugin, the label in English and German, the plugin version, the required Shopware version and the changelog. It also makes sense to set the plugin SwagCustomProducts as requirement, as we want to extend it.

<?xml version="1.0" encoding="utf-8"?>
<plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/5.2/engine/Shopware/Components/Plugin/schema/plugin.xsd">

    <label lang="de">Erweiterung für CustomProducts (v2)</label>
    <label lang="en">Extension for CustomProducts (v2)</label>

    <version>1.0.0</version>
    <copyright>(c) by shopware AG</copyright>
    <license>proprietary</license>
    <link>http://store.shopware.com</link>
    <author>shopware AG</author>
    <compatibility minVersion="5.2.0"/>

    <changelog version="1.0.0">
        <changes lang="de">Erstveröffentlichung;</changes>
        <changes lang="en">First release;</changes>
    </changelog>

    <requiredPlugins>
        <requiredPlugin pluginName="SwagCustomProducts"/>
    </requiredPlugins>
</plugin>

SwagExtendCustomProducts/Resources/services.xml

defines in this case Subscribers which react to certain events. Make sure that Subscriber-Services implement the tag.

<tag name="shopware.event_subscriber" />
<?xml version="1.0" ?>

<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="swag_extend_custom_products.subscriber.backend" class="SwagExtendCustomProducts\Subscriber\Backend">
            <argument>%swag_extend_custom_products.plugin_dir%</argument>
            <tag name="shopware.event_subscriber"/>
        </service>

        <service id="swag_extend_custom_products.subscriber.frontend" class="SwagExtendCustomProducts\Subscriber\Frontend">
            <argument>%swag_extend_custom_products.plugin_dir%</argument>
            <tag name="shopware.event_subscriber"/>
        </service>

        <service id="swag_extend_custom_products.subscriber.type_factory" class="SwagExtendCustomProducts\Subscriber\TypeFactory">
            <tag name="shopware.event_subscriber"/>
        </service>

        <service id="swag_extend_custom_products.subscriber.file_type_decorator" class="SwagExtendCustomProducts\Subscriber\FileTypeDecorator">
            <argument type="service" id="service_container"/>
            <tag name="shopware.event_subscriber"/>
        </service>
    </services>
</container>

Decorate the WhiteListService

We decide to decorate the service by the event method because the plugin we want to expand is still based on the old plugin system. Thus, the service is not injected into the container yet.

To add new items into the white list we must decorate the service. That means, call the original service and create our own service. This contains the original service, implements the same interface and extends the functions. At the end set the service to the container instead of the original.

  • To decorate the WhiteListService of the Custom Product (v2) plugin we need the event name that invokes the Service. custom_products.file_upload.file_type_whitelist
  • In combination with the prefix Enlight_Bootstrap_AfterInitResource we create the full event name: Enlight_Bootstrap_AfterInitResource_custom_products.file_upload.file_type_whitelist

For more information about Shopware events read the Event guide

The Subscriber SwagExtendCustomProducts/Subscriber/FileTypeDecorator.php

<?php

namespace SwagExtendCustomProducts\Subscriber;

use Enlight\Event\SubscriberInterface;
use Shopware\Components\DependencyInjection\Container;
use ShopwarePlugins\SwagCustomProducts\Components\FileUpload\FileTypeWhitelistInterface;
use SwagExtendCustomProducts\Decorators\FileTypeWhiteListDecorator;

class FileTypeDecorator implements SubscriberInterface
{
    /**
     * @var Container
     */
    private $container;

    /**
     * @param Container $container
     */
    public function __construct(Container $container)
    {
        $this->container = $container;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'Enlight_Bootstrap_AfterInitResource_custom_products.file_upload.file_type_whitelist' => 'decorateFileTypeWhiteList',
        ];
    }

    public function decorateFileTypeWhiteList()
    {
        /** @var FileTypeWhitelistInterface $fileTypeWhiteList */
        $fileTypeWhiteList = $this->container->get('custom_products.file_upload.file_type_whitelist');

        $this->container->set(
            'custom_products.file_upload.file_type_whitelist',
            new FileTypeWhiteListDecorator($fileTypeWhiteList)
        );
    }
}

If Custom Products (v2) calls the event, set the decorator into the container to replace the original service.

SwagExtendCustomProducts/Decorators/FileTypeWhiteListDecorator.php adds an array of new mime types to the white list.

Now the customer in the frontend can upload the files with the new mime types.

<?php

namespace SwagExtendCustomProducts\Decorators;

use ShopwarePlugins\SwagCustomProducts\Components\FileUpload\FileTypeWhitelist;
use ShopwarePlugins\SwagCustomProducts\Components\FileUpload\FileTypeWhitelistInterface;
use ShopwarePlugins\SwagCustomProducts\Components\Types\Types\FileUploadType;

class FileTypeWhiteListDecorator implements FileTypeWhitelistInterface
{
    /**
     * @var FileTypeWhitelistInterface
     */
    private $fileTypeWhitelist;

    /**
     * Inject the original FileTypeWhiteListDecorator
     *
     * @param FileTypeWhitelistInterface $fileTypeWhitelist
     */
    public function __construct(FileTypeWhitelistInterface $fileTypeWhitelist)
    {
        $this->fileTypeWhitelist = $fileTypeWhitelist;
    }

    /**
     * {@inheritdoc}
     */
    public function getMimeTypeWhitelist($type)
    {
        if ($type === FileUploadType::TYPE) {
            return $this->getMimeTypeWhitelistForFiles();
        }

        return $this->fileTypeWhitelist->getMimeTypeWhitelist($type);
    }

    /**
     * {@inheritdoc}
     */
    public function getExtensionWhitelist($type)
    {
        return $this->fileTypeWhitelist->getExtensionWhitelist($type);
    }

    /**
     * {@inheritdoc}
     */
    public function getMediaOverrideType($extension)
    {
        return $this->fileTypeWhitelist->getMediaOverrideType($extension);
    }

    /**
     * Add new mimeTypes to whiteList
     *
     * @return array
     */
    private function getMimeTypeWhitelistForFiles()
    {
        $newMimeTypes = [
            'video/x-ms-asf',           // .asf
            'video/x-ms-asf',           // .asx
            'video/x-ms-wvx',           // .wvx
            'video/x-ms-wm',            // .wm
            'video/x-ms-wmx',           // .wmx
            'audio/x-ms-wma',           // .wma
            'audio/x-ms-wax',           // .wax
            'audio/x-ms-wmv',           // .wmv
            'application/x-ms-wmz',     // .wmz
            'application/x-ms-wmd',     // .wmd
        ];

        return array_merge(
            FileTypeWhitelist::$mimeTypeWhitelist['file'],
            $newMimeTypes
        );
    }
}

Create the new option type

For adding a new option type to Custom Products (v2) subscribe to SwagCustomProduct_Collect_Types event, which is fired in SwagCustomProducts/Components/Types/TypeFactory.php

$this->eventManager->collect('SwagCustomProduct_Collect_Types', $collection);

Create a new Subscriber.

SwagExtendCustomProducts/Subscriber/TypeFactory.php

<?php

namespace SwagExtendCustomProducts\Subscriber;

use Doctrine\Common\Collections\ArrayCollection;
use Enlight\Event\SubscriberInterface;
use SwagExtendCustomProducts\Components\Types\CustomType;

class TypeFactory implements SubscriberInterface
{
    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'SwagCustomProduct_Collect_Types' => 'onCollectTypes',
        ];
    }

    /**
     * Returns our new type(s) as ArrayCollection
     *
     * @return ArrayCollection
     */
    public function onCollectTypes()
    {
        return new ArrayCollection(
            [
                CustomType::TYPE => new CustomType(),
            ]
        );
    }
}

The new custom type must implement the interface ShopwarePlugins\SwagCustomProducts\Components\Types\TypeInterface.

Define the type of the "customType" with a string and control whether the class has values like the image selection or has no values like a text area with a true or false.

SwagExtendCustomProducts/Components/Types/CustomType.php

<?php

namespace SwagExtendCustomProducts\Components\Types;

use ShopwarePlugins\SwagCustomProducts\Components\Types\TypeInterface;

class CustomType implements TypeInterface
{
    const TYPE = 'customType';
    const COULD_CONTAIN_VALUES = false;

    /**
     * {@inheritdoc}
     */
    public function getType()
    {
        return self::TYPE;
    }

    /**
     * {@inheritdoc}
     */
    public function couldContainValues()
    {
        return self::COULD_CONTAIN_VALUES;
    }
}

After that, add the translation to the SwagCustomProducts/Views/backend/swag_custom_products/view/components/type_translator.js. Use the block "backend/swag_custom_products/components/typeTranslator/snippets" to add new Snippets.

More information about extending the backend.

snippets: {
    types: {
        //{block name="backend/swag_custom_products/components/typeTranslator/snippets"}{/block}
        checkbox: '{s name="combo/value/name/checkbox"}Checkbox{/s}',
        multiselect: '{s name="combo/value/name/multiselect"}Multiselect{/s}',
        numberfield: '{s name="combo/value/name/numberfield"}Numberfield{/s}',
        ...
        ...
        ...
        ...
    }
},

Create a new directory structure and the file SwagExtendCustomProducts/Resources/Views/backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js

It's necessary to call //{$smarty.block.parent} because other plugins can extend the same Smarty block.

//{block name="backend/swag_custom_products/components/typeTranslator/snippets"}
//{$smarty.block.parent}
    customType: 'My CustomType Name',
//{/block}

After that, use a subscriber to add the js file to the template view.

SwagExtendCustomProducts/Subscriber/Backend.php

To extend ExtJs with our files we need two cases:

  • Extend ExtJs with new functions an classes: use the index action.
  • Overwrite classes or smarty blocks: use the load action.
<?php

namespace SwagExtendCustomProducts\Subscriber;

use Enlight\Event\SubscriberInterface;

class Backend implements SubscriberInterface
{
    /**
     * @var string
     */
    private $path;

    /**
     * @param string $pluginPath
     */
    public function __construct($pluginPath)
    {
        $this->path = $pluginPath;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'Enlight_Controller_Action_PostDispatch_Backend_SwagCustomProducts' => 'extendBackendModule',
        ];
    }

    /**
     * @param \Enlight_Event_EventArgs $arguments
     */
    public function extendBackendModule(\Enlight_Event_EventArgs $arguments)
    {
        /** @var \Shopware_Controllers_Backend_SwagCustomProducts $subject */
        $subject = $arguments->get('subject');

        $view = $subject->View();

        $view->addTemplateDir($this->path . '/Resources/Views/');

        if ($arguments->get('request')->getActionName() === 'index') {
            $view->extendsTemplate('backend/swag_custom_products/view/option/types/custom_type.js');
        }

        if ($arguments->get('request')->getActionName() === 'load') {
            $view->extendsTemplate('backend/swag_extend_custom_products/swag_custom_products/view/components/type_translator.js');
        }
    }
}

Add the option type in ExtJs. Create SwagExtendCustomProducts/Resources/Views/backend/swag_custom_products/view/option/types/custom_type.js which extends "Shopware.apps.SwagCustomProducts.view.option.types.AbstractTypeContainer"

The AbstractTypeContainer is an abstract ExtJs class which defines functions and "template functions" you can use or overwrite.

//{block name="backend/swag_custom_products/view/option/types/customType"}
// Take the original Custom Product Type and only use "Custom Type" as suffix.
// Custom Products is building this path in "Shopware.apps.SwagCustomProducts.view.option.Detail"
Ext.define('Shopware.apps.SwagCustomProducts.view.option.types.CustomType', {
    extend: 'Shopware.apps.SwagCustomProducts.view.option.types.AbstractTypeContainer'
});
//{/block}

At last create a template for the frontend to display the new option type. SwagExtendCustomProducts/Resources/Views/frontend/swag_custom_products/options/customType.tpl

{block name="frontend_detail_swag_custom_products_options_customtype"}
    <input class="wizard--input" type="text" name="custom-option-id--{$option['id']}"
           id="custom-products-option-{$key}"
           data-field="true"
        {if $option['required']}
           data-validate="true"
           data-validate-message="{s name='detail/validate/textfield'}{/s}"
        {/if}/>
{/block}

Also create a subscriber to add the option template to the view.

SwagExtendCustomProducts/Subscriber/Frontend.php

<?php

namespace SwagExtendCustomProducts\Subscriber;

use Enlight\Event\SubscriberInterface;

class Frontend implements SubscriberInterface
{
    /**
     * @var string
     */
    private $path;

    /**
     * @param string $pluginPath
     */
    public function __construct($pluginPath)
    {
        $this->path = $pluginPath;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'Enlight_Controller_Action_PostDispatchSecure_Frontend_Detail' => 'extendFrontendDetail',
        ];
    }

    /**
     * @param \Enlight_Event_EventArgs $arguments
     */
    public function extendFrontendDetail(\Enlight_Event_EventArgs $arguments)
    {
        /** @var \Shopware_Controllers_Frontend_Detail $subject */
        $subject = $arguments->get('subject');

        $view = $subject->View();

        $view->addTemplateDir($this->path . '/Resources/Views/');
    }
}

Now we can use the plugin that extends the plugin Custom Products (v2)

  • We can upload files with a new mime type
  • We can use our own custom type to configure a product

The full Plugin is available for download here: Example plugin here.