In this blog post, we are delving deeper into the topic of plugin development, and demonstrating how to connect an external system to Shopware 6. We will transfer the products into Shopware, create plugin configurations and define automated tasks.
Those who still have a functioning demo environment can safely skip the next section.
Setting up a demo environment
We are using the Docker environment running on a Linux client. First, we will clone the GitHub development template using git clone firstname.lastname@example.org:shopware/development.git. The development template for Shopware 6 is now in the “development” directory. We can now go into the directory using cd development and clone the actual Shopware platform repository into the standard directory using git clone email@example.com:shopware/platform.git.
Caution: Please do not specify any other directory when cloning, as this is important for autoloading.
We now have all the source code we need to get started on the computer. To build and start the necessary Docker containers, we enter ./psh.phar docker:start. This command builds and starts all Docker containers. We can now 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 the initial execution requires several caches to be created. To check whether the installation was successful, you can simply open your preferred browser and access http://localhost:8000.
Mac users can also perform the entire set-up locally. You can find an example virtual host configuration here under “Setting up your webserver”. All you have to do then is execute bin/setup and you will be guided through an interactive installation process.
If something doesn’t work during the installation, check if the .psh.yaml.override exists. If not, restart the setup script using ./psh.phar install.
Let’s start by creating the sample plugin: We first switch to the shopware-root/custom/plugins directory and create the following directory: ShopwareBlogFastBillConnector. We name the directory after the VendorPrefix ShopwareBlog and the technical plugin name FastBillConnector. Other directory names are possible in principle, but should not be used for reasons of clarity. In order to prepare for the steps to come, we create all directories as depicted in Listing 1.
Listing 1: Plugin structure
Every plugin is based on a composer.json which includes the name, version, requirements and lots of other meta information. Developers who are familiar with Composer will be no stranger to many of the features. Every plugin we develop, like any other package, can be added automatically via composer require. The use of composer schema elements of any kind [https://getcomposer.org/doc/04-schema.md] is expressly desired.
A detailed description of each entry from composer.json can be found in the previous Shopware 6 blog post.
Listing 2: Composer.json
Unlike our last plugin, we have now defined an external dependency in the composer.json. We use the PHP SDK published by FastBill itself. This saves us a considerable amount of effort and allows us to get straight into the import programming without having to worry about communicating with the API.
Another small change introduced by Shopware version 6.1.1 [https://github.com/shopware/platform/pull/395] allows us to specify which author from composer.json should be used and subsequently displayed in the plugin manager. We have updated the author entry with “role”: “Manufacturer” for this purpose. If no role is defined, the output will remain the same as before, i.e. all authors will be displayed.
We have now deposited all necessary meta-information in the composer.json and can create the FastBillConnector.php in the plugin-root/src directory. The content of the file can be seen in Listing 3. This time, we won’t be using an empty class but implementing the install method.
Listing 3: Plugin base class
Using this method, we create a CustomField to store the FastBill ArticleId with the product. You can use the CustomFieldSetRepository to create CustomFields for each Shopware Core Entity. The link is established via the ArrayKey relations. The CustomFields resemble the free-text fields in Shopware 5. The big difference is that the CustomFields are saved as json-blob and not in separately defined attribute tables as in Shopware 5. This completes the plugin basics class.
In order to avoid having to store the API access data from FastBill in the source code, we now create a plugin configuration that contains the FastBill username and FastBill-ApiKey.
To do so, we create the config.xml in the plugin-root/src/Resources/config directory as shown in Listing 4. We can group different configuration elements together in so-called “cards”. Each card has a title and can be translated into different languages.
Listing 4: config.xml
Any number of input fields can be defined within a card. In our case, we create fastBillUserName and fastBillApiKey. To ensure that the ApiKey is not visible to everyone, we configure the input field as a password field with type=”password”. You can read up on additional possibilities here [https://docs.shopware.com/en/shopware-platform-dev-en/internals/plugins/plugin-config].
Just like the title of a card, the labels for the input fields can also be translated. The default language is always English.
Now we want to see if our newly developed plugin can be installed. To do so, we go to the shopware-root folder and navigate to the bin directory. Using the command ./console plugin:refresh, we update all existing and installable plugins. We then use ./console plugin:install --activate --clearCache FastBillConnector to install and activate our plugin, and clear the shop cache. Unless we receive an error message, we have just successfully installed our Shopware 6 plugin.
We can now go into the administration and access the plugin configuration via the plugin manager. This is depicted in the following image.
We have created the basic framework of the plugin and created a configuration option. We now want to instantiate the ApiClient from the FastBill SDK so that we can use it later in our importer.
To do so, we create the file ApiClientFactory.php in plugin-root/src/Api with the content taken from Listing 5.
Listing 5: ApiClientFactory.php
We can easily access the previously created plugin configuration via the SystemConfigService. The underlying naming scheme is very simple: $technicalPluginName.config.$configKey. The $technicalPluginName prefix prevents potential name clashes with other plugins.
All we need now is the configuration in the Symfony Dependency Injection Container. For this, we create the file services.xml under plugin-root/src/Resources/config with the content taken from Listing 7. First, the factory is made known to the DI Container. Adding another service entry then creates our FastBillApiClient that we’ll need later for our importer.
We’re now getting to the exciting part of the development. The ProductImporter (see Listing 6) has three dependencies. The first one is the ProductService from the FastBill SDK with which we retrieve all products via the API. The ProductService is only instantiated via the services.xml (see Listing 7). The other two dependencies are EntityRepositories from Shopware 6 that we need in order to check for existing VAT rates and products.
Listing 6: ProductImporter.php
The importProducts() method is where the “magic” happens. We use the getProducts() method from ProductService to retrieve all products created in FastBill. We iterate through these products and enrich them with the necessary information.
To do so, we search the taxRepository for the appropriate UUID at the VAT rate of the FastBill product. The UUID is required to set up the product. At this point, we assume that every VAT rate from FastBill is already set up in Shopware. If this is not the case, the import would terminate with an exception at this point.
The next step is to see if we have already created a product with the FastBillId. To do so, we query the swb_fastbill_id CustomField that was set up at the beginning. If we receive a positive response to the query, the UUID is transferred and the product is updated rather than newly created.
Last but not least, the product data from FastBill is transferred into an array structure. We can now use the ArrayKey customFields to fill our previously created CustomField with data. All other ArrayKeys should be self-explanatory.
Finally, we transfer the ProductArray to the ProductRepository. The upsert method used ensures that the data records are updated in the case of a stored UUID, or otherwise newly created.
Listing 7: Dependency Injection Container
So what is the easiest way to execute our newly finished importer? The answer is a command. We create the file ProductImportCommand.php under plugin-root/src/Command with the content taken from Listing 8.
Listing 8: ProductImportCommand.php
We use the constructor to define the ProductImporter as dependencies and register our command in the DI Container (see Listing 7). It is important that the service entry gets the tag “console.command” in order for the automatic registration to work.
Provided that valid access data is stored in the plugin configuration, we can now import all FastBill products into our Shopware 6 shop via ./bin/console fastbill:import-products.
As a shop owner, you of course don’t want to have to manually execute the command for every new product created in FastBill. In Shopware 5, you would have simply set up a cron job for this purpose. In Shopware 6, you create a so-called ScheduledTask instead. In the broadest sense, this is nothing more than a message that gets placed in a queue and then processed by a worker. We create the required task as ProductImportTask.php under plugin-root/src/ScheduledTask. The required source code can be found in Listing 9. As you can see, there are two things defined here: a task name and then the interval in which it should be executed. In this example, it would be every 5 minutes.
Listing 9: ProductImportTask.php
In order to ensure that the messages are regularly written into the queue, the following command must be executed: bin/console scheduled-task:run --memory-limit=128M
The ScheduledTask is again configured via the DI Container. The tag required for this is “shopware.scheduled.task”.
We now have a message that gets placed in the queue every 5 minutes, but no one to take care of it. We therefore create a handler under plugin-root/src/ScheduledTask named ProductImportHandler.php using the content as in Listing 10.
Listing 10: ProductImportTaskHandler.php
Using the getHandleMessage() method, we define the task for which the handler is responsible. Our previously created ProductImporter is then called up in the run() method. The queue is processed via bin/console messenger:consume.
More information on the ScheduledTask can be found in our online documentation.
Although we have made assumptions in some places for the sake of simplifying the development a little, we have very quickly achieved a useful result. Of course, several things remain to be implemented and extended for productive use, but it is impressive how little source code is needed to import products into a Shopware 6 shop.