Eigene Artikelattribute/Freitextfelder auf eigenem Reiter im Backend - Anleitung

Hallo zusammen. Leider habe ich zu diesem Thema im Forum keine umfassende Antwort finden können. Ich habe mir Informationen aus ein paar Beiträgen zusammengetragen, und nach einigem Ausprobieren und Rumgefrickel habe ich es endlich geschafft. Da es für einige andere interessant sein dürfte, teile ich hier mal meine Ergebniss mit euch. Los geht’s. Da die komplette Anleitung zu lang für einen Beitrag ist, muss ich das aufsplitten.

Ordner und Dateien

Hier ist die Strukturübersicht. Das Plugin wird natürlich etwas mehr machen, als nur Attribute anzulegen und auf einem separaten Reiter im Backend darzustellen. Daher dürfen die Ordner „Components“, „Controllers“ und „Models“ ignoriert werden. Ich werde auch nicht auf die Uninstaller-Klasse eingehen.

custom/plugins/FdIcecat/FdIcecat.php

Die Plugin-Basisdatei. Installation und Deinstallation sind in eigene Klassen ausgelagert.

container->get('models'),
            $this->container->get('shopware_attribute.crud_service')
        );
        $installer->install();
    }

    /**
     * {@inheritdoc}
     * @throws \Exception
     */
    public function uninstall(UninstallContext $context)
    {
        $uninstaller = new Uninstaller(
            $this->container->get('models'),
            $this->container->get('shopware_attribute.crud_service'),
            $this->container->get('dbal_connection')
        );
        $uninstaller->uninstall($context->keepUserData());

        $context->scheduleClearCache(UninstallContext::CACHE_LIST_ALL);
    }
}

custom/plugins/FdIcecat/Setup/Installer.php

Die Installer-Klasse. Auf installModels() gehe ich nicht weiter ein, da die Funktion für diesen Thread nicht relevant ist. Der Installer legt vier neue Produktattribute an, die standardmäßig nicht im Backend angezeigt werden.

modelManager = $modelManager;
        $this->attributeCrudService = $crudService;
    }

    /**
     * @throws ToolsException
     * @throws Exception
     */
    public function install()
    {
        $this->installModels();
        $this->installAttributes();
    }

    /**
     * @throws ToolsException
     */
    private function installModels() {
        $tool = new SchemaTool($this->modelManager);
        $classMetaData = [
            $this->modelManager->getClassMetadata(Category::class),
            $this->modelManager->getClassMetadata(FeatureGroup::class),
            $this->modelManager->getClassMetadata(ProductFeature::class),
            $this->modelManager->getClassMetadata(ProductFeatureValue::class),
        ];

        $tool->createSchema($classMetaData);
    }

    /**
     * @throws Exception
     */
    private function installAttributes()
    {
        $this->attributeCrudService->update(
            's_articles_attributes',
            'fd_icecat_id',
            'integer',
            [
                'label' => 'Icecat product id',
                'position' => 20,
            ]
        );
        $this->attributeCrudService->update(
            's_articles_attributes',
            'fd_icecat_similar_id',
            'integer',
            [
                'label' => 'Similar Icecat product id',
                'position' => 20,
            ]
        );
        $this->attributeCrudService->update(
            's_articles_attributes',
            'fd_icecat_use_icecat_data',
            'boolean',
            [
                'label' => 'Use Icecat data',
                'helpText' => 'Activate this option to populate this article with data from Icecat',
                'position' => 20
            ],
            null,
            false,
            true
        );
        $this->attributeCrudService->update(
            's_articles_attributes',
            'fd_icecat_last_update',
            'datetime',
            [
                'label' => 'Last Icecat update',
                'position' => 20
            ]
        );
    }
}

Damit habe wir unsere neuen Attribute. Jetzt kümmern wir uns um das Backend.

custom/plugins/FdIcecat/Resources/views/backend/article_icecat/view/detail/window.js

