Introduction
In most cases the mapping extension is used to populate tables that were added to Shopware by plugins. Therefore, this guide explains how to configure the mapping-extension and your documents using a fictitious plugin. The plugin used in this example intends to provide examples for references and translations and does not try to be as realistic as possible. In order to understand this guide it is highly recommended to read all documentations about the general concepts of the mapping-extension beforehand.
Inspecting the Plugin
The first step is always to analyze the tables added by the plugin. The plugin adds the possibility to customers to make a request for products, for example because they are not available at the moment. This is implemented by adding the following tables:
- The table
request
represents a customer request, so it has a reference to thecustomer
table from Shopware. - A single request can affect several products, so there is a many-to-many reference to the
product
table from Shopware (implemented by the junction tableproduct_request
added by the plugin). You can also describe this as "request positions". - A request can change its state (e.g. open, in progress, closed), so it has a reference to the
request_state
table added by the plugin as well. - The name of a request state is translatable, so it is stored in a translation table called
request_state_translation
It is a naming convention to add a prefix to our table names (e.g. s_request
instead of request
). That's why you will find both
names in this guide.
In the following you can find a simplified representation of the migration to create the tables:
CREATE TABLE `s_request_state`
(
`id` BINARY(16) NOT NULL,
`code` VARCHAR(2) NOT NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NULL,
PRIMARY KEY (`id`)
);
CREATE TABLE `s_request_state_translation`
(
`s_request_state_id` BINARY(16) NOT NULL,
`language_id` BINARY(16) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NULL,
PRIMARY KEY (`s_request_state_id`, `language_id`),
CONSTRAINT `...` FOREIGN KEY (`language_id`) REFERENCES `language` (`id`) ON ...,
CONSTRAINT `...` FOREIGN KEY (`s_request_state_id`) REFERENCES `s_request_state` (`id`) ON ...
);
CREATE TABLE `s_request`
(
`id` BINARY(16) NOT NULL,
`s_request_state_id` BINARY(16) NOT NULL,
`customer_id` BINARY(16) NOT NULL,
`language_id` BINARY(16) NOT NULL,
`comment` varchar(255) NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
CONSTRAINT `...` FOREIGN KEY (`s_request_state_id`) REFERENCES `s_request_state` (`id`) ON ...,
CONSTRAINT `...` FOREIGN KEY (`customer_id`) REFERENCES `customer` (`id`) ON ...,
CONSTRAINT `...` FOREIGN KEY (`language_id`) REFERENCES `language` (`id`) ON ...
);
CREATE TABLE `s_request_product`
(
`product_id` BINARY(16) NOT NULL,
`s_request_id` BINARY(16) NOT NULL,
PRIMARY KEY (`product_id`, `s_request_id`),
CONSTRAINT `...` FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON ...,
CONSTRAINT `...` FOREIGN KEY (`s_request_id`) REFERENCES `s_request` (`id`) ON ...
);
Create Documents and Mapping Configurations
The next steps are to derive corresponding documents for your tables and to generate an object-mapping configuration for that document. We will perform those steps for all Shopware tables the plugin implements.
Request State
We start with the request_state
and request_state_translation
tables. This shows you how simple it actually is to map translations
with the mapping-extension. All you have to do is to add the field name
with type TranslationCollection
to your
document and set its isTranslated
configuration to true.
All documents need a valid and unique identifier since the mapping extension calculates the uuids to use in Shopware from the
identifiers. That's why Shopware field code
is used as identifier.
The following figure shows you the document RequestState
.
/**
* Document for the tables 'request_state' and 'request_state_translations'.
*
* @MongoDB\Document
*/
class RequestState extends BaseDocument
{
use UniqueIdentifiableDocument;
/**
* Example for automatic translations mapping * * @var TranslationCollection
* @MongoDB\EmbedMany(
* collectionClass=TranslationCollection::class,
* targetDocument=Translation::class,
* strategy="set"
* )
*/
private TranslationCollection $name;
// constructor, getters, setters, ...
}
Afterwards you have to create the object-mapping configuration. The object-mapping configuration tells the mapping-extension how to populate a Shopware table with the data of your documents.
As mentioned before there is no separate configuration for the translations table. So you only have to add two fields to the
configuration in this case: code
and name
.
The field code
is a string field and is configured as follows:
{
"targetFieldName": "code",
"sourcePath": "identifier",
"stringFieldConfig": {
"maxLength": 2,
"allowsHtml": false,
"isTranslated": false,
"allowsNullValues": false
}
}
- Since code is a varchar column we choose a field configuration of type
stringFieldConfig
- The column only allows strings of length 2, so we set the
maxLength
value accordingly - Please note that the config values for
allowsHtml
,isTranslated
andallowsNullValues
are equal to their default values and could be omitted in this case
The field name
is a translated field. All you have to do is to set isTranslated to true. So the configuration looks like this:
{
"targetFieldName": "name",
"sourcePath": "name",
"stringFieldConfig": {
"maxLength": 255,
"isTranslated": true
}
}
If you combine those configurations you get the following result for the object-mapping configuration for our RequestState
document:
{
"mappings": [
{
"targetTableName": "s_request_state",
"sourceDocumentFqcn": "Synqup\\...\\RequestState",
"fields": [
{
"targetFieldName": "code",
"sourcePath": "identifier",
"stringFieldConfig": {
"maxLength": 2,
"allowsHtml": false,
"isTranslated": false,
"allowsNullValues": false
}
},
{
"targetFieldName": "name",
"sourcePath": "name",
"stringFieldConfig": {
"maxLength": 255,
"allowsHtml": false,
"isTranslated": true
}
}
]
}
]
}
Please note that you have to configure a valid locale configuration in order to map translations successfully.
Request
The next table we want to map is request
. So we implement the corresponding document Request
:
/**
* Document for the table 'request'.
*
* @MongoDB\Document
*/
class Request extends BaseDocument
{
use UniqueIdentifiableDocument;
/**
* Example for an external reference with documents. References a customer (CustomerEntity) in Shopware.
*
* @var Customer
* @MongoDB\ReferenceOne(targetDocument=Customer::class, cascade="persist")
*/
private Customer $customer;
/**
* Example for an internal reference.
*
* @var RequestState
* @MongoDB\ReferenceOne(targetDocument=RequestState::class, cascade="persist")
*/
private RequestState $state;
/**
* Requested products
*
* @var Collection<Product>
* @MongoDB\ReferenceMany(targetDocument=Product::class)
*/
protected Collection $products;
/**
* @var string
* @MongoDB\Field(type="string")
*/
private string $comment;
/**
* Example for an external reference without documents. References a language (LanguageEntity) in Shopware.
*
* @var string
* @MongoDB\Field(type="string")
*/
private string $language;
// construct, getters, setters, ...
}
This document contains three foreign key relations:
- Internal reference to the document
RequestState
- External reference to the document
Customer
- External reference to the Shopware entity
Language
As you can see there are actually two types of external references. You can either reference a document from your database or just use simple values like a string to generate a reference. Please refer to the documentation about general concepts for more information.
Internal Reference to States
It is pretty straight forward to generate a reference to a document that is mapped by the mapping-extension as well. The mapping extension detects internal foreign keys automatically. All you have to do is the following:
- set a value into
references.table
that is equal to atargetTableName
of another (the referenced) object-mapping configuration - set the
sourcePath
of the field to the referenced document (do not reference its identifier or id). This leads to the following configuration:
{
"targetFieldName": "s_request_state_id",
"sourcePath": "state",
"foreignKeyFieldConfig": {
"references": {
"table": "s_request_state"
},
"allowsNullValues": false
}
}
External Reference to Customer
External references a more complicated to set up than internal references. As mentioned before there are two possible ways to set up an external reference (document or single value). They are not that different to configure though. The reference to customers is an example for a reference via document.
The concept of external references is easiest to understand with an example. Let's assume the document Request
references
a Customer
whose identifier is 12345. You would implement this reference as follows:
$request = new Request(...);
$customerRepository = $this->documentManager->getRepository(Customer::class);
$customer = $customerRepository->findOneBy(['identifier' => '12345']);
$request->setCustomer($customer);
We make a small excursion into the internals of the mapping extension before generating the configuration of this field. After setting
up the reference in our document we want the mapping extension to reference the corresponding customer in Shopware. In this case this
is done by the customer number. In other words: You want the mapping extension to generate a foreign key reference to the Shopware
customer whose customer number is equal to 12345. For this the mapping-extension has to write the id of the referenced customer into
our customer_id
column. This requirement as a mysql query looks like this:
- In General:
SELECT {{references.field}} FROM {{references.table}} WHERE {{filterSearchField}} = {{value @ sourcePath}}
. - With values:
SELECT id FROM customer WHERE customer_number = 12345
Of course the mapping-extension uses the API to find the corresponding customer. But the query illustrates very well what which value actually does in the configuration. The same example would look like this as api-request:
In General
{{HOST}}/api/search/{{references.table}}/
{
"filter": [
{
"type": "equals",
"field": "{{filterSearchField}}",
"value": "{{value @ sourcePath}}"
}
]
}
With values:
{{HOST}}/api/search/customer/
{
"filter": [
{
"type": "equals",
"field": "customerNumber",
"value": "12345"
}
]
}
This request returns the matching customer entity. Its UUID is written to the customer_id
field.
Now we are able to generate the configuration for this foreign key field:
{
"targetFieldName": "customer_id",
"sourcePath": "customer.identifier",
"foreignKeyFieldConfig": {
"references": {
"table": "customer"
},
"allowsNullValues": false,
"filterSearchField": "customerNumber"
}
}
So what the mapping extension will do in order to generate a reference to a customer from this configuration:
-
sourcePath
: Read the value from "customer.identifier" (12345) -
references.table
: Generate a search request to the table customer (.../search/customer) -
filterSearchField
: Add an equals filter to the search request for the field "customerNumber" with the value from sourcePath (12345) -
allowsNullValues
: If no customer was found the document will be sorted out by validation.
References: On-Demand vs Bulk
Please refer to the general documentation for an in-depth explanation of the different reference modes.
This example is also able to explain the difference between the two reference loading modes the mapping extension provides (on-demand
and bulk
).
The on-demand mode is exactly what was explained in the section before: Read a single corresponding Shopware entity when needed by the help of the request shown above.
The difference in the bulk
mode is that entities are not searched when needed but all at once. This would lead to a request like
this:
{{HOST}}/api/search/customer/
{
"filter": [
{
"type": "equalsAny",
"field": "customerNumber",
"value": ["12345", "112233", "...", "..."]
}
]
}
To be able to search several referenced entities at once the module needs to extract those referenced beforehand. This is done in the
subsection relation-extract
. The entities are searched in the subsection relation-cache
. Those subsections are only added to the
subsection tree of an object-mapping with the bulk mode is present.
External Reference to Language
This is an example for an external reference via single value. Let's assume our Request
document references a language whose locale
is equal to en-GB
:
$request = new Request(...);
$request->setLanguage('en-GB');
We want to reference the language
in Shopware whose locale code is equal to en-GB
. If you take a look at the structure
of the table in shopware there is one problem though:
-- simplified language DDL
create table language
(
id binary(16) not null primary key,
name varchar(50) not null,
parent_id binary(16) null,
locale_id binary(16) not null,
translation_code_id binary(16) null,
custom_fields json null,
-- timestamps ...
constraint `fk.language.locale_id` foreign key (locale_id) references locale (id) on update ...,
-- other constraints ...
);
As you can see there is no locale
field in the table. Instead, locales are stored in a separate table. However, the language has a
reference to that table which we can use as an association in our configuration and api request. If we want to find the language whose
locale code is equal to en-GB we can use the following search request:
{
"filter": [
{
"type": "equals",
"field": "locale.code",
"value": "de-DE"
}
],
"associations": {
"locale": {}
}
}
This request returns the language whose locale is en-GB. If you want the mapping-extension to recreate this request in order to set the uuid of that language to your entity we can use the following configuration:
{
"targetFieldName": "language_id",
"sourcePath": "language",
"foreignKeyFieldConfig": {
"references": {
"table": "language"
},
"allowsNullValues": false,
"filterSearchField": "locale.code"
}
}
Using filter paths
In this example there is a very important detail: the value in filterSearchField
is actually a path instead of a field.
You can search external referenced entities with associations. But please note that this must be a valid filter path in Shopware.
There are some limitations to using filter paths instead of fields:
- The path must be a valid association in Shopware. Please test the request you want to generate (e.g. in Postman) to avoid
FRAMEWORK_ASSOCIATION_NOT_FOUND
errors. - Only single layered associations are supported, the path
one.two.three
would be invalid. - Although it is not an association you can also search for values of customFields (e.g.
customFields.identifier
).
External Reference to Products
Since this is an example for many-to-many associations and will populate the table request_product
please refer to the next section.
Request Positions - Many-To-Many References
Now It's time to configure the last table that is left which is product_request
.
As mentioned in the introduction a request can have several "request positions", which basically means that a customer can request
several products at the same time. So this is a many-to-many association between the tables request
and product
, which is
implemented by the help of the junction table product_request
.
As a preparation we already added the field products
to the document Request
which contains all requested products (see above):
/**
* @var Collection<Product>
* @MongoDB\ReferenceMany(targetDocument=Product::class)
*/
protected Collection $products;
Unfortunately there is a limitation regarding the mapping of many-to-many associations. It is not possible to use this collection of
products to populate the table product_request
automatically yet. However, the procedure to populate this table is not that different
from the examples above. You have to create a corresponding document for the product_request
table that the mapping extension will
use to populate this association:
/**
* Document for the junction table "product_request" since many-to-many associations are not mapped automatically yet.
*
* @MongoDB\Document
*/
class RequestProduct extends BaseDocument
{
use UniqueIdentifiableDocument;
/**
* Example for an internal reference.
*
* @var DemoRequest
* @MongoDB\ReferenceOne(targetDocument=DemoRequest::class, cascade="persist")
*/
private DemoRequest $request;
/**
* Example for an external reference without documents.
*
* @var string
* @MongoDB\Field(type="string")
*/
private string $productNumber;
// construct, getters, setters, ...
}
A possible workaround could be to populate your document (including the products
collection) as you normally would. In a second step
you could take your document and generate the "junction documents":
$requests = $documentManager->getRepository(Request::class)->findAll();
foreach ($requests as $request) {
foreach ($request->getProducts() as $product) {
$requestProduct = new RequestProduct();
$requestProduct->setIdentifier($request->getIdentifier() . $product->getIdentifier());
$requestProduct->setRequest($request);
$requestProduct->setProductNumber($product->getIdentifier());
$documentManager->persist($requestProduct);
}
}
$documentManager->flush();
Of course this is not the best solution and should be adjusted to your needs (depending on the amount of entities it can be a bad idea to iterate all documents at once).
The next step is to generate the field configuration for our junction table product_request
. It does not look that different from all
examples above:
{
"targetTableName": "s_request_product",
"isJunctionTable": true,
"sourceDocumentFqcn": "Synqup\\...\\RequestProduct",
"fields": [
{
"targetFieldName": "s_request_id",
"sourcePath": "request",
"foreignKeyFieldConfig": {
"references": {
"table": "s_request"
}
}
},
{
"targetFieldName": "product_id",
"sourcePath": "productNumber",
"foreignKeyFieldConfig": {
"references": {
"table": "product"
},
"allowsNullValues": false,
"filterSearchField": "productNumber"
}
}
]
}
Please note the configuration field isJunctionTable
which is set to true since the table is a junction table for a
many-to-many association. Apart from that the configuration follows the same principles as before.
Configuration of the Mapping Extension
Since object-mappings are handled in sequential order, referenced entities must be configured before referencing entities. In this
case: The object mapping of DemoRequestState
must be added to the configuration before DemoRequest
and DemoRequest
must be
added before DemoRequestProduct
.
After all mappings are done we have to add the configuration to the main module. The following configuration contains the default settings for the mapping extension:
-
primaryKeyMode
is the highly recommended primary key mode -
referenceLoadingMode
isbulk
( see reference loading modes) for details -
batchSizes
are the defaults as well. If you feel your extension is running too slow feel free to increase those numbers ( see batch-sizes) -
deleteAnomalyThresholdPercentage
is the default value of 75 ( see document deletion for details)
{
"extensions": {
"Synqup\\Modules\\Shopware6MappingExtensionBundle\\Subscriber\\Shopware6MappingExtensionBundleSubscriber": {
"primaryKeyMode": "identifier",
"referenceLoadingMode": "bulk",
"deleteAnomalyThresholdPercentage": 75,
"batchSizes": {
"validate": 200,
"upsert": 75,
"delete": 100,
"referenceCache": 50,
"referenceExtract": 200
},
"mappings": [
{
"targetTableName": "s_request_state",
"sourceDocumentFqcn": "Synqup\\Modules\\IntegrationBundle\\Document\\Request\\DemoRequestState",
"fields": [
{
"targetFieldName": "code",
"sourcePath": "identifier",
"stringFieldConfig": {
"maxLength": 2,
"allowsHtml": false,
"isTranslated": false,
"allowsNullValues": false
}
},
{
"targetFieldName": "name",
"sourcePath": "name",
"stringFieldConfig": {
"maxLength": 255,
"allowsHtml": false,
"isTranslated": true
}
}
]
},
{
"targetTableName": "s_request",
"sourceDocumentFqcn": "Synqup\\Modules\\IntegrationBundle\\Document\\Request\\DemoRequest",
"fields": [
{
"targetFieldName": "comment",
"sourcePath": "comment",
"stringFieldConfig": {
"maxLength": 255,
"allowsHtml": false,
"isTranslated": false,
"allowsNullValues": false
}
},
{
"targetFieldName": "customer_id",
"sourcePath": "customer.identifier",
"foreignKeyFieldConfig": {
"references": {
"table": "customer"
},
"allowsNullValues": false,
"filterSearchField": "customerNumber"
}
},
{
"targetFieldName": "language_id",
"sourcePath": "language",
"foreignKeyFieldConfig": {
"references": {
"table": "language"
},
"allowsNullValues": false,
"filterSearchField": "locale.code"
}
},
{
"targetFieldName": "s_request_state_id",
"sourcePath": "state",
"foreignKeyFieldConfig": {
"references": {
"table": "s_request_state"
},
"allowsNullValues": false
}
}
]
},
{
"targetTableName": "s_request_product",
"isJunctionTable": true,
"sourceDocumentFqcn": "Synqup\\Modules\\IntegrationBundle\\Document\\Request\\DemoRequestProduct",
"fields": [
{
"targetFieldName": "s_request_id",
"sourcePath": "request",
"foreignKeyFieldConfig": {
"references": {
"table": "s_request"
}
}
},
{
"targetFieldName": "product_id",
"sourcePath": "productNumber",
"foreignKeyFieldConfig": {
"references": {
"table": "product"
},
"allowsNullValues": false,
"filterSearchField": "productNumber"
}
}
]
}
]
}
}
}
That's it - next time you start the output module the mapping extension will transfer your documents to the plugin tables.