Recent updates

Table of contents

Here you can find recent information about technical updates and news regarding shopware platform.

New: Our public admin component library for easy scaffolding of your admin modules

https://component-library.shopware.com

September 2019

2019-09-17: System config in plugin lifecycle methods

We've made the SystemConfigService public. This allows using the system config service in all plugin lifecycle methods. For example, one use case is setting default configuration values in the install method.

Example:

use \Shopware\Core\System\SystemConfig\SystemConfigService;

public function install(InstallContext $context)
{
    /** @var SystemConfigService $systemConfig */
    $systemConfig = $this->container->get(SystemConfigService::class);
    $domain = $this->getName() . '.config.';

    $currentValue = $systemConfig->get($domain . 'foobar');
    if ($currentValue === null) {
        $systemConfig->set($domain . 'foobar', 'myDefaultValue');
    }
}

2019-09-04: tool refactoring

The tooling, e.g. used for static analysis and code style fixing, has been refactored. The phar files are no longer part of the platform repository. Instead they are separated composer dependencies in development/dev-ops/analyze. To prevent cross dependencies between shopware and the different tools, we use a composer plugin, which symlinks the required binaries to the default composer vendor directory. But each tool has its own composer.json file, so there will be no conflict, if one tool uses this Symfony version and the other tool another Symfony version.

Changes

  • The psh command phpstan has been renamed to static-analyze
    • It no longer executes PHPStan only, but also Psalm
    • One could say Psalm does the same as PHPStan, but in fact Psalm recognizes things which PHPStan doesn't and vice versa
  • The platform/bin directory has been removed completely
    • The code for the pre-commit hook could now be found in the development/dev-ops directory
    • The tools are located in development/dev-ops/analyze
    • The config files for the different tools remain in the platform repository
  • If you have relied on the phar files in the platform/bin directory, e.g. in your plugin, use the binaries from development/dev-ops/analyze/vendor/bin now
    • Assuming you are in the development root directory:
    • Old: php vendor/shopware/platform/bin/php-cs-fixer.phar fix
    • New: php dev-ops/analyze/vendor/bin/php-cs-fixer fix
  • The pre-commit hook location has been changed, so you need to set it new, which should be done automatically with composer update

Additions

  • Psalm has been added as static analysis tool
  • PHPStan PHPUnit plugin has been added
    • If you check with assertNotNull you do not need to check again by yourself, if the variable is null for the further code
    • Mock support

Update path

  • git pull both development and platform
  • composer update should set the pre-commit
  • ./psh.phar init-composer initialises also the analysis tools

2019-09-02: Shopping Experiences - New data handling

The sw-cms module has been moved to the new data handling. To get an entity resolved in an element you now need to configure a configfield like this:

product: {
  source: 'static',
  value: null,
  required: true,
  entity: 
    { 
      name: 'product',
      criteria: criteria
    }
  }

in the cmsService.registerCmsElement 's defaultConfig. Where the criteria is a criteria instance with the required criterias for this entity (in this case const criteria = new Criteria(); criteria.addAssociation('cover');).

For each element you can define your custom collect and enrich method in the cmsService.registerCmsElement method to add custom logic if required. If none is defined the cmsService will add the default methods. These methods are required to resolve the entitydata in the new cmsDataResolverService.

The cmsDataResolverService fetches the data for each element with the new data handling. resolve is called from the sw-cms-detail page (in the loadData method). Here all collect methods from the elements are executed. After this, the optimizeCriteriaObjects seperates the required entites by criteria and checks if the can be merged. After this, alle entites are fetched, either by id (if no criteria is given) or by criteria. At last the resolved entities are distributed back to the elements and the enrich method of each element is called.

To get an example of a custom enrich method you can look up the module/sw-cms/elements/image-slider/index.js

Sorry for the inconvenience!

August 2019

2019-08-23: state machine refactoring

The state machine has been refactored. Previously dedicated routes for every state machine were needed. To simplify the handling with state machine and ensure that you can not change a state machine state without writing a history entry, a new field called StateMachineStateField has been added.

Usage: new StateMachineStateField('state_id', 'stateId', 'stateMachineName')

The OrderActionController, OrderDeliveryActionController and OrderTransactionActionController and their corresponding api services in the administration have been removed.

Instead a more generic StateMachineActionController has been added. You can now get the available transitions by using the route: GET /api/v1/_action/state-machine/{entityName}/{entityId}/state and change a state with: POST /api/v1/_action/state-machine/{entityName}/{entityId}/state/{transition?}. There is also an api service called state-machine.api.service.js in the administration.

2019-08-20: Breaking Change - Route Scopes added

We have added Scopes for Routes. The Scopes hold and resolve information of allowed paths and contexts. A RouteScope is mandatory for a Route. From now on every Route defined, needs a defined RouteScope.

RouteScopes are defined via Annotation:

/**
 * @RouteScope(scopes={"storefront"})
 * @Route("/account/login", name="frontend.account.login.page", methods={"GET"})
 */

 /**
  * @RouteScope(scopes={"storefront", "my_additional_scope"})
  * @Route("/account/login", name="frontend.account.login.page", methods={"GET"})
  */

RouteScope details

Current implemented RouteScopes are:

  • api - ApiRouteScope - Scope for API-Routes. Only allowed when AdminApiSource is set as SourceContext and basePath begins with "/api"
  • sales-channel-api - SalesChannelApiRouteScope - Scope for SalesChannelAPI-Routes. Only allowed when SalesChannelApiSource is set as SourceContext and basePath begins with "/sales-channel-api"
  • administration - AdministrationRouteScope - Scope for Administration-Routes. Only allowed when basePath begins with "/admin".
  • storeront - StorefrontRouteScope - Scope for Storefront-Routes. Only allowed when request is a qualified SalesChannelRequest.

When multiple scopes are defined for one Route, it is an OR conjunction - if one RouteScope matches the request, the Route will be allowed to process.

The RouteScopeInterface defines the public methods

  • isAllowedPath(string $path):bool to determine if a given route path is allowed.
  • isAllowed(Request $request): bool to determine if the given request is allowed for this route
  • getId(): string to return the id of the ReouteScope

    interface RouteScopeInterface
    {
    public function isAllowedPath(string $path): bool;
    
    public function isAllowed(Request $request): bool;
    
    public function getId(): string;
    }

The isAllowedPath-method has a base implementation in AbstractRouteScope to check if the actual path begins with an allowed path of the given scope.

The isAllowed-method has to be implemented for new RouteScopes.

RouteScopeListener

The RouteScopeListener is called on the event KernelEvents::CONTROLLER and checks if the scope is allowed. The Listener has a whitelist for Controller and/or complete namespaces to be ignored by this check. The namespace "Smyfony\" is already whitelisted to allow redirect- and exception-controllers to work as usual without a defined RouteScope.

Adding whitlisted controllers namespaces

Additional whitelisted controllers or namespaces can be added by subscribing to the event RouteScopeEvents::ROUTE_SCOPE_WHITELIST_COLLECT.

public static function getSubscribedEvents(): array
    {
        return [
            RouteScopeEvents::ROUTE_SCOPE_WHITELIST_COLLECT => [
                ['add'],
            ],
        ];
    }

    public function add(RouteScopeWhitlistCollectEvent $event): void
    {
        $whitelist = $event->getWhitelistedControllers();
        $whitelist[] = 'MyNameSpace\Controller\OpenController';
        $whitelist[] = 'MyOpenNameSpace\*';
        $event->setWhitelistedControllers($whitelist);
    }

An asterisk (*) at the end of an entry whitelists the complete Namespace while no asterisk will only whitelist the specific controller. Only use this whitlisting for controllers you need and cannot change. For your own controllers and Routes add a new RouteScope if required.

2019-08-19: Main language files changed and renamed

Until now, Shopware's main snippet files were the messages.<locale>.json files, which included all the storefront snippets. Because these files are located in the storefront, Shopware had an unintended dependency on it. So we renamed some files and made changes to the behaviour of:

  • Storefront snippet files messages.<locale>.json were renamed to storefront.<locale>.json
  • Core/document snippet files core.<locale>.json were renamed to messages.<locale>.json
  • Core snippet files' isBase now is true instead false, while storefront's isBase was changed vice versa

This rename was made necessary due to Symfony's need for a basic file (using the messages domain) when constructing catalogues.

2019-08-01: Best practice - Storefront jQuery & Bootstrap usage

We optimized the usage of jQuery & Bootstrap in Storefront plugins as well as themes. Libraries, helpers and the plugin system are now using the same instance across multiple entry points.

Technically speaking we added chunk splitting, moved all of our third party libraries to a separate chunk which will be shared over the multiple entry points (Storefront default entry point + plugins + themes). To share this chunk we had to generate a "runtime" chunk which contains the glue code for the sharing of the instance.

Do you need to explicitly import Bootstap & jQuery?

No, you don't have to but it's strongly suggested for enhanced auto completion support in IDEs, easier refactoring and cleaner code style.

How can I work with Bootstrap SCSS system?

The Bootstrap SCSS components / mixins / variables don't have to be imported explicitly. It's strongly suggested for the same reasons: Enhanced auto completion support in IDEs, easier refactoring and cleaner code style.

Can I access jQuery without using Webpack?

Yes, you can. We exposing the jQuery object to the global window object to enable legacy jQuery plugins as well as using it without working with Webpack.

July 2019

2019-07-10: Removed Defaults::LANGUAGE_SYSTEM_DE

We've removed the constant Defaults::LANGUAGE_SYSTEM_DE. Furthermore it's not guaranteed that Defaults::LANGUAGE_SYSTEM references the 'en-GB' language. (The same holds for Defaults::CURRENCY). The installer and migration assistant may change the underlying language/currency.

Example

If you need the id of the de-DE language, you have to search it by using the translationCode:


$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('language.translationCode.code', 'de-DE'));
$language = $languageRepository->search($criteria, $context)->first();
dump($language->getId());

2019-07-09: Updated Entity Extension Handling

We have updated the handling of Entity Extensions.

Breaking Changes

  1. It is no longer possible to add Fields, that need an DB column on the extended Entity, as Extension.

    This means that you are not allowed to add any scalar value fields, like StringField or FkField, as an Extension anymore. The only exception are fields flagged as Runtime() as these fields are not stored in the Database, but evaluated and set at runtime.

    The reason behind this decision is that you were forced to edit the schema of the tables you wanted to extend, which is very fragile and can lead to a number of problems.

  2. Extensions are an own Relationship in JSON:API responses.

    Previously the extensions were json serialized and put under the extension key in JSON:API responses. This wasn't quite compatible with the JSON:API spec as all relationships added as extension were also json serialized and added to every entity.

    To use the auto-discovery and deduplication mechanisms of the JSON:API spec we decided to add the extensions as an own Relationship, that can have references to other entities.

    Before:

    {
       "data": {
         "id": "1d23c1b015bf43fb97e89008cf42d6fe",
         "type": "product",
         "attributes": {
           "name": "My awesome Product",
           ...
           "extensions": {
             "seoUrls": [
               {
                 "id": "ffffffffffffffffff",
                 ...
               },
               {
                 "id": "1111111111111111",
                 ...
               }
             ]
           }
         },
         "relationships": {...}
       }
    }

    Now:

    {
       "data": {
         "id": "1d23c1b015bf43fb97e89008cf42d6fe",
         "type": "product",
         "attributes": {
           "name": "My awesome Product",
           ...
         },
         "relationships": {
           ...
           "extensions": {
             "data": {
               "type": "extension",
               "id": "1d23c1b015bf43fb97e89008cf42d6fe"
             }
           }
         }
       },
       "included": [
         {
           "id": "1d23c1b015bf43fb97e89008cf42d6fe",
           "type": "extension",
           "attributes": {},
           "relationships": {
             "seoUrls": {
               "data": [
                   {
                     "type": "seo_url",
                     "id": "ffffffffffffffffff"
                   },
                   {
                     "type": "seo_url",
                     "id": "1111111111111111"
                   }
               ]
             }
           }
         },
         {
           "id": "ffffffffffffffffff",
           "type": "seo_url",
           "attributes": {...},
           "relationships": {...}
         },
         {
           "id": "1111111111111111",
           "type": "seo_url",
           "attributes": {...},
           "relationships": {...}
         }
       ]
    }

Fixes

  1. You can now add nested Associations from your ToOne-Extensions to your criteria object.

    This previously lead to an error:

       $criteria->addAssociationPath('myToOneExtension.myNestedAssociation');
  2. The new data handling works out of the box with EntityExtensions

    Previously you got plain arrays or JS-Objects inside the administration if you accessed your extensions with e.g. this.product.extensions.seoUrls. This lead to problems with the data handling as the data handling expects either Entity-Objects or EntityCollection.

    With the previously described changes in the JSON:API responses the new data handling now automatically hydrates you extensions into Entity-Objects or EntityCollection. So now you would get an EntityCollection back when you access you extension with this.product.extensions.seoUrls and the data handling just works with your extensions.

2019-07-08: Error pointer returns write index

Shopware 6 uses batch operations for every action the DAL. Therefore, the paths in the exceptions are now prefixed with their write index of the batch operation. This also applies to the response in the API which introduces a major break for existing error parser.

Before

/name

After

/0/name

2019-07-05: UUID format change

Shopware 6 no longer accepts upper case letters or dashes in UUIDs. So the format is now a single string containing numbers 0 til 9 and the characters a til f.

The reason is that Shopware would formerly accept the dashed version and upper case letters (74d25156-60e6-444c-A177-A96E67ECfC5F) but strip the information without being able to reproduce them for the response format factually deleting information.

Valid: 74d2515660e6444ca177a96e67ecfc5f Invalid: 74D2515660E6444CA177A96E67ECFC5F Invalid: 74d25156-60e6-444c-a177-a96e67ecfc5f

2019-07-04: Storefront Thumbnail helper

Iterating over the thumbnails of a media object was exhausting and produced a lot of boilerplate code? This ends now! We added a new thumbnail helper which automatically creates a picture element including the right source tags.

The helper accepts a string which will act as the class attribute of the picture element. The second parameter is a configuration object which supports the following properties:

  • thumbnails - MediaThumbnailCollection
  • default - URL to the default image if no thumbnail matches the screen real estate of the user
  • alt - Text for the "alt" attribute
  • attributes - Additional attributes for the "picture" element

Example

{% sw_thumbnails "my-image-class" with {
    thumbnails: page.product.media.first.media.thumbnails,
    default: page.product.media.first.media.thumbnails.last.url,
    attributes: {
        'data-plugin-slider': true
    },
    alt: page.product.translated.name
} %}

2019-07-04: Mailhog

Besides that you can now reset your password in the administration and actually get an email, we've added Mailhog to the development stack and Shopware is pre-configured to use it.

You can open the web interface of mailhog on port 8002 at http://localhost:8002.

2019-07-03: Worker notification extensibility

We have refactored the handling of worker / message queue responses and added a middleware pattern to it. This enables third-party developers to react to messages in the queue easily.

Implementation

import { WorkerNotification } from 'src/core/shopware';

WorkerNotification.register('newsletterRecipientTask', {
    name: 'Shopware\\Core\\Content\\Newsletter\\ScheduledTask\\NewsletterRecipientTask',
    fn: (next, data) => {
        console.log(data);

        // do your stuff and call next then
        next();
    }
});

What is available in the middleware function?

{
    $root,          // Vue root instance
    entry,          // Found entry
    name,           // Class name to filter the queue
    notification: { // Helper methods for notifications
        create,
        update
    },
    queue,          // Entire queue
    response        // Queue status HTTP response
}

Full example

import { WorkerNotification } from 'src/core/shopware';

let notificationId = null;
WorkerNotification.register('generateThumbnailsMessage', {
    name: 'Shopware\\Core\\Content\\Media\\Message\\GenerateThumbnailsMessage',
    fn: function middleware(next, { entry, $root, notification }) {
        // Create notification config object
        const config = {
            title: $root.$t('global.notification-center.worker-listener.thumbnailGeneration.title'),
            message: $root.$tc(
                'global.notification-center.worker-listener.thumbnailGeneration.message',
                entry.size
            ),
            variant: 'info',
            metadata: {
                size: entry.size
            },
            growl: false,
            isLoading: true
        };

        // Create new notification
        if (entry.size && notificationId === null) {
            notification.create(config).then((uuid) => {
                notificationId = uuid;
            });
            next();
        }

        // Update existing notification
        if (notificationId !== null) {
            config.uuid = notificationId;

            if (entry.size === 0) {
                config.title = $root.$t(
                    'global.notification-center.worker-listener.thumbnailGeneration.titleSuccess'
                );
                config.message = $root.$t(
                    'global.notification-center.worker-listener.thumbnailGeneration.messageSuccess'
                );
                config.isLoading = false;
            }
            notification.update(config);
        }

        // do your stuff and call next then
        next();
    }
});

Best practice

It's best practice to create an initializer decorator to add new worker notification middleware function.

import { Application } from 'src/core/shopware';

Application.addInitializerDecorator('worker', (service) => {
    const factory = Application.getContainer('factory').workerNotification;

    factory.register('newsletterRecipientTask', {
        name: 'Shopware\\Core\\Content\\Newsletter\\ScheduledTask\\NewsletterRecipientTask',
        fn: (next, data) => {
            console.log(data);

            // do your stuff and call next then
            next();
        }
    });

    return service;
});

2019-07-02: Symfony Event names removed

Symfony as of version 4.3 allows to dispatch events without an event name. In this case the class name can be used to subscribe to events.

We removed the event names where it was possible in favor of using the class name for subscribing to events. We also removed the getName() method from the ShopwareEvent interface.

Especially the StorefrontPage events are affected here, as we removed the names from all of them. Furthermore the BusinessEvents aren't dispatched by their name anymore, but by the class, the name is only used to add Actions to the given BusinessEvents.

The generic DAL events that get dispatched when an Entity gets written or loaded are untouched from this changes. If you have custom NestedEvents that are generic and therefore need to be dispatched by name implement the new GenericEvent interface.

2019-07-02: Added ReadProtected Flag

We have added an ReadProtected Flag to the DAL.

You can mark fields with this flag, so that they won't be included in Api-Responses and can't be read from outside the system. The flag allows you to define for which Api the restrictions should apply.

This makes the Internal flag obsolete, so we removed it.

Examples

To protect read access to field for the SalesChannelApi use the flag like this:

(new StringField('test', 'test'))->addFlags(new ReadProtected(SalesChannelApiSource::class)),

The Example for protecting the read access for the AdminApi would look like this:

(new StringField('test', 'test'))->addFlags(new ReadProtected(AdminApiSource::class)),

Breaking change

We've removed the Internal flag, because it can be represented with the new ReadProtected Flag.

Before
(new StringField('test', 'test'))->addFlags(new Internal()),

After

(new StringField('test', 'test'))->addFlags(new ReadProtected(SalesChannelApiSource::class, AdminApiSource::class)),

June 2019

2019-06-28: Storefront Plugin system refactoring

We reorganized the directory structure of the JavaScript in the Storefront as well as added a new convenient method to override plugins. This is a breaking change!

The import path for the plugin manager, the plugin class as well as the plugin config manager has changed:

Breaking change

Before:

import Plugin from 'src/script/helper/plugin/plugin.class.js';
import PluginManager from 'src/script/helper/plugin/plugin.manager.js'
import PluginConfigManager from 'src/script/helper/plugin/plugin.config.manager.js'

After:

import Plugin from 'src/script/plugin-system/plugin.class.js';
import PluginManager from 'src/script/plugin-system/plugin.manager.js'
import PluginConfigManager from 'src/script/plugin-system/plugin.config.manager.js'

Overriding plugins

It was possible to override Storefront plugins using the PluginManager.extend() method using the following syntax:

PluginManager.extend('OffCanvasCart', 'OffCanvasCart', MyOffCanvasCartPlugin, '[data-offcanvas-cart]');

The new convenient method can be used as the following:

PluginManager.override('OffCanvasCart', MyOffCanvasCartPlugin, '[data-offcanvas-cart]');

2019-06-28: Administration: Add SCSS linting in administration

Main usage

The SCSS files in the Administration are linted by Stylelint for a consistent code style. It triggers automatically on a pre-commit for each edited SCSS file and shows errors.

The linter can also be started manually with PSH :

  • check changed files (git diff): ./psh.phar administration:lint-scss
  • check changed files and fix errors automatically in files (git diff): ./psh.phar administration:lint-scss-fix

It is also possible to lint every file in the Administration. These commands shouldn't be used on daily basis and therefore they are not included in the PSH commands. To run these commands you have to be in this folder platform/src/Storefront/Resources/ and start the NPM commands directly.

  • Lint all files: npm run lint:scss-all
  • Lint and fix all files npm run lint:scss-all:fix (Warning: This changes data)

PhpStorm Integration

You can show linting errors directly in PhpStorm. When you want to enable the live linting you have to open your Preferences and navigate to: Language & Frameworks -> Style Sheets -> Stylelint. Here you have to enable the linting and change the path to your Stylelint Package (YOUR_PATH/development/platform/src/Storefront/Resources/node_modules/stylelint). Now you can see directly the errors in your SCSS files.

2019-06-27: Action support for notifications and alerts

The notifications can now receive an additional actions array. This array contains very small objects which can be used to define a label and what should happen when clicking the action button.

Single action example:

{
    label: 'Button label',
    route: { name: 'sw.settings.index' }
}
  • $router.push() will get called when the action button was clicked.
  • By default the notification will be closed when the route change is run.
  • When no route is given the notification will close on click by default.
  • When actions are in use, a button bar with the defined actions will appear automatically inside the notification.

Full example for notification actions:

