If you want to migrate your data from a different source system than Shopware, create a new migration profile for the Migration Assistant. But if you want to convert your plugin data from a Shopware system to Shopware 6, please have a look at this HowTo: extend a shopware migration profile.
First of all, it is required that you already have installed the Migration Assistant plugin in Shopware 6 and have created a demo source system database with a product
table. To create the table, use this SQL statement:
CREATE TABLE product
(
id int PRIMARY KEY NOT NULL AUTO_INCREMENT,
product_number varchar(255) NOT NULL,
price float NOT NULL,
stock int NOT NULL,
product_name varchar(255) NOT NULL,
tax float NOT NULL
);
This table should simulate simple a third party source system, which should be migrated in the following steps.
In the first step, you have to create a new profile for your source system:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample;
use SwagMigrationAssistant\Migration\Profile\ProfileInterface;
class OwnProfile implements ProfileInterface
{
public const PROFILE_NAME = 'ownProfile';
public const SOURCE_SYSTEM_NAME = 'MySourceSystem';
public const SOURCE_SYSTEM_VERSION = '1.0';
public const AUTHOR_NAME = 'shopware AG';
public const ICON_PATH = '/swagmigrationassistant/static/img/migration-assistant-plugin.svg';
public function getName(): string
{
return self::PROFILE_NAME;
}
public function getSourceSystemName(): string
{
return self::SOURCE_SYSTEM_NAME;
}
public function getVersion(): string
{
return self::SOURCE_SYSTEM_VERSION;
}
public function getAuthorName(): string
{
return self::AUTHOR_NAME;
}
public function getIconPath(): string
{
return self::ICON_PATH;
}
}
The profile itself does not contain any logic and is used to bundle the executing classes. To use this profile, you have to register and tag it in the service.xml
with shopware.migration.profile
:
<service id="SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile">
<tag name="shopware.migration.profile"/>
</service>
Next, you have to create a new gateway, which supports your profile:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample\Profile\OwnProfile\Gateway;
use Shopware\Core\Framework\Context;
use SwagMigrationAssistant\Migration\EnvironmentInformation;
use SwagMigrationAssistant\Migration\Gateway\GatewayInterface;
use SwagMigrationAssistant\Migration\Gateway\Reader\ReaderRegistry;
use SwagMigrationAssistant\Migration\MigrationContextInterface;
use SwagMigrationAssistant\Migration\RequestStatusStruct;
use SwagMigrationAssistant\Profile\Shopware\Exception\DatabaseConnectionException;
use SwagMigrationAssistant\Profile\Shopware\Gateway\Connection\ConnectionFactoryInterface;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile;
class OwnLocaleGateway implements GatewayInterface
{
public const GATEWAY_NAME = 'local';
/**
* @var ConnectionFactoryInterface
*/
private $connectionFactory;
/**
* @var ReaderRegistry
*/
private $readerRegistry;
public function __construct(
ReaderRegistry $readerRegistry,
ConnectionFactoryInterface $connectionFactory
) {
$this->readerRegistry = $readerRegistry;
$this->connectionFactory = $connectionFactory;
}
public function getName(): string
{
return self::GATEWAY_NAME;
}
public function supports(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile;
}
public function getSnippetName(): string
{
return 'swag-migration.wizard.pages.connectionCreate.gateways.shopwareLocal';
}
/**
* Reads the given entity type from via context from its connection and returns the data
*/
public function read(MigrationContextInterface $migrationContext): array
{
// TODO: Implement read() method.
return [];
}
public function readEnvironmentInformation(
MigrationContextInterface $migrationContext,
Context $context
): EnvironmentInformation {
$connection = $this->connectionFactory->createDatabaseConnection($migrationContext);
$profile = $migrationContext->getProfile();
try {
$connection->connect();
} catch (\Exception $e) {
$error = new DatabaseConnectionException();
return new EnvironmentInformation(
$profile->getSourceSystemName(),
$profile->getVersion(),
'-',
[],
[],
new RequestStatusStruct($error->getErrorCode(), $error->getMessage())
);
}
$connection->close();
$totals = $this->readTotals($migrationContext, $context);
return new EnvironmentInformation(
$profile->getSourceSystemName(),
$profile->getVersion(),
'Example Host Name',
$totals,
[],
new RequestStatusStruct(),
false
);
}
public function readTotals(MigrationContextInterface $migrationContext, Context $context): array
{
$readers = $this->readerRegistry->getReaderForTotal($migrationContext);
$totals = [];
foreach ($readers as $reader) {
$total = $reader->readTotal($migrationContext);
if ($total === null) {
continue;
}
$totals[$total->getEntityName()] = $total;
}
return $totals;
}
}
As you have seen above, the gateway uses the ConnectionFactory
to test the connection to the source system. You can also implement your own way to check this, but to use this factory is the simplest way for a gateway to connect a local database. Like the profile you have to register the new gateway in the service.xml
and tag it with shopware.migration.gateway
:
<service id="SwagMigrationOwnProfileExample\Profile\OwnProfile\Gateway\OwnLocaleGateway">
<argument type="service" id="SwagMigrationAssistant\Migration\Gateway\Reader\ReaderRegistry"/>
<argument type="service" id="SwagMigrationAssistant\Profile\Shopware\Gateway\Connection\ConnectionFactory"/>
<tag name="shopware.migration.gateway"/>
</service>
If you would like to try your current progress in the administration, you could select the profile and gateway in the migration wizard already. If you try to go to the next page, there will be an error message, because no credentials page was found. To create a new credentials page, you have to add an index.js
for your new component into Resources/app/administration/src/own-profile/profile
:
import { Component } from 'src/core/shopware';
import template from './swag-migration-profile-ownProfile-local-credential-form.html.twig';
Component.register('swag-migration-profile-ownProfile-local-credential-form', {
template,
props: {
credentials: {
type: Object,
default() {
return {};
}
}
},
data() {
return {
inputCredentials: {
dbHost: '',
dbPort: '3306',
dbUser: '',
dbPassword: '',
dbName: ''
}
};
},
watch: {
credentials: {
immediate: true,
handler(newCredentials) {
if (newCredentials === null) {
this.emitCredentials(this.inputCredentials);
return;
}
this.inputCredentials = newCredentials;
this.emitOnChildRouteReadyChanged(
this.areCredentialsValid(this.inputCredentials)
);
}
},
inputCredentials: {
deep: true,
handler(newInputCredentials) {
this.emitCredentials(newInputCredentials);
}
}
},
methods: {
areCredentialsValid(newInputCredentials) {
return (newInputCredentials.dbHost !== '' &&
newInputCredentials.dbPort !== '' &&
newInputCredentials.dbName !== '' &&
newInputCredentials.dbUser !== '' &&
newInputCredentials.dbPassword !== ''
);
},
emitOnChildRouteReadyChanged(isReady) {
this.$emit('onChildRouteReadyChanged', isReady);
},
emitCredentials(newInputCredentials) {
this.$emit('onCredentialsChanged', newInputCredentials);
this.emitOnChildRouteReadyChanged(
this.areCredentialsValid(newInputCredentials)
);
},
onKeyPressEnter() {
this.$emit('onTriggerPrimaryClick');
}
}
});
As you can see above, currently the template does not exists and you have to create this file: swag-migration-profile-ownProfile-local-credential-form.html.twig
{% block own_profile_page_credentials %}
<div class="swag-migration-wizard swag-migration-wizard-page-credentials"
@keypress.enter="onKeyPressEnter">
{% block own_profile_page_credentials_content %}
<div class="swag-migration-wizard__content">
{% block own_profile_page_credentials_information %}
<div class="swag-migration-wizard__content-information">
{% block own_profile_page_credentials_local_hint %}
{{ $tc('swag-migration.wizard.pages.credentials.shopware55.local.contentInformation') }}
{% endblock %}
</div>
{% endblock %}
{% block own_profile_page_credentials_credentials %}
<div class="swag-migration-wizard__form">
{% block own_profile_page_credentials_local_db_host_port_group %}
<sw-container columns="1fr 80px"
gap="16px">
{% block own_profile_page_credentials_local_dbhost_field %}
<sw-text-field v-autofocus
name="sw-field--dbHost"
:label="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbHostLabel')"
:placeholder="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbHostPlaceholder')"
v-model="inputCredentials.dbHost">
</sw-text-field>
{% endblock %}
{% block own_profile_page_credentials_local_dbport_field %}
<sw-field name="sw-field--dbPort"
:label="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbPortLabel')"
v-model="inputCredentials.dbPort">
</sw-field>
{% endblock %}
</sw-container>
{% endblock %}
{% block own_profile_page_credentials_local_dbuser_field %}
<sw-field name="sw-field--dbUser"
:label="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbUserLabel')"
:placeholder="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbUserPlaceholder')"
v-model="inputCredentials.dbUser">
</sw-field>
{% endblock %}
{% block own_profile_page_credentials_local_dbpassword_field %}
<sw-field name="sw-field--dbPassword"
type="password"
:label="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbPasswordLabel')"
:placeholder="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbPasswordPlaceholder')"
v-model="inputCredentials.dbPassword">
</sw-field>
{% endblock %}
{% block own_profile_page_credentials_local_dbname_field %}
<sw-field name="sw-field--dbName"
:label="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbNameLabel')"
:placeholder="$tc('swag-migration.wizard.pages.credentials.shopware55.local.dbNamePlaceholder')"
v-model="inputCredentials.dbName">
</sw-field>
{% endblock %}
</div>
{% endblock %}
</div>
{% endblock %}
</div>
{% endblock %}
A few things to notice: The component name isn't random, it has to consist of:
- The prefix:
swag-migration-profile-
- The name of the profile
- The name of the gateway
- The suffix:
-credential-form
To see your credentials page, you have to register this component in your main.js
:
import './own-profile/profile';
Now the credentials page is loaded in the administration and the connection check will succeed. But there is no data selection, if you open the data selection table. To add an entry to this table, you have to create a ProductDataSet
first:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection\DataSet;
use SwagMigrationAssistant\Migration\DataSelection\DataSet\DataSet;
use SwagMigrationAssistant\Migration\MigrationContextInterface;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile;
class ProductDataSet extends DataSet
{
/**
* Returns the entity identifier of this DataSet
*/
public static function getEntity(): string
{
return 'product';
}
/**
* Supports only an OwnProfile
*/
public function supports(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile;
}
}
Now you have to use this ProductDataSet
in the new ProductDataSelection
:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection;
use SwagMigrationAssistant\Migration\DataSelection\DataSelectionInterface;
use SwagMigrationAssistant\Migration\DataSelection\DataSelectionStruct;
use SwagMigrationAssistant\Migration\MigrationContextInterface;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection\DataSet\ProductDataSet;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile;
class ProductDataSelection implements DataSelectionInterface
{
/**
* Identifier of this DataSelection
*/
public const IDENTIFIER = 'products';
/**
* Supports only an OwnProfile
*/
public function supports(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile;
}
public function getData(): DataSelectionStruct
{
return new DataSelectionStruct(
self::IDENTIFIER,
$this->getDataSets(),
$this->getDataSetsRequiredForCount(),
/*
* Snippet of the original ProductDataSelection, if you
* want to use your own title, you have to create a new snippet
*/
'swag-migration.index.selectDataCard.dataSelection.products',
100
);
}
/**
* Returns all DataSets, which should be migrated with this DataSelection
*/
public function getDataSets(): array
{
return [
new ProductDataSet()
];
}
public function getDataSetsRequiredForCount(): array
{
return $this->getDataSets();
}
}
{% hint style="info" %}
The order in the getDataSets
array is important, because this it determines the order in which the entities are processed. Because of that, the manufacturers, for example, have to be positioned before the products, so that the products can use those later on.
{% endhint %}
To see the created ProductDataSelection
in the administration, you have to register it both in the services.xml
and tag them with shopware.migration.data_selection
and shopware.migration.data_set
:
<service id="SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection\ProductDataSelection">
<tag name="shopware.migration.data_selection"/>
</service>
<service id="SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection\DataSet\ProductDataSet">
<tag name="shopware.migration.data_set"/>
</service>
Currently, you can see the DataSelection
in the administration, but if you select it and start a migration, there will be no product migrated. That's because the gateway read
function isn't implemented, yet. But before you can implement this function, you have to create a new ProductReader
first:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample\Profile\OwnProfile\Gateway\Reader;
use Doctrine\DBAL\Driver\ResultStatement;
use SwagMigrationAssistant\Migration\MigrationContextInterface;
use SwagMigrationAssistant\Migration\TotalStruct;
use SwagMigrationAssistant\Profile\Shopware\Gateway\Local\Reader\AbstractReader;
use SwagMigrationAssistant\Profile\Shopware\Gateway\Local\ShopwareLocalGateway;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection\DataSet\ProductDataSet;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile;
class ProductReader extends AbstractReader
{
/**
* Supports only an OwnProfile and the ProductDataSet
*/
public function supports(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile
&& $migrationContext->getDataSet()::getEntity() === ProductDataSet::getEntity();
}
/**
* Supports only an OwnProfile and the ProductDataSet for totals
*/
public function supportsTotal(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile
&& $migrationContext->getGateway()->getName() === ShopwareLocalGateway::GATEWAY_NAME;
}
/**
* Creates a database connection and sets the connection class variable
*/
protected function setConnection(MigrationContextInterface $migrationContext): void
{
$this->connection = $this->connectionFactory->createDatabaseConnection($migrationContext);
}
public function readTotal(MigrationContextInterface $migrationContext): ?TotalStruct
{
$this->setConnection($migrationContext);
$query = $this->connection->createQueryBuilder()
->select('COUNT(*)')
->from('product')
->execute();
$total = 0;
if ($query instanceof ResultStatement) {
$total = (int) $query->fetchColumn();
}
return new TotalStruct(ProductDataSet::getEntity(), $total);
}
/**
* Fetches all entities out of the product table with the given limit
*/
public function read(MigrationContextInterface $migrationContext, array $params = []): array
{
$this->setConnection($migrationContext);
$query = $this->connection->createQueryBuilder();
$query->from('product');
$query->addSelect('*');
$query->setFirstResult($migrationContext->getOffset());
$query->setMaxResults($migrationContext->getLimit());
return $query->execute()->fetchAll(\PDO::FETCH_ASSOC);
}
}
Then you have to register this in services.xml
and tag it with shopware.migration.reader
:
<service id="SwagMigrationOwnProfileExample\Profile\OwnProfile\Gateway\Reader\ProductReader"
parent="SwagMigrationAssistant\Profile\Shopware\Gateway\Local\Reader\AbstractReader">
<argument type="service" id="SwagMigrationAssistant\Profile\Shopware\Gateway\Connection\ConnectionFactory"/>
<tag name="shopware.migration.reader"/>
</service>
Once the ProductReader
is created and registered, you can use it in the read
method of the OwnLocaleGateway
:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample\Profile\OwnProfile\Gateway;
use Shopware\Core\Framework\Context;
use SwagMigrationAssistant\Migration\EnvironmentInformation;
use SwagMigrationAssistant\Migration\Gateway\GatewayInterface;
use SwagMigrationAssistant\Migration\Gateway\Reader\ReaderRegistry;
use SwagMigrationAssistant\Migration\MigrationContextInterface;
use SwagMigrationAssistant\Migration\RequestStatusStruct;
use SwagMigrationAssistant\Profile\Shopware\Exception\DatabaseConnectionException;
use SwagMigrationAssistant\Profile\Shopware\Gateway\Connection\ConnectionFactoryInterface;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile;
class OwnLocaleGateway implements GatewayInterface
{
public const GATEWAY_NAME = 'local';
/**
* @var ConnectionFactoryInterface
*/
private $connectionFactory;
/**
* @var ReaderRegistry
*/
private $readerRegistry;
public function __construct(
ReaderRegistry $readerRegistry,
ConnectionFactoryInterface $connectionFactory
) {
$this->readerRegistry = $readerRegistry;
$this->connectionFactory = $connectionFactory;
}
public function getName(): string
{
return self::GATEWAY_NAME;
}
public function supports(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile;
}
public function getSnippetName(): string
{
return 'swag-migration.wizard.pages.connectionCreate.gateways.shopwareLocal';
}
public function read(MigrationContextInterface $migrationContext): array
{
$reader = $this->readerRegistry->getReader($migrationContext);
return $reader->read($migrationContext);
}
public function readEnvironmentInformation(
MigrationContextInterface $migrationContext,
Context $context
): EnvironmentInformation {
$connection = $this->connectionFactory->createDatabaseConnection($migrationContext);
$profile = $migrationContext->getProfile();
try {
$connection->connect();
} catch (\Exception $e) {
$error = new DatabaseConnectionException();
return new EnvironmentInformation(
$profile->getSourceSystemName(),
$profile->getVersion(),
'-',
[],
[],
new RequestStatusStruct($error->getErrorCode(), $error->getMessage())
);
}
$connection->close();
$totals = $this->readTotals($migrationContext, $context);
return new EnvironmentInformation(
$profile->getSourceSystemName(),
$profile->getVersion(),
'Example Host Name',
$totals,
[],
new RequestStatusStruct(),
false
);
}
public function readTotals(MigrationContextInterface $migrationContext, Context $context): array
{
$readers = $this->readerRegistry->getReaderForTotal($migrationContext);
$totals = [];
foreach ($readers as $reader) {
$total = $reader->readTotal($migrationContext);
if ($total === null) {
continue;
}
$totals[$total->getEntityName()] = $total;
}
return $totals;
}
}
By using the gateway reader, you fetch all products, but don't use this data yet. In this step you implement the logic of the converter:
<?php declare(strict_types=1);
namespace SwagMigrationOwnProfileExample\Profile\OwnProfile\Converter;
use Shopware\Core\Framework\Context;
use SwagMigrationAssistant\Migration\Converter\ConvertStruct;
use SwagMigrationAssistant\Migration\DataSelection\DefaultEntities;
use SwagMigrationAssistant\Migration\MigrationContextInterface;
use SwagMigrationAssistant\Profile\Shopware\Converter\ShopwareConverter;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\DataSelection\DataSet\ProductDataSet;
use SwagMigrationOwnProfileExample\Profile\OwnProfile\OwnProfile;
class ProductConverter extends ShopwareConverter
{
/**
* @var string
*/
private $connectionId;
/**
* @var Context
*/
private $context;
public function getSourceIdentifier(array $data): string
{
return $data['id'];
}
/**
* Supports only an OwnProfile and the ProductDataSet
*/
public function supports(MigrationContextInterface $migrationContext): bool
{
return $migrationContext->getProfile() instanceof OwnProfile &&
$migrationContext->getDataSet()::getEntity() === ProductDataSet::getEntity();
}
/**
* Writes the created mapping
*/
public function writeMapping(Context $context): void
{
$this->mappingService->writeMapping($context);
}
public function convert(array $data, Context $context, MigrationContextInterface $migrationContext): ConvertStruct
{
$this->generateChecksum($data);
$this->connectionId = $migrationContext->getConnection()->getId();
$this->context = $context;
/**
* Gets the product uuid out of the mapping table or creates a new one
*/
$this->mainMapping = $this->mappingService->getOrCreateMapping(
$migrationContext->getConnection()->getId(),
ProductDataSet::getEntity(),
$data['id'],
$context,
$this->checksum
);
$converted['id'] = $this->mainMapping['entityUuid'];
$this->convertValue($converted, 'productNumber', $data, 'product_number');
$this->convertValue($converted, 'name', $data, 'product_name');
$this->convertValue($converted, 'stock', $data, 'stock', self::TYPE_INTEGER);
if (isset($data['tax'])) {
$converted['tax'] = $this->getTax($data);
$converted['price'] = $this->getPrice($data, $converted['tax']['taxRate']);
}
unset(
$data['id'],
$data['product_number'],
$data['product_name'],
$data['stock'],
$data['tax'],
$data['price']
);
if (empty($data)) {
$data = null;
}
$this->updateMainMapping($migrationContext, $context);
return new ConvertStruct($converted, $data, $this->mainMapping['id']);
}
private function getTax(array $data): array
{
$taxRate = (float) $data['tax'];
/**
* Gets the tax uuid by the given tax rate
*/
$taxUuid = $this->mappingService->getTaxUuid($this->connectionId, $taxRate, $this->context);
/**
* If no tax rate is found, create a new one
*/
if ($taxUuid === null) {
$mapping = $this->mappingService->createMapping(
$this->connectionId,
DefaultEntities::TAX,
$data['id']
);
$taxUuid = $mapping['entityUuid'];
}
return [
'id' => $taxUuid,
'taxRate' => $taxRate,
'name' => 'Own profile tax rate (' . $taxRate . ')',
];
}
private function getPrice(array $data, float $taxRate): array
{
$gross = (float) $data['price'] * (1 + $taxRate / 100);
/**
* Gets the currency uuid by the given iso code
*/
$currencyUuid = $this->mappingService->getCurrencyUuid(
$this->connectionId,
'EUR',
$this->context
);
if ($currencyUuid === null) {
return [];
}
$price = [];
$price[] = [
'currencyId' => $currencyUuid,
'gross' => $gross,
'net' => (float) $data['price'],
'linked' => true,
];
return $price;
}
}
If you don't know which properties or requirements your entity has in Shopware 6, you may check the corresponding EntityDefinition
. For this example, please take a look at the ProductEntityDefinition
to know how to convert the data exactly.
To use this converter, you must register it in the services.xml
:
<service id="SwagMigrationOwnProfileExample\Profile\OwnProfile\Converter\ProductConverter">
<argument type="service" id="SwagMigrationAssistant\Migration\Mapping\MappingService"/>
<argument type="service" id="SwagMigrationAssistant\Migration\Logging\LoggingService"/>
<tag name="shopware.migration.converter"/>
</service>
To write new entities, you have to create a new writer class, but for the product entity, you can use the ProductWriter
:
<?php declare(strict_types=1);
namespace SwagMigrationAssistant\Migration\Writer;
use SwagMigrationAssistant\Migration\DataSelection\DefaultEntities;
class ProductWriter extends AbstractWriter
{
public function supports(): string
{
return DefaultEntities::PRODUCT;
}
}
This writer will automatically be called, because the getEntityName
method of your ProductDataSet
is compared with the return value of the supports
method of the writer in the WriterRegistry
. These values are identically and so the writer will be used to write your product entities.
There's a GitHub repository available, containing a full example source. Check it out here.