//{block name="backend/article/view/detail/window" append}
Ext.define('Shopware.apps.ArticleIcecat.view.detail.Window', {
    override: 'Shopware.apps.Article.view.detail.Window',

    createMainTabPanel: function () {
        var me = this;

        me.callParent(arguments);
        me.registerAdditionalTab(
            {
                'title': 'Icecat',
                'contentFn': me.createIcecatTab,
                'articleChangeFn': me.refreshIcecatTab,
                'insertIndex': 1,
                'tabConfig': {
                    autoscroll: true,
                    bodyPadding: 10
                },
            },
            'Ext.form.Panel'
        );

        return me.mainTab;
    },
    createIcecatTab: function (article, stores, eOpts) {
        var me = this;

        me.icecatTab = Ext.create('widget.article-icecat');
        eOpts.tab.add(me.icecatTab);
        eOpts.tab.setDisabled(false);
        me.refreshIcecatTab();
    },
    refreshIcecatTab: function () {
        var me = this;

        if (me.article.get('mainDetailId')) {
            Ext.Ajax.request({
                url: '{url controller=AttributeData action=loadData}',
                params: {
                    _foreignKey: me.article.get('mainDetailId'),
                    _table: 's_articles_attributes'
                },
                success: function(responseData, request) {
                    var response = Ext.JSON.decode(responseData.responseText);
                    me.setIcecatFormValues(response.data);
                }
            });
        }
    },
    setIcecatFormValues: function (data) {
        var me = this;

        me.icecatTab.icecatId.setValue(data['__attribute_fd_icecat_id']);
        me.icecatTab.similarIcecatId.setValue(data['__attribute_fd_icecat_similar_id']);
        me.icecatTab.useDataField.setValue(data['__attribute_fd_icecat_use_icecat_data']);
        me.icecatTab.lastUpdate.setValue(data['__attribute_fd_icecat_last_update'])
    }
});
//{/block}

Es wird die Standarddetailansicht der Artikel überschrieben. createMainTabPanel ist für das Erstellen der Reiter zuständig. Zunächst wird die Originalfunktion aufgerufen, danach füge ich meinen eigenen Reiter über registerAdditionalTab hinzu. Mit ‚insertIndex‘: 1 lege ich fest, dass dieser direkt nach dem Stammdaten-Reiter eingefügt werden soll. Ferner benutze ich als Typ ein Formular-Panel („Ext.form.Panel“), weil ich damit später im Controller die Feldwerte dieses Reiters bequem per getValues() abrufen kann. createIcecatTab instanziiert den Icecat-Reiter („widget.article-icecat“), der im nächsten Schritt definiert wird, fügt ihn in die Reiterliste ein, aktiviert ihn und lädt dann die Werte aus der Datenbank über refreshIcecatTabrefreshIcecatTab ist fast 1:1 aus dem Developer Guide übernommen, prüft aber noch auf das Vorhandensein eines Artikels, weil es sonst Probleme bei der Neuanlage gibt. Dieselbe Funktion wird ausserdem als Callback für ‚articleChangeFn‘ genutzt. Damit funktioniert das ganze auch in der SplitView.

Teil 2

custom/plugins/FdIcecat/Resources/views/backend/article_icecat/view/detail/icecat.js

