it-bens/shopware-code-based-plugin-configuration

Package to provide a code based and typed way to create Shopware configurations besides XML files.

v0.2.0 2024-07-27 23:40 UTC

This package is auto-updated.

Last update: 2024-10-28 00:16:54 UTC


README

Tests codecov

Shopware provides an easy way to create a plugin configuration without any implementing any frontend code: https://developer.shopware.com/docs/guides/plugins/plugins/plugin-fundamentals/add-plugin-configuration.html

While the underlying system for the internal configuration generation is very flexible, the config.xml file is the only way to leverage this system. This package aims to provide another method: code-based configuration generation.

Is this a Shopware Plugin?

No. While this package is tied to Shopware, it won't react on any events on it's own. It is meant to be used by a Shopware plugin.

How can add this package in a Shopware plugin?

First, the package has to be installed via composer:

composer require it-bens/shopware-code-based-plugin-configuration

This can be done in the plugin's composer.json file. But remember to enable composer in the plugin class.

public function executeComposerCommands(): bool
{
    return true;
}

This package provides two Symfony compiler passes. The ConfigurationCardConfigReaderPass will add all the services necessary for the configuration generation to the service container. It can be added to the plugin's build method.

use ITB\ShopwareCodeBasedPluginConfiguration\DependencyInjection\ConfigurationCardConfigReaderPass;

public function build(ContainerBuilder $container): void
{
    // ...
    parent::build($container);

    $container->addCompilerPass(new ConfigurationCardConfigReaderPass());
}

Why are the default configuration values missing?

While the mentioned compiler pass is sufficient to generate the plugin configuration at runtime, a convenient feature is missing: default values. The ConfigurationCardConfigSaverPass is required to "hack" into the Shopware plugin configuration persistence.

use ITB\ShopwareCodeBasedPluginConfiguration\DependencyInjection\ConfigurationCardConfigReaderPass;
use ITB\ShopwareCodeBasedPluginConfiguration\DependencyInjection\ConfigurationCardConfigSaverPass;

public function build(ContainerBuilder $container): void
{
    // ...
    parent::build($container);

    $container->addCompilerPass(new ConfigurationCardConfigReaderPass());
    $container->addCompilerPass(new ConfigurationCardConfigSaverPass());
}

For detailed information why this is necessary and why the usage is "hacky" look at How does the package work? - Shopware plugin configuration persistence.

How can I use this package?

Coding the configuration into cards

This package provides classes to define configuration cards (as seen in the Shopware plugin administration).

A ConfigurationCard has a title in english, a title in german and a list of ConfigurationInputField objects. The supported ConfigurationInputField implementations are:

  • BoolField
  • IntegerField
  • SingleSelectField
  • TextField

Some of these implementations differ in their required information. But because they mostly require the same information, all require a GeneralFieldInputInformation object. Here is an example for a configuration card with all types of input fields:

use ITB\ShopwareCodeBasedPluginConfiguration\ConfigurationCard;
use ITB\ShopwareCodeBasedPluginConfiguration\ConfigurationInputField\BoolField;
use ITB\ShopwareCodeBasedPluginConfiguration\ConfigurationInputField\IntegerField;
use ITB\ShopwareCodeBasedPluginConfiguration\ConfigurationInputField\SingleSelectField;
use ITB\ShopwareCodeBasedPluginConfiguration\ConfigurationInputField\SingleSelectFieldOption;
use ITB\ShopwareCodeBasedPluginConfiguration\ConfigurationInputField\TextField;
use ITB\ShopwareCodeBasedPluginConfiguration\GeneralFieldInputInformation;

