This blog post is about the new plugin system in Shopware 6. We will create a little plugin that will display, in a separate controller, the most recently updated plugins with an image and purchase link to the store. In addition to the basic plugin, we will need our own entity, migration, and controller with a theme. You can also find this plugin example on Github.
Many concepts and structures in Shopware 6 will look familiar to an experienced Shopware developer - even if the plugin structure, for example, has changed significantly. We will now create a demo environment, examine the new plugin structure and explore composer.json. After that, we will create our own database table via migration as well as our own entity via the Data Abstraction Layer (DAL). Finally, we will integrate everything into a controller and use it.
Creating a demo environment
We are using the Docker environment which is executed on a Linux client. First, we clone the GitHub development template using git clone email@example.com:shopware/development.git. The development template for Shopware 6 is now saved in the development directory. Now, we’re going into the directory using cd development to clone the actual Shopware platform repository via git clone firstname.lastname@example.org:shopware/platform.git, saving it in the standard directory.
Caution: Please do not specify a different directory during cloning because it’s important for autoloading.
This gives us all of the source code that we need to start on our computer. To build and to start the required Docker containers, we enter ./psh.phar docker:start. This command builds and starts all Docker containers. Afterward, we connect to the application container via ./psh.phar docker:ssh and start the installation using ./psh.phar install.
This may take a while the first time because a lot of caches have to be created during the initial execution. To check if the installation was successful, open a browser of your choice and access http://localhost:8000.
If you’re a Mac user, you can set up the whole thing locally. A sample virtual host configuration can be found here  at “Setting up your webserver”. You then have to execute bin/setup, which will guide you through an interactive installation process.
If something went wrong during the installation, check if there is .psh.yaml.override. If not, re-start the setup script using ./psh.phar install.
Let’s start by creating the sample plugin. Please see Listing 1 for a sample structure. First, we switch to the directory shopware-root/custom/plugins and create the directory StorePlugin. The directory must be named after the technical plugin name. That’s why we’re using StorePlugin as the directory name here.
Listing 1: The new plugin structure
Every plugin is based on composer.json which includes the name, version, requirements and a lot of other meta information. Developers who know Composer will find a lot of familiar features. Every plugin that we’re developing, can be added automatically via composer require, just like any other package. The use of any Composer scheme elements  is expressly encouraged.
We will now create composer.json in the plugin root directory. Listing 2 contains sample contents. Every Composer package needs a technical name as a unique identifier. The naming scheme is identical to that recommended by Composer. The name should be made up of a vendor and project name separated by a / e.g. "name": "swag/store-plugin".
Listing 2: Composer JSON
Furthermore, the vendor name can be simultaneously the vendor prefix. In this example, we’re using the prefix swag. If the project name consists of several words, they should be concatenated using a -. This is often described as the kebab-case style.
What else do we need? A description ("description": "Store-Plugin Example"), a plugin version number ("version": "v1.0.0"), the licence used to publish the plugin ("license": "MIT") and the plugin author.
We also have to define the element type as follows: "type": "shopware-platform-plugin". If this is missing, the plugin will be invalid and the installation of the plugin will not be possible.
In the next step, we will define the autoloader property, which works exactly as described in the Composer documentation . In short: The storage location and the namespace of the relevant PHP classes which are used by the plugin are being defined. This freedom lets the developer structure his or her plugin just as desired. Strict specifications on how the namespace and structure of the directories have to look are now a thing of the past.
Because every plugin is basically a Composer package, I saved the source code in the src directory in the example, which is also the location for the Shopware 6 source code. You can structure your plugin in any manner. However, we recommend you follow these instructions and keep as closely as possible to the Composer package standard.
Now, last but not least, we have to add the additional property  to composer.json. This is where all other information can be saved that wouldn’t otherwise have space. Shopware 6 uses this property to obtain some more meta-information about the plugin such as copyright, label and plugin icon. The most important item, however, is “shopware-plugin-class” where the fully qualified class name (FQCN) of the plugin basic class is saved. This information is necessary for Shopware 6 because plugin developers don’t have to comply with specifications any longer. This means Shopware doesn’t know where the plugin basic class is located.
This comprehensively covers the area of meta-information for a Shopware 6 plugin. We now have to create the basic class to make the plugin work. This has to be created with the file name StorePlugin.php in plugin-root/src/ as already specified in composer.json. Please see Listing 3 for the contents of the basic class.
Listing 3: Plugin basic class
It will look very familiar to the experienced Shopware developer. Apart from the now extended class Shopware\Core\Framework\Plugin, everything has remained identical to Shopware 5.
Now we want to see if our newly developed plugin can be installed. To do this, we will go to the shopware-root folder and navigate to the directory. Using the command ./console plugin:refresh, we will update all existing and installable plugins. After that, using ./console plugin:install --activate --clearCache StorePlugin, our plugin should be installed and activated, and then the shop cache emptied. If we don’t get an error message, we have successfully developed and installed our first Shopware 6 plugin. Initially without any functionality, of course.
So we can create our own entity in a later step, we need a database table. With Shopware 6, a separate option was provided for plugins to create migrations and for those to be automatically executed during the update and installation process. This allows us to easily create a migration, as can be seen in Listing 4. It’s important here that the timestamp from getCreationTimestamp() is identical to that of the migration’s class name. In the update() method, only non-destructive changes can be implemented to enable a simple blue-green deployment for plugins with little effort.
Listing 4: Plugin Migration
All destructive changes have a place in the updateDestructive() method. So we can use this database table via DAL later, in addition to the required title, cover and buy_link column, an id, created_at, and updated_at field also need to be created. DAL will automatically populate these fields later.
Now that we have successfully created our own database table for our plugin, we have to tell Shopware 6 DAL that this table exists. This happens via an EntityDefinition (see Listing 5). In our sample plugin, we will create this in src/Entity with the name StorePluginEntity.php. Our own definition must be derived from Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition, which forces us to implement the methods getEntityName() and defineFields().
Listing 5: Store Plugin definition
The method getEntityName() returns a string that is identical to our table name, in this example: store_plugin. The other method defineFields()contains all fields that are included in our own database table. We have columns for id, title, cover and buy_link. The columns created_at and updated_at don’t need definitions because they are automatically added by DAL. In our example, the columns are all string values. Other options to define columns can be found here . The StringField class needs two parameters for instantiation: a storage name that represents the column at the database level and a property name that will later be allocated in the entity. Furthermore, we can also add flags to a string field such as PrimaryKey or Required. Other available flags can be found here: .
We have now created a valid definition, but we’re still missing the actual entity. In src/Entity, we create the file StorePluginEntity.php and add contents from Listing 6. Caution: For reasons of space, the two properties $cover and $buyLink including their getters and setters are not replicated. Please create those yourself. The entity class should now contain all fields from the definition except the ID including getters and setters. Implementing the ID field takes place via EntityIdTrait in our example.
Listing 6: Store Plugin Entity
Last but not least, we have to register the definition in the dependency injection container. In Shopware 6, services.xml (Listing 7) must be stored in Resources/config relative to our plugin basic class, not, as previously, in Resources.
Listing 7: Services XML
It’s important that our definition contains the tag shopware.entity.definition including the entity attribute. The entity attribute must be identical to the name from the store plugin definition, in our case: store_plugin.
And that’s it. Our own entity is fully registered in Shopware 6, and administration via the API is possible from now on.
We will now build our own controller. We will first create StorePluginController.php in the directory src/Controller. The class StorePluginController, which must be created there, extends Shopware\Storefront\Controller\StorefrontController. As opposed to Shopware 5, it’s no longer a Zend Framework controller but a standard Symfony controller. As you can see in Listing 8, the relevant routes can be defined using annotations @Route("/storePlugin").
Listing 8: Own controller
So these routes can be loaded in the Shopware context, we need a routes.xml in Resources/config (see Listing 9). In this XML file, we will specify in which directories Shopware should look for route annotations so it can use them. In our example, the controller can be retrieved from shop.tld/storePlugin.
Listing 9: Routes XML
Because we have declared our table via a definition in the Shopware context, DAL will automatically create a repository with the ID store_plugin.repository. We will use this repository in the index() method so all available data records from the database table store_plugin will be output.
At the end, we have to define a template for the output and transfer the entries obtained from the database to the template. The file index.html.twig will be automatically searched for in the plugin directory Resources/views. When developing bigger plugins, please use sub-directories to keep an overview.
A very simple sample for index.html.twig, which outputs all entries from the database, can be found in Listing 10. Please see GitHub  for the entire sample plugin.
Listing 10: Index Html
Shopware 6 makes it easy, even for inexperienced developers, to handle plugins because of its further focus on general standards and convergence to Symfony in the plugin system. Furthermore, the new plugin system in Shopware 6 offers developers much more freedom in comparison with Shopware 5.
However, developers need to know how to handle these new types of flexibility. Just because we are now able to save all files in one directory, doesn’t mean that we should do that. It is now solely up to the developer to structure the relevant classes into sensible modules and domains.