//{namespace name="backend/article_icecat/view/detail/icecat"}
//{block name="backend/article_icecat/view/detail/icecat"}
Ext.define('Shopware.apps.ArticleIcecat.view.detail.Icecat', {
    /**
     * Define that the icecat field set is an extension of the Ext.form.Fieldset
     * @string
     */
    extend: 'Ext.form.FieldSet',
    /**
     * The Ext.container.Container.layout for the fieldset's immediate child items.
     * @object
     */
    layout: 'column',
    /**
     * List of short aliases for class names. Most useful for defining xtypes for widgets.
     * @string
     */
    alias: 'widget.article-icecat',
    /**
     * Set css class for this component
     * @string
     */
    cls: Ext.baseCSSPrefix + 'article-icecat-field-set',

    /**
     * Contains all snippets for the view component
     * @object
     */
    snippets: {
        title: '{s name=icecat/title}Icecat{/s}',
        useicecat: '{s name=icecat/field/useIcecatData}Use Icecat data{/s}',
        icecatid: '{s name=icecat/field/icecatId}Icecat product{/s}',
        similaricecatid: '{s name=icecat/field/similarIcecatId}Similar Icecat product{/s}',
        lastupdated: '{s name=icecat/field/lastUpdated}Last update{/s}'
    },

    initComponent: function () {
        var me = this;

        me.title = me.snippets.title;
        me.items = me.createElements();
        me.callParent(arguments);
    },

    createElements: function () {
        var me = this;

        me.formElements = Ext.create('Ext.container.Container', {
            layout: 'column',
            items: [
                {
                    xtype: 'container',
                    columnWidth: 0.5,
                    items: [
                        {
                            xtype: 'form',
                            layout: 'anchor',
                            border: false,
                            padding: '0 20 0 0',
                            fieldDefaults: {
                                labelWidth: 155,
                                anchor: '100%'
                            },
                            items: me.createLeftElements()
                        }
                    ]
                },
                {
                    xtype: 'container',
                    columnWidth: 0.5,
                    items: [
                        {
                            xtype: 'form',
                            layout: 'anchor',
                            border: false,
                            fieldDefaults: {
                                labelWidth: 155,
                                anchor: '100%'
                            },
                            items: me.createRightElements()
                        }
                    ]
                }
            ]
        });

        return [me.formElements];
    },

    createLeftElements: function () {
        var me = this;

        me.useDataField = Ext.create('Ext.form.field.Checkbox', {
            name: '__attribute_fd_icecat_use_icecat_data',
            fieldLabel: me.snippets.useicecat,
            inputValue: 1,
            checked: true,
            uncheckedValue: 0
        });

        me.lastUpdate = Ext.create('Shopware.apps.Base.view.element.DateTime', {
            name: 'fdIcecatLastUpdate',
            fieldLabel: me.snippets.lastupdated,
            disabled: true,
            dateCfg: {
                submitFormat: 'Y-m-d'
            },
            timeCfg: {
                format: 'H:i:s'
            }
        });

        return [
            me.useDataField,
            me.lastUpdate
        ];
    },

    createRightElements: function () {
        var me = this;

        me.icecatId = Ext.create('Ext.form.field.Number', {
            name: 'fdIcecatProductId',
            fieldLabel: me.snippets.icecatid,
            disabled: true,
            allowBlank: true
        });
        me.similarIcecatId = Ext.create('Ext.form.field.Number', {
            name: '__attribute_fd_icecat_similar_id',
            fieldLabel: me.snippets.similaricecatid,
            allowBlank: true
        });

        return [
            me.icecatId,
            me.similarIcecatId
        ];
    }
});
//{/block}

Dies ist die Inhaltsdefinition des Icecat-Reiters. //{namespace name=„backend/article_icecat/view/detail/icecat“} ist notwendig, damit die Snippets, die in custom/plugins/FdIcecat/Resources/snippets/backend/article_icecat/view/detail/icecat.ini  definiert sind, gezogen werden. Ich habe hier eine 2-spaltige Darstellung verwendet. Die Namen der Felder sind bewusst gewählt, um das Zusammenstellen der Parameter zum Speichern zu vereinfachen. getValues() wird später so etwas zurückgeben:

{
    __attribute_fd_icecat_use_icecat_data: 1,
    fdIcecatLastUpdate: null,
    __attribute_fd_icecat_similar_id: "1337"
}

__attribute_fd_icecat_use_icecat_data und __attribute_fd_icecat_similar_id sind damit schon passend benannt und werden bei Übergabe an den Attributscontroller automatisch gespeichert. fdIcecatLastUpdate hingegen nicht und wird somit ignoriert. Das ist in diesem Fall gewünscht, da dieses Feld nicht vom Benutzer gepflegt werden soll. Ich musste aber feststellen, dass - obwohl es disabled ist - dieses Feld trotzdem von getValues() berücksichtigt wird. Eine Feldkonfiguration von submitValue: false hatte hier weder beim Feld selbst, noch als Parameter in dateCfg und timeCfg einen Effekt. Daher habe ich einfach einen Namen gewählt, der beim Speichern ignoriert wird. Da ich nicht geprüft habe, wie sich andere Felder hier verhalten, muss man eventuell etwas aufpassen bei solchen, die vom Benutzer nicht geändert werden dürfen.

custom/plugins/FdIcecat/Resources/views/backend/article_icecat/controller/detail.js

//{namespace name="backend/article_icecat/article/view/detail"}
//{block name="backend/article/controller/Icecat"}
Ext.define('Shopware.apps.ArticleIcecat.controller.Detail', {
    override: 'Shopware.apps.Article.controller.Detail',

    onSaveArticle: function (win, article, options) {
        var me = this,
            originalCallback = options.callback,
            customCallback = function (newArticle, success) {
                var params;

                Ext.callback(originalCallback, this, arguments);
                params = Ext.merge(
                    {
                        _foreignKey: newArticle.get('mainDetailId'),
                        _table: 's_articles_attributes',
                    },
                    me.getMainWindow()
                        .icecatTab
                        .up('form') // go up in the hierarchy to find the nearest parent form
                        .getForm()
                        .getValues()
                );

                Ext.Ajax.request({
                    method: 'POST',
                    url: '{url controller=AttributeData action=saveData}',
                    params: params
            });
        };

        if (!options.callback || options.callback.toString() !== customCallback.toString()) {
            options.callback = customCallback;
        }

        me.callParent([win, article, options]);
    },
});
//{/block}