$enabledFieldInformation = new GeneralFieldInputInformation(
    name: 'enabled', 
    labelInEnglish: 'Enabled', 
    labelInGerman: 'Aktiviert', 
    helpTextInEnglish: 'Enable or disable the feature', 
    helpTextInGerman: 'Aktiviere oder deaktiviere das Feature'
);
$enabledField = new BoolField(new GeneralFieldInformation(generalInformation: $enabledFieldInformation, defaultValue: false);

$integerFieldInformation = new GeneralFieldInputInformation(
    name: 'position', 
    labelInEnglish: 'Position', 
    labelInGerman: 'Position', 
    helpTextInEnglish: 'The position in the list', 
    helpTextInGerman: 'Die Position in der Liste'
);
$integerField = new IntegerField(new GeneralFieldInformation(generalInformation: $integerFieldInformation, defaultValue: 0);

$selectFieldInformation = new GeneralFieldInputInformation(
    name: 'color', 
    labelInEnglish: 'Color', 
    labelInGerman: 'Farbe', 
    helpTextInEnglish: 'The color of the item', 
    helpTextInGerman: 'Die Farbe des Elements'
);
$selectField = new SingleSelectField(
    new GeneralFieldInformation(generalInformation: $selectFieldInformation, defaultValue: 'red'),
    [
        new SingleSelectFieldOption(id: 'red', nameInEnglish: 'Red', nameInGerman: 'Rot'),
        new SingleSelectFieldOption(id: 'blue', nameInEnglish: 'Blue', nameInGerman: 'Blau'),
        new SingleSelectFieldOption(id: 'green', nameInEnglish: 'Green', nameInGerman: 'Grün'),
    ]
);

$textFieldInformation = new GeneralFieldInputInformation(
    name: 'description', 
    labelInEnglish: 'Description', 
    labelInGerman: 'Beschreibung', 
    helpTextInEnglish: 'A description of the item', 
    helpTextInGerman: 'Eine Beschreibung des Elements'
);
$textField = new TextField(new GeneralFieldInformation(generalInformation: $textFieldInformation, defaultValue: '');

$configurationCard = new ConfigurationCard(
    titleInEnglish: 'General Settings',
    titleInGerman: 'Allgemeine Einstellungen',
    inputFields: [$enabledField, $integerField, $selectField, $textField]
);

Injecting the cards into the configuration

The package uses services that implement the ConfigurationCardProvider interface. You can create as many implementations in service instances as you like. Because Shopware uses the same loading mechanism for all plugins, the scope of the ConfigurationCardProvider has to be declared. This is done in the getBundleClasses method of the plugin class.

/**
 * @return class-string<Bundle>[]
 */
public function getBundleClasses(): array {
    return [
        YourPluginClass::class
    ];
}

The getPriority method is used to sort the providers if more than one provider is registered for the same plugin.

public function getPriority(): int {
    return 100;
}

And finally the getConfigurationCards method is used to return the configuration cards.

/**
 * @return ConfigurationCard[]
 */
public function getConfigurationCards(): array {
    return $this->configurationCards;
}

How does the package work?

The package decorates the Shopware\Core\System\SystemConfig\Util\ConfigReader service from Shopware. This service is used to read the config.xml file of a plugin. It decodes the XML file into an array. This package keeps the XML-based configuration of plugin and extends it with the data from matching configuration card providers.

The decoration is done via compiler pass in three steps:

  1. all services that implement the ConfigurationCardProvider interface are collected and tagged.
  2. a ConfigurationCardProviderProvider definition is created that receives all services from (1).
  3. the ConfigurationCardConfigReader definition is created with decoration of the Shopware\Core\System\SystemConfig\Util\ConfigReader service and the ConfigurationCardProviderProvider is injected into the ConfigurationCardConfigReader.
  4. the ConfigurationCardConfigReader is set as an alias for the Shopware\Core\System\SystemConfig\Util\ConfigReader service to replace it.

Shopware plugin configuration persistence

Shopware saves plugin configurations in it's database as key-value pairs. When a plugin is installed, Shopware reads the configuration with the Shopware\Core\System\SystemConfig\Util\ConfigReader and persists the default values. However, the services of a plugin are added to the DI container during the plugin activation and not the installation. This means that the service decoration that allows the code-based configuration generation is not used during the installation. Only the default values from the config.xml file are persisted as a result.

This package provides a service subscriber that subscribes to the Shopware PluginPostActivateEvent and the PluginPostUpdateEvent. The service requires the plugin/bundle instance to perform the configuration persistence. The subscriber checks if the configuration persistence is necessary by using the plugin base class from the events the bundle classes defined in the registered ConfigurationCardProvider instances and fetches the plugin instance from the KernelPluginCollectionservice. The compiler pass takes care of the service definition, argument injection and the subscription tagging.

Shopware checks for already persisted configuration values and does not overwrite them. So multiple installations/deinstallations/activations/deactivations of a plugin that uses this package will not reset the configuration values unless the "remove all data" option is used.

The HTTP cache might be reset after the plugin activation. To avoid issues with the newly persisted configuration values, the event subscriber is registered with a priority of 100 to be executed as early as possible (at least earlier before other event subscribers with priority 0).

Contributing

I am really happy that the software developer community loves Open Source, like I do! ♥

That's why I appreciate every issue that is opened (preferably constructive) and every pull request that provides other or even better code to this package.

You are all breathtaking!