this.$store.dispatch('notification/createNotification', {
    title: 'Shopware update',
    message: 'Shopware 6.0-ea2 is now available. Do you want to update now?',
    variant: 'info',
    system: true,
    actions: [{
        label: 'Cancel'
    }, {
        label: 'Update now',
        route: { name: 'sw.settings.index' }
    }],
    autoClose: false
});

Manual mode for sw-alert

Notifications are essentially only sw-alert components which are composed together.

You can also use the actions on the alert directly by putting sw-button components inside the new actions slot. This also happens in the notifications base file as well.

Manual example:

<sw-alert variant="error" title="Error">
    An error occurred when trying to save the entity.
    <template #actions>
        <sw-button>Ignore</sw-button>
        <sw-button>View error log</sw-button>
        <sw-button>Try again</sw-button>
    </template>
</sw-alert>
  • All color variants and appearances of sw-alert are supported.
  • The buttons will get different styling automatically. Please do not use further variants/props like primary or size. This may break the appearance.

2019-06-27: Breaking change - Rename DateField to DateTimeField and add real DateField

Renaming: Shopware\Core\Framework\DataAbstractionLayer\Field\DateField => Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\DateFieldSerializer => Shopware\Core\Framework\DataAbstractionLayer\FieldSerializer\DateTimeFieldSerializer Shopware\Core\Defaults::STORAGE_DATE_FORMAT => Shopware\Core\Defaults::STORAGE_DATE_TIME_FORMAT

New:

Shopware\Core\Framework\DataAbstractionLayer\Field\DateField which only stores dates (without time)

2019-06-27: Add currecny twig modifier

We added a convenient method to format a price in the Storefront. Before we introduced the modifier it was quite inconvenient to format numbers using the built in filter:

{{ page.cart.price.positionPrice|currency(context.currency.translated.shortName, app.request.locale) }}{{ "general.star"|trans }}

Now the new filter automatically detects it's context, the currently used currency and locale. We haven't added the star snippet due to the fact you may don't want the symbol in the checkout process:

{{ page.cart.price.positionPrice|currency }}{{ "general.star"|trans }}

2019-06-25: Api error handling in administration

We reorganized the way we store errors that are received from the api.

We figured out that the binding between an api error and its input field's v-model expression is not practicable due to naming conflicts in collections. In addition, not every input field is bound to entity data or can/should receive an api error. That's why we removed the error-pointer property and the resetFormError method from fields and replaced it with an error property.

We also decoupled nested association errors to have a flatter store which looks like:

(state)
 |- entityNameA
    |- id1
        |- property1
        |- property2
        ...
    |- id2
        |- property1
        |- property2
        ...
 |- entityNameB
   ...         

Read errors from the store

Errors can be read from the store by calling the getter method getApiErrorFromPath.

function getApiErrorFromPath (state) => (entityName, id, path)

Where path is an array representing the nested property names of your entity. Also we provide a wrapper which can also handle nested fields in object notation

function getApiError(state) => (entity, field)

which is much easier to use for scalar fields. In your Vue component, use computed properties to not flood your templates with store calls.

<script>
    computed: {
        propertyError() {
            return this.$store.getters.getApiError(myEntity, 'myFieldName');
        }
        nestedpropertyError() {
            return this.$store.getters.getApiError(myEntity, 'myFieldName.nested');
        }
    }
</script>

<template>
    <sw-field ... :error="propertyError"></sw-field>
</template>

mapErrors Service

Like every Vuex mapping, fetching the errors from the store may be very repetitive and error-prone. Because of this we provide you an Vuex like mapper function

mapApiErrors(subject, properties)

where subject is the variable name (not the entity itself) and properties is an array of properties you want to map. You can spread its result to create computed properties in your component. The functions returned by the mapper are named like a camelcase representation of your input suffixed with Error.
This is an example from the sw-product-basic-form component:

<script>
    import { mapApiErrors } from 'src/app/service/map-errors.service';

    Component.register('sw-product-basic-form', {

        computed: {
            ...mapApiErrors('product', ['name', 'active', 'description', 'productNumber', 'manufacturerId', 'tags']),
        }
</sript>

<template>
    <sw-field type="text" v-model="product.name" :error="productNameError"
</template>

Error configuration for pages

When working with nested views you need a way to tell the user that an error occurred on another view, e.g tab. For this you can write a config for your sw-page component which (currently) looks like:

{
    "nested.route.name": {
        "entityVariable": [ prop1, prop2 ...]
    }
}

We provide the mapPageErrors(errorConfig) mapper function to create computed properties from it.

2019-06-24: Bulk functions for sw-data-grid and sw-entity-listing

To handle multiple table entries at once, sw-data-grid and sw-entity-listing received the possibility to perform bulk actions. Switching the showSelection to true automatically shows the bulk action bar after the first checkbox selection. While sw-entity-listing provides bulk deletion by default, you have to slot it in sw-data-grid to add functionality.

Examples

Setting up the bulk action bar

Add a links with link class or optional variant classes like link-danger or link-warning inside the bulk slot.

<template #bulk>
    <a class="link link-danger" @click="showBulkDeleteModal = true">
        {{ $tc('global.entity-components.deleteAction') }}
    </a>

    <a class="link" @click="showBulkAwesomeModal = true">
        Awesome feature
    </a>
</template>

Adding bulk modals

Typical modal using the #bulk-modal slot with content as you already know e.g. from sw-context-menu

<template #bulk-modals>
    <sw-modal v-if="showBulkDeleteModal"
              @modal-close="showBulkDeleteModal = false"
              :title="$tc('global.entity-components.deleteTitle')"
              variant="small">
        <p class="sw-data-grid__confirm-bulk-delete-text">
            {{ $tc('global.entity-components.deleteMessage', selectionCount, { count: selectionCount }) }}
        </p>

        <template #modal-footer>
            <sw-button @click="showBulkDeleteModal = false" size="small">
                {{ $tc('global.entity-components.deleteCancel') }}
            </sw-button>

            <sw-button @click="deleteItems" variant="primary" size="small" :isLoading="isBulkLoading">
                {{ $tc('global.entity-components.deleteAction') }}
            </sw-button>
        </template>
    </sw-modal>
</template>

Bulk delete functionality in JS

It's recommended to clear the selection during getList() using this.selection = {};

deleteItems() {
    this.isBulkLoading = true;
    const promises = [];

    Object.values(this.selection).forEach((selectedProxy) => {
        promises.push(this.repository.delete(selectedProxy.id, this.items.context));
    });

    return Promise.all(promises).then(() => {
        return this.deleteItemsFinish();
    }).catch(() => {
        return this.deleteItemsFinish();
    });
},

deleteItemsFinish() {
    this.resetSelection();
    this.isBulkLoading = false;
    this.showBulkDeleteModal = false;

    return this.doSearch();
},

resetSelection() {
    this.$refs.swMyGrid.selection = {};
    this.$refs.swMyGrid.allSelectedChecked = false;
},

Updating your item total

This update also adds an update-records event, so you can keep track of your item total. It comes with the complete items in the grid, but mostly you will use it for your total.

updateTotal({ total }) {
    this.total = total;
},

2019-06-21: New UI language handling

The administration changed the former way of switching between ui languages by deleting the button right above the logout button. To switch to the ui language of your choice, you have to either go to your profile near the logout button, or the users and permissions module in the settings.

In addition, after logging in or changing the language in the before mentioned places the administration will save your active language in your local storage to provide the language of choice even after you logged out.

First time users with no information in their local storage will get to see the login screen in their browser language per default.

2019-06-19: Event Emitter Storefront

The storefront contains a new event emitter based on native Custom Events. It allows to publish events either globally or per plugin. The plugin class initializes the event emitter automatically.

The global event emitter is available under document.$emitter & window.eventEmitter. Inside a plugin the emitter is available under the property this.$emitter;

Basic usage

// Subscribe to an event
document.$emitter.subscribe('my-event-name', (event) => {
    console.log(event);
});

// Publish event
document.$emitter.publish('my-event-name');

Provide additional data to the event

document.$emitter.subscribe('my-event-name', (event) => {
    console.log(event.detail);
});

document.$emitter.publish('my-event-name', {
    custom: 'data'
});

**Providing an different scope***

document.$emitter.subscribe('my-event-name', (event) => {
    console.log(event.detail);
}, { scope: myScope });

document.$emitter.publish('my-event-name', {
    custom: 'data'
});

**Event listeners which will be fired once***

document.$emitter.subscribe('my-event-name', (event) => {
    console.log(event.detail);
}, { once: true });

document.$emitter.publish('my-event-name', {
    custom: 'data'
});

Namespaced events

document.$emitter.publish('my-event-name.my-plugin');

Unsubscribe events

document.$emitter.unsubscribe('my-event-name.my-plugin');

Reset & remove all listeners from emitter

document.$emitter.reset();

2019-06-19: Currency price refactoring

The structure of the extended product prices has been changed. The gradings can no longer be defined for each currency, but are valid for all currencies per rule. The following things have been changed:

  • PriceField now expects an array with the following values:
    'currencyId' => [new NotBlank(), new Uuid()],
    'gross' => [new NotBlank(), new Type('numeric')],
    'net' => [new NotBlank(), new Type('numeric')],
    'linked' => [new Type('boolean')],
  • PriceRuleFieldAccessorBuilder renamed to ListingPriceFieldAccessorBuilder
  • ProductEntity::price now returns a PriceCollection.

2019-06-19: Cart data collection

In connection with the new processor pattern of the cart, we had to replace the StructCollection with a CartDataCollection.

As a result, the following interfaces have changed:

  • \Shopware\Core\Checkout\Cart\CartDataCollectorInterface
  • \Shopware\Core\Checkout\Cart\CartProcessorInterface
<?php declare(strict_types=1);

namespace Shopware\Core\Checkout\Cart;

use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

interface CartDataCollectorInterface
{
    public function collect(CartDataCollection $data, Cart $original, SalesChannelContext $context, CartBehavior $behavior): void;
}
<?php declare(strict_types=1);

namespace Shopware\Core\Checkout\Cart;

use Shopware\Core\Checkout\Cart\LineItem\CartDataCollection;
use Shopware\Core\System\SalesChannel\SalesChannelContext;

interface CartProcessorInterface
{
    public function process(CartDataCollection $data, Cart $original, Cart $toCalculate, SalesChannelContext $context, CartBehavior $behavior): void;
}

Furthermore we removed the old interface Shopware\Core\Checkout\Cart\CollectorInterface which is no longer used.

2019-06-19: Breaking Change: Admin Entity Collection

We refactored the class EntityCollection to extend it from the native Array class. This has several advantages.

  • The EntityCollection behaves like a normal array and you can use all methods and operations you would use on a normal array.
  • You do not longer have to access the items in the collection via .items because they are just normal items in the array.
  • The class still holds the typical collection information like context or critera.
  • You can still use the helper methods of the collection to perform operations based on the ids of the entities.
  • We no longer have to use the setReactive helpers for the reactivity system caveats, which has an enormous performance impact.
  • The class comes along with a lot of helper methods, for example for drag and drop sorting

With the refactoring of the EntityCollection we removed its descendant class SearchResult because the new class combines now both of them. All repository methods always return an EntityCollection. All associated properties in an entity are also an instance of EntityCollection, so now you can access them directly, for example product.categories.

2019-06-18: Write validation refactoring

The write validation has grown big since the beginning of the DAL. Therefore, it has been cleaned up and here are the changes:

  • The trait FieldValidatorTrait has been replaced by the abstract class AbstractFieldSerializer which implements all of it's methods
  • The AbstractFieldSerializer has a new method validateIfNeeded() as a shorthand to requiresValidation() + validate()
  • FieldSerializers can overwrite the getConstraints() method to use a standardized way to validate the data
  • FieldSerializers should throw exceptions of type WriteConstraintViolationException.
  • The following exceptions have been removed in favour of the WriteException:
    • FieldExceptionStack
    • InsufficientWritePermissionException
    • InvalidFieldException
    • InvalidJsonFieldException
  • The WriteException is the only exception which will be thrown in case something went wrong during the write. It contains all thrown exceptions.
  • The WriteParameterBag no longer contains an exception stack, it has been moved to the WriteContext.
  • The CommandQueueValidator has been removed as we now use events for Pre/Post write validation.
    • Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PreWriteValidationEvent: is called pre write. One use case is to catch invalid deletes.
    • Shopware\Core\Framework\DataAbstractionLayer\Write\Validation\PostWriteValidationEvent: is called after the WriteCommands are executed, but before the transaction is committed. You can check new data in combination with existing data.
    • Validators must add an exception to the event's context found at $event->getExceptions() to signal a constraint violation. Any added exception aborts and rollbacks the transaction.

2019-06-18: New possibility for indexing

Up until now you could start all indexer with the help of the IndexerRegistry (IndexerRegistryInterface). This way the indexer are run in the current PHP process (for example in the request).

Now it is also possible to index via the IndexerMessageSender (IndexerRegistryInterface). This way each indexer will run in the message queue worker and your request can finish much faster.

2019-06-18: IndexerRegistry changes

The IndexerRegistry no longer implements the IndexerInterface, but provides a new IndexerRegistryInterface and now dispatches events before and after all indexer have been run.

As the indexing process is a general pattern for the DAL, it has been moved from Shopware\Core\Framework\DataAbstractionLayer\DBAL\Indexing\IndexerRegistry to Shopware\Core\Framework\DataAbstractionLayer\Indexing\IndexerRegistry. The namespace change also applies to the IndexerInterface and the default indexer provided by us.

New events

  • Shopware\Core\Framework\DataAbstractionLayer\Indexing\IndexerRegistryStartEvent
  • Shopware\Core\Framework\DataAbstractionLayer\Indexing\IndexerRegistryEndEvent

2019-06-18: Cart refactoring

To handle dynamic line items, which has a dependency to already added cart line items, the extension points for the cart processor has changed. There are two new interface for the cart:

  • \Shopware\Core\Checkout\Cart\CartProcessorInterface
  • \Shopware\Core\Checkout\Cart\CartDataCollectorInterface

The CartDataCollectorInterface is an replacement for the previous CollectorInterface. Instead of splitting the data fetching and enrichment into three steps we decided to merge this steps into a single one to keep it more simple. The provided StructCollection $data persists for a request. This should prevent to fetching the same entity data multiple times inside a single request.

By adding the CartProcessorInterface we implemented a new extension point for the cart process. It allows to get access to the different subtotals of the cart. This processors can be registered in the di container: <tag name="shopware.cart.processor" priority="xxx" />. This allows to add an additional calculation step after the products calculated and calculate simple a discount for the previous calculated products.

Important change: Line items which are not handled by a processor will be removed from the cart. It is required to implement a CartProcessorInterface for each kind of line items.

What should a processor do?
There are two different kinds of processors:

  1. Lets call it static elements processor A static elements processors handles line items which added to the cart over sources which are called outside the cart process. The ProductCartProcessor is the best example for this kind of processors. Products added over the CartLineItemController controller and will be handled by the ProductCartProcessor. This fetches the required data over the gateway and enrich the line items with labels, description or prices. Additionally the processor is responsible to transfer the product line items of the provided Cart $original to the provided Cart $toCalculate. Otherwise the products would be removed from the cart.

  2. Lets call it dynamic element processor A dynamic element processor has no line items which are added to the cart which has to be enriched, calculated and transferred. This kind of processor checks for a specify cart/context state and adds own line items to the cart if the state reached. The PromotionCartProcessor is the best example for this kind of processors. It checks if a cart state reached (for example: Cart amount > 500), and calculates a discount for -10% for the already added line items. Instead of checking the provided Cart $original, this kind of processor works only with the provided Cart $toCalculate to check if a corresponding state reached.

This new pattern simplifies the way to working with the cart for the dynamic element situation. Processors of this kind, has no more to validate if their are already discounts of their own type which are in conflict with the current state.

2019-06-17: Cypress as E2E testing framework

As from today, we made the shift from Nightwatch.js to Cypress as our primary E2E testing framework. Cypress provides lots of features aiding us keep the quality high:

  • More features concerning debuggability, e.g. taking snapshots as the tests run
  • Automatic waits for commands, assertions and animations
  • Possibility to run tests in parallel
  • Spies, stubs, and clocks for controlling the behavior of functions, server responses, or timers

The existing Nightwatch tests have already been migrated to Cypress. Consequently, the e2e project structure has been changed distinctly:

  • In general, Cypress e2e tests can be found in Administration/Resources/e2e/cypress/integration
  • We separated administration and storefront test in order to move them to their corresponding repository, so you need to change the path accordingly: For example, please use Storefront/Resources/e2e/cypress/integration to run storefront-related tests
  • You can find the test files in Administration/Resources/e2e/cypress/integration
  • Configuration can be done in cypress.json and cypress.env.json for environment-related configuration

Running tests

If you use docker, Cypress is shipped in an own container. In this case, please keep in mind that you need to run Cypress commands from your local machine, as you cannot run docker commands in docker containers.

At first, you need to set up your environment for running E2E tests:

./psh.phar e2e:init

Afterwards, you should be able to run the test suite in command line using the following command:

./psh.phar e2e:run

Of course you can still use own parameters, as you did using Nightwatch:

  • Similar to usual Nightwatch usage, you can add own parameter via --CYPRESS_PARAMS
  • By default, Administration tests will be selected. If you want to switch to the storefront tests, please add --CYPRESS_ENV="storefrontto your command

Using the test runner

A main feature is Cypress' test runner which provides us a bunch of features to help with debugging tests. To start the test runner, you simply use the following command:

./psh.phar e2e:open

Please keep in mind that your operating system needs a graphical interface to run the test runner. This way, you need to install Cypress locally if you want to use it, even if you use docker for everything else.

Running selected tests

You may only want to run one or more selected tests. For this you can use the following parameters:

  • If you want to run selected spec files, use --spec path/to/file.spec.js
  • If you want to use Mocha-like selection, user ./psh.phar e2e:run --CYPRESS_PARAMS="--env grep=yourSearchTerm"
    • e.g.@p to select only tests that are relevant for package testing, you can use ./psh.phar e2e:run --CYPRESS_PARAMS="--env grep=@p"
    • For selecting a single test, you need to use a unique string from the tests' title --CYPRESS_PARAMS="--env grep='delete sales channel' in your parameters

Documentation

2019-06-11: sw-label refactoring

We refactored sw-label component due to a new size and several unused properties. In addition, we made the usage more simple. Below, you can find the changes in detail:

From now on, you don't need to set the dismiss property to make the label dismissable. The label will automatically provide this possibility as soon as the listener @dismiss is registered.

We added the property size, providing three sizes:

  • default: The former default size, with height of 32px
  • medium: This size replaces the former small property, with height of 24px
  • small: A new, small size with 12px as height.

Furthermore, we merged pill into the new property appearance. The rectangle appearance of the label will stay as default value. As the property variant will not automatically style borders anymore, please use appearance="pill" to get the well known look with round edges.

As a result, we deleted following properties:

  • small
  • dismiss
  • circle
  • pill
  • light

You can find more detailed information about sw-label usage in the Component Library.

2019-06-07: Renamed Flag Deferred to Runtime

We renamed the DAL-Flag Deferred to Runtime

To put it more clearly that the flag marks field to be hydrated at runtime, we renamed the former Flag Deferred to Runtime

As always: sorry for the inconvenience!

2019-06-06: Administration: Update design system color variables

The color variables in the administration have been aligned with the design system colors. The main color variables are now available in several gradations from light to dark.

For example the gray color now has those gradations:

// platform/src/Administration/Resources/administration/src/app/assets/scss/variables.scss

$color-gray-50: #F9FAFB; // Light
$color-gray-100: #F0F2F5;
$color-gray-200: #E0E6EB;
$color-gray-300: #D1D9E0;
$color-gray-400: #C2CCD6;
$color-gray-500: #B3BFCC; // Medium
$color-gray-600: #A3B3C2;
$color-gray-700: #94A6B8;
$color-gray-800: #8599AD;
$color-gray-900: #758CA3; // Dark

This should make it a lot easier to work with the design system in general because every design system color has it's counterpart in the administration variables.

This also means that you can use the original colors from the design system directly and you don't have to deal with custom lighten() or darken() functions in your SCSS. In the past we did not have enough variables to cover all cases.

However there are deprecations to some variables. Those are inside a @deprecated category in the variables file. There are now less base colors in general - but with more gradations for each color. We have assigned the old variables with the new variables to be compatible with current modules or plugins. But we will migrate the old colors from now on.

For example:

$color-steam-cloud: $color-gray-300; // Old color #D8DDE6

Some colors have been slightly updated to achieve better contrast ratios.

For new SCSS code please use the new gradation variables.

If you want to take a look at the design system visit shopware.design.

2019-06-06: Removed context from page in all storefront templates

We removed the context object from the page object in all storefront templates. If you need to access the context object it is automatically available.

Example:

Previously:

{% set billingAddress = page.context.customer.defaultBillingAddress %}

Now:

{% set billingAddress = context.customer.defaultBillingAddress %}

2019-06-05: Update to symfony 4.3

We've updated the symfony version to 4.3, therefore take a look into the changes.

2019-06-05: Refactored fillSwSelectComponent command

A first step to unify all sw-select behaviour was merged today. We refactored the E2E custom command fillSwSelectComponent to support sw-select in both variants (single and multi select) as well as sw-single-select, sw-multi-select and sw-tag-field.

We also renamed it to fillSwSelect instead of fillSwSelectCommand

fillSwSelect(
    selector,
    {
      value,
      clearField = false,
      isMulti = false,
      searchTerm = null,
      resultPosition = 0
    }
) 
  • selector The css selector of your select component.
  • value The value to search and check against after selecting it.
  • clearField Indicates if all selections should be cleared before selecting a new value. (multi select only)
  • isMulti Indicates if the select field is a multi select.
  • searchTerm Overrides the value to search for, if it differs from the actual value. (e.g. search for a locale but the displayed value is the locale description)
  • resultPosition Tells nightwatch to select a specific option from the results list rather than the first. This might be necessary if your search has more than one result.

To avoid duplications we removed the E2E commands fillSwSingleSelect and fillSwMultiSelect.

May 2019

2019-05-31: Added plugin:create command

We added a plugin:create command which creates a plugin skeleton structure inside the custom/plugins directory. The skeleton contains the required implementation of \Shopware\Core\Framework\Plugin, the required composer.json file and an empty services.xml for further service declarations.

2019-05-31: Disabled auto load of many to one associations

We changed the default value of $autoload to false in the \Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField. Additionally we disable this flag for the most core associations to prevent unnecessary data loading. It is now required to specify which data has be loaded on php or on javascript side.

For the sake of all developers we added the \Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria::addAssociationPath function which allows to added nested associations to the criteria:

$criteria = new Criteria();

// adds an empty criteria for the following associations
    // category.products
    // product.prices
    // price.rule

$criteria->addAssociationPath('products.prices.rule');

$categoryRepository->search($criteria, $context);

The same function exists for the admin Vue part:

criteria = new Criteria();

// adds an empty criteria for the following associations
    // category.products
    // product.prices
    // price.rule

criteria.addAssociationPath('products.prices.rule');

repo = this.repositoryFactory.create('category');

repo.search(criteria, this.context);

2019-05-29: Added private flag for MediaItems

The MediaItems and MediaFolderConfiguration have been refactored. The property private has been added as a boolean field.

The private-flag provides a way to mark a folder or a MediaItem as private. PrivateMediaItems will be stored in via the filesystem.private instead of filesystem.public and therefor not accessible via web. PrivateMediaItems are not shown in the MediaItems-Admin by now. They are excluded from any API-Call of the Media-Entity. To get access to those PrivateMediaItems the Repository-Request has to be made with a SYSTEM_SCOPE-Context.

Example of a SYSTEM_SCOPE-Context:

$mediaService = $this->mediaService;
$context->scope(Context::SYSTEM_SCOPE, function (Context $context) use ($mediaService, $document, &$fileBlob) {
    $fileBlob = $mediaService->loadFile($document->getDocumentMediaFileId(), $context);
});
$generatedDocument->setFileBlob($fileBlob);

2019-05-29: Added static Property to DocumentEntity

The DocumentEntity has been refactored. A boolean property named static has been added.

This property is used to determine wether a document can be generated or not. If static is set to true. The DocumentEntity is marked as linked to a static file which can not regenerated. The file is stored in the documentMediaFile association of the DocumentEntity. The main purpose for the static property is to provide a way to store documents of legacy shopware versions or third-party systems to the new document structure and asure these files will stay untouched by any regeneration of documents.

When calling the DocumentService::getDocument()-Method even with the parameter $regenerate set to true these documents won't be regenerated or changed in any way.

Now you can import legacy documents to the system via the following way:

  • Add a new MediaItem with the private-property set to true
    $newMediaId = Uuid::randomHex();
    $mediaRepository->create(
    [
        'id' => $newMediaId,
        'private' => true
    ],
    );
  • Save the file to the server via the MediaService
    $mediaService->saveFile(
    $fileBlob,      // Blob of file to save
    $fileExtension, // Extension of the file (eg. 'pdf')
    $contentType,   // ContentType of the file (eg. 'application/pdf')
    $filename,      // Filename fof the file (eg. 'invoice_23_12_1977_123')
    $context,       // Context
    'document',     // MediaFolder to save the new file (default for documents is 'document')
    $newMediaId     // Id of the MediaItem created as "private")
    );
  • Create DocumentEntity as static with associating MediaId
    $documentRepository->create([
        [
            'id' => Uuid::randomHex(),
            'documentTypeId' => $documentType->getId(), // (id of type ('invoice', 'storno',...))
            'fileType' => FileTypes::PDF,
            'orderId' => $orderId, // Id of the order this document is associated with.
            'orderVersionId' => $orderVersionId, // VersionId of the order this document is associated with.
            'config' => ['documentNumber' => '1001'], // documentConfig with at least the documentNumber
            'deepLinkCode' => 'xyz123', // Deeplinkcode 
            'static' => true,   // static = true is important to prevent overwriting of the document 
            'documentMediaFile' => [
                'id' => $newMediaId,
            ],
        ],
    ],
    $this->context
    );

    It is impotant that you need an already imported order to associate the document with. Currently every document needs a valid orderId to be associated with.

2019-05-29: Added MediaItem association to DocumentEntity

The DocumentEntity has been refactored. An association with MediaItem to the property documentMediaFile has been added.

This association is used to store an already generated document-file to the document-entry.
A document will be generated once and stored as a privateMediaItem. Following calls for document generation will not regenerate the file, but load the already generated file. The DocumentService::preview()-Method will still generate the document on the fly.

2019-05-28: Breaking change - Change default timezone to UTC

On kernel construct, the platform now uses UTC as default timezone. This means that all dates in the database are now also UTC. The administration formats the DateTime objects correctly by default by simply using the |date filter.

In the storefront we have a Timezone utility which automatically detects the timezone of the user and sets a cookie which will be processed by the platform/src/Storefront/Framework/Twig/TwigDateRequestListener.php. This means that you can use the |localizeddate('short', 'none', app.request.locale) filter and the time will automatically be converted to the client timezone.

If you have to convert the time on the client side, you can use the DateFormatPlugin. Example:

<span data-date-format="true">
    {{ order.orderDateTime.format(shopware.dateFormat) }}
</span>

2019-05-15: Breaking change - Refactor line item

The LineItem has been refactored. The property key has been renamed to id to be a bit more consistent with other structs/entities.

The id must be a unique string. In most cases you use the id of the entity which the line item refers to. If you want to have different line items of the same entity (e.g. different prices, children), you must use a different id.

Previously you often had to do the following:

<?php
$lineItem = new LineItem('98432def39fc4624b33213a56b8c944d', 'product');
$lineItem->setPayload(['id' => '98432def39fc4624b33213a56b8c944d']);

// New way:

$lineItem = new LineItem('98432def39fc4624b33213a56b8c944d', 'product', '98432def39fc4624b33213a56b8c944d');

All tests and the documentation has been updated.

2019-05-13: Added RequestDataBag to interface of payment handler

The \Shopware\Core\Framework\Validation\DataBag\RequestDataBag was added to the pay methods of \Shopware\Core\Checkout\Payment\Cart\PaymentHandler\SynchronousPaymentHandlerInterface and \Shopware\Core\Checkout\Payment\Cart\PaymentHandler\AsynchronousPaymentHandlerInterface. With this, you are able to send custom parameter into the payment handling.

2019-05-06: Split setting UI in categories

The Settings are now split up in the categories Shop, System and Plugins.

To add your setting to the different categories you just have to extend the right template block.

Instead of the old sw_settings_content_card_slot_default block you now have three specific blocks for each category:

sw_settings_content_card_slot_shop

sw_settings_content_card_slot_system

sw_settings_content_card_slot_plugins

2019-05-06: New E2E commands for select components

There are two new custom E2E commands for the new sw-single-select and sw-multi-select components:

.fillMultiSelect(
    '.selector', 
    'Search term', 
    'Value'
);

.fillSingleSelect(
    '.selector', 
    'Value', 
    1 /* Desired result position */
);

Those are also valid when using the entity select components.

2019-05-03: Changes in sw-field component

The sw-field component got a complete overhaul in order to remove unused properties, doubled configuration and a lot of unnecessary template and logic inheritance.

New structure for fields

The sw-field component now uses slot mechanics and property consumption instead of component inheritance.

If you inspect an sw-field you may notice that it has several child components depending on your fields type:

<sw-field>
    <sw-contextual-field>
        <sw-block-field>
            <sw-base-field>

This child components consume basic information for example label and size properties, apply them to their own elements and expose them back to you via slots.

Remove properties, types and components

  • The sw-number-field now changes its model on 'change' instead of input. This makes it easier to validate numbers
  • sw-fiel-addition: Was removed. Its purpose was to style the prefix and suffix sections of sw-field
  • sw-field-label: Was removed.
  • sw-field-help-text: We removed the sw-field-help-text because it wasn't really used in the project. Also this old grey description text did not comply with our design system any more.
  • tooltipText: Having two properties for helpText and tooltipText could be confusing so we removed the property in favor of helpText. The helpText property is now rendered in an sw-help-text bubble.
  • type="bool": It was very confusing to have to two switch typed variants of sw-field (type="switch" and type="bool") which' only difference was a border. We removed <sw-field type="bool"> an replace it with an bordered attribute.
    
    <sw-field type="switch" bordered [...] ></sw-field>
* `prefix` and `suffix`: We removed the `prefix` and `suffix` properties in all fields in favour of using slots instead.
This change removes the possibility to define two different values. With Vue.js' new slot syntax defining prefixes and suffixes is easy:
```HTML
<sw-fiel type="text" [...]>
    <template #prefix>
      {{ dynamicPrefix }}
    </template>

    <template #suffix>
        constant
    </template>
</sw-fiel>

Added properties

  • size (String): Got new values 'medium' and small. Also the the sizes of the input fields changed to small = 32px medium = 40px and default = 48px height.
  • type="switch" got a new prop bordered (Boolean) that made type="boolean" obsolete.
  • type="select" got a new prop aside (Boolean) to set the label left to the select box.

"Strict types" in properties

We removed the sw-inline-snippet mixin from all sw-fields and set the translated properties (e.g. label) to type string. That means you can't pass objects with translations anymore.

sw-base-field

The sw-base-field component consumes basic information about your form field and is intended to display a basic header and error information.

Props

  • name (String): Use this to override the exposed identifier
  • label (String): The label of the component
  • helpText (String): The help text is displayed as an sw-help-text component on the top right corner of the field
  • error/errorMessage (String, Object): Don't mind them now as they will be refactored soon
  • disabled (Boolean): Sets the disabled State
  • required (Boolean): (not fully implemented yet)
  • inherited (Boolean): (not fully implemented yet)

Slots

  • sw-field-input scope: { identification, disabled }
    • identification (String): If name property is set this will be just the given name. If not this the value is sw-field--${Uuid}. which can be used to link elements that have an for attribute to your inputs.
    • disabled (Boolean): You may need the information in your slot since you actually do not want to have an own disabled prop.

sw-block-field

The main purpose of the sw-block-field component is intended to take care of sizes borders and border colors

Props

  • size (String): Defines the can be either 'small', 'medium', or 'default'

Slots

  • sw-field-input scope: { identification, disabled, swBlockSize, setFocusClass, removeFocusClass }
    • identification, disabled are just exposed from sw-base-field
    • swBlockSize (String): A CSS selector that can be used by your component to react to different sizes
    • setFocusClass, removeFocusClass (function): You can use this in your component to set or remove the focuses state of an sw-block-field

(e.g. bind it to the click of a button)

sw-contextual-field

The sw-contextual-field is intended to render a "context" to the field. This context is displayed as pre and suffix.

Props

  • None

Slots

  • sw-contextual-field-prefix scope: { disabled, identification }
  • sw-contextual-field-suffix scope: { disabled, identification }
  • sw-field-input scope: { identification, error, disabled, swBlockSize, setFocusClass, removeFocusClass, hasSuffix, hasPrefix }
    • identification, error, disabled, swBlockSize, setFocusClass, removeFocusClass - as described above
    • hasSuffix, hasPrefix: selfexplaining

Follow ups

  • In the next few days we will apply the sw-field structure to sw-media-field and sw-select.
  • Errorhandling and fieldvalidation are in progress
  • integrate inherited values

2019-05-03: Product streams

Product streams are now released. With this feature you can filter products based on DAL fields in the admin and via API.

The filters start from the product entity and can be restricted for the admin with a blacklist.

This blacklist can be found in the module app/service/product-stream-condition. There you can add blacklist keywords for general or entity based purpose.

With a ServiceProviderDecorator you can extend the blacklists for the admin view with a plugin. An rule-builder based implementation can be found here: platform/src/Administration/Resources/administration/src/app/decorator/condition-type-data-provider.js.

If you extend the DAL, please check the admin for a possible new restriction with the blacklists. If the new DAL field should be used in the product streams, then translate the field. The translations can be found here: platform/src/Administration/Resources/administration/src/module/sw-product-stream/snippet.

2019-05-03: Database debugging


>  bin/console database:generate-debug-views

Execute this command to create views in the database that replace all binary ids with hex values. All these views are named debug_ORIGINAL_TABLE_NAME.

Example

Executing SELECT * FROM debug_tax will result in:

+----------------------------------+----------+------+---------------+-------------------------+------------+
| id                               | tax_rate | name | custom_fields | created_at              | updated_at |
+----------------------------------+----------+------+---------------+-------------------------+------------+
| 12cb17bab0264ae4a518c0e053146a9c |    20.00 | 20%  | NULL          | 2019-05-03 12:04:10.000 | NULL       |
| 1eceb1547afe476ca39d49ca6a9c0047 |     5.00 | 5%   | NULL          | 2019-05-03 12:04:10.000 | NULL       |
| 6a456bf51f0b4a7c8655de754372be59 |     7.00 | 7%   | NULL          | 2019-05-03 12:04:10.000 | NULL       |
| 9015c672349c43f88c1143f6e382f52d |    19.00 | 19%  | NULL          | 2019-05-03 12:04:10.000 | NULL       |
| c081e2c696d04426840073a925634914 |     1.00 | 1%   | NULL          | 2019-05-03 12:04:10.000 | NULL       |
+----------------------------------+----------+------+---------------+-------------------------+------------+

2019-05-02: Rule scope in administration

The rule scope of rules are now supported and required to define in the administration.

The scopes filter the matching rules so it's possible to show only cart or lineItem based rules.

The scopes can be added in the in the condition type data provider. See platform/src/Administration/Resources/administration/src/app/decorator/condition-type-data-provider.js

At the moment, these types are supported

  • global --> used for rules which has no restriction (like DateRangeRule)
  • cart --> used for rules which require the CartRuleScope (like CartAmountRule)
  • checkout --> used for rules which require the CheckoutRuleScope (like LastNameRule)
  • lineItem --> used for rules which require the LineItemScope (like LineItemTagRule)

2019-05-02: Remove static methods from EntityDefinition

We just removed a lot of static calls from DataAbstractionLayer. All EntityDefinitions are now instances provided through the container. This changed a lot of internals and a few but breaking public API methods.

New rule of thumb: If you need something from the EntityDefinition inject it

EntityDefinition

The EntityDefinition now must not contain any static methods.

 public static function getEntityName() { ... }

Is now invalid and will throw a compile error from php. This now must be called

public function getEntityName() {... }

Please adjust all method calls accordingly. (Usually getEntityName, defineFields, getCollectionClass, getEntityClass)

EntityDefinition service declaration

The service definition tags already had a entity="entity_name"" property. From now on this is required and the build will fail if it's not provided.

<service id="Shopware\Core\Checkout\Promotion\Aggregate\PromotionDiscountRule\PromotionDiscountRuleDefinition">
    <tag name="shopware.entity.definition" entity="promotion_discount_rule"/>
</service>

EntityRepository

The EntityRepositoryInterface now expects an instance of a Definition - as well as all subsequent Classes (Write, Read, ...). In order to ease the migration the repository now has a getDefinition() method to return the repositories definition if inspection or result rendering is necessary.

ResponseFactoryInterface

All responses from custom actions rely on Entity definitions. Now you need to provide an instance of the EntityDefinition. You should be able to use the Repository you injected for most instances.

/**
 * @var EntityRepositoryInterface
 */
private $orderRepository;

public function __construct(EntityRepositoryInterface $orderRepository) 
{
    $this->orderRepository = $orderRepository;
}

Calls to the ResponseFactoryInterface will now look like this:

return $responseFactory->createDetailResponse(
    $orders,
    $this->orderRepository->getDefinition,
    $request,
    $context->getContext()
);

::class references

The dependency injection container secures that a particular instance of a definition is created only once per request. If you need equality checks please use the strict comparison operator on the objects themselves. $assiciation->getReferenceDefinition() === $definition works and is the recommended way.

SalesChannel-API

The SalesChannel-API's implementation got revamped. Since the definitions themselves are instances now, the SalesChannel-API is now an entirely second cluster of instances in memory that does not touch the original instances. By that we removed the SalesChannelDefinitionTrait and calls to decorateDefinitions are no longer available. Everything will be injected automatically through the container at compile time.

This is achieved through a decorated registry. Object comparsion with the SalesChannelDefinitionInstanceRegistry yields different results from the base registry.

// Sales channle entities are different classes
$salesChannelProductDefinition instanceof $sproductDefinition // === true
$productDefinition instanceof $salesChannelProductDefinition // === false

// The decorated registry allways returns the sales channel object, regardless of the provided service id
$salesChannelRegistry->get(ProductDefinition::class) instanceof $slaesChannelRegistry->get(SalesChannelProductDefinition::class) // == true
$salesChannelRegistry->get(ProductDefinition::class) === $slaesChannelRegistry->get(SalesChannelProductDefinition::class) // == true

This replacement is done based on the entityName so overwriting a base definition takes a service declaration like this:

<service id="Shopware\Core\Content\Product\SalesChannel\SalesChannelProductDefinition">
    <tag name="shopware.sales_channel.entity.definition" entity="product"/>
</service>

Overwrites the product definition in SalesChannel-API requests.

Internal changes

The change to instances brought many changes to internal implementations in the DataAbstractionLayer. Conceptually the whole configuration now relies on a compile step that is automatically triggered by the container on object creation.

All Definitions and Fields now refer to concrete instances of EntityDefinition as opposed to the class reference. By that you can navigate the entire Object tree without using any registry or container calls:

$productDefinition // ProductDefinition
    ->getFields() // CompiledFieldCollection
    ->get('categories') // ManyToManyAssociationField
    ->getToManyDefinition() // CategoryDefinition
    ->getFields() // CompiledFieldCollection
    ->get('tags') // ManyToManyAssociationField
    ->getToManyDefinition() // TagDefinition

Removal of *Registries

The formerly necessary FieldSerializerRegistry, FieldAccessorBuilderRegistry and FieldResolverRegistry were removed in favor of getters on the field class. The fields lazily acquire these objects from the service container through the DefinitionInstanceRegistry.

class Field
{
    ...

    public function getSerializer(): FieldSerializerInterface { ... }

    public function getResolver(): ?FieldResolverInterface { ... }

    public function getAccessorBuilder(): ?FieldAccessorBuilderInterface { ... }

    ...
}

2019-05-02: Adding the process button component

A new component called sw-button-process was added to Shopware platform. The button is introduced to display the status of the process the button should start. E.g. if you click the button to save an entity, it will display a loading indicator while the save process is running and a tick icon if the process was finished successfully. This way, we tend to get rid of those "Success" notifications which does not provide any other useful information.

Usage

The sw-button-process component looks as stated below:

<sw-button-process
        class="sw-product-detail__save-action"
        :isLoading="isLoading"
        :processSuccess="isSaveSuccessful"
        :disabled="isLoading"
        variant="primary"
        @process-finish="saveFinish"
        @click="onSave">
        {{ $tc('sw-cms.detail.labelButtonSave') }}
</sw-button-process>

As you can see, you can use the sw-button-process component similar as you're used to with sw-button. We just need some further information:

  • isLoading: Necessary to indicate the time when the process is currently running.
  • processSuccess: This prop signalizes if the process was finished successfully, so that the sw-button-process button can start its success animation.

If you want to use the sw-button-process button, you need to change those props accordingly to your module's behavior.

Events and creation as edge case

The success animation needs 1.25 seconds to run per default. This way, the create pages were a difficulty since they reload the whole page including the process button, interrupting the animation in the process. For this reason, we use the @process-finish event to signalize that the save process is finished. In a create page, you need to override this event to navigate to the detail page after the animation ran. As seen in the example below, you just need to move the routing to the saveFinish event to make it run:

saveFinish() {
    this.isSaveSuccessful = false;
    this.$router.push({ name: 'sw.cms.detail', params: { id: this.page.id } });
},

onSave() {
    this.$super.onSave();
}

April 2019

2019-04-30: Removing the "X-" header prefix

The leading "X-" in a header name has been deprecated for years (https://tools.ietf.org/html/rfc6648) and therefore should not be used anymore.

Before:

  • x-sw-context-token
  • x-sw-access-key
  • x-sw-language-id
  • x-sw-inheritance
  • x-sw-version-id

After:

  • sw-context-token
  • sw-access-key
  • sw-language-id
  • sw-inheritance
  • sw-version-id

2019-04-30: Refactored plugin entity

We refactored the plugin entity as follows:

  • Renamed plugin.name => plugin.baseClass
    • The plugin.baseClass property holds the fully qualified domain name (FQDN)
  • Added plugin.name
    • The plugin.name holds the technical plugin name

2019-04-30: Notification center

The administration now has a notification center in every sw-page. This is not a breaking change, you can still make notifications the same way as before. But there are some nice new Features that you may want to know:

The notification mixin

Currently there is a notification.mixin.js which only abstracts the store and sets a notification variant, depending on the method you call. It is recommended to use the store directly instead to create or update notifications (because of the update functionality).

Store functionality

You can create notficiations the following way:

this.$store.dispatch('notification/createNotification', {
    title: 'My title',
    message: 'My message',
    variant: 'info'
}).then((notificationId) => {
    // Save the id to modify the notification in the future.
});

And you can update your notification the following way:

this.$store.dispatch('notification/updateNotification', {
    uuid: mySavedNotificationId
    message: 'changed message'
}).then((notificationId) => {
    // The notification id stays the same here
});

The update action is very flexible. You can for example set growl: true to show the user a growl message (again). You can also set visited: false to mark the notification as not seen by the user. There is also a way to set the visited parameter dynamically depending on data changes (have a look at the metadata parameter). If the user deletes the notification and you update it, it will be recreated with the default values and your specified values.

Possible notification parameters

  • titlerequired -> The title of the notification.
  • messagerequired -> The text of the notification.
  • variantrecommended -> The styling of the notification. Possible values are success, info, warning and error. If set to success the notification will be growl only. The default value is info.
  • systemoptional -> Applies also to the styling of the notification. If set to true it will be darker. The default is false.
  • autoCloseoptional -> If set to true the growl notification will close after the specified duration. The default is true.
  • durationoptional -> The duration of the growl message in ms. The default is 5000

New parameters

  • growlrecommended -> Show the notification as a growl message. It will also be in the notification center. The default is true, but you should consider setting this to false to not overwhelm the user in notifications.
  • visitedoptional -> If set to false, the notification is mark as not seen by the user and will be displayed so. The default is false.
  • isLoadingoptional -> Shows a loading indicator if set to true. Also the notification will not be saved if it is set to true. If The default is false
  • metadataoptional -> You can store a object here. If the object is different from the already attached one, the notification will automatically set visited to false (as long as not other specified). This is useful to show a progress in the notification where you want to notify the user about progress changes.

2019-04-30: New ManyToManyIdField

The new \Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyIdField allows to store ids of an \Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToManyAssociationField inside the entity.

How to implement

  1. Add the field to the entity definition class:

    new ManyToManyIdField('property_ids', 'propertyIds', 'properties')

    The third parameter has to be the property name of the related association

    (new ManyToManyAssociationField('properties', ...

  2. Add property, getter and setter to entity class

    
    /**
     * @var array|null
     */
    protected $propertyIds;
    
    public function getPropertyIds(): ?array
    {
        return $this->propertyIds;
    }
    
    public function setPropertyIds(?array $propertyIds): void
    {
        $this->propertyIds = $propertyIds;
    }

The DAL will detect this field automatically and updates the data each time the entity changed.

## When do i really need this field?

This field is required for a special kind of filter. The above example shows the relation between a `product` and its `properties`.
Adding this field to the product definition allows to send the following requrest to the DAL:

**select all products which has the property `red` or `green` AND `xl` or `l`**

$criteria = new Criteria(); $criteria->addFilter( new EqualsAnyFilter('product.propertyIds', ['red-id', 'green-id']) ); $criteria->addFilter( new EqualsAnyFilter('product.propertyIds', ['xl-id', 'l-id']) );

2019-04-30: Attributes are now custom fields

We have got the feedback that the intended usage of attributes is unclear and often mistaken for product properties. So we decided to rename attributes to custom fields.

What do I have to do?

If you've used AttributesField in definitions, you need to replace it with CustomFields and rename the column attributes to custom_fields. Alternatively, you can override the storageName in the CustomFields constructor.

2019-04-29: New shortcut methods for transaction state changes

There is now a new class which can help you to deal with the StateMachine. Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStateHandler contains some methods to improve the readability of your code when changing the state.

old way:

public function example()
{
    $completeStateId = $this->stateMachineRegistry->getStateByTechnicalName(
        OrderTransactionStates::STATE_MACHINE,
        OrderTransactionStates::STATE_PAID,
        $context
    )->getId();

    $data = [
        'id' => $transaction->getOrderTransaction()->getId(),
        'stateId' => $completeStateId,
    ];

    $this->transactionRepository->update([$data], $context);
}

new way:

public function example()
{
    $transactionStateHandler = $this->getContainer()->get(OrderTransactionStateHandler::class);
    $transactionStateHandler->complete($transaction->getOrderTransaction()->getId(), $context);
}

2019-04-29: CreatedAt and UpdatedAt are set as default

By extending the EntityDefinition-Class all Definitions now automatically have a cratedAt- and UpdatedAt-Field, so you don't have to add them manually. Also every Entity-Struct extending the Entity-Class has the associated Properties + Getters and Setters automatically.

The only Exception are MappingDefinitions, there these Fields aren't added automatically.

What do you have to do?

We have extended the dal:validate-command to check for fields that don't have a mapped Column. So run this command to check for Definitions that previously didn`t had these fields. For those entities you have to write Migrations and add these fields. For every definitions that has those fields you can remove them from the FieldDefinitions and EntityStructs.

2019-04-29: Changed context injected to payment handler

From now on the \Shopware\Core\System\SalesChannel\SalesChannelContext is injected into the methods of \Shopware\Core\Checkout\Payment\Cart\PaymentHandler\SynchronousPaymentHandlerInterface and \Shopware\Core\Checkout\Payment\Cart\PaymentHandler\AsynchronousPaymentHandlerInterface. This should give you more information about the current checkout and saleschannel context, but it breaks the current interfaces. Please adjust your payment handler accordingly. Please be also aware that the SalesChannelContext may contain certain information. Some of its properties are nullable, so make sure they are set, before you use them.

2019-04-29: Added Api-Browser for SalesChannelEntities

We have added the ApiBrowser functionality of the EntityDefinitions for the SalesChannelApi-Entities as well. You can find the SwaggerUI under /sales-channel-api/v1/_info/swagger.html.

We also added a configuration, to control whether the ApiBrowser functionality is available or not. You can control it over the api.api_browser.public entry in your shopware.yaml:

api:
  api_browser:
    public: true

2019-04-26: Removed public shorthand functions of context

The context used to have the shorthand functions getUserId and getSalesChannelId.

The problem is that it depended on the Source of the Context whether these Ids were set or not. Especially in the case of the salesChannelId this was problematic, because it silently returned the DefaultId in case the source didn't match.

We removed the shorthand functions, so noe you have to take care of checking the ContextSource:

if (!$context->getSource() instanceof AdminApiSource) {
    throw new InvalidContextSourceException(AdminApiSource::class, \get_class($context->getSource()));
}

We have added the InvalidContextSourceException for the case that a different CntextSource was expected.

2019-04-26: Public controllers

In the past you had to extend the whitelist in the SalesChannelApi-/ApiAuthenticationLister if you wanted to create a controller which doesn't required a logged in user. Since it was almost impossible for third party developers to extend this list, the behavior has changed. If you now want to create a public route you can do this with an annotation. Example:

* @Route("/api/v{version}/_action/user/my-unprotected-route", defaults={"auth_required"=false})

2019-04-26: Currency iso code added

The currency entity now has an additional field called isoCode which is required and not translated. This field contains a 3 letter code according to the ISO 4217 standard.

2019-04-25: Vue Vuex in the Administration

Vuex is a Vue.js plugin and library that allows you to share state between components. This is done by using a single object which is accessible by all component via the $store property. Vuex applies a flux pattern on this store which means every change of the store's state must be done via commits (synchronous changes) or actions (asynchronous changes).

Well this is not completely true because we deactivated strict mode (more on this later).

This is not a documentation about Vuex but an overview how we want to use Vuex in our application. If you are not familiar with Vuex I strongly recommend reading the documentation at vuex.vuejs.org.

Define Own Store Modules

Store modules should only be registered by the top-level components of complex structures (e.g. your sw-page component or things like the sw-component-tree). Keep in mind that the preferred way to share state in Vue.js still is passing properties to children and emitting events back.

We recommend to create your module in a separate Javascript file in your components folder named state.js that exports your state definition

// state.js
export default {
   namespaced: true,
   state: { },
   mutations: { ... }
}

To register the module use the registerModule function of the Vuex store in the beforeCreated lifecycle hook of your component. Also don't forget to clean up your state when your component is destroyed.

If you register a module on the store keep in mind that it follows the same rules as if you would create a component. That means that store modules which are created from the same source share a static state. if you need a "clean" store module every time you register it and (in most cases this is exactly what you want) define your state property as a function. see https://vuex.vuejs.org/guide/modules.html#module-reuse for an explanation

export default {
  state() {
      return { ... };
  }  

As convention your store module name should be your component's name in camelcase (because you must be able to access the name in object notation).

import componentNameState from './state.js'

export default {
  name: 'component-name'

  beforeCreated() {
    this.$store.registerModule('componentName', componentNameState);
    }
  beforeDestroye() {
    this.$store.unregisterModule('componentName');
    }

You may note that we don't follow our usual convention to wrap the functionality of the lifecyclehook in an extra method. This is because the registration of your state is mandatory and should not be overwritten by components extending your component.

Strict mode and Problems with v-model

Because Vuex does not work well with Vue.js' v-model directive we turned off strict mode. That means that state can be written directly. However, avoid changing the state directly as much as possible because it could cause problems with Vue.js' reactivity. At least first level properties of your module must be commited.

// state.js
export default {
   state: {
    // product is a first level property
    product {
      // id may be changed directly with full reactivity
      id: ''
    }
   },
   mutations: { ... }
}

Global State

Right now we're migration global state to vuex stores. This includes the current language and admin locale as well as notification management and error handling. All global actions and mutations will be documented in the component library eventually.

If you need to create global state on your own you can create an Vuex module in the /src/app/state/ folder of the application. Because the Vuex modules must be named we could not apply automatic registration (yet). So You must add your global module manually in /src/app/state/index.js .

2019-04-25: Naming database constraints

With the newest MySQL version CONSTRAINTS must be unique across all tables. This means that

CONSTRAINT json.custom_fields CHECK (JSON_VALID(custom_fields)) is no longer valid. The new constraint name should be:

CONSTRAINT json.table_name.custom_fields CHECK (JSON_VALID(custom_fields)). This is true for all CONSTRAINT, not only JSON_VALID().

2019-04-25: Consistent locale codes

Until now we used two locale code standards.

Bcp-47 inside vue.js (administration) and IEC_15897 inside the Php backend.

Now we use the BCP-47 standard for both. This means, that the locale codes changed from en_GB to en-GB.

Where does this effect you?

  • First you need to reinitialize your Shopware installation after you pulled these changes (./psh.phar init)
  • composer.json of your plugins
  • Changelogs of your plugins
  • Locale Repository
  • Snippet files of all modules

composer.json

You have to change the locale codes inside the extra section of your plugin composer.json from:

"extra": {
  "shopware-plugin-class": "Swag\\Example",
  "copyright": "(c) by shopware AG",
  "label": {
    "de_DE": "Example Produkte für Shopware",
    "en_GB": "Example Products for Shopware"
  }
},

to:


"extra": {
  "shopware-plugin-class": "Swag\\Example",
  "copyright": "(c) by shopware AG",
  "label": {
    "de-DE": "Example Produkte für Shopware",
    "en-GB": "Example Products for Shopware"
  }
},

Changelogs

The en-GB changelog file still is: CHANGELOG.md.

The format for all other locales changed from CHANGELOG-??_??.md to CHANGELOG_??-??.md. For example a german changelog file changed from CHANGELOG-de_DE.md to CHANGELOG_de-DE.md.

Locale Repository

If you use the locale repository inside your code, the locale codes will now return in the new format.

Snippet files of all modules

We renamed all snippet files, from en_GB.json to en-GB.json.

For consistency, you should do the same in your plugins.

2019-04-18: Document title using VueMeta

We just implemented VueMeta 1.6.0 to shopware!

For now, it's only used to configure dynamic document titles in addition to the recently implemented favicons per module and will be added to every, already implemented module. Please be sure to add it to every new module! Additonally: Every Moduels name property has been refactored in the style of its identifier. (If its sw-product-stream the name now is product-stream.)

To provide more detailed information, we added the this.$createTitle() method to get an easily generated document title like Customers | Shopware administration or Awesome Product | Products | Shopware administration

Therefore every Module should set a title property with a snippet for its in the modules index.js:

Module.register('sw-product', {
    name: 'sw-product.general.mainMenuItemGeneral',
    ...

And also add the metaInfo property on every pagesindex.js:

...
metaInfo() {
    return {
        title: this.$createTitle()
    };
},

computed: {
    ...

Alternativly for every detail page add an identifier (e.g. using the placeholder mixin to ensure fallback-translations):

...
mixins: [
    Mixin.getByName('placeholder')
],

metaInfo() {
    return {
        title: this.$createTitle(this.identifier)
    };
},

computed: {
    identifier() {
        return this.placeholder(this.product, 'name');
    },
},
...

The $createTitle(String = null, ...String) method uses the current page component to read its module title. The first parameter should be used in detail pages to also display its identifier like the product name or a full customer name to add it to the title. Every following parameter is fully optional and not in use yet, but if used would be added to the title in the same fashion.

2019-04-18: Storefront page ajax

To secure the Storefront we made every Controller-Action inside the StorefrontBundle not requestable via XmlHttpRequests/AJAX.

You can override this by allowing XmlHttpRequests in the Route Annotation

with the defaults={"XmlHttpRequest"=true} Option.

Example:

/**
@Route("/widgets/listing/list/{categoryId}", name="widgets_listing_list", methods={"GET"}, defaults={"XmlHttpRequest"=true})
*/
public function listAction(Request $request, SalesChannelContext $context): JsonResponse

For more Examples take a look inside the PageletControllers.

2019-04-18: SalesChannel Entity definition

We impemented a central way to define entities which should be available over the sales-channel-api.

An EntityDefinition can now define a decoration definition for the sales-channel-api:

<?php
// ...
class ProductDefinition extends EntityDefinition
{
    public static function getEntityName(): string
    {
        return 'product';
    }

    public static function getSalesChannelDecorationDefinition()
    {
        return SalesChannelProductDefinition::class;
    }
}

Declaring the SalesChannelDefinition

A decorating sales channel definition for an entity should extend the original definition class.

<?php declare(strict_types=1);

namespace Shopware\Core\Content\Product\SalesChannel;

class SalesChannelProductDefinition 
    extends ProductDefinition 
    implements SalesChannelDefinitionInterface
{
    use SalesChannelDefinitionTrait;
}

These declaration allows to replace different functionalities for an entity:

  • Rewriting the storage - getEntityName()
    • Example usage: I want to denormalize my entities in a different table for better performance
  • Rewriting the DTO classes - getEntityClass getEntityCollection
    • Example usage: I want to provide some helper functions or more properties in the storefront
  • Adding or removing some fields - defineFields
    • Example usage: I can add more fields for an entity which will be displayed in a storefront or in other clients

Association Handling

It is import to override the defineFields function to rewrite association fields with their sales channel decoration definition:

protected static function defineFields(): FieldCollection
{
    $fields = parent::defineFields();

    self::decorateDefinitions($fields);

    return $fields;
}

This decoration call replaces all entity definition classes of the defined association fields with the decorated definition.

Basic filters

Additionally by implementing the \Shopware\Core\System\SalesChannel\Entity\SalesChannelDefinitionInterface interface the developer has the opportunity to add some basic filters if the entity will be fetched for a sales channel:

public static function processCriteria(
    Criteria $criteria, 
    SalesChannelContext $context
) : void {
    $criteria->addFilter(
        new EqualsFilter('product.active', true)
    );
}

Reigster the definition

Like the EntityDefinition classes the Definition classes for sales channel entities has to be registered over the Dependency Injection Container by tagging the definition with the shopware.sales_channel.entity.definition tag.

<service 
        id="Shopware\Core\Content\Product\SalesChannel\SalesChannelProductDefinition">

<tag name="shopware.sales_channel.entity.definition" entity="product"/>

</service>

Repository registration

Like the entity repository for the DAL, the each registered sales channel entity definition gets an own registered repository. The repository is registered like the original entity definition but with an additional sales_channel. prefix:

Example: sales_channel.product.repository

The registered class is an instance of \Shopware\Core\System\SalesChannel\Entity\SalesChannelRepository.

This repository provides only a read functions:

public function search(
    Criteria $criteria, 
    SalesChannelContext $context
) : EntitySearchResult

public function aggregate(
    Criteria $criteria, 
    SalesChannelContext $context
) : AggregatorResult

public function searchIds(
    Criteria $criteria, 
    SalesChannelContext $context
) : IdSearchResult

Api Routes

All registered sales channel definitions has registered api routes: (example for product)

sales-channel-api.product.detail
    /sales-channel-api/v{version}/product/{id}

sales-channel-api.product.search-ids
    /sales-channel-api/v{version}/search-ids/product

sales-channel-api.product.search
    /sales-channel-api/v{version}/product

Event Registration

Entities which loaded for a sales channel has own events. All Events of the Data Abstraction Layer are prefixed with sales_channel. (Example for product):

  • sales_channel.product.loaded
  • sales_channel.search.result.loaded
  • sales_channel.product.id.search.result.loaded
  • sales_channel.product.aggregation.result.loaded

2019-04-17: Refactored rules match function

The Rules were refactored, so that the match function not longer returns a reason object which contains the debug messages. Instead the match function directly returns a bool if the rule is matching or not.

2019-04-17: Internal request removed

The InternalRequest class alternative to the Symfony Request has been removed as it is not required anymore.

To check required parameters etc. use the Symfony Request or even better the RequestDataBag or QueryDataBag and validate your input using the DataValidator. You can see some examples in the AccountService.

2019-04-17: Administration open API Housekeeping

We are currently working on a lot of housekeeping tasks to make the administration code base as clean as possible. It should be easier for new developers to spot best practices inside the code. This is why we have to adjust some general things like events.

This changes are not merged yet! We will write more update logs when some of the mentioned topics are inside the master branch.

Here are the most important changes:

Custom Vue events

  • All custom Vue events will be kebab-case.
  • This is also a Vue.js best practice: https://vuejs.org/v2/guide/components-custom-events.html#Event-Names camelCase and snake_case are not allowed.
  • We don't put the whole component name inside the event name any longer. The event can only be used on the component itself in most cases. So there should be no duplicate issues whatsoever. For more complex "flows" you can add names like "folder-item" or "selection" inside your event name.
  • The event name itself should follow this order: object -> prefix -> action

For example:

// product (object)
// before (prefix)
// load (action)

this.$emit('product-before-load');

Object and prefix are only needed in more complex scenarios when more events are involved in one context. E.g. events for "folders" and "products" being called on a single component. When you just want to trigger a single save action on one small component a simple 'save' as an event name should be fine.

More examples:

// Bad
this.$emit('itemSelect'); // No camel case
this.$emit('item_select'); // No snake case
this.$emit('item--select'); // No double dash
this.$emit('sw-component-item-select'); // No component names
this.$emit('select-item'); // Object always before action

// Good
this.$emit('item-select');

/* ----------------------- */

// Bad
this.$emit('folder-saving');
this.$emit('column-sorting');

// Good
this.$emit('folder-save');
this.$emit('folder-sort');

/* ----------------------- */

// Bad
this.$emit('customer-saved'); // No past tense

// Good
this.$emit('customer-save')
this.$emit('customer-finish-save'); // Or use success prefix instead

/* ----------------------- */

// Bad
this.$emit('on-save'); // No filler or stateul words like "on" or "is"

// Good
this.$emit('save')

SCSS Variables

We will remove the scoped / re-mapped color variables from all components. From now on you can use the color variables directly. Component specific variables should only be used when you really have multiple usages of a value e.g. "$sw-card-base-width: 800px".

We decided to remove this pattern because plugin developers are not able to override those variables anyway. For us internally the benefit isn't that great because we are not changing component colors all the time.

SCSS Code style

Improve and fix some more code style rules:

  • Only use spaces, not tabs
  • Always indent 4 spaces
  • No !important when possible
  • No camelCase or snake_case in selectors

JS/Vue.js Code style and housekeeping

  • Empty lines after methods and props
  • No methods with complex logic inside "data"
  • Remove unused props
  • Remove default value for required props
  • Check usage of methods for lifecycle hooks (createdComponent etc.)

2019-04-16: Configuration files

For the extraction of a production-ready community edition template, some configuration variables will be moved into the platform.

The routing configuration has already been moved into the bundles of the platform and they will be registered automatically. You can find the configuration file in the same folder structure like our plugins: Resources/config/route.xml

More configuration files will most likely follow in the near future.

2019-04-16: Composer dependencies

To ensure every bundle inside our mono-repository can be used standalone, their dependencies in the bundles composer.json must be maintained. Therefore we no longer update the platform's composer.json manually, except for metadata updates.

There is a new script ran as pre-commit hook, which collects every dependency of the bundles and merges them into the platforms composer.json. If the script notices any difference, you'll get a warning and have to review the changes:

ERROR! The platform composer.json file has changed. Please review your commit and add the changes.

2019-04-15: Storefront-API is now SalesChannel-API

For plausibility reasons we removed the Storefront term from the core bundles and named it SalesChannel. The idea beeing:

The Core knows about sales channels and exposes an API for SalesChannels. All customer facing applications then connect to this SalesChannel-API.Doesn't matter whether its a fully featured store front, a buy button, something with voice or whatever.

Therefore:

  • All former storefront-controllers now reside in a SalesChannel Namespace as SalesChannel controllers
  • The api is now under .../sales-channel-api/...
  • A test exists to secure this

2019-04-11: Roadmap update

Here you will find a current overview of the epics that are currently being implemented, which have been completed and which will be implemented next.

Open
Work on these Epics has not yet begun.

  • Sales Channel

Next
These epics are planned as the very next one

  • CLI and Web Installer

In Progress
These Epics are in the implementation phase

  • Theme System
  • SEO Basics
  • Products
  • Core Settings
  • Promotions
  • Plugin Manager
  • Variants / Properties
  • Shipping / Payment
  • Import / Export
  • Mail Templates
  • Storefront API / Page, Pagelets
  • CMS
  • Categories / Navigation

Review
All Epics listed here are in the final implementation phase and will be reviewed again.

  • Backend Search
  • Rule Builder
  • Plugin System
  • Product Streams
  • Newsletter Integeration
  • Custom Fields
  • Documents
  • User

Done
These epics are finished

  • Tags
  • Customer
  • Number Ranges
  • User Profile
  • Snippets
  • Media Manager
  • Order
  • Content Translations
  • Supplier
  • Background processes

2019-04-09: Product table renaming

We renamed the following tables as follow:

  • product.variatios => product.options
    • Relation between variants and their options which used for the generation. *product.configurators => product.configuratorSettings
    • Relation between products and the configurator settings. This table are used for the administration configurator wizard
  • product.datasheet => product.properties
    • Relation between products and their property options. This options are not related to physical variants with own order numbers
  • configuration_group => property_group
    • Defines a group for possible options like color, size, ...
  • configuration_group_option => property_group_option

All related api routes and associations are renamed too:

  • /api/v1/property-group
  • /api/v1/property-group-option
  • /api/v1/product/{id}/options
  • ...

Detail changes can be found here: https://github.com/shopware/platform/commit/1d8af890792df21bed13ef94afa1ac684d6d7f7d

2019-04-09: Plugin structure refactoring

We made a refactoring of the plugin structure which affect ALL plugins!

  • The "type" in the composer.json must now be shopware-platform-plugin. This is necessary to differentiate between Shopware 5 and Shopware platform plugins
  • You now have to provide the whole FQN of your plugin base class in the composer.json. Add something like this to the "extra" part of the composer.json: "shopware-plugin-class": "SwagTest\\SwagTest", The old identifier installer-name is no longer used
  • You now have to provide valid autoload information about your plugin with the composer.json:
"autoload": {
    "psr-4": {
        "SwagTest\\": ""
    }
}

This give also the opportunity to do something like this:

"autoload": {
    "psr-4": {
        "Swag\\Test\\": "src/"
    }
}

Which should really tidy up the root directory of a plugin

  • If you want to provide a plugin icon, you have to specify the path of the icon relative to your plugin base class in the composer.json. Add a new field to the "extra" part of the composer.json: "plugin-icon": "Resources/public/plugin.png",
  • We introduced a default path for the plugin config file. It points to Resources/config/config.xml relative from your plugin base class. So if you put your config there, Shopware will automatically generated a configuration form for your plugin. If you want another path, just overwrite the \Shopware\Core\Framework\Bundle::getConfigPath method
  • We introduced some more defaults path which could all be changed by overwriting the appropriate method. The "Resources" directory is always relative to the base class of your plugin
    • Resources/config/services.xml path to your default services.xml to register your custom services
    • [Resources/views] Array of views directorys of your plugin
    • Resources/adminstration the location of your administration files and entry point of extensions of the administration
    • Resources/storefront same for the storefront
    • Resources/config/ directory which will be used to look for route config files

All in all, the composer.json should contain descriptive information and the plugin base class the runtime configuration

2019-04-08: Unique default ids

We made all IDs defined in Shopware\Core\Defaults.php unique, so the Ids changed.

If you experience some problems with logging in to the Admin after rebasing your branch please check the localStorage for the key sw-admin-current-language and delete this key.

After that it should work as before.

2019-04-08: Jest as testing framework (admin)

We made the shift from Karma (including Chai, Jasmine and Sinon) to Jest as our primary JavaScript testing framework. Jest provides us with a lot of functionality:

  • Snapshot testing using the offical @vue/test-utils tool
  • Mocks for ES6 classes, Timers including automatic mocking & clearing
  • Spies are part of Jest, we don't need a separate framework anymore
  • Running tests in parallel
  • Debugging Support using Chrome Inspect
  • Code Coverage report using Istanbul with a Clover Report + inline in the terminal

Documentation links:

The existing tests have been converted to Jest' Matcher API using https://github.com/skovhus/jest-codemods

The test specs can be found in Administration/Resources/administration/test

Running tests

./psh.phar administration:unit
./psh.phar administration:unit-watch # Watch mode

Snapshot Testing

snapshot testing

Snapshot tests are a very useful tool whenever you want to make sure your UI does not change unexpectedly. It's supported using @vue/test-utils:

import { shallowMount } from '@vue/test-utils';
import swAlert from 'src/app/component/base/sw-alert';

it('should render correctly', () => {
    const title = 'Alert title';
    const message = '<p>Alert message</p>';

    const wrapper = shallowMount(swAlert, {
        stubs: ['sw-icon'],
        props: {
            title
        },
        slots: {
            default: message
        }
    });
    expect(wrapper.element).toMatchSnapshot();
});

Snapshots are specialized files from Jest which are representing the actual DOM structure. If a refactoring changes the DOM structure unintentionally, the test will fail. The developer can either update the snapshot to reflect the new DOM structure when the structure change was intended or fix the structure until the test passes again.

Shallow Mounting

Please consider prefering shallowMount instead of mount. Shallow mounting a component lets you stub additional components, fill slots, set props etc. Here's the documentation: https://vue-test-utils.vuejs.org/api/#shallowmount

Vue Router Support

If your compomnent is using router-link or router-view, you can simply stub them:

import { shallowMount } from '@vue/test-utils'

shallowMount(Component, {
    stubs: ['router-link', 'router-view']
});

INSTALLING VUE ROUTER FOR A TEST

import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'

const localVue = createLocalVue()
localVue.use(VueRouter)

shallowMount(Component, {
    localVue
});

MOCKING $ROUTE

import { shallowMount } from '@vue/test-utils'

const $route = {
    path: '/some/path'
};

const wrapper = shallowMount(Component, {
    mocks: {
      $route
    }
});

console.log(wrapper.vm.$route.path);

Triggering events

const wrapper = shallowMount(Component);

wrapper.trigger('click');

// With options
wrapper.trigger('click', { button: 0 })

2019-04-08: Association loading

With the latest DAL change, you are no longer able to auto-load toMany associations as they have a huge performance impact. From now on, please enrich your criteria object by adding associations like:

$criteria->addAssociation('comments');

Please think about when to load toMany associations and if they are really necessary there.

Every toOne association will be fetched automatically unless you've disabled it. Some fields like the ParentAssociationField are disabled by default because they may lead to a circular read operation.

AssociationInterface

The AssociationInterface has been removed in favor of the abstract AssociationField class because there were some useless type-hints in the code and it just feels right now.

2019-04-05: Favicons for each module

It is now possible to define a favicon for each module in the administration. The favicon, which is just a .png version of the module-icon, is switched dynamically depending on what module is active at the moment. Currently there are 7 favicons that are located in administration/static/img/favicon/modules/.

When no favicon is defined for the module the default shopware signet is used as a fallback.

The favicon can be defined in the module registration.

Module.register('sw-category', {
    name: 'Categories',
    icon: 'default-package-closed',
    favicon: 'icon-module-products.png'
});

2019-04-05: Renamed CheckoutContext to SalesChannelContext

We renamed the CheckoutContext to SalesChannelContext and moved Checkout\Context to `System\SalesChannelConte

In perspective it is planned to

  • move all SalesChannel related classes from Framework to System\SalesChannel
  • Rename the StoreFront files in the Core to SalesChannel
  • Rename the API-Routes
  • but keep the Controllers / Services / Repositories in the corresponding domain modules

As always: sorry for the inconvenience!

2019-04-04: Refactored viewData (Breaking change)

We have completely removed the Entity::viewData property.

Why was it removed?

  • ViewData was always available in the json response under meta. However, this led to deduplication becoming obsolete.
  • It had a massive impact on the hydrator performance


What was viewData needed for?

  • Generally this was needed for translatable fields. The name from the language inheritance was available under viewData.name.
  • Furthermore, this was also used for the parent-inheritance (currently used only for products). If a varaint has no own assigned manufacturer, the manufacturer of the parent should be available. Under viewData.manufacturer therefore the manufacturer of the father product was available

How do I get this information now?

  • Translate fields are now available under the translated. The values listed there were determined using the language inheritance.
  • The context object contains a switch "considerInheritance". This can be sent via api as header (sw-inheritance) to consider the inheritance in search and read requests.

This value is initialized for the following routes as follows

/api Default false
/sales-channel-api Default true
twig-frontend Default true

2019-04-03: Rename product_price_rule to product_price

We changed the naming of product_price_rule table and all corresponding php classes, api routes, php properties.

The new name of the table is product_price.

This is reflected in the corresponding api routes:

  • /api/v1/product-price
  • /api/v1/product/{id}/prices

2019-04-03: LESS has been removed

  • LESS has been removed from the administration source code
  • The duplicated LESS base variables are no longer available. Components LESS which uses base variables will not be functional.
  • Please do not use LESS inside core components any longer because it is also no longer supported by the component library.
  • However the package.json dependency has not been completely removed. External plugins should still have the posibility to use LESS. But we will recommend SCSS in our documentation.
  • Some documentation markdown files may still include LESS examples. Those will be edited by the documentation squad soon.

2019-04-03: Admin scaffolding components

With the new data handling, we implemented a list of scaffolding components to prevent boiler plate code and keep data handling as simple as possible.

The following components are now available:

  • sw-entity-listing
  • sw-entity-multi-select
  • sw-entity-single-select
  • sw-one-to-many-grid

This components are related to different use cases in an administration module.

sw-entity-listing
A decoration of the sw-data-grid which can be used as primary listing component of a module. All functions of the sw-data-grid are supported.

Additionally configuration for the sw-entity-listing component are:

  • `repository [required] - Repository
    • Provide the source repository where the data can be loaded. All operations are handled by the component itself. Pagination, sorting, row editing are supported and handled out of the box.
  • items [required] - SearchResult
    • The first result set must be provided in order to avoid unnecessary server request when the initial load must contain certain logics.
  • detailRoute [optional| - String
    • allows to define a route for a detail page. If set the grid creates a edit action to open the detail page

import { Component } from 'src/core/shopware';
import Criteria from 'src/core/data-new/criteria.data';
import template from './sw-show-case-list.html.twig';

Component.register('sw-show-case-list', {
    template,
    inject: ['repositoryFactory', 'context'],
    
    data() {
        return {
            repository: null,
            products: null
        };
    },
    
    computed: {
        columns() {
            return this.getColumns();
        }
    },
    
    created() {
        this.createdComponent();
    },
    
    methods: {
        createdComponent() {
            this.repository = this.repositoryFactory
                .create('product', '/product');
        
            return this.repository
                .search(new Criteria(), this.context)
                .then((result) => {
                    this.products = result;
                });
        },
        
        getColumns() {
            return [{
                property: 'name',
                dataIndex: 'name',
                label: this.$tc('sw-product.list.columnName'),
                routerLink: 'sw.show.case.detail',
                inlineEdit: 'string',
                allowResize: true,
                primary: true
            }];
        }
    }
});

<sw-page>
    <template slot="content">
    
    <sw-entity-listing v-if="products"
                        :items="products"
                        :repository="repository"
                        :columns="columns"
                        detailRoute="sw.show.case.detail" />
    
    </template>
</sw-page>


sw-one-to-many-grid
A decoration of the sw-data-grid which can be used (As the name suggested) to display OneToMany association in a detail page. All functions of the sw-data-grid are supported.

Additionally configuration for the sw-one-to-many-grid component are:

  • collection [required] - EntityCollection
    • Provide the association collection for this grid. The grid uses it to detect the entity schema and the api route where the data can be loaded or processed.
  • localMode [optional] - Boolean - default false
    • If set to false, the grid creates a repository (based on the collection data) and sends all changes directly to the server.
    • If set to true, the grid works only with the provided collection. Changes (delete, update, create) are not send to the server directly - they will be only applied to the provided collection. Changes will be saved with the parent record.

<sw-one-to-many-grid slot="grid"
                    :collection="product.prices"
                    :localMode="record.isNew()"
                    :columns="priceColumns">

</sw-one-to-many-grid>


sw-entity-single-select
A decoration of sw-single-select. This component is mostly used for ManyToOne association where the related entity can be linked but not be modified (like product.manufacturer, product.tax, address.country, ...). All functions of the sw-single-select are supported.

Additionally configuration for the sw-entity-single-select component:

  • entity [required] - String
    • Provide the entity name like product, product_manufacturer. The component creates a repository for this entity to display the available selection.
  •  

<sw-entity-single-select 
    label="Entity single select" 
    v-model="product.manufacturerId" 
    entity="product_manufacturer">
</sw-entity-single-select>


sw-entity-multi-select
A decoration of sw-multi-select. This component is mostly used for ManyToMany asociation where the related entity can be linked multiple times but not be modified (like product.categories, customer.tags, ...).

All functions of the sw-multi-select are supported.

Additionally configuration for the sw-entity-multi-select component:

  • collection [required] - EntityCollection
    • Provide an entity collection of an association (Normally used for ManyToMany association). The component creates a repository based on the collection api source and entity schema. All CRUD operations are handled inside the component and can easly be overridden in case of handling the request by yourself.

<sw-entity-multi-select 
    label="Entity multi select for product categories" 
    :collection="product.categories">
</sw-entity-multi-select>

    2019-04-02: New admin data handling

    The new data handling was created to remove the active record pattern in the admininstration. It uses a repository pattern which is strongly based on the DAL from the PHP part.

    Relevant classes

    • Repository
      • Allows to send requests to the server - used for all CRUD operations
    • Entity
      • Object for a single storage record
    • Entity Collection
      • Enable object-oriented access to a collection of entities
    • Search Result
      • Contains all information available through a search request
    • RepositoryFactory
      • Allows to create a repository for an entity
    • Context
      • Contains the global state of administration (Language, Version, Auth, ...)
    • Criteria
      • Contains all information for a search request (filter, sorting, pagination, ...)

    Get access to a repository
    To create a repository it is required to inject the RepositoryFactory:

    
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory'],
        
        created() {
            this.repository = this.repositoryFactory.create('product');
        }
    });


    How to fetch listings
    To fetch data from the server, the repository has a search function. Each repository function requires the admin context. This can be injected like the repository factory:

    
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            // create a repository for the `product` entity
            this.repository = this.repositoryFactory.create('product');
        
            this.repository
                .search(new Criteria(), this.context)
                .then((result) => {
                    this.result = result;
                });
        }
    });


    Working with the criteria class
    The new admin criteria class contains all functionalities of the php criteria class.

    
    Component.register('sw-show-case-list', {
        created() {
            const criteria = new Criteria();
            criteria.setPage(1);
        
            criteria.setLimit(10);
        
            criteria.setTerm('foo');
        
            criteria.setIds(['some-id', 'some-id']);
        
            criteria.setTotalCountMode(2);
        
            criteria.addFilter(
                Criteria.equals('product.active', true)
            );
        
            criteria.addSorting(
                Criteria.sort('product.name', 'DESC')
            );
        
            criteria.addAggregation(
                Criteria.avg('average_price', 'product.price')
            );
        
            const categoryCriteria = new Criteria();
            categoryCriteria.addSorting(
                Criteria.sort('category.name', 'ASC')
            );
        
            criteria.addAssociation('product.categories', categoryCriteria);
        }
    });


    How to fetch a single entity

    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.repository = this.repositoryFactory.create('product');
        
            const id = 'a-random-uuid';
        
            this.repository
                .get(entityId, this.context)
                .then((entity) => {
                    this.entity = entity;
                });
        }
    });
    Update an entity
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.repository = this.repositoryFactory.create('product');
        
            const id = 'a-random-uuid';
        
            this.repository
                .get(entityId, this.context)
                .then((entity) => {
                    this.entity = entity;
                });
        },
        
        // a function which is called over the ui
        updateTrigger() {
            this.entity.name = 'updated';
        
            // sends the request immediately
            this.repository
                .save(this.entity, this.context)
                .then(() => {
        
                    // the entity is stateless, the new data has be fetched from the server, if required
                    this.repository
                        .get(entityId, this.context)
                        .then((entity) => {
                        this.entity = entity;
                    });
                });
        }
    });


    Delete an entity

    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.repository = this.repositoryFactory.create('product');
        
            this.repository.delete('a-random-uuid', this.context);
        }
    });
    Create an entity
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.repository = this.repositoryFactory.create('product');
        
            this.entity = this.productRepository.create(this.context);
        
            this.entity.name = 'test';
        
            this.repository.save(this.entity, this.context);
        }
    });


    Working with associations
    Each association can be accessed via normal property access:

    
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.repository = this.repositoryFactory.create('product');
        
            const id = 'a-random-uuid';
        
            this.repository
                .get(entityId, this.context)
                .then((product) => {
                    this.product = product;
        
                    // ManyToOne: contains an entity class with the manufacturer data
                    
                    console.log(this.product.manufacturer);
        
        
                    // ManyToMany: contains an entity collection with all categories.
                    // contains a source property with an api route to reload this data (/product/{id}/categories)
                    
                    console.log(this.product.categories);
        
        
                    // OneToMany: contains an entity collection with all prices
                    // contains a source property with an api route to reload this data (/product/{id}/priceRules)
                    
                    console.log(this.product.priceRules);
                });
        }
    });


    Set a ManyToOne

    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.manufacturerRepository = this.repositoryFactory.create('manufacturer');
        
            this.manufacturerRepository
                .get('some-id', this.context)
                .then((manufacturer) => {
        
                    // product is already loaded in this case
                    this.product.manufacturer = manufacturer;
        
                    // only updates the foreign key for the manufacturer relation
                    
                    this.productRepository
                        .save(this.product, this.context);
                });
        }
    });


    Working with lazy loaded associations
    In most cases, ToMany assocations are loaded over an additionally request. Like the product prices are fetched when the prices tab will be activated.

    Working with OneToMany associations

    
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.productRepository = this.repositoryFactory.create('product');
        
            this.productRepository
                .get('some-id', this.context)
                .then((product) => {
                    this.product = product;
        
                    this.priceRepository = this.repositoryFactory.create(
                        // `product_price`
                        this.product.prices.entity,
                        
                        // `product/some-id/priceRules`
                        this.product.prices.source
                    );
                });
        },
        
        loadPrices() {
            this.priceRepository
                .search(new Criteria(), this.context)
                .then((prices) => {
                    this.prices = prices;
                });
        },
        
        addPrice() {
            const newPrice = this.priceRepository.create(this.context);
        
            newPrice.quantityStart = 1;
            // update some other fields
        
            this.priceRepository
                .save(newPrice, this.context)
                .then(this.loadPrices);
        },
        
        deletePrice(priceId) {
            this.priceRepository
                .delete(priceId, this.context)
                .then(this.loadPrices);
        },
        
        updatePrice(price) {
            this.priceRepository
                .save(price, this.context)
                .then(this.loadPrices);
        }
    });


    Working with ManyToMany associations

    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.productRepository = this.repositoryFactory.create('product');
        
            this.productRepository
                .get('some-id', this.context)
                .then((product) => {
                    this.product = product;
        
                    // creates a repository which working with the associated route
                    
                    this.catRepository = this.repositoryFactory.create(
                        // `category`
                        this.product.categories.entity,
                    
                        // `product/some-id/categories`    
                        this.product.categories.source
                    );
                });
        },
        
        loadCategories() {
            this.catRepository
                .search(new Criteria(), this.context)
                .then((categories) => {
                    this.categories = categories;
                });
        },
        
        addCategoryToProduct(category) {
            this.catRepository
                .assign(category.id, this.context)
                .then(this.loadCategories);
        },
        
        removeCategoryFromProduct(categoryId) {
            this.catRepository
                .delete(categoryId, this.context)
                .then(this.loadCategories);
        }
    });


    Working with local associations
    In case of a new entity, the associations can not be send directly to the server using the repository, because the parent association isn't saved yet.

    For this case the association can be used as storage as well and will be updated with the parent entity.

    In the following examples, this.productRepository.save(this.product, this.context) will send the prices and category changes.

    Working with local OneToMany associations

    
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.productRepository = this.repositoryFactory.create('product');
        
            this.productRepository
                .get('some-id', this.context)
                .then((product) => {
                    this.product = product;
        
                    this.priceRepository = this.repositoryFactory.create(
                        // `product_price`
                        this.product.prices.entity,
                        
                        // `product/some-id/priceRules`
                        this.product.prices.source
                    );
                });
        },
        
        loadPrices() {
            this.prices = this.product.prices;
        },
        
        addPrice() {
            const newPrice = this.priceRepository
                .create(this.context);
        
            newPrice.quantityStart = 1;
            // update some other fields
        
            this.product.prices.add(newPrice);
        },
        
        deletePrice(priceId) {
            this.product.prices.remove(priceId);
        },
        
        updatePrice(price) {
            // price entity is already updated and already assigned to product, no sources needed
        }
    });


    Working with local ManyToMany associations

    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.productRepository = this.repositoryFactory.create('product');
        
            this.productRepository
                .get('some-id', this.context)
                .then((product) => {
                    this.product = product;
        
                    // creates a repository which working with the associated route
                    
                    this.catRepository = this.repositoryFactory.create(
                        // `category`
                        this.product.categories.entity,
                        
                        // `product/some-id/categories`
                        this.product.categories.source
                    );
                });
        },
        
        loadCategories() {
            this.categories = this.product.categories;
        },
        
        addCategoryToProduct(category) {
            this.product.categories.add(category);
        },
        
        removeCategoryFromProduct(categoryId) {
            this.product.categories.remove(categoryId);
        }
    });


    Working with version
    The new data handling supports the php DAL versioning too. This allows the user to make changes that are not applied directly to the live shop. This is required when content such as products, CMS pages, orders are processed where the user needs the possibility to revert the changes.

    
    Component.register('sw-show-case-list', {
        inject: ['repositoryFactory', 'context'],
        
        created() {
            this.productRepository = this.repositoryFactory.create('product');
        
            this.entityId = 'some-id';
        
            this.productRepository
                .createVersion(this.entityId, this.context)
                .then((versionContext) => {
                    // the version context contains another version id
                    this.versionContext = versionContext;
                })
                .then(() => {
                    // association has a reference to this version context
                    return this.productRepository
                        .get(this.entityId, this.versionContext);
                })
                .then((entity) => {
                    this.product = entity;
                    return entity;
                });
        },
        
        cancel() {
            this.productRepository.deleteVersion(this.entityId, this.versionContext.versionId, this.versionContext);
        },
        
        merge() {
            this.productRepository
                .save(this.product, this.versionContext)
                .then(() => {
                    this.productRepository.mergeVersion(
                        this.versionContext.versionId, 
                        this.versionContext
                    );
            });
        }
    });

    2019-04-02: Default constants removed

    We removed a ton of constants from our super global Defaults-object.

    Please rebase your branch and run phpstan to check that you don't use any of the removed constants.

    If you use some stateMachineConstants ->
    They are moved to its own classes:

    OrderStates

    • Shopware\Core\Checkout\Order\Aggregate\OrderDelivery\OrderDeliveryStates
    • Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionStates
    • Shopware\Core\Checkout\Order\OrderStates

     If you used some other constants, you have to replace them by a query to get the correct Id

    2019-04-01: sw-icon update

    The icon system in the administration has been updated.

    Please execute administration:install to install new dependencies.

    Usage
    The open API of the <sw-icon> component has not been changed. You can use it as before.

    Adding or updating icons

    • All SVG icons can now be found in /platform/src/Administration/Resources/administration/src/app/assets/icons/svg as separate files.
    • TLDR: To add a new icon simply add the icon SVG file to the mentioned direcory.
    • All icons have to be prefixed with icons-.
    • The file names come from the directory structure of our design library. The export via Sketch automatically gives us a file name like icons-default-action-bookmark.
    • Please keep in mind that these icons are the core icons. Do never add random icons from the web or stuff like that! We always receive the icons from the design department with properly optimized SVG files. When you need a completely new core icon please talk to the design department first.
    • All icons from this directory are automatically registered as small functional components which are automatically available when using <sw-icon name="your-icon-name">. The component gets its name from the SVG file name. This is why a correct name is really important.
    • When updating an icon simply override the desired SVG file.

    Icon demo

    • New demo: https://component-library.shopware.com/#/icons/
    • The icon demo is now part of the component library. It can also be found at the very bottom of the main menu. This is the source of truth from now on.
    • No more separate demos for default and multicolor icons.
    • The icon demo gets updated automatically when icons are added, removed or updated.

    Chrome bug
    The icon bug in Google Chrome has been fixed. The SVG's source code is now directly inside the document. The use of an external SVG sprite is no longer in place. This caused the rendering issues under some circumstances in Vue.

    Why we made this change

    • Easier workflow to add or update icons
    • Inline SVGs do fix the Chrome bug
    • No more dependencies of third party grunt tasks to generate the icon sprite
    • No grunt dependency to build the icon demo.
    • No extra request from the browser to get the icon sprite
    • No extra repository required

    2019-04-01: Payment handler exception

    Payment handler are now able to throw special exceptions if certain error cases occur.

    • Shopware\Core\Checkout\Payment\Cart\PaymentHandler\SynchronousPaymentHandlerInterface::pay should throw the SyncPaymentProcessException if something goes wrong.
    • Same for the Shopware\Core\Checkout\Payment\Cart\PaymentHandler\AsynchronousPaymentHandlerInterface::pay Throw an AsyncPaymentProcessException e.g. if a call to an external API fails
    • The finalize method of the AsynchronousPaymentHandlerInterface could also throw an AsyncPaymentFinalizeException. Additionally it could throw a CustomerCanceledAsyncPaymentException if the customer canceled the process on the payment provider page.

    In every case, Shopware catches these exceptions and set the transaction state to canceled before the exceptions are thrown again. So a caller of the Shopware pay API route will get an exception message, if something goes wrong during the payment process and could react accordingly.

    Soonish it will be possible to transform the order into a cart again and let the customer update the payment method or something like that. Afterwards the order will be updatet und persisted again.

    Have a look at the Docs or at our PayPal Plugin for examples

    March 2019

    2019-03-29: Exception Locations

    Just removed the last of the global exceptions. From now on, please move custom exceptions into the module that throws it.

    For example:

    • Shopware\Core\Checkout\Cart\Exception
    • Shopware\CoreFrmaework\DataAbstractionLayer\Exception

    Not

    • Shopware\Core\Checkout\Exception
    • Shopware\Core\Content\Exception


    In Perspective all Exception will move to a \Exception Folder, so pleas do no longer put them inline with the executing classes

    FYI: There is a test that checks this :zwinkern:

    2019-03-29: Backend UUID

    The Uuid class was moved from FrameworkStruct\Uuid to Framework\Uuid\Uuid please adjust your branches.

    Changes:

    • The new class does no longer support a ::uuid4() please use ::randomHex() or ::randomBytes() instead
    • The string format (with the dashes like 123456-1234-1234-1234-12345679812) is no longer supported, methods are removed
    • The Exceptions moved to Framework\Uuid\Exception 


    Backwards Compatibility:

    You can still use the old class, but it is deprecated and will be removed next week.

    2019-03-28: Validation / input validation

    #1 Request / Query data
    Request data from either the body (POST) or from the query string (GET) is now wrapped in a DataBag. It's an extension to Symfony's ParameterBag with some sugar for arrays. This allows you to access nested arrays more fluently:

    
    // before:
    $bag->get('billingAddress')['firstName']
    
    

     

    
    // after:
    $bag->get('billingAddress')->get('firstName')


    To prevent boilerplate code like new DataBag($request->request->all()); you can type-hint the controller arguments to either RequestDataBag and QueryDataBag which automatically creates a DataBag with the data from the request.

    #2 DataValidation
    We leverage Symfony's validation constraints for our input validation. They already implement a ton of constraints like NotBlank, Length or Iban and they provide a documented way on how to add custom constraints.

    #2.1 DATA VALIDATION DEFINITION
    We've introduced a DataValidationDefinition which contains the validation constraints for a given input.

    Example

    
    $definition = new DataValidationDefinition('customer.update');
    $definition->add('firstName', new NotBlank())
        ->add('email', new NotBlank(), new Email())
        ->add('salutationId', new NotBlank(), new EntityExists('entity' => 'salutation', 'context' => $context));
        
    // nested validation
    $billingValidation = new DataValidationDefinition('billing.update');
    $billingValidation->add('street', new NotBlank());
    
    $definition->addSub('billingAddress', $billingDefinition);
    You can now pass the definition with your data to the DataValidator which does the heavy lifting.
    
    // throws ConstraintViolationException
    $this->dataValidator->validate($bag->all(), $definition);
    
    // gets all constraint violations
    $this->dataValidator->getViolations($bag->all(), $definition);


    #2.2 EXTENDING EXISTING/RECURRING VALIDATION DEFINITIONS
    If you need the same validation over and over again, you should consider a ValidationService class which implements the ValidationServiceInterface. This interface provides to methods for creating and updating recurring input data, like addresses.

    You may decorate the services but we prefer the way using events. So the calling class should throw an BuildValidationEvent which contains the validation definition and the context. As a developer, you can subscribe to framework.validation.VALIDATION_NAME (e.g. framework.validation.address_create) to extend the existing validation.

    #2.3 EXTENDING THE DATA MAPPING TO DAL SYNTAX
    After validation, your data needs to be mapped to the syntax of the DAL to do a successful write. After the data has been mapped to the DAL syntax, you should throw a DataMappingEvent so that plugin developers can modify the payload to be written.

    Example:

    
    $mappingEvent = new DataMappingEvent(CustomerEvents::MAPPING_CUSTOMER_PROFILE_SAVE, $bag, $mapped, $context->getContext());
    
    $this->eventDispatcher->dispatch($mappingEvent->getName(), $mappingEvent);
    
    $mapped = $mappingEvent->getOutput();


    The $mapped variable will then be passed to the DAL repository.

    2019-03-28: Payment refactoring

    We deleted the entity properties:

    • template
    • class
    • percentageSurcharge
    • absoluteSurcharge
    • surchargeText

    and renamed the technicalName to handlerIdentifier, which isn´t unique anymore.

    The handlerIdentifier is only internal and can not be written by the API. It contains the class of the identifier. If a plugin is created via the admin, the Shopware\Core\Checkout\Payment\Cart\PaymentHandler\DefaultPayment handler will be choosed.

    Also we divided the PaymentHandlerInterface into two payment handler interfaces:

    • AsynchronousPaymentHandlerInterface
    • SynchronousPaymentHandlerInterface

    and also added the two new structs:

    • AsyncPaymentTransactionStruct
    • SyncPaymentTransactionStruct

    The AsynchronousPaymentHandlerInterface has a finalize Method and the pay Method returns a RedirectResponse. In the SynchronousPaymentHandlerInterface we only have the pay Methods wich has no return.

    Another change is a decoration of the payment repository which prevents to delete a plugin payment via API. Payments without a plugin id can be deleted via API. For plugin payments deletions, the plugin itself has to use the new method internalDelete, which uses the normal undecorated delete method without restrictions.

    2019-03-28: Exception conventions

    Error codes
    Exceptions are not translated when they are thrown. There there must be an identifier to translate them in the clients. Every exception in Shopware should implement ShopwareException or better extend from ShopwareHttpException. The interface now requires an getErrorCode() method, which returns an unique identifier for this exception.

    The identifier is built of two components. Domain and error summary separated by an underscore, all uppercase and spaces replaced by underscores. For example: CHECKOUT__CART_IS_EMPTY

    Placeholder
    In addition, the placeholders in exceptions have been implemented. The ShopwareHttpException constructor has 2 parameters, a message and an array of parameters to be replace within the message. Please do not use sprintf() anymore!

    Example:

    
    parent::__construct(
        'The type "{{ type }}" is not supported.', 
        ['type' => 'foo']
    );

    2019-03-27: BC: SourceContext removed

    We've removed the SourceContext as it was global mutable State.

    Now the Context has a Source, that is either a SystemSource, AdminApiSource or SalesChannelSource.

    If you want to get the SalesChannel or user from the Context you have to explicitly check the Source as these things aren't always set.

    Don't use the shortcut function to get the SalesChannelId od userId directly on the Context-Object, as these will be removed soon.

    2019-03-27: Plugin system: New flag managed_by_composer on plugin table

    The entity for plugins got a new boolean field managedByComposer which determines if a plugin is required with composer. The field is set during bin/console plugin:refresh

    So if you are currently developing or working with plugins you might need to recreate your database.

    2019-03-20: PaymentTransactionStruct changed

    We changed the contents of the \Shopware\Core\Checkout\Payment\Cart\PaymentTransactionStruct

    Now it contains the whole \Shopware\Core\Checkout\Order\Aggregate\OrderTransaction\OrderTransactionEntity object, from which you should get all necessary information about the order, customer and transaction. The second property is the returnUrl.

    Due to this change, you need to adjust your PaymentHandler. Have a look at our PayPal plugins which changes are necessary: https://github.com/shopwareLabs/SwagPayPal/commit/af5532361be7d0d54c055896a340ee7574df2d66

    2019-03-20: PaymentMethodEntity changed

    We changed the properties of src/Core/Checkout/Payment/PaymentMethodEntity.php from additionalDescription to description and surcharge_string to surcharge_text. surcharge_text is also now translateable.

    Further changes of the PaymentHandler and the PaymentMethodEntity are in development.

    2019-03-20: !!! Public admin component library available !!!

    The component library is now public on https://component-library.shopware.com !

    2019-03-20: Major issues fixed in admin data handling

    We fixed two issues in the current data handling of the administration.

    Hydration of associated entities as instances of EntityProxy
    Because of an issue with the method of deep copying objects, the hydrated asssociations of an entity were simple objects. We now fixed this behaviour, so the N:M relations are also instances of EntityProxy. The hydrated associations in the draft of the entity keep the reference to the entity in the association store.

    BEFORE

    
    // EntityProxy
    product = {
        categories: [
            Object {}
            Object {}
        ]
    }

    AFTER

    
    // EntityProxy
    product = {
        categories: [
            Proxy {}
            Proxy {}
        ]
    }

    2019-03-19: First plugin-manager version

    The first Plugin-Manager version is now merged, but it's behind the NEXT-1223 feature flag.

    Before you can use the Plugin-Manager, you have to set your host in the shopware.yml file. Additionally, you have to change the Framework Version in Framework.php to a version that the SBP knows.

    You can now upload zips, install, deinstall, update, activate and deactivate plugins in the Administration instead of using the CLI.

    Furthermore, it is possible to download and update plugins directly from the Community Store if you have a license for that plugin in your account and you are logged with your Shopware ID.

    2019-03-19: Salutations

    We changed the salutation property of Customer, CustomerAddress, OrderCustomer and OrderAddress from StringField to a reference of the SalutationEntity. Since this property is now required to all of these entities, you have to provide a salutationId. In some cases these constants can be helpful for that:

    • Defaults::SALUTATION_ID_MR
    • Defaults::SALUTATION_ID_MRS
    • Defaults::SALUTATION_ID_MISS
    • Defaults::SALUTATION_ID_DIVERSE
    • Defaults::SALUTATION_KEY_MR
    • Defaults::SALUTATION_KEY_MRS
    • Defaults::SALUTATION_KEY_MISS
    • Defaults::SALUTATION_KEY_DIVERSE

    Additionally you can now easily format a full name with salutation, title and name using either the salutation mixin via salutation(entity, fallbackString) or the filter in the twig files via e.g. {{ customer | salutation }}or {{ customer | salutation(fallbackString) }}. The only requirement for that is to use an entity like Customer which contains firstname, lastname, title and/or a salutation.

    2019-03-14: sw-tree refactoring

    The sw-tree was refactored again, due to the changing of the sorting.

    To use the tree, each item should have an afterIdProperty which should contain the id of the element which is before the current item.

    You now do not have to deconstruct the template in your component anymore to pass your own wording and functions.

    The tree has its own addElement and addSubElement methods which need two methods from the parent-component: createNewElement, which needs to return a new entity and getChildrenFromParent, which needs to load child-items from the passed parentId.

    If you delete an item, delete-element will be emited and can be used in the parent.

    To get translations you can pass the translationContext prop, which is by default sw-tree. To get your desired translations you can simply ducplicate the sw-tree translations and edit them to your needs and pass sw-yourcomponent to the prop.

    To link the elements you can use the onChangeRoute prop which needs to be a function and is applied to all sw-tree-items

    If you need to disable the contextmenu you can pass disableContextMenu

    A visual example can be found in the sw-category-tree

    2019-03-12: Updates for upload handling

    In order to react to upload events and errors globally, we made some changes how uploads are stored and run.

    Events are now fired directly by the upload store and the <sw-media-upload> component now only handles file and url objects to create upload data. So it is not possible anymore to subscribe to the sw-media-upload-new-uploads-added, sw-media-upload-media-upload-success and sw-media-upload-media-upload-failure events from it.

    Handling upload events with vue.js
    We added an additional component <sw-upload-store-listener> in order to take over all the listener registration an most of the event handling. The upload handler has only two properties.

    uploadTag: String - the upload tag you want to listen to
    autoupload: Boolean indicating that the upload added events should be skiped and youre only interested in when the upload was successfull or errored
    The component emits vue.js events back to your wrapping component

    sw-media-upload-added: Object { UploadTask[]: data } - this will be skipped if you set autoupload to true
    sw-media-upload-finished: Object { string: targetId }
    sw-media-upload-failed: UploadTask
    In most cases you will set autoupload to true but it isn't the default. Sometimes you want to do additional work, before the the real upload process starts (e.g. creating associations). To do so listen to the sw-media-upload-added event use the data array to get the media ids of the entities that are just created for the upload.

    Common Example
    The following code snippet is simplified from the sw-media-index component

    
    // template
    <sw-media-upload
        variant="compact"
        :targetFolderId="routeFolderId"
        :uploadTag="uploadTag">
    </sw-media-upload>
    <sw-upload-store-listener
        :uploadTag="uploadTag"
        @sw-media-upload-added="onUploadsAdded"
        @sw-media-upload-finished="onUploadFinished"
        @sw-media-upload-failed="onUploadFailed">
    </sw-upload-store-listener>
    
    // .js
    onUploadsAdded({ data }) {
        data.forEach((upload) => {
            // do stuff with each upload that was added
        });
    
        // run the actual upload process
        this.uploadStore.runUploads(this.uploadTag);
    },
    
    onUploadFinished({ targetId }) {
        // refresh media entity
        this.mediaItemStore.getByIdAsync(targetId).then((updatedItem) => {
                // do something with the refreshed entitity;
        });
    }
    
    // if your are only interested in the target entity's id
    // you can use destructuring
    onUploadFailed({ targetId }) {
        // tidy up
        this.mediaItemStore.getByIdAsync(targetId).then((updatedMedia) => {
            if (!updatedMedia.hasFile) {
                updatedMedia.delete(true);
            }
        });
    }

    Subscribe to the store manually
    You can also subscribe to the store directly but it is not the preferred way. You can add and remove your listener with the following methods:

    addListener(string: uploadTag, function: callback)
    removeListener(string: uploadTag, funtion: callback)
    addDefaultListener(function: callback)
    removeDefaultListenre(function: callback)
    The store will pass you a single object back to your callback when an upload event occurs:

    
    uploadStore = State.getStore('upload');
    uploadStore.addListener('my-upload-tag', myListener);
    
    function myListener({action, uploadTag, payload }) {...}

    The action and payload is similar to the vue.js event name and $event data described above.

    2019-03-12: Number ranges added

    We implemented a configurable number range.

    Number ranges are defined unique identifiers for specific entities.

    The new NumberRangeValueGenerator is used to generate a unique identifier for a given entity with a given configuration.

    The configuration will be provided in the administration where you can provide a pattern for a specific entity in a specific sales channel.

    You can reserve a new value for a number range by calling the route /api/v1/number-range/reserve/{entity}/{salesChannelId} with the name of the entity like product or order and, for sales channel dependent number ranges, also the salesChannelId

    In-Code reservation of a new value for a number range can be done by using the NumberRangeValueGenerator method getValue(string $definition, Context $context, ?string $salesChannelId) directly.

    PATTERNS
    Build-In patterns are the following:

    increment('n'): Generates a consecutive number, the value to start with can be defined in the configuration

    date('date','date_ymd'): Generates the date by time of generation. The standard format is 'y-m-d'. The format can be overwritten by passing the format as part of the pattern. The pattern date_ymd generates a date in the Format 190231. This pattern accepts a PHP Dateformat-String

    PATTERN EXAMPLE
    Order{date_dmy}_{n} will generate a value like Order310219_5489

    ValueGeneratorPattern

    The ValueGeneratorPattern is a resolver for a part of the whole pattern configured for a given number range.

    The build-in patterns mentioned above have a corresponding pattern resolver which is responsible for resolving the pattern to the correct value.

    A ValueGeneratorPattern can easily be added to extend the possibilities for specific requirements.

    You only need to derive a class from ValueGeneratorPattern and implement your custom rules to the resolve-method.

    IncrementConnector

    The increment pattern is somewhat special because it needs to communicate with a persistence layer in some way.

    The IncrementConnector allows you to overwrite the connection interface for the increment pattern to switch to a more perfomant solution for this specific task.

    If you want to overwrite the IncrementConnector you have to implement the IncrementConnectorInterface in your new connector class and register your new class with the id of the interface.

     

    
    <service class="MyNewIncrementConnector" id="Shopware\Core\System\NumberRange\ValueGenerator\IncrementConnectorInterface">
            <tag name="shopware.value_generator_connector"/>
    </service>

    2019-03-12: Details column removed from OrderTransaction

    We removed the details column from the order_transaction table. It was introduced in the past, to store additional data to the transaction, e.g. from external payment providers. This is now unnecessary since the introduction of the custom field. If you stored data to the details field, create a new custom field and store the data in this custom field field. An example migration could be found in our PayPal integration

    2019-03-11: sw-data-grid update

    • The prop "identifier" is no longer required. When no identifier is set the grid will not save any settings to the localStorage.
    • The prop "dataSource" now also accepts Objects. (This is needed for the new data handling with "repository")
    • Columns do now have a resize limit and can not be resized smaller than 65 pixel.
    • Accidental sorting when doing a column resize should now be fixed.

    2019-03-11: New component sw-data-grid

    The sw-data-grid is a new component to render tables with data. It works similar to the sw-grid component but it has some additional features like hiding columns or scrolling horizontally.

    To prevent many data lists from breaking the sw-data-grid is introduced as a new independent component. The main lists for products, orders and customers are already using the sw-data-grid component. Other lists like languages or manufactureres will be migrated in the future.

    How to use it
    To render a very basic data grid you need two mandatory props:

    dataSource: Result from Store getList
    columns: Array of columns which should be displayed

    
    <sw-data-grid
        dataSource="products"
        columns="productColumns">
    <sw-data-grid>

    How to configure columns

    
    methods: {
        // Define columns
        getProductColumns() {
            return [{
                property: 'name',
                label: 'Name'
            }, {
                property: 'price.gross',
                label: 'Price'
            }]
        }
    }
    
    computed: {
        // Access columns in the template
        productColumns() {
            return getProductColumns();
        }
    }

    Theoretically, you could define your columns directly in the template but it is recommended to do this inside your JavaScript. The extra method allows plugin developers to exdend the columns.

    AVAILABLE COLUMN PROPERTIES

    
    {
        property: 'name',
        label: 'Name'
        dataIndex: 'name',
        align: 'right',
        inlineEdit: 'string',
        routerLink: 'sw.product.detail',
        width: 'auto',
        visible: true,
        allowResize: true,
        primary: true,
        rawData: false
    }

    property (string, required)

    The field/property of the entity that you want to render.

    label (string, recommended)

    The label text will be shown in the grid header and the settings panel. The grid works without the label but the header and the settings panel expect a label and will show empty content when the label is not set. The settings panel and the header should be set to hidden when using no label.

    dataIndex (string, optional)

    Define a property that should be sorted when clicking the grid header. This works similar to sw-grid. The sorting is active when dataIndex ist set. The sortable property is not needed anymore.

    align (string, optional)

    The alignment of the cell content.

    Available options: left, right, center
    Default: left
    inlineEdit (string, optional)

    Activates the inlineEdit for the column. The sw-data-grid can display default inlineEdit fields out of the box. At the moment this is only working with very basic fields and properties which are NOT an association. However, you have the possibility to render custom inlineEdit fields in the template via slot.

    Available options: string, boolan, number
    routerLink (string, optional)

    Change the cell content text to a router link to e.g. redirect to a detail page. The router link will automatically get a parameter with the id of the current grid item. If you want to have different router links you can render a custom <router-link> via slot.

    width (string, optional)

    The width of the column. In most cases the grid gets it's columns widths automatically based on the content. If you wan't to give a column a minimal width e.g. 400px this can be helpful.

    Default: auto
    visible (boolean, optional)

    Define if a column is visible. When it is not visible initially the user could toggle the visibility when the grid settings panel is activated.

    Default: true
    allowResize (boolean, optional)

    When true the column header gets a drag element and the user is able to resize the column width.

    Default: false
    primary (boolean, recommended)

    When true the column can not be hidden via the grid settings panel. This is highly recommended if the settings panel is active.

    Default: false
    rawData (boolean, otional)

    Experimental: Render the raw data instead of meta.viewData

    Available props


    dataSource (array/object, required)

    Result from Store getList

    columns (array, required)

    Array of columns which should be displayed

    identifier (string, required)

    A unique ID is needed for saving columns in the localStorage individually for each grid. When no identifier is set the grid will not save any settings like column visibility or column order.

    showSelection (boolean, optional)

    Shows a column with selection checkboxes.

    showActions (boolean, optional)

    Shows a column with an action menu.

    showHeader (boolean, optional)

    Shows the grid header

    showSettings (boolean, optional)

    Shows a small settings panel. Inside the panel the user can control the column order and visibility.

    fullPage (boolean, optional)

    Positions the grid absolute for large lists.

    allowInlineEdit (boolean, optional)

    Defines if the grid activates the inline edit mode when the user double clicks a row.

    allowColumnEdit (boolean, optional)

    Shows a small action menu in all column headers.

    isLoading (boolean, recommended)

    The isLoading state from the listing call e.g. Store getList

    skeletonItemAmount (number, optional)

    The number of skeleton items which will be displayed when the grid is currently loading.

    Available slots

    • actions (scoped slot width "items")
    • action-modals (scoped slot width "items")
    • pagination


    DYNAMIC SLOTS FOR COLUMN CONTENT
    Every column creates a dynamic slot in which you can put custom HTML. This dynamic slots are prefixed with "column-" followed by the property of the column you want to change.

    
    <sw-data-grid
        :dataSource="products"
        :columns="productColumns"
        :identifier="my-grid">
    
        <template slot="column-firstName" slot-scope="{ item }">
            {{ item.salutation }} {{ item.firstName }} {{ item.lastName }}
        </template>
    </sw-data-grid>

    The dynamic slots provide the following properties via slot-scope:

    item

    The current record

    column

    The current column

    compact

    Info if the grid is currently in compact mode.

    isInlineEdit

    Is the inline edit active for the current column. This can be helpful for customized form components inside the inline edit cell.

    2019-03-08: Module specific snippets

    We had the problem that our two big snippets files for the administration caused a bunch of merge conflicts in the past.

    Now it's possible to register module specific snippets, e.g. each module can have their own snippet files.

    
    import deDE from './snippet/de_DE.json';
    import enGB from './snippet/en_GB.json';
    
    Module.register('sw-configuration', {
        // ...
    
        snippets: {
            'de-DE': deDE,
            'en-GB': enGB
        },
    
        // ...
    });

    The module has a new property called snippetswhich should contain the ISO codes for different languages.

    Inside the JSON files you still need the module key in place:

    
    {
       "sw-product": { ... }
    }

    The usage inside components and component templates haven't changed.

    2019-03-06: JsonField-Serializer changes

    It's already possible to define types for the values in the json object by passing an array of Fields into propertyMapping. The values are then validated and encoded by the corresponding FieldSerializer.

    We implemented two changes:

    • the decode method now calls the fields decode method and formats \DateTime as \DateTime::ATOM (Example: BoolField values are now decoded as true/false instead of 0/1)
    • the JsonFieldAccessorBuilder now casts according to the matching types on the SQL side. That means it's now possible to correclty filter and aggregate on many typed fields. The following fields are supported:
    • IntField
    • FloatField
    • BoolField
    • DateField

    All other Fields are handled as strings.

    2019-03-05: Make entityName property private

    In order to avoid naming conflicts wirth entities, that define a entityName field, we decided to mark the entityName property in EntityStore and EntityProxy as private by adding a preceding undersore.

    In the most cases this will not affect you directly since you should always know, what entities you're working on. However in mixed lists it can be usefull to make decisions depending on the type. To do so use the new getEntityName() function provided by proxy and store.

    
    //example testing vue.js properties
    props: {
        myEntity: {
            type: Object,
            required: true,
            validator(value)  {
                return value.getEntityName() === 'some_entity_name';
            }
        }
    }

    2019-03-06: CustomFields

    We added an easy way to add custom fields to entities. The CustomField is like the JsonField only dynamically typed. To save custom fields to entities you first have to define the custom field:

    
    $customFieldsRepository->create([[
            'id' => '<uuid>',
            'name' => 'sw_test_float',
            'type' => CustomFieldType::Float,
        ]],
        $context
    );
    

    Then you can save it like a normal json field

    
    $entityRepository->update([[
            'id' => '<entity id'>',
            'customFields' => [
                'sw_test_float' => 10.1
            ]
        ]],
        $context
    );

    Unlike the JsonField, the CustomField patchs the data instead of replacing it completely. So you dont need to send the whole object to update one property.

    2019-03-05: New tab component

    The new tab component got a redesign. It supports now horizontal and vertical mode. The vertical mode looks and works like the side-navigation component. This is the reason why it was replaced with this component. You can switch between a left and right alignment.

    You can use the sw-tabs-item component for each tab item. It accepts a vue route. When no route is provided then it will be used like a normal link which you can use for every case.

    
    <sw-tabs isVertical small alignRight>
    
        <sw-tabs-item :to="{ name: 'sw.explore.index' }">
            Explore
        </sw-tabs-item>
    
        <sw-tabs-item href="https://www.shopware.com">
            My Plugins
        </sw-tabs-item>
    
    </sw-tabs>
    

    2019-03-01: NPM dependency

    Recently we used an npm feature (clean-install) which is available since version 6.5.0

    We already updated the docs accordingly, but the npm dependency in the package.json was still wrong. this is fixed now. so please check the npm version on your machine!

    2019-03-01: New code style fixer rules

    We added the following new rules to our coding style rule set

    • NoUselessCommentFixer
    • PhpdocNoSuperfluousParamFixer
    • NoImportFromGlobalNamespaceFixer
    • OperatorLinebreakFixer
    • PhpdocNoIncorrectVarAnnotationFixer
    • NoUnneededConcatenationFixer
    • NullableParamStyleFixer

    Have a look here https://github.com/kubawerlos/php-cs-fixer-custom-fixers#fixers what they mean and what they do.

    Additionally the option "allow-risky" is now part of the php_cs.dist config. So it is not necessary anymore to call the cs-fixer with the "–allow-risky" parameter

    2019-03-01: Breaking change - GroupBy-Aggregations

    It is now possible to group aggregations by the value of given fields. Just like GROUP BY in SQL works.

    Every aggregation now takes a list of groupByFields as the last parameters.

    The following Aggregation will be grouped by the category name and the manufacturer name of the product.

    
    new AvgAggregation('product.price.gross', 'price_agg', 'product.categories.name', 'product.manufacturer.name')
    

    Aggregation Result
    As aggregations can now return more than one result the `getResult()`-method returns now an array in the following form for non grouped aggregations.

    
    [
        [
            'key' => null,
            'avg' => 13.33
        ]
    ]

    For grouped Aggregations it will return an array in this form:

    
    [
        [
            'key' => [
                'product.categories.name' => 'category1',
                'product.manufacturer.name' => 'manufacturer1'
            ],
            'avg' => 13.33
        ],
        [
            'key' => [
                'product.categories.name' => 'category2',
                'product.manufacturer.name' => 'manufacturer2'
            ],
            'avg' => 33
        ]
        
    ]

    The AggregationResult has a helper method `getResultByKey()` which returns the specific result for a given key:

    
    $aggregationResult->getResultByKey([
        'product.categories.name' => 'category1',
        'product.manufacturer.name' => 'manufacturer1'
    ]);

    will return:

    
    [
            'key' => [
                'product.categories.name' => 'category1',
                'product.manufacturer.name' => 'manufacturer1'
            ],
            'avg' => 13.33
        ],

    The Aggregation result for the specific aggregations are deleted and just the generic AbstractAggregationResult exists.

    FIXING EXISTING AGGREGATIONS
    As existing aggregations can't use groupBy you can simply use the first array index of the returned result:

    
    /** @var AvgAggregationResult **/
    $aggregationResult->getAverage();

    will become:

    
    $aggregationResult->getResult()[0]['avg'];

    In the administration you also have to add the zero array index.

    
    response.aggregations.orderAmount.sum;

    will become:

    
    response.aggregations.orderAmount[0].sum;
    

    February 2019

    2019-02-28: Dynamic Form Field Renderer

    We have a new component for dynamic rendering of form fields. This component is useful whenever you want to render forms based on external configurations or user configuration(e.g. custom fields).

    Here are some examples:

    
    * {# Datepicker #}
    * <sw-form-field-renderer
    *         type="datetime"
    *         v-model="yourValue">
    * </sw-form-field-renderer>
    *
    * {# Text field #}
    * <sw-form-field-renderer
    *         type="string"
    *         v-model="yourValue">
    * </sw-form-field-renderer>
    *
    * {# sw-colorpicker #}
    * <sw-form-field-renderer
    *         componentName="sw-colorpicker"
    *         type="string"
    *         v-model="yourValue">
    * </sw-form-field-renderer>
    *
    * {# sw-number-field #}
    * <sw-form-field-renderer
    *         config="{
    *             componentName: 'sw-field',
    *             type: 'number',
    *             numberType:'float'
    *         }"
    *         v-model="yourValue">
    * </sw-form-field-renderer>
    *
    * {# sw-select - multi #}
    * <sw-form-field-renderer
    *         :config="{
    *             componentName: 'sw-select',
    *             label: {
    *                 'en-GB': 'Multi Select'
    *             },
    *             multi: true,
    *             options: [
    *                 { id: 'option1', name: { 'en-GB': 'One' } },
    *                 { id: 'option2', name: 'Two' },
    *                 { id: 'option3', name: { 'en-GB': 'Three', 'de-DE': 'Drei' } }
    *             ]
    *         }"
    *         v-model="yourValue">
    * </sw-form-field-renderer>
    *
    * {# sw-select - single #}
    * <sw-form-field-renderer
    *         :config="{
    *             componentName: 'sw-select',
    *             label: 'Single Select',
    *             options: [
    *                 { id: 'option1', name: { 'en-GB': 'One' } },
    *                 { id: 'option2', name: 'Two' },
    *                 { id: 'option3', name: { 'en-GB': 'Three', 'de-DE': 'Drei' } }
    *             ]
    *         }"
    *         v-model="yourValue">
    * </sw-form-field-renderer>

    @description - Workflow
    Dynamically renders components. To find out which component to render it first checks for the componentName prop. Next it checks the configuration for a componentName. If a componentName isn't specified, the type prop will be checked to automatically guess a suitable component for the type. Everything inside the config prop will be passed to the rendered child prop as properties. Also all additional props will be passed to the child.

    2019-02-26 : Auto configured repositories

    We implemented a compiler pass which configures all entity repositories automatically.

    The compiler pass iterates all configured EntityDefinition and creates an additionally service for the EntityRepository.

    The repository is available over the service id {entity_name}.repository.

    If a repository is already registered with this service id, the compiler pass skips the definition

    2019-02-25 : Moved the sidebar component to the global page component

    The sidebar component is now placed in the global page component and was removed from the grid and the card-view. This makes the usage of the sidebar consistent in all pages.

    All existing pages were updated in the same PR to match the new structure.

    2019-02-25 : Type-hinting for collections

    We recently have introduced a way to prevent mixing of class types in a collection. Now we are adding some sugar based on this issue on github.

    With this change, your IDE will detect the type of the collection and provides the correct type hints when used in foreach loops or when calling the add() method, etc.

    In case you're creating a new collection class, please implement getExpectedClass() because this will be the prerequisite for automatically adding the needed doc block above the class.

    
    <?php
    
    /**
    * @method void              add(PluginEntity $entity)
    * @method PluginEntity[]    getIterator()
    * @method PluginEntity[]    getElements()
    * @method PluginEntity|null get(string $key)
    * @method PluginEntity|null first()
    * @method PluginEntity|null last()
    */
    class PluginCollection extends EntityCollection
    {
        protected function getExpectedClass(): string
        {
            return PluginEntity::class;
        }
    }

    If you implement a method that co-exists in the doc block, please remove the line from the doc block as it will no longer have an effect in your IDE and report it as error.

    2019-02-22 : Feature: Product visibility

    We introduced a new Entity product_visibility which is represented by the \Shopware\Core\Content\Product\Aggregate\ProductVisibility\ProductVisibilityDefinition class.

    It allows the admin to define in which sales channel a product is visible and in which cases:

    • only deeplink
    • only over search
    • everywhere

    2019-02-22 : Administration: Loading all entities

    We implemented a getAll function inside the EntityStore.js class which allows to load all entities of this store. The function fetches all records of the store via queue pattern. The functionality is equals to the getList function (associations, sorting, ...).

    2019-02-21 : Small changes to core components

    sw-modal
    It is now possible to hide the header of the sw-modal.

    
    <sw-modal title="Example" showHeader="false"></sw-modal> 

    sw-avatar
    Instead of the user's initials, you can now show a placeholder avatar image (default-avatar-single).

    
    <sw-avatar placeholder></sw-avatar>

    sw-context-button

    Now you can specify an alternative icon for the context button. For example you can insert the "default-action-more-vertical" for the vertical three dots. To make sure the opening context menu is correctly aligned, modify the menu offset.

    
    <sw-context-button icon="default-action-more-vertical" :menuOffsetLeft="18"></sw-context-button>

    2019-02-21 : PHPUnit - random seeds gets printed

    We now print the seed used to randomize the order of test execution.

    Therefor we updated PhpUnit to 8.0.4, so you may have run composer update to see the difference.

    When the test run fails you can copy the used seed and start phpunit again with the --random-order-seed option.

    This makes the test results reproducable and helps you debug dependencies between test cases.

    2019-02-21 : New <sw-side-navigation> component

    A new base component is ready for usage. It is an alternative to the tabs when the viewport width is large enough. The active page is automatically detected and visualized in the component.

    Usage:

    
    <sw-side-navigation>
    
        <sw-side-navigation-item 
            :to="{ name: 'sw.link.example.page1' }">
            Page 1
        </sw-side-navigation-item>
        
        <sw-side-navigation-item 
            :to="{ name: 'sw.link.example.page2' }">
            Page 2
        </sw-side-navigation-item>
        
        <sw-side-navigation-item 
            :to="{ name: 'sw.link.example.page3' }">
            Page 3
        </sw-side-navigation-item>
        
        <sw-side-navigation-item 
            :to="{ name: 'sw.link.example.page4' }">
            Page 4
        </sw-side-navigation-item>  
        
    </sw-side-navigation>

    The <sw-side-navigation-item> works exactly like a router link and can receive the same props.

    Important: In the future the component will be combined with the new tabs component which has the same styling. It will be a switchable component with a horizontal and vertical mode.

    2019-02-20 : ESLint disabled by default

    ESLint in the Hot Module Reload mode is now disabled by default. You can re-enable it in your psh.yaml file with ESLINT_DISABLE: "true".

    To keep our rules still applied, we've added eslint with –-fix to our pre-commit hook like we do with PHP files. If there are changes, that cannot be safely fixed by eslint, it will show you an error log. Please fix the shown issues and try to commit again.

    In addition, both code styles in PHP and JS will be checked in our CI environment, so don't try to commit with –-no-verify.

    2019-02-19 : Configuration changes to sw-datepicker

    In order to prevent conflicts between the type properties of sw-field and sw-datepicker we replaced sw-datepicker's type with a new property dateType. The new property works similar to the old type property of the datepicker.

    
    <sw-field type="date" dateType="datetime" ...></sw-field>

    Valid values for dateType are:

    • time
    • date
    • datetime
    • datetime-local

    2019-02-18 : New OneToOneAssociationField

    The new OneToOneAssociationField allows to register a 1:1 relation in the DAL.

    This is especially important for plugin developers to extend existing entities where the values are stored in separate columns in the database.

    Important for the 1:1 relation is to set the RestrictDelete and CascadeDelete.

    Furthermore, the DAL always assumes a bi-directional association, so the association must be defined on both sides. Here is an example where a plugin adds another relation to the ProductDefinition:

    
    ProductDefinition.php
    
    protected static function defineFields(): FieldCollection
    {
        return new FieldCollection([
           //...
           (new OneToOneAssociationField(
               'pluginEntity',
               'id',
               'product_id',
               PluginEntityDefinition::class,
               false)
           )->addFlags(new CascadeDelete())
    ]);
    
    

     

    
    PluginEntityDefinition.php
    
    protected static function defineFields(): FieldCollection
    {
        return new FieldCollection([
            //...
            (new OneToOneAssociationField(
                'product',
                'product_id',  
                'id',
                ProductDefinition::class,
                false)
            )->addFlags(new RestrictDelete())
        ]);

     

    2019-02-12 : PHPUnit 8 + PCOV

    In order to get faster CodeCoverage we updated to PHPUnit 8 and installed PCOV on the Docker app container. So please rebuild your container and do a composer install.

    PHPUnit 8
    Sadly PHPUnit 8 comes with BC-Changes that may necessitate changes in your open PRs. The most important ones:

    • setUp and tearDown now require you to add a void return typehint
    • assertArraySubset is now deprecated. Please no longer use it

    For a full list of changes please see the official announcement: https://phpunit.de/announcements/phpunit-8.html

    PCOV
    The real reason for this change! PCOV generates CodeCoverage in under 4 minutes on a docker setup.

    If you want to generate coverage inside of your container you need to enable pcov through a temporary ini setting first. As an example this will write coverage information to /coverage:

    
    php -d pcov.enabled=1 vendor/bin/phpunit --configuration vendor/shopware/platform/phpunit.xml.dist --coverage-html coverage

    you are developing directly on your machine please take a look at https://github.com/krakjoe/pcov/blob/develop/INSTALL.md for installation options.

    2019-02-11 :Refactoring of <sw-field> and new url field for ssl switching

    <SW-FIELD> REFACTORING
    The sw-field was refactored for simpler usage of suffix, prefix and tooltips. You can use it now simply as props. The suffix and prefix is also slotable for more advanced solutions.

    Usage example:

    
    <sw-field 
        type="text"
        label="Text field:"
        placeholder="Placeholder text…"
        prefix="Prefix"
        suffix="Suffix"
        :copyAble="false"
        tooltipText="I am a tooltip!"
        tooltipPosition="bottom"
        helpText="This is a help text."
    >
    </sw-field>

    NEW FIELD: <SW-FIELD TYPE="URL">
    Another news is the new SSL-switch field. It allows the user to type or paste a url and the field shows directly if its a secure or unsecure http connection. The user can also change the url with a switch from a secure to an unsecure connection or the other way around.

    The field is extended from the normal sw-field. Hence it also allows to use prefix, tooltips, …

    Usage example:

    
    <sw-field 
        type="url"
        v-model="theNeededUrl"
        label="URL field:"
        placeholder="Type or paste an url…"
        switchLabel="The description for the switch field">
    </sw-field>

    2019-02-08: <sw-tree> refactoring

    The sw-tree now has a function prop createFirstItem whicht will be calles when there are no items in the tree. This should be used to create an initial item if none are given. All other items shoud be created via functions from the action buttons on each item. e.g.: addCategoryBefore or addCategoryAfter. You'll have to create these functions for the given case and override the slot actions of the sw-tree-item.

    2019-02-07: Rule documentation

    The rules documentation is now available. You are now able to read how to create your own rules using the shopware/platform! Any feedback is appreciated.

    See it at: https://github.com/shopware/platform/blob/master/src/Docs/60-plugin-system/35-custom-rules.md

    2019-02-07: System requirements

    The platform now requires PHP >= 7.2.0. We've also included a polyfill library for PHP 7.3 functions, so feel free to use them.

    2019-02-06: Plugin configuration

    It is now possible for plugins to create a configuration. This configuration gets dynamically rendered in the administration, however this feature is not actively used right now. Add a new Resources/config.xml to your plugin. Take a look at this short example:

    
    <?xml version="1.0" encoding="UTF-8"?>
    <config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/master/src/Core/System/SystemConfig/Schema/config.xsd">
    
        <card>
            <title>Basic Configuration</title>
            <title lang="de_DE">Grundeinstellungen</title>
            <input-field type="password">
                <name>secret</name>
                <label>Secret token</label>
                <label lang="de_DE">Geheim Schlüssel</label>
                <helpText>Your secret token for xyz...</helpText>
                <helpText lang="de_DE">Dein geheimer Schlüssel</helpText>
            </input-field>
        </card>
    </config>

    The configuration is completely optional and meant to help people create a configuration page for their plugin without requiring any knowledge of templating or the Shopware Administration. Read more about the plugin configuration here.

    2019-02-07: New project setup

    Hey folks, currently, the project setup steps are

    • checkout shopware/development
    • run composer install
    • change directory to vendor/shopware/platform
    • setup stash as remote
    • setup PHPStorm to allow editing files in the vendor folder

    PROBLEMS
    One of the big problems is, that if a new dependency is required to be installed, you may break your current project setup as you'll loose your history in vendor/shopware/platform because composer will detect changes and restores it to a new checkout from github. You have to do the setup all over again.

    And to be honest, the setup process isn't straightforward.

    New project setup
    From now on, you don't have to work in vendor/shopware/platform to make changes. In order to use the new process, follow these instructions:

    • clone shopware/development
    • clone shopware/platform in folder platform in the development root directory
    • run composer install

    WHAT HAS CHANGED?
    If the platform directory exists, composer will use it as source for the shopware/platform dependency and symlinks it into vendor/shopware/platform. In PHPStorm, you'll always work in ./platform for your platform changes. They will be automatically be synced to the vendor directory - because it's a symlink. :zwinkern: This change will also speed up the CI build time significantly.

    UPGRADE FROM CURRENT SETUP
    To make sure you can use the new setup:

    • save your current work (push) in vendor/shopware/platform
    • clone shopware/platform into in the development root directory as platform
    • remove vendor/ and composer.lock
    • run composer install
    • 2019-02-05: Plugin changelogs

    • Changelogs could now be provided by plugins:
      Add a new `CHANGELOG.md` file in the root plugin directory. The content has to look like this:
    • 
      # 1.0.0
      - initialized SwagTest
      * refactored composer.json
      
      # 1.0.1
      - added migrations
      * done nothing
    • If you want to provide translated changelogs, create a `CHANGELOG-de_DE.md`
      The changelog is optional
    • 2019-02-04: Sample payment plugin available

    • A first prototype of a payment plugin is now available on github https://github.com/shopwareLabs/SwagPayPal

     

    2019-02-04: sw-field refactoring

    The sw-field is now a functional component which renders the single compontents based on the supplied type. There are no changes in the behavior.

    It is now possible to pass options to the select-type which will be rendered as the options with option.id as the value and option.name as the option-name. If you want to use the select as before with slotted options you now don't need to set slot="options" because the options will now be passed via the default slot.

    All input-types are now available as single components.

    sw-text-field for <input type="text"

    sw-password-field for type="password"

    sw-checkbox-field for type="checkbox"

    sw-colorpicker for a colorpicker

    sw-datepicker for a datepicker. Here you can pass type time, date, datetime and datetime-local to get the desired picker.

    sw-number-field for an input which supports numberType int and float and the common type="number" params lik step, min and max.

    sw-radio-field for type="radio" with options for each radio-button where option.value is the value and option.name is the label for each field

    sw-select-field for <select> where the usage is as described above.

    sw-switch-field for type="checkbox" with knob-styling (iOS like).

    sw-textarea-field for <textarea>

    sw-field should be used preferably. Single components should be used to save perforamnce in given cases.

    2019-02-01: Storefront building pipeline

    Shopware 6 Storefront Building Pipline provides the developer with the ability to use a Node.js based tech stack to build the storefront.

    This has many advantages:

    • Super fast building and rebuilding speed
    • Hot Module Replacement
    • Automatic polyfill detection based on a browser list
    • Additional CSS processing for example automatic generation of vendor prefixes
    • The building pipeline is based on Webpack including a dozen plugins. In the following we're talking a closer look on the pipeline:

    JS Compilation

    • babel 7 including babel/preset-env for the ES6-to-ES5 transpilation
    • eslint including eslint-recommended rule set for JavaScript linting
    • terser-webpack-plugin for the minification
    •  
    • CSS Compilation

    • sass-loader as SASS compiler
      postcss-loader for additional CSS processing
      autoprefixer for the automatic generation of vendor prefixes
      pxtorem for automatic transformation from pixel to rem value
      stylelint for SCSS styles linting based on stylelint-config-sass-guidelines

    Hot Module Replacement Server

    • based on Webpack's devServer
    • Overlay showing compilation as well as linting errors right in the browser

    Additional tooling

    friendly-errors-webpack-plugin for a clean console output while using the Hot Module Replacement Server

    • webpack-bundle-analyzer for analyizing the bundle structure and finding huge packages which are impacting the client performance
    •  

    Installation
    All commands which are necessary for storefront development can be accessed from the root directory of your shopware instance. The storefront commands are prefixed with storefront:

    
    ./psh.phar storefront:{COMMAND}

    Find out more about about PSH.

    INSTALL DEPENDENCIES
    To get going you first need to install the development dependencies with the init command:

    
    ./psh.phar storefront:install

    This will install all necessary dependencies for your local environment using NPM.

    Development vs. production build

    The development build provides you with an uncompressed version with source maps. The production build on the other hand minifies the JavaScript, combines it into a single file as well as compresses the CSS and combines it into a single file.

    The linting of JavaScript and SCSS files is running in both variants.

    DEVELOPMENT BUILD

    
    ./psh.phar storefront:dev

    PRODUCTION BUILD

    
    ./psh.phar storefront:prod
    

    Hot module replacement

    The hot module replacement server is a separate node.js server which will be spawned and provides an additional websocket endpoint which pushes updates right to the client. Therefore you don't have to refresh the browser anymore.

    
    ./psh.phar storefront:watch

    January 2019

    2019-01-31: Roadmap update

    Here you will find a current overview of the epics that are currently being implemented, which have been completed and which will be implemented next.

    Open
    Work on these Epics has not yet begun.

    • Theme Manager
    • Tags
    • Product Export
    • First Run Wizard
    • Backend Search
    • Caching
    • Sales Channel
    • Additional Basket Features
    • Shipping / Payment
    • Import / Export
    • Mail Templates
    • Installer / Updater
    • SEO Basics
    • Newsletter Integeration

    Next
    These epics are planned as the very next one

    • Documents
    • Custom Fields
    • Plugin Manager
    • Customer
    • Core Settings
    •  
    • In Progress

    These Epics are in the implementation phase

    • Products
    • Variants / Properties
    • SalesChannel API / Page, Pagelets
    • Order
    • CMS
    • Categories
    • Product Streams
    • ACL
    • Background processes

    Review
    All Epics listed here are in the final implementation phase and will be reviewed again.

    • Rule Builder
    • Plugin System
    • Snippets

    Done
    These epics are finished

    • Media Manager
    • Content Translations
    • Supplier

    2019-01-29: LESS becomes SAAS

    We changed the core styling of the shopware administration from LESS to SASS/SCSS. We did that because the shopware storefront will also have SCSS styling with Bootstrap in the future and we wanted to have a similar code style.

    Do we use the SASS or SCSS syntax?
    We use SCSS! When it comes to brackets and indentations everything stays the same. For comparison: https://sass-lang.com/guide (You can see a syntax switcher inside the code examples)

    What if my whole module or plugin is still using LESS?
    This should have no effect in the first place because SCSS is only an addition. All Vue components do support both LESS and SCSS. All LESS variables and mixins are still available for the moment in order to prevent plugins from breaking. When all plugins are migrated to SCSS styles we can get rid of the LESS variables and mixins.

    How do I change my LESS to SCSS?

    • Run administration:init
      • The new SASS has to be installed first.
    • Change file extension from .less to .scss
      • Please beware of the import inside the index.js file.
    • Change the alias inside the style imports:
      • The alias inside the style imports changes from ~less to ~scss:
    • 
      // Old
      @import '~less/variables';
      
      // New
      @import '~scss/variables';
      • Change variable prefixes:
        Variable prefixes has to be changed from @ to $:
      • 
        // Old
        color: @color-shopware;
        
        // New
        color: $color-shopware;
      • If you do a replace inside your IDE, please take care of the Style Imports as well as the MediaQueries.

        All base variables have been migrated to SCSS and can be used as before.

      • Change mixin calls:

      • 
        // Old
        .truncate();
        
        // New
        @include truncate();

    2019-01-29: Clone entities

    It is now possible to clone entities in the system via the following endpoint:

    /api/v1/_action/clone/{entity}/{id}

    As a response you will get the new id

    { id: "a3ad........................................ }

    What will be cloned with the entity?

    • OneToMany associations marked with CascadeDelete flag
    • ManyToMany associations (here only the mapping tables entries)
    • For example product N:M category (mapping: product_category)

    The category entities are not cloned with product_category entries are cloned

    2019-01-29: Object cache

    The cache can be referenced at shopware.cache. Here you find a \Symfony\Component\Cache\Adapter\TagAwareAdapterInterface behind. This allows additional tags to be stored on a CacheItem:

    
    $item = $this->cache->getItem('test');
    $item->tag(['a', 'b'];
    $item->set('test')
    $this->cache->save($item);

    What do we use the cache for?

    • Caching Entities
    • Caching from Entity Searches

    Where is the caching located in the core?

    
    \Shopware\Core\Framework\DataAbstractionLayer\Cache\CachedEntityReader
    \Shopware\Core\Framework\DataAbstractionLayer\Cache\CachedEntitySearcher

    When do I have to consider the cache?

    • In all indexers, i.e. whenever you write directly to the database

    Here you can find an example \Shopware\Core\Content\Rule\DataAbstractionLayer\Indexing\RulePayloadIndexer

    2019-01-29: sw-button new features

    The sw-button component has been extended by some smaller features.

    • Square Button (For buttons which only contain an icon)
    • Button Group (Shows buttons in a "button bar" without spacing in between)
    • Split Button (A combination of Button Group, Square Button and Context Menu)

    Here are some code examples which show you how to use the new features:

    
    <!-- Square buttons -->
    <sw-button square size="small">
        <sw-icon name="small-default-x-line-medium" size="16"></sw-icon>
    </sw-button>
    <sw-button square size="small" variant="primary">
        <sw-icon name="small-default-checkmark-line-medium" size="16"></sw-icon>
    </sw-button>
    
    <!-- Default button group -->
    <sw-button-group splitButton>
        <sw-button-group>
            <sw-button>Button 1</sw-button>
            <sw-button>Button 2</sw-button>
            <sw-button>Button 3</sw-button>
        </sw-button-group>
    </sw-button-group>
    
    <!-- Primary split button with context menu -->
    <sw-button-group splitButton>
        <sw-button variant="primary">Save</sw-button>
        
        <sw-context-button>
            <sw-button square slot="button" variant="primary">
                <sw-icon name="small-arrow-medium-down" size="16"></sw-icon>
            </sw-button>
            
            <sw-context-menu-item>Save and exit</sw-context-menu-item>
            <sw-context-menu-item>Save and publish</sw-context-menu-item>
            <sw-context-menu-item variant="danger">Delete</sw-context-menu-item>
        </sw-context-button>
    </sw-button-group>

    2019-01-29: Automatic generation of api services based on entity scheme

    We changed the handling of API services. The services are now generated automatically based on the entity scheme.

    It's still possible to create custom API serivces. To do so, as usual, create a new file in the directory src/core/service/api. You don't have to deal with the registration of these services - the administration will automatically import and register the service into the application for you.

    Something has changed though - the base API serivce is located under src/core/service instead of src/core/service/api.

    There's something you have to keep in mind tho. Please switch the pfads accordingly and custom API services are needing a name property which represents the name the application uses to register the service.

    Here's an example CustomerAddressApiService:

    
    // Changed import path
    import ApiService from '../api.service';
    
    /**
     * Gateway for the API end point "customer_address"
     * @class
     * @extends ApiService
     */
    class CustomerAddressApiService extends ApiService {
        constructor(httpClient, loginService, apiEndpoint = 'customer_address') {
            super(httpClient, loginService, apiEndpoint);
        
            // Name of the service
            this.name = 'customerAddressService';
        }
    
        // ...
    }

     

    2019-01-29: Feature flags

    In Shopware 6 you can switch off features via environment variables and also merge "Work in Progress" changes into the master. So how does this work?

    Create
    When you start developing a new feature, you should first create a new flag. As a convention we use a Jira reference number here. Remember, this will be published to GitHub, so just take the issue number.

    
    bin/console feature:add NEXT-1128
    

    Creates

    
    application@d162c25ff86e:/app$ bin/console feature:add NEXT-1128
    
    Creating feature flag: NEXT-1128
    ==============================================
    
     ---------- -------------------------------------------------------------------------------------------------------------- 
      Type       Value                                                                                                         
     ---------- -------------------------------------------------------------------------------------------------------------- 
      PHP-Flag   /app/components/platform/src/Core/Flag/feature_next1128.php                                        
      JS-Flag    /app/components/platform/src/Administration/Resources/administration/src/flag/feature_next1128.js  
      Constant   FEATURE_NEXT_1128                                                                               
     ---------- -------------------------------------------------------------------------------------------------------------- 
    
                                                                                                                            
     [OK] Created flag: NEXT-1128                                                                             
                                                                                                                            
    
     ! [NOTE] Please remember to add and commit the files 

    After that you should make a git add to add the new files.

    Enable
    The system disables all flags per default. To switch the flags on you can simply add them to your .psh.yaml.override. An example might look like this:

    
    const:
      FEATURES: |
        FEATURE_NEXT_1128=1

    This is automatically written to the .env file and from there imported into the platform.

    USAGE IN PHP
    The interception points in the order of their usefulness:

    
    <service ...>
       <tag name="shopware.feature" flag="next1128"/>
    </service>

    If possible, you should be able to toggle your additional functionality over the DI container. The service exists only if the flag is enabled.

    Everything else is implemented in the form of PHP functions. These are created through the feature:add command.

    
    use function Flag\skipTestNext1128NewDalField;
    
    class ProductTest
    {
      public function testNewFeature() 
      {
         skipTestNext1128NewDalField($this);
    
         // test code
      }
    }

    If you customize a test, you can flag it by simply calling this function. Also works in setUp

    If there is no interception point through the container, you can use other functions:

    
    use function Flag\ifNext1128NewDalFieldCall;
    class ApiController
    {
    
      public function indexAction(Request $request)
      {
        // some old stuff
        ifNext1128NewDalFieldCall($this, 'handleNewFeature', $request);
        // some old stuff
      }
    
      private function handleNewFeature(Request $request)
      {
        // awesome new stuff
      }
    }

    Just create your own, private, method in which you do the new stuff, nobody can mess with you!

    
    use function Flag\ifNext1128NewDalField;
    class ApiController
    {
    
      public function indexAction(Request $request)
      {
        // some old stuff
        ifNext1128NewDalField(function() use ($request) {
          // awesome stuff
        });
        // some old stuff
      }
    
    }

    If this seems like 'too much' to you, use a callback to connect your new function. Also here it will be hard for others to mess you up.

    
    use function Flag\next1128NewDalField;
    class ApiController
    {
      public function indexAction(Request $request)
      {
        // some old stuff
        if (next1128NewDalField()) {
          //awesome new stuff
        }
        // some old stuff
      }
    }

    If there is really no other way, there is also a simple function that returns a Boool. Should really only happen in an emergency, because you're in the same scope as everyone else. So other flags can easily overwrite your variables.

    USAGE IN THE ADMIN SPA
    This works very similar to the PHP hook points. The preferred interception points are only slightly different though

    
    <sw-field type="text"
        ...
        v-if="next1128NewDalField"
        ...>
    </sw-field>

    To simply hide an element, use v-if with the name of your flag. This is always registererd and defaults to false. In this case the whole component will not even be instantiated.

    
    import { NEXT1128NEWDALFIELD } from 'src/flag/feature_next1128NewDalField';
    
    Module.register('sw-awesome', {
        flag: NEXT1128NEWDALFIELD,
        ...
    });

    With this you can remove a whole module from the administration pannel.

    If these intervention points are not sufficient the functions from PHP are also available - almost 1:1.

    
    import {
       ifNext1128NewDalField,
       ifNext1128NewDalFieldCall,
       next1128NewDalField
    } from "src/flag/feature_next1128NewDalField";
    
    ifNext1128NewDalFieldCall(this, 'changeEverything');
    
    ifNext1128NewDalField(() => {
       // something awesome
    });
    
    if (next1128NewDalField) {
       // something awesome
    }

    These can also be used freely in the components. However, the warnings from the PHP part also apply here!

    2019-01-29: Symfony service naming

    Until now, we have always used the following format for service definitions:

    <service class="Shopware\Core\Checkout\Cart\Storefront\CartService" id="Shopware\Core\Checkout\Cart\Storefront\CartService"/>

    The reason for this was that PHPStorm could only resolve the class and the ID was not recognized as a class. Therefore we maintained the two parameters. This is no longer a problem. Therefore we changed the platform repo and development template. The new format now looks like this:

    <service id="Shopware\Core\Checkout\Cart\Storefront\CartService"/>

    There is also a test which enforces the new format.

    2019-01-29: Entity changes

    Each entity now has the getUniqueIdentifier and setUniqueIdentifier methods necessary for the DAL. The uniqueIdentfier is the first step to support multi column primary keys.

    The getId/setId and Property $id methods are no longer implemented by default, but can be easily added with the EntityIdTrait. This default implementation automatically sets the uniqueIdentifier, which has to be set for a manual implementation.

    2019-01-29: System language

    There is now a system language that serves as the last fallback. At the moment it is still hardcoded en_GB. This should be configurable in the future. Important: If you create new entities, you must always provide a translation for Defaults::LANGUAGE_SYSTEM.

    The constant Defaults::LANGUAGE_SYSTEM replaces Defaults::LANGUAGE_EN, which is now deprecated. Please exchange this everywhere. Since there can be a longer translation chain now, it is now also stored as an array in the context. Context::getFallbackLanguageId was removed, instead there is Context::getLanguageIdChain.

    2019-01-29: EntityTranslationDefinition simplification

    Changed defineFields to only define the translated fields. Primary key, Foreign key und die standard associations are determined automatically. But EntityTranslationDefinition::getParentDefinitionClass (previously called getRootEntity) is no longer optional.

    Before:

    
    class OrderStateTranslationDefinition extends EntityTranslationDefinition
    {
        public static function defineFields(): FieldCollection
        {
            return new FieldCollection([
                (new FkField('order_state_id', 'orderStateId', OrderStateDefinition::class))->setFlags(new PrimaryKey(), new Required()),
                (new ReferenceVersionField(OrderStateDefinition::class))->setFlags(new PrimaryKey(), new Required()),
                (new FkField('language_id', 'languageId', LanguageDefinition::class))->setFlags(new PrimaryKey(), new Required()),
                (new StringField('description', 'description'))->setFlags(new Required()),
                new CreatedAtField(),
                new UpdatedAtField(),
                new ManyToOneAssociationField('orderState', 'order_state_id', OrderStateDefinition::class, false),
                new ManyToOneAssociationField('language', 'language_id', LanguageDefinition::class, false),
            ]);
        }
        public static function getRootEntity(): ?string
        {
            return OrderStateDefinition::class;
        }
    }

    After:

    
    class OrderStateTranslationDefinition extends EntityTranslationDefinition
    {
        public static function getParentDefinitionClass(): string
        {
            return OrderStateDefinition::class;
        }
        protected static function defineFields(): FieldCollection
        {
            return new FieldCollection([
                (new StringField('description', 'description'))->setFlags(new Required()),
            ]);
        }
    }

    In addition, all defineFields methods have been set to protected, since they should only be called internally.

    2019-01-29: Collection Classes

    The collection classes have been cleaned up and a potential bug has been resolved. Since there are no generics in PHP, you can overwrite the method getExpectedClass() and specify a type in the derived classes. The value is checked on add() and set() and throws an exception if an error occurs. If you want to use your own logic for the keys, overwrite the add() method.

    Additionally the IteratorAggregate interface has been implemented.

    2019-01-29: TranslationEntity

    After the EntityTranslationDefinition has made its way into the system to save boilerplate code, we continue with the Entities.

    There is now the TranslationEntity class, which already contains the boilerplate code (properties and methods) and only needs to be extended by its own fields and the relation. Here is an example!

    2019-01-29: Color-Picker component

    There is a new form component, namely the <sw-color-picker> . With the colorpicker it is possible to select a hex string with a colorpicker UI.

    Here is a small example how this can look like in Action:

    
    <sw-color-picker
        label="My Color"
        :disabled="disabled"
        v-model="$route.meta.$module.color">
    </sw-color-picker>

    2019-01-29: Refactoring plugin system

    • To update plugins in the system execute bin/console plugin:refresh
      • all Lifecycle commands call the refresh method before, so you don't need execute the refresh command before the installation of a plugin
    • a Pre and Post event is fired for every change of the lifecycle state of a plugin
    • Every plugin now needs a valid composer.json in the pluugin root directory
      • Have a look here, how it have to look like: src/Docs/60-plugin-system/05-plugin-information.md