Shopware 6: Add property/attribute/feature to product and order by it

The mission: We are selling art and want to make it possible to order the products by creation year.

Shopware has an excellent documentation about how to add a new sorting to product listing. But unfortunately no one is telling us how to add a new feature/property/attribute/you name it to a product, so here is our solution (which most likely can me improved a lot -> feel free to comment!)

Extend the product

This is documented as well.

After this, we have a new database table which contains our creation year.

Edit the creation year on administration

We add it right to the labelling, but feel free to add it somewhere else.

Everything we do now is JavaScript, living in the following directory, for easier reading I’ll use paths relative to the src directory. I’ll post the files first and afterwards I’ll try to explain what is going on, where are references and why we did some things the way they are.

/custom/static-plugins/WinkelwagenManufactureYear/src/Resources/app/administration/src

We start with the template. It lives in:

extension/sw-product-settings-form/sw-product-settings-form.html.twig
{% block sw_product_settings_form_content %}
    {% parent %}
    <sw-container columns="repeat(auto-fit, minmax(250px, 1fr)" gap="0px 30px">
        <sw-text-field
            :label="$tc('ww.product.settingsForm.manufactureYear')"
            :placeholder="$tc('ww.product.settingsForm.placeholderManufactureYear')"
            @change="onManufacturerYearChanged($event, product, this)"
            :value="currentValue"
        ></sw-text-field>
    </sw-container>
{% endblock %}

And here is the main.js for this:

import template from './extension/sw-product-settings-form/sw-product-settings-form.html.twig';
import deDE from './snippet/de-DE';
import enGB from './snippet/en-GB';

Shopware.Component.override('sw-product-settings-form', {
    template,

    inject: ['repositoryFactory'],

    snippets: {
        'de-DE': deDE,
        'en-GB': enGB
    },

    data: function () {
        return {
            repository: undefined,
            currentValue: ''
        }
    },

    created() {
        this.repository = this.repositoryFactory.create('ww_manufacture_year');
        let productLoaded = setInterval(function () {
            if (!this.product.extensions) {
                return;
            }
            this.currentValue = this.product.extensions.manufactureYear.manufactureYear
                ? this.product.extensions.manufactureYear.manufactureYear
                : '';
            clearInterval(productLoaded);
        }.bind(this), 50);

    },

    methods: {
        onManufacturerYearChanged: function (value, product) {
            this.currentValue = value;
            const yearObj = product.extensions.manufactureYear ? product.extensions.manufactureYear : this.repository.create();
            yearObj.productId = product.id;
            yearObj.manufactureYear = value;
            product.extensions.manufactureYear = yearObj;
        }
    }
});

The most important thing to note: We did NOT implement our own module, but instead extended/overwrote sw-product-settings-form. I’m sure this is not ideal and there are other ways, but we are still learning and if it works, it works.

:label="$tc('ww.product.settingsForm.manufactureYear')" :placeholder="$tc('ww.product.settingsForm.placeholderManufactureYear')"

The label and the placeholder are just translated strings. Make sure to load the snippets it in your main.js. The json files live here:

src/Resources/app/administration/src/snippet/de-DE.json
src/Resources/app/administration/src/snippet/en-GB.json

The interesting part is the behaviour if you @change the value and the value itself:

:value="currentValue"
@change="onManufacturerYearChanged($event, product, this)"

The value is a property on the Component and is watched by VueJS. If we change it, it is automatically updated. Here comes the first problem. The value we want to read and write is this: this.product.extensions.manufactureYear.manufactureYear but unfortunately I couldn’t make it work, because of at least one reason: When the Component is loaded, product isn’t filled with data yet and therefore accessing this.product.extensions.manufactureYear.manufactureYear throws an error. So we fixed that by waiting until the product with its extension is loaded (which can be found in created()). We create an Interval which checks every 50ms wether product.extension exists already, if this is the case, we set currentValue, but only if the product has already a manufactureYear and stop the retrying.

let productLoaded = setInterval(function () {
    if (!this.product.extensions) {
        return;
    }
    this.currentValue = this.product.extensions.manufactureYear.manufactureYear
        ? this.product.extensions.manufactureYear.manufactureYear
        : '';
    clearInterval(productLoaded);
}.bind(this), 50);

The change event handler is so long because of the same reason. We had problems accessing product.extensions.manufactureYear in the beginning and updating it. But this works via our indirection well, too.

We set the currentValue, so the field is updated, we either create our extension object or update an existing one and set it on the product, so once the product is saved, our extension is saved as well.

methods: {
        onManufacturerYearChanged: function (value, product) {
            this.currentValue = value;
            const yearObj = product.extensions.manufactureYear ? product.extensions.manufactureYear : this.repository.create();
            yearObj.productId = product.id;
            yearObj.manufactureYear = value;
            product.extensions.manufactureYear = yearObj;
        }
    }

And that’s it, we now have a new field in the Shopware administration to update an extension.

We hope that helps you implementing your own and if you have any comments, feel free to leave them here!

Leave a Reply