Hier klinke ich mich analog zum Beispiel aus dem Developer Guide in das saveArticle-Event des Original-Controllers ein. Ich verschmelze die notwendigen Parameter _foreignKey und _table mit der Rückgabe von getValues(). Habe ich ja bereits oben erläutert.

custom/plugins/FdIcecat/Resources/views/backend/article_icecat/app.js

Hier erweitere ich einfach das Artikelmodul um meinen Controller und den Icecat-Reiter.

//{block name="backend/article/application" append}
    //{include file="backend/article_icecat/controller/detail.js"}
    //{include file="backend/article_icecat/view/detail/icecat.js"}
//{/block}

Teil 3

Jetzt muss das ganze noch im Backend eingebunden werden. Dazu lege ich mir eine Subscriber-Klasse an.

custom/plugins/FdIcecat/Subscriber/BackendArticle.php

Hier klinke ich mich in das Laden der Artikelmoduls ein und registriere zunächst meine Ressourcen per addTemplateDir_()_. Beim Laden des Moduls (index-Action) mache ich der App die neuen Komponenten in meiner app.js bekannt, beim Nachladen der Kompenten (load-Action) dann die Modifikation an der Detailansicht in meiner window.js.

pluginDirectory = $pluginDirectory;
    }

    /**
     * {@inheritdoc}
     */
    public static function getSubscribedEvents()
    {
        return [
            'Enlight_Controller_Action_PostDispatchSecure_Backend_Article' => 'onPostDispatchArticle'
        ];
    }

    /**
     * @param ActionEventArgs $args
     */
    public function onPostDispatchArticle(
        ActionEventArgs $args
    ) {
        /** @var \Enlight_Controller_Action $controller */
        $controller = $args->getSubject();
        $request = $controller->Request();
        $view = $controller->View();

        $view->addTemplateDir($this->pluginDirectory . '/Resources/views/');

        if ($request->getActionName() === 'index') {
            $view->extendsTemplate('backend/article_icecat/app.js');
        }

        if ($request->getActionName() === 'load') {
            $view->extendsTemplate('backend/article_icecat/view/detail/window.js');
        }
    }
}

Als letztes registriere ich nun noch den Subscriber.

custom/plugins/FdIcecat/Resources/services.xml

        ⋮
        
            %fd_icecat.plugin_dir%

Damit ist das ganze erledigt. Der Vollständigkeit halber hier noch die Snippet-Dateien.

custom/plugins/FdIcecat/Resources/snippets/backend/attribute_columns.ini

[en_GB]
s_articles_attributes_fd_icecat_id_label = "Icecat product id"
s_articles_attributes_fd_icecat_similar_id_label = "Similar Icecat product id"
s_articles_attributes_fd_icecat_use_icecat_data_label = "Use Icecat data"
s_articles_attributes_fd_icecat_use_icecat_data_helpText = "Activate this option to populate this article with data from Icecat"
s_articles_attributes_fd_icecat_last_update_label = "Last Icecat update"

[de_DE]
s_articles_attributes_fd_icecat_id_label = "Icecat Produkt-ID"
s_articles_attributes_fd_icecat_similar_id_label = "Ähnliche Icecat Produkt-ID"
s_articles_attributes_fd_icecat_use_icecat_data_label = "Icecat-Daten benutzen"
s_articles_attributes_fd_icecat_use_icecat_data_helpText = "Aktivieren Sie diese Option, um Artikel mit Daten von Icecat zu befüllen"
s_articles_attributes_fd_icecat_last_update_label = "Letzte Icecat-Aktualisierung"

custom/plugins/FdIcecat/Resources/snippets/backend/article_icecat/view/detail/icecat.ini

[en_GB]
icecat/title = "Icecat"
icecat/field/useIcecatData = "Use Icecat data"
icecat/field/icecatId = "Icecat product"
icecat/field/similarIcecatId = "Similar Icecat product"
icecat/field/lastUpdated = "Last update"

[de_DE]
icecat/title = "Icecat"
icecat/field/useIcecatData = "Icecat-Daten benutzen"
icecat/field/icecatId = "Icecat-Produkt"
icecat/field/similarIcecatId = "Ähnliches Icecat-Produkt"
icecat/field/lastUpdated = "Letzte Aktualisierung"

