Introduction
This is a guide that explains how to develop a module-extension which extends already existing entities. If you wish to add additional entities to the output-module you should use the mapping-extension. You will learn how a module-extension is able to validate and manipulate the entities generated by the output-module. For this purpose we will develop an extension, which implements the following requirements:
- Customers without a birthday must not be transferred to Shopware
- Assignment of B2B roles to customers
- Assignment of contact persons to customers
- The property "Tax display" of customer groups must be set to "gross" for one specific customer group
- Information about packing stations must be added to the addresses of orders
- Manufacturer links must not be transferred to Shopware
Module Structure
A module extension is an independent module that does not have a progress definition and does not receive a "start message".
Instead, you define one to many EventSubscriberInterfaces
which subscribe to the events dispatched by the output-module.
The module extension will have the following structure after completing this guide:
Shopware6ExampleExtension
│ composer.json
│
└───src
└───Config
│ ExampleExtensionConfigProvider.php
│
└───DependencyInjection
│ Shopware6ExampleExtensionExtension.php*
│
└───Output
│ └───Customer
│ │ CustomerB2bRoleSubscriber.php
│ │ CustomerBirthdayValidationSubscriber.php
│ │ CustomerContactPersonSubscriber.php
│ │
│ └───CustomerGroup
│ │ CustomerGroupSubscriber.php
│ │
│ └───Order
│ OrderAddressSubscriber.php
│
└───Resources
│ └───config
│ │ services.yaml
│ └───translations
│ messages+intl-icu.en.php
│
└───Subscriber
Shopware6ExampleExtensionBundleSubscriber.php
* The name of the extension class must end on "Extension", which is the reason for the "ExtensionExtension" in this case.
Preparations
First, the following elements need to be prepared:
- composer.json
- Shopware6ExampleExtensionExtension.php
- Shopware6ExampleExtensionBundleSubscriber.php
composer.json
{
"name": "synqup/shopware-6-example-extension",
"description": "shopware 6 example extension",
"type": "symfony-bundle",
"version": "0.0.1",
"authors": [
{
"name": "Daniel Rhode",
"email": "dr@synqup.com"
}
],
"autoload": {
"psr-4": {
"Synqup\\Modules\\Shopware6ExampleExtensionBundle\\": "src/"
}
}
}
Shopware6ExampleExtensionExtension.php
class Shopware6ExampleExtensionExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container) : void
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config'));
$loader->load('services.yaml');
}
Shopware6ExampleExtensionBundleSubscriber.php
class Shopware6ExampleExtensionBundleSubscriber extends ModuleExtensionBaseSubscriber
{
private MessageBusInterface $messageBus;
public function __construct(JobDispatcherMappingRepository $dispatcherMappingRepository, MessageBusInterface $messageBus)
{
parent::__construct($dispatcherMappingRepository);
$this->messageBus = $messageBus;
}
static function relevancyBasedSubscribedEvents(): array
{
return [];
}
public function modifyProgress(SubsectionProgressDefinition $progress, array $config): SubsectionProgressDefinition
{
return $progress;
}
}
Requirement 1 - Validation of Birthdays
Synqup\Modules\Shopware6Bundle\Output\Core\Mapping\ValidationInfo\ValidationKeys
Documents of type Customer
without a birthday should not be transferred to Shopware. This goal can be achieved by the implementation
of a CustomerBirthdayValidationSubscriber
that subscribes to the ShopwareEntityValidationEvent
:
class CustomerBirthdayValidationSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
ShopwareEntityValidationEvent::class => 'onShopwareEntityValidation'
];
}
public function onShopwareEntityValidation(ShopwareEntityValidationEvent $event)
{
if (!$this->eventContainsCustomer($event)) {
return;
}
/** @var Customer $sourceCustomer */
$sourceCustomer = $event->getSourceObject();
$birthday = $sourceCustomer->getCustomerInformation()?->getPersonalInformation()?->getDob() ?? null;
if ($birthday === null) {
$event->addValidationInfo(
true, # $invalidateEntity
'missing_customer_birthday', # $technicalIdentifier
'birthday', # $targetFieldName
'customerInformation.personalInformation.dob' # $sourcePath
);
}
}
private function eventContainsCustomer(ShopwareEntityValidationEvent $event): bool
{
return ShopwareEntityNames::isEqual(ShopwareEntityNames::CUSTOMER, $event->getShopwareEntityName());
}
}
There are things in this example that should be looked at in more detail. The first thing are shopware entity names and the second is how to add translatable validation messages to our extension.
Names of Shopware Entities
The method eventContainsCustomer
checks the name of the validated entity with the expression ShopwareEntityNames::isEqual(...)
. The
reason for this are different formats of names that can occur in Shopware and thus throughout the module as well:
- API requests require the name in the URL in
kebab-case
- API aliases of Shopware entities are delivered in
snake_case
- API associations are provided in
camelCase
The ShopwareEntityNames::isEqual(...)
method is used to ensure that different formats of the same entity name are considered equal
(e.g. customerAddress
, customer_address
and customer-address
).
Validation Infos
You can attach information about your validation result to the ShopwareEntityValidationEvent
by the help of the addValidationInfo
method. The following parameters are available:
-
$invalidateEntity
determines whether the entity will be flagged as invalid and therefore be ignored by the module. -
$technicalIdentifier
is the key used to provide a translatable validation message (see below) that contains the reason for your validation result. -
$targetFieldName
is optional and determines the name of the field of the Shopware entity that was validated. -
$sourcePath
is optional and specifies the path to the source value that was validated.
The parameters $targetFieldName
and $sourcePath
are optional. They exist to improve the readability of validation infos in logs and
the frontend.
You may have noticed the file messages+intl-icu.en.php
in the module structure. This is the file that contains the validation keys.
In our case it should look like this:
<?php
return [
'missing_customer_birthday' => 'Birthdays must not be zero',
];
The key missing_customer_birthday
added to the event will generate a validation info with the translated message provided by this
file.
Please refer to the symfony documentation for more information about translations.
Requirement 2 - Assigning B2B Flags to Customers
The second requirement is to assign B2B flags to generated customers. For this purpose we implement a subscriber for the
ShopwareEntityTransformedEvent
. This subscriber is then used to update the customer that was generated by the module.
class CustomerB2bRoleSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
ShopwareEntityTransformedEvent::class => 'onShopwareEntityTransformed'
];
}
public function onShopwareEntityTransformed(ShopwareEntityTransformedEvent $event)
{
if (!ShopwareEntityNames::isEqual(ShopwareEntityNames::CUSTOMER, $event->getShopwareEntityName())) {
return;
}
$shopwareCustomer = $event->getTransformedEntity();
// b2b data
$shopwareCustomer['b2bCustomerData'] = [
'isDebtor' => true,
'isSalesRepresentative' => true,
'customerId' => $shopwareCustomer['id']
];
$event->setTransformedEntity($shopwareCustomer);
}
}
This is a fairly simple example. But it shows the general way how we can extend entities:
- Subscribe to the
ShopwareEntityTransformedEvent
event - Check if the generated entity corresponds to the type of entity to be manipulated (by comparing names)
- Manipulate the Shopware entity according to your use case
- Update the processed entity in the event
Requirement 3 - Assignment of Contact Persons to Customers
The next step is to create a CustomerContactPersonSubscriber
that assigns contact persons (e.g. provided by a plugin) to customers.
For this requirement we assume that an import module extends the document Customer
with the CustomerCustomisation
extension. The procedure we learned in requirement 2 is applied to a more complex example now:
class CustomerContactPersonSubscriber implements EventSubscriberInterface
{
public const CONTACT_PERSON_INTERNAL_CUSTOM_FIELD_NAME = 'e_contact_person_number_internal';
public const CONTACT_PERSON_EXTERNAL_CUSTOM_FIELD_NAME = 'e_contact_person_number_external';
public static function getSubscribedEvents()
{
return [
ShopwareEntityTransformedEvent::class => 'onShopwareEntityTransformed'
];
}
public function onShopwareEntityTransformed(ShopwareEntityTransformedEvent $event)
{
// check if the transformed entity is a customer
if (!ShopwareEntityNames::isEqual(ShopwareEntityNames::CUSTOMER, $event->getShopwareEntityName())) {
return;
}
// ignore the entity if the source customer does not provide the extension that includes the customer contacts
/** @type Customer $synqupCustomer */
$synqupCustomer = $event->getSourceObject();
/** @type MicrotechCustomerCustomisation $customerCustomisationExtension */
$customerCustomisationExtension = $this->getCustomerCustomisationExtension($synqupCustomer);
if ($customerCustomisationExtension === null) {
return;
}
// add the customer contacts to the custom fields of the transformed shopware customer
$shopwareCustomer = $event->getTransformedEntity();
$customFields = $shopwareCustomer['customFields'] ?? [];
/** @var MicrotechCustomerContact $customerContactInternal */
$customerContactInternal = $customerCustomisationExtension->getOfficeCustomerContact() ?? null;
if ($customerContactInternal !== null && $this->hasValidIdentifier($customerContactInternal)) {
$customFields[self::CONTACT_PERSON_INTERNAL_CUSTOM_FIELD_NAME] = $customerContactInternal->getIdentifier();
}
/** @var MicrotechCustomerContact $customerContactExternal */
$customerContactExternal = $customerCustomisationExtension->getFieldCustomerContact() ?? null;
if ($customerContactExternal !== null && $this->hasValidIdentifier($customerContactExternal)) {
$customFields[self::CONTACT_PERSON_EXTERNAL_CUSTOM_FIELD_NAME] = $customerContactExternal->getIdentifier();
}
// update the transformed entity
$shopwareCustomer['customFields'] = $customFields;
$event->setTransformedEntity($shopwareCustomer);
}
private function hasValidIdentifier(MicrotechCustomerContact $contact): bool
{
$identifier = $contact->getIdentifier() ?? null;
return is_string($identifier) && $identifier !== '';
}
private function getCustomerCustomisationExtension(Customer $customer): ?MicrotechCustomerCustomisation
{
foreach ($customer->getExtensions() ?? [] as $extension) {
if ($extension instanceof MicrotechCustomerCustomisation) {
return $extension;
}
}
return null;
}
}
Of course, it would also be possible to assign the contact persons and B2B flags in the same subscriber. In this example we use two subscribers to demonstrate that multiple subscribers for the same entity are possible.
Requirement 4 - Manipulate Tax Display of Customer Groups
The next requirement is to set the tax display of a customer group to "gross". The customer group to change must be configurable.
Therefore, we will first discuss how to create an individual configuration for a module extension.
Provide a Configuration for Extensions
The module extension is automatically recognized and executed. If you need to provide a configuration for your extension you can do
this via the configuration of the output-module. Simply add your configuration to extensions
and use the FQCN of
the BundleSubscriber
as extension key. In this case we define the field customerGroupIdentifier
to set the customer group whose tax
representation should be set to "gross".
{
"extensions": {
"Synqup\\Modules\\Shopware6ExampleExtensionBundle\\Subscriber\\Shopware6ExampleExtensionBundleSubscriber": {
"customerGroupIdentifier": "1"
}
},
"lastSync": "...",
"batchSizes": {},
"shopwareApi": {},
"locales": {},
"identifier": {},
"subsections": {}
}
Finally, the configuration can be accessed via the context. In this case we create the class ExampleExtensionConfigProvider
for
a more convenient access of our configuration (this not required at all):
class ExampleExtensionConfigProvider
{
public const EXTENSION_KEY = "Synqup\\Modules\\Shopware6ExampleExtensionBundle\\Subscriber\\Shopware6ExampleExtensionBundleSubscriber";
public static function getConfig(ModuleJobDispatchContext $context): array
{
return $context->getConfig()['extensions'][self::EXTENSION_KEY];
}
}
ExampleCustomerGroupSubscriber.php
Now we can access our configuration in our subscriber. For this we implement the ExampleCustomerGroupSubscriber
. Apart from the
configuration it follows the same pattern as the previously created subscribers.
class CustomerGroupSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
ShopwareEntityTransformedEvent::class => 'onShopwareEntityTransformed'
];
}
public function onShopwareEntityTransformed(ShopwareEntityTransformedEvent $event)
{
if (!ShopwareEntityNames::isEqual(ShopwareEntityNames::CUSTOMER_GROUP, $event->getShopwareEntityName())) {
return;
}
$extensionConfig = ExampleExtensionConfigProvider::getConfig($event->getContext());
$customerGroupIdentifier = $extensionConfig['customerGroupIdentifier'];
/** @type CustomerGroup $synqupCustomerGroup */
$synqupCustomerGroup = $event->getSourceObject();
$shopwareCustomerGroup = $event->getTransformedEntity();
$shopwareCustomerGroup['displayGross'] = $synqupCustomerGroup->getIdentifier() === $customerGroupIdentifier;
$event->setTransformedEntity($shopwareCustomerGroup);
}
}
Requirement 5 - Extend Addresses by Packing Stations
This requirement concerns addresses of orders. If the delivery address of an order is a packing station, both the postal number
and packing station number should be written into a CustomField
. For this task we implement the OrderAddressSubscriber
.
Since the OrderAddressEntity
is an "embedded entity" we are faced with the problem that there is
no ShopwareEntityTransformedEvent
we can subscribe to. That's why we are using the ShopwareEntityGeneratedEvent
in this case. The reason for this is the structure of
the Shopware entities: An OrderAddressEntity
is transformed together with the parent OrderEntity
, so the
ShopwareEntityTransformedEvent
will only be generated for the OrderEntity
in this case.
class OrderAddressSubscriber implements EventSubscriberInterface
{
private const PACKSTATION_NUMBER_CUSTOM_FIELD = 'dhl_packstation_address_packstation_number';
private const POST_NUMBER_CUSTOM_FIELD = 'dhl_packstation_address_post_number';
public static function getSubscribedEvents()
{
return [
ShopwareEntityGeneratedEvent::class => 'onShopwareEntityGenerated'
];
}
public function onShopwareEntityGenerated(ShopwareEntityGeneratedEvent $event)
{
// check shopware entity type
if (!$event->getGeneratedEntity() instanceof OrderAddressEntity) {
return;
}
// check source address type
if (!$event->getSourceObject() instanceof DHLPackstationAddress) {
return;
}
// read generated entity
/** @type OrderAddressEntity $shopwareAddress */
$shopwareAddress = $event->getGeneratedEntity();
// get the shipping address of the order
/** @var DHLPackstationAddress $synqupAddress */
$synqupAddress = $event->getSourceObject();
// add station number as custom field
$stationNumber = $synqupAddress->getPackstationNumber() ?? null;
if(!empty($stationNumber)) {
$this->addCustomFieldValue($shopwareAddress, self::PACKSTATION_NUMBER_CUSTOM_FIELD, $stationNumber);
}
// add post number as custom field
$postNumber = $synqupAddress->getPostNumber() ?? null;
if(!empty($postNumber)) {
$this->addCustomFieldValue($shopwareAddress, self::POST_NUMBER_CUSTOM_FIELD, $postNumber);
}
// update the generated entity
$event->setGeneratedEntity($shopwareAddress);
}
private function addCustomFieldValue(OrderAddressEntity $shopwareAddress, string $fieldName, string $value): void
{
$customFields = $shopwareAddress->getCustomFields() ?? [];
$customFields[$fieldName] = $value;
$shopwareAddress->setCustomFields($customFields);
}
}
Requirement 6 - Manufacturer links must not be transferred to Shopware
In the next example, the links from manufacturers should not be sent to Shopware, because they are maintained by the customer manually.
class ManufacturerLinkSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents()
{
return [
ShopwareEntityTransformedEvent::class => 'onShopwareEntityTransformed'
];
}
public function onShopwareEntityTransformed(ShopwareEntityTransformedEvent $event)
{
if (!ShopwareEntityNames::isEqual(ShopwareEntityNames::PRODUCT_MANUFACTURER, $event->getShopwareEntityName())) {
return;
}
$outgoingManufacturer = $event->getTransformedEntity();
unset($outgoingManufacturer['link']);
$event->setTransformedEntity($outgoingManufacturer);
}
}