Shopware 6 plugin programming tutorial
In this article, we will create a sample plugin for Shopware 6, that will serve as a groundwork for us to use, whenever we need to create a new plugin. First, we will create a minimal plugin. Then we will add some commonly used structures like a subscriber, command, controller etc.. The resulting “skeleton plugin” will be available for you to download. You can just copy it, do some renaming, delete the structures, that you will not need and bang – you are ready to create some of the best Shopware 6 plugins out there!
Minimal Shopware 6 plugin setup
The absolute bare minimum for a functional plugin can be obtained by using the following command:
1 |
php bin/console plugin:create |
Note: If you don’t know, how to use the Shopware 6 Console and its commands, I suggest you go read this Console tutorial first.
This command will ask you for the name of the plugin. Let us name it SkeletonPlugin to highlight its purpose. Alternatively, you could also put the name of the plugin to the command as a parameter like this:
1 |
php bin/console plugin:create SkeletonPlugin |
The command will then create a directory, named SkeletonPlugin, in your custom/plugins directory. It will also generate a subdirectory structure, that contains the composer.json, SkeletonPlugin.php and services.xml file.
I suggest we change the name of the plugin in composer.json file, so that it is unique. The generated default name is “swag/plugin-skeleton”, but we will change it to “shopwarian/plugin-skeleton” in my case or “you/plugin-skeleton” in your case. This way, it will be compliant with the standard, that I have arrogantly established in my plugin development guidelines article. 🙂
If you go to the Administration now, you can see the plugin in the Extensions – My Extensions listing:
Note: In Shopware version 6.3. and lower, the plugin listing is under Settings – System – Plugins – My Plugins:
It does nothing, but it works! 😀
Adding the config settings for the Shopware 6 plugin
The topic of plugin configs is covered quite thoroughly in an article, named “How to create config for your Shopware 6 plugin“. So if you are new to this, you should probably read it later on, but for now, we will just create a minimal config file, which looks like this:
1 2 3 4 5 6 7 8 |
<?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>SkeletonPlugin settings</title> </card> </config> |
It should reside in the src/Resources/config directory of the plugin. Alternatively to creating the config.xml file manually, you can also create it automatically, when you call the plugin:create command with the option -c:
1 |
php bin/console plugin:create -c |
Adding a service to the Shopware 6 plugin
What do I mean by “service” here? Well, essentially just a PHP class, that provides some functionality for other parts of our application. For example you could have a ProductService, CustomerService etc., each handling a certain area of your app. Such a service usually contains methods, that are called by controllers or even other services. In order to be able to call them, the other classes must “inject” our service in their constructors. And for that, our service must be registered in Shopware, using the services.xml file.
So let’s create a skeleton of a service class. We will call it simply SkeletonService for now and we will also put it to the Service subdirectory. In real life, I recommend naming the service classes based on their topic or area plus the keyword Service. It is much easier to orient yourself, when you have your classes named properly, like ImportService, ExportService and so on. So this is how the skeleton service setup looks like in src/Service/SkeletonService.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
<?php namespace SkeletonPlugin\Service; class SkeletonService { public function skeletonMethod ($variable) { var_dump($variable); } } |
And this is the updated version of services.xml file with the entry, containing the new service:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
<?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="SkeletonPlugin\Service\SkeletonService"> </service> </services> </container> |
Adding an event subscriber to the Shopware 6 plugin
Event subscribers are one of the most useful features in Shopware 6. They allow you to trigger your code, if a specific event happens. In order to to create a subscriber, we need to create a PHP class, that implements EventSubscriberInterface and then register this class to the services.xml file and tag it as a subscriber. A standard name and location for the new PHP class would be src/Subscriber/Subscriber.php.
This is a basic event subscriber for Shopware 6, that contains some frequently used event handlers for the product detail page and checkout:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 |
<?php namespace SkeletonPlugin\Subscriber; use Shopware\Storefront\Page\Checkout\Cart\CheckoutCartPageLoadedEvent; use Shopware\Storefront\Page\Checkout\Confirm\CheckoutConfirmPageLoadedEvent; use Shopware\Storefront\Page\Product\ProductPageLoadedEvent; use Symfony\Component\EventDispatcher\EventSubscriberInterface; class Subscriber implements EventSubscriberInterface { public static function getSubscribedEvents(): array { return [ ProductPageLoadedEvent::class => 'onProductPageLoaded', CheckoutCartPageLoadedEvent::class => 'onCheckoutPageLoaded', CheckoutConfirmPageLoadedEvent::class => 'onCheckoutConfirmPageLoaded' ]; } /** * Handles the stuff, that happens in the product detail page * * @param ProductPageLoadedEvent $event */ public function onProductPageLoaded (ProductPageLoadedEvent $event) { //do something, when the product detail page loads } /** * Calls onCheckoutPagesRefresh for handling stuff in all checkout pages * * @param CheckoutConfirmPageLoadedEvent $event */ public function onCheckoutConfirmPageLoaded(CheckoutConfirmPageLoadedEvent $event) : void { $this->onCheckoutPagesRefresh($event); } /** * Calls onCheckoutPagesRefresh for handling stuff in all checkout pages * * @param CheckoutCartPageLoadedEvent $event */ public function onCheckoutPageLoaded(CheckoutCartPageLoadedEvent $event) : void { $this->onCheckoutPagesRefresh($event); } /** * Handles the stuff, that happens in both checkout pages * * @param CheckoutCartPageLoadedEvent|CheckoutConfirmPageLoadedEvent $event */ private function onCheckoutPagesRefresh ($event) { //do something in the checkout } } |
This is the entry in the services.xml file, containing the new subscriber:
1 2 3 4 |
<service id="SkeletonPlugin\Subscriber\Subscriber"> <argument type="service" id="Symfony\Contracts\EventDispatcher\EventDispatcherInterface"/> <tag name="kernel.event_subscriber"/> </service> |
Adding a command to the Shopware 6 plugin
Symfony console, that is used in Shopware 6, allows us not just to run some very useful commands, but also create our own. The basic command setup is not very complicated. We just need to create a class, that extends the Command class and has the two most important methods. The first one is the “configure” method. It is not mandatory, but most of the time we need it anyway, because it contains the options and arguments for our command. The second method is, and in fact, must be named “execute”, because it gets automatically called, when you run the command from the console.
Obviously, in this method should be the main logic of your command, however I suggest keeping a bare minimum here, like for example argument validation, and calling a service from here instead. Why? To keep your application’s logic independent of from where or how is it called. The command itself should be just an entry, from which your main logic gets instructions and data, but the operations should be kept separately. There are cases, when you may want to have more ways to perform the same operation. For example you want to have a product import, that runs regularly as a command, using cron. But you also want to let the shop owner, who does not have access to the command line, to call it manually from the backend. If you put your main logic to both the command and backend, you will duplicate code unnecessarily and will run into problems rather soon. If you put it into a separate service and call this service from the command and the backend, then you will be able to maintain it easily, because your changes will have to be made in one place only.
We will name our command SkeletonCommand and put it into src/Command directory. In real life, I suggest naming the command classes, based on their purpose plus the keyword “Command”. For example ImportCommand, OldOrderArchivationCommand etc., so that it is apparent at first glance, what is the command’s main job.
Here comes the class SkeletonCommand.php:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
<?php namespace SkeletonPlugin\Command; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class SkeletonCommand extends Command { const ARG_NAME = 'argument'; const OPT_NAME = 'option'; protected static $defaultName = 'skeletonplugin:skeletoncommand'; protected function configure() { $this->addArgument(self::ARG_NAME, InputArgument::OPTIONAL, 'This is an optional argument.'); $this->addOption(self::OPT_NAME, null, InputOption::VALUE_OPTIONAL, 'This is an optional option.'); } protected function execute(InputInterface $input, OutputInterface $output) : int { //get the arguments $arguments = $input->getArguments(); //write a line to the console $output->writeln('Skeleton command works.'); //return success code return 1; } } |
Of course, we also need to add an entry to the services.xml file and tag it as a command to let Shopware know, that the “execute” method of our class should be called, when the specified command is executed in the console.
1 2 3 |
<service id="SkeletonPlugin\Command\SkeletonCommand"> <tag name="console.command" command="skeletonplugin:skeletoncommand"/> </service> |
And this is how the command is executed and how its output looks like:
1 |
php bin/console skeletonplugin:skeletoncommand |
Adding a controller to the Shopware 6 plugin
Generally speaking, a controller is a class, that receives requests and handles them. There are two most commonly used types of controllers – storefront controllers and backend controllers. As the names suggest, the former is used in the publicly visible part of the store, while the latter is used in the administration. For the purposes of our base plugin, we will add a storefront controller. The backend controller can be derived from it relatively easily, if needed.
First of all, we will add the storefront controller itself:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
<?php namespace SkeletonPlugin\Storefront\Controller; use Shopware\Core\Framework\Routing\Annotation\RouteScope; use Shopware\Core\System\SalesChannel\SalesChannelContext; use Shopware\Storefront\Controller\StorefrontController; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; /** * @RouteScope(scopes={"storefront"}) */ class SkeletonController extends StorefrontController { /** * @Route("/skeleton", name="frontend.skeletonplugin.skeleton", methods={"GET"}) */ public function showPage(Request $request, SalesChannelContext $context): Response { return $this->renderStorefront('@SkeletonPlugin/storefront/page/skeleton/index.html.twig', [ 'customParameter' => 'Custom parameter value' ]); } } |
It resides in the src/Storefront/Controller directory and is called SkeletonController. It is a class, that extends the Shopware 6 core class Shopware\Storefront\Controller\StorefrontController. In case you needed a controller for the backend, you should extend Symfony\Bundle\FrameworkBundle\Controller\AbstractController class.
Our basic storefront controller contains one method, that gets called, when the URL /skeleton is loaded in the user’s browser. For this to work, we need to add the /src/Resources/config/routes.xml file to our plugin’s structure:
1 2 3 4 5 6 7 8 |
<?xml version="1.0" encoding="UTF-8" ?> <routes xmlns="http://symfony.com/schema/routing" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/routing https://symfony.com/schema/routing/routing-1.0.xsd"> <import resource="../../Storefront/Controller/**/*Controller.php" type="annotation" /> </routes> |
It tells the system, that it should check the annotations (the commented code above the method declaration) and call the appropriate code, if a matching controller and method are found, when the user loads a page on our store. You can change the rules for the controller name matching later on, but an established standard is, that controllers are named like ‘SomethingController.php’.
Now we need to make sure, that the controller is registered in the system, it is made public and has the dependency injection container set to it. As usual, this is done in the services.xml:
1 2 3 4 5 |
<service id="SkeletonPlugin\Storefront\Controller\SkeletonController" public="true"> <call method="setContainer"> <argument type="service" id="service_container"/> </call> </service> |
One last thing remains to be added and we are ready to test the controller on the URL address /skeleton on our Shopware 6 store. One of the most common tasks for storefront controllers to do, is to prepare a page to be displayed to the customer. And this is precisely what our storefront controller is doing – rendering a page, using a template. In our case, it is a sample twig template, named index.html.twig, located in the src/Resources/views/storefront/page/skeleton directory:
1 2 3 4 5 6 |
{% sw_extends '@Storefront/storefront/base.html.twig' %} {% block base_content %} <h1>Skeleton controller works!</h1> {{ dump() }} {% endblock %} |
This template extends the base Shopware 6 page template, overriding its main base_content block. The output on your store’s page could look like this:
The sample template contains just the Twig’s dump command. It is extremely useful, because it displays all the variables, available in the template. As you can see, there is also a ‘customParameter’ present to demonstrate the way, how you can pass variables from the controller to the template. You will find the very same ‘customParameter’ in the SkeletonController.php. Please note, that you also have the Request and the Sales Channel Context at your disposal there. Imagine, what magic you could do with them.. 😉
That is it! Our plugin now contains the basic mechanisms for handling the requests from our store’s frontend. You can find more on the topic of controllers in the official Shopware 6 documentation. You might also want to check the articles on how to send parameters to the controllers and how to access parameters in the controllers in Shopware 6 here on my blog. If you want to make standard page components like the category menu or the currency switch visible on your custom page, you may want to take a look at the article on how to add basic page data to a custom page in Shopware 6.
Adding a migration to the Shopware 6 plugin
In the terminology of Shopware 6, a migration is a class, that manages the changes in the database schema. To put it more simply – it automatically runs the SQL commands, that change the structure of the database tables.
For the purposes of our groundwork plugin, we will create a migration, that will create a simple table with an ID, name and a custom fields column. By now, you have probably guessed the name of the new table.. Yes, it is ‘skeleton_table’. 🙂
This is the raw SQL code, that we would manually use to create such a table:
1 2 3 4 5 6 7 8 |
CREATE TABLE IF NOT EXISTS `skeleton_table` ( `id` BINARY ( 16 ) NOT NULL, `name` VARCHAR ( 255 ) NOT NULL COMMENT 'Name', `custom_fields` json DEFAULT NULL, `created_at` datetime(3) NOT NULL, `updated_at` datetime(3) DEFAULT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8; |
However, we want our plugin to be portable and therefore we will add this code to the migration and make it a part of our plugin. A migration is a class, that we can create manually as any other, but it is easier to use a command, that is made specifically for this purpose. We just need to specify the name of the plugin, to which we want the migration be generated by Shopware and the name, that we want to append to the new migration (mainly for better readability purposes). The command looks like this:
1 |
php bin/console database:create-migration -p SkeletonPlugin --name SkeletonEntity |
This will create a PHP file such as this in the src/Migration directory within our plugin:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
<?php declare(strict_types=1); namespace SkeletonPlugin\Migration; use Doctrine\DBAL\Connection; use Shopware\Core\Framework\Migration\MigrationStep; class Migration1620904408SkeletonEntity extends MigrationStep { public function getCreationTimestamp(): int { return 1620904408; } public function update(Connection $connection): void { // implement update } public function updateDestructive(Connection $connection): void { // implement update destructive } } |
All we need to do now, is to add the code to the update method of this migration class, that will execute the SQL command for creating the skeleton_table. The final migration therefore looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
<?php declare(strict_types=1); namespace SkeletonPlugin\Migration; use Doctrine\DBAL\Connection; use Shopware\Core\Framework\Migration\MigrationStep; class Migration1620904408SkeletonEntity extends MigrationStep { public function getCreationTimestamp(): int { return 1620904408; } public function update(Connection $connection): void { $connection->executeUpdate(" CREATE TABLE IF NOT EXISTS `skeleton_table` ( `id` BINARY ( 16 ) NOT NULL, `name` VARCHAR ( 255 ) NOT NULL COMMENT 'Name', `custom_fields` json DEFAULT NULL, `created_at` datetime(3) NOT NULL, `updated_at` datetime(3) DEFAULT NULL, PRIMARY KEY ( `id` ) ) ENGINE = INNODB DEFAULT CHARSET = utf8; "); } public function updateDestructive(Connection $connection): void { // implement update destructive } } |
This migration will be executed in the background, when you install your plugin. So make sure to remove or edit it, before you install your plugin, because every migration will be executed just once!
Adding an entity to the Shopware 6 plugin
So now, that we have taken care of the database table creation, we want to be able to work with this table via the DAL (data abstraction layer). This means, that we will be able to use a repository for our table, which is a standard, very convenient way to work with data in Shopware 6. In order to achieve this, we will have to define an entity.
In Shopware 6, we need three classes for creating a new entity over a database table: entity definition, entity class itself and entity collection.
Entity definition
Let us create the entity definition first. This is where you will want to put the name of your table. The same name will be used, when using the table as a repository. In this class, you also define the fields for Shopware, based on your table columns.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 |
<?php declare(strict_types=1); namespace SkeletonPlugin\Core\Content\SkeletonEntity; use Shopware\Core\Framework\DataAbstractionLayer\Field\CreatedAtField; use Shopware\Core\Framework\DataAbstractionLayer\Field\UpdatedAtField; use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition; use Shopware\Core\Framework\DataAbstractionLayer\Field\CustomFields; use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField; use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField; use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey; use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required; use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection; class SkeletonEntityDefinition extends EntityDefinition { public const ENTITY_NAME = 'skeleton_table'; public function getEntityName(): string { return self::ENTITY_NAME; } public function getCollectionClass(): string { return SkeletonEntityCollection::class; } public function getEntityClass(): string { return SkeletonEntity::class; } protected function defineFields(): FieldCollection { return new FieldCollection([ (new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()), new StringField('name', 'name'), new CustomFields('custom_fields', 'customFields'), new CreatedAtField(), new UpdatedAtField() ]); } } |
Entity
Entity contains the setters and getters for its fields.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
<?php declare(strict_types=1); namespace SkeletonPlugin\Core\Content\SkeletonEntity; use Shopware\Core\Framework\DataAbstractionLayer\Entity; use Shopware\Core\Framework\DataAbstractionLayer\EntityIdTrait; class SkeletonEntity extends Entity { use EntityIdTrait; /** * @var string */ protected $name; /** * @var array|null */ protected $customFields; public function getName(): string { return $this->name; } public function setName(string $name): void { $this->name = $name; } public function getCustomFields(): ?array { return $this->customFields; } public function setCustomFields(?array $customFields): void { $this->customFields = $customFields; } } |
Entity collection
Entity collection is the most simple of the three, but very important nonetheless.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
<?php declare(strict_types=1); namespace SkeletonPlugin\Core\Content\SkeletonEntity; use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection; /** * @method void add(SkeletonEntity $entity) * @method void set(string $key, SkeletonEntity $entity) * @method SkeletonEntity[] getIterator() * @method SkeletonEntity[] getElements() * @method SkeletonEntity|null get(string $key) * @method SkeletonEntity|null first() * @method SkeletonEntity|null last() */ class SkeletonEntityCollection extends EntityCollection { protected function getExpectedClass(): string { return SkeletonEntity::class; } } |
Entity registration
The last thing we need to do, if we want our new entity to work, is to register it in the services.xml file:
1 2 3 |
<service id="SkeletonPlugin\Core\Content\SkeletonEntity\SkeletonEntityDefinition"> <tag name="shopware.entity.definition" entity="skeleton_table" /> </service> |
Shopware 6 plugin skeleton download
So, the new groundwork plugin is finished. I hope it will serve you well, save you some time, if you are an experienced Shopware 6 developer and help you in the learning process, if you are a Shopware 6 beginner. Not every feature was thoroughly described, but I think it is an excellent playground for experimenting and learning the programming in Shopware 6.
Copying or even writing the code samples above by yourself could of course be helpful for learning the structure of Shopware 6 plugins. However, the more convenient way to go is to just download the ZIP archive, containing the plugin structure and files, unzip it to your custom/plugins directory and start editing.
Here is the link for the Shopware 6 base plugin download (ZIP archive):
Note: Do not forget to delete the files, that you do not need for your plugin and to remove the unnecessary entries from the services.xml file. Because nearly everything is named ‘skeletonSomething’, you can use mass replace text in your favorite editor quite conveniently, but please be aware of the case sensitivity – some expressions start with a lower case character, some with upper case.
If you have any suggestions on how to improve the skeleton plugin or what other commonly used features it should contain, feel free to leave a comment below. In any case, have fun with Shopware 6 plugin creation! 🙂
Hi, thank a lot for your tutorial. finally someone who explains it to inexperienced people.
I have just one little problem. When i go to myshop.de/skeleton the main menu with all categories is missing. Only “Home” is shown. Did i miss something? Is there a trick?
Hi Dominik, thank you for your encouraging words and also for making me aware of the problem with the missing page components. I have dug into it, resolved it and the result with source code is available in this new article: https://shopwarian.com/how-to-add-basic-page-data-to-a-custom-page-in-shopware-6/.
Hi Jan,
thank you for this tutorial – it is great work. Although one can find the basics in the Shopware documentation you are putting all the pieces together in one article and explain things very nicely.
Realy good introduction to this topic! I liked to work with that 🙂
Also the code examples helped allot!
Hey, This is the best guide about Shopware 6 Plugin Programming I have ever seen on the internet today. Very informative blog. I got to learn so many things from this article. Thanks for the wonderful piece of article.