Und das ist das Ergebnis:

Meine Attribute werden wie gewünscht geladen und gespeichert, und auch das Wechseln des Artikels in der SplitView aktualisiert die Daten auf dem Reiter.

Falls jemand weiß, wie ich das Einsammeln des Wertes von fdIcecatLastUpdate durch getValues() verhindern kann, bin ich für einen Hinweis dankbar. Ist zwar so kein Beinbruch, aber wie ich finde eben nicht ganz “sauber”.  Wink

3 „Gefällt mir“

Sehr schöne Anleitung!

Eine ganz kleine Sache ist mir beim überfliegen aufgefallen. In der Plugin Klasse wird ein Container Parameter gesetzt:

$container->setParameter('fd_icecat.plugin_dir', $this->getPath());

Das ist m. E. nicht notwendig, da dies ja auch in der Methode der Basis Klasse genau so geschieht, welche du mit “parent::build($container);” ja auch aufrufst. 

1 „Gefällt mir“

@hhmarco73 schrieb:

Sehr schöne Anleitung!

Eine ganz kleine Sache ist mir beim überfliegen aufgefallen. In der Plugin Klasse wird ein Container Parameter gesetzt:

$container->setParameter(‚fd_icecat.plugin_dir‘, $this->getPath());

Das ist m. E. nicht notwendig, da dies ja auch in der Methode der Basis Klasse genau so geschieht, welche du mit „parent::build($container);“ ja auch aufrufst. 

In der Tat. Ist mir vorhin auch aufgefallen. Daher ist die ganze build-Methode überflüssig. Habe den Code entsprechend angepasst. Danke!

Addendum:

Ich habe mir noch ein weiteres Attribut vom Typ multi_selection angelegt. Da ich damit auch ein bisschen kämpfen musste, bis es funktionierte, ergänze ich hier die notwendigen Schritte. Dazu habe ich mir angeschaut, was Shopware.attribute.MultiSelectionFieldHandler macht und dies reproduziert.

Zunächst wird das Attribut in  custom/plugins/FdIcecat/Setup/Installer.php  in der install-Methode ergänzt:

        $this->attributeCrudService->update(
            's_articles_attributes',
            'fd_icecat_locked_options',
            'multi_selection',
            [
                'entity' => \Shopware\Models\Property\Option::class,
                'helpText' => 'Choose properties which should not be updated with Icecat data',
                'label' => 'Locked product properties',
                'position' => 20
            ],
            null,
            true
        );

In  custom/plugins/FdIcecat/Resources/views/backend/article_icecat/view/detail/icecat.js  ergänze ich das Feld in der createLeftElements-Methode folgendermaßen:

        store = Ext.create('Ext.data.Store', {
            model: 'Shopware.model.Dynamic',
            proxy: {
                type: 'ajax',
                url: '{url controller="EntitySearch" action="search"}?model=Shopware\\Models\\Property\\Option',
                reader: Ext.create('Shopware.model.DynamicReader')
            }
        });
        me.lockedOptions = Ext.create('Shopware.form.field.Grid', {
            name: '__attribute_fd_icecat_locked_options',
            fieldLabel: me.snippets.lockedOptions,
            store: store,
            searchStore: store
        });

Das ist die Nachbildung der Funktionalität des Multiselection-Fieldhandlers. me.lockedOptions kann dann einfach zum zurückgegebenen Array hinzugefügt werden. Auch hier wieder die passende Namensgebung für den Attribute-Controller. Damit ist das Ganze auch schon erledigt. Die Ergänzungen der Snippets spare ich mir an dieser Stelle. Das sollte eigentlich klar sein.

Und hier das Ergebnis:

 

3 „Gefällt mir“

Danke, dass du diese tolle Anleitung teilst!

Hallo,

vielen Dank für deine Arbeit.
Hat jemand eine Idee, wie das bei den Zahlungsarten funktionieren kann?
Ich habe da erfolgreich einen Reiter erstellt und die Werte meines Dropdowns werden mir auch angeziegt, leider funktioniert das speichern und Laden der Werte nicht.

Ich nehme an, dass ich mich in die “onChangeTab” und “onSavePayment” unter “/themes/Backend/ExtJs/backend/payment/controller/payment.js” reinhängen muss.
Vielleicht hat jemand einen Ansatz. Die Werte der Attribute werden geladen und sind in der console sichtbar, ich schaffe es leider aktuell nicht diese in meinem Reiter zu speichern und zu laden.

VG

Pascal