Introduction

Shopware 6 represents a significant architectural overhaul from its predecessor, Shopware 5. This migration journey requires careful attention to technical details, especially when transitioning existing plugins. The platform shift introduces new concepts, improved dependency injection, and a more modular approach that fundamentally changes how developers interact with the system.

This comprehensive guide will walk you through the essential technical considerations and implementation strategies for migrating your Shopware 5 plugin to Shopware 6, covering everything from core architecture differences to practical migration steps.

Understanding the Architectural Changes

Core Architecture Differences

Shopware 6 introduces a completely redesigned architecture based on Symfony components. The most significant change is the shift from the traditional MVC pattern to a more modern service-oriented approach using Symfony's dependency injection container. This means that your plugin's services, controllers, and business logic need to be restructured to align with Symfony's best practices.

The routing system has been completely overhauled. Shopware 5 used a more straightforward routing mechanism, while Shopware 6 leverages Symfony's powerful routing component with enhanced flexibility and performance characteristics.

Dependency Injection Container

One of the most critical changes involves how services are managed. In Shopware 5, plugins were typically loaded through the plugin manager, but Shopware 6 uses Symfony's service container for dependency injection. This means you'll need to define your services in services.xml files and ensure proper autowiring configuration.

<service id="your_plugin.service.example">
    <argument type="service" id="shopware.context_factory"/>
</service>

Plugin Structure Changes

The plugin structure has evolved significantly. Shopware 6 uses a more standardized approach with clear separation of concerns. Your plugin directory should now contain:

  • src/ - Main source code
  • Resources/config/ - Configuration files
  • Resources/views/ - Template files
  • Resources/public/ - Public assets

Key Technical Migration Steps

1. Plugin Registration and Manifest

The first step involves updating your plugin's manifest file (plugin.xml). In Shopware 6, you'll need to specify the correct namespace and use the new plugin structure:

<?xml version="1.0" encoding="UTF-8"?>
<plugin xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/Plugin/PluginManifest.xsd">
    <label>My Plugin</label>
    <version>1.0.0</version>
    <author>Your Company</author>
    <store>https://store.shopware.com</store>
    <compatibility>
        <minVersion>6.7.0</minVersion>
    </compatibility>
</plugin>

2. Service Registration and Dependency Injection

In Shopware 5, services were often accessed through the global container. In Shopware 6, you must explicitly register your services in src/Resources/config/services.xml:

<?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="YourPlugin\Service\ExampleService">
            <argument type="service" id="Shopware\Core\Framework\Context"/>
            <argument type="service" id="Shopware\Core\System\SalesChannel\SalesChannelRepository"/>
        </service>
        
        <service id="YourPlugin\Controller\ExampleController">
            <argument type="service" id="YourPlugin\Service\ExampleService"/>
            <call method="setContainer">
                <argument type="service" id="Symfony\Component\DependencyInjection\ContainerInterface"/>
            </call>
        </service>
    </services>
</container>

3. Database Migration Strategy

Shopware 6 introduces a new migration system using the MigrationCollection pattern. You'll need to create migrations for your plugin's database changes:

<?php declare(strict_types=1);

namespace YourPlugin\Migration;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1234567890Example extends MigrationStep
{
    public function getCreationTimestamp(): int
    {
        return 1234567890;
    }

    public function update(Connection $connection): void
    {
        $connection->executeStatement('CREATE TABLE IF NOT EXISTS your_plugin_table (
            id BINARY(16) NOT NULL,
            name VARCHAR(255) NOT NULL,
            created_at DATETIME NOT NULL,
            updated_at DATETIME DEFAULT NULL,
            PRIMARY KEY (id)
        )');
    }

    public function updateDestructive(Connection $connection): void
    {
        // Destructive operations go here
    }
}

4. Controller Migration

Controllers in Shopware 6 follow Symfony's controller conventions more closely. Here's an example of a modern controller structure:

<?php declare(strict_types=1);

namespace YourPlugin\Controller;

use Psr\Http\Message\ResponseInterface;
use Psr\Http\Message\ServerRequestInterface;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\Routing\Annotation\Route;

#[RouteScope(routes: ['api'])]
class ExampleController extends AbstractController
{
    #[Route('/api/_action/your-plugin/example', name: 'api.action.your-plugin.example', methods: ['POST'])]
    public function exampleAction(ServerRequestInterface $request): ResponseInterface
    {
        // Your logic here
        return new JsonResponse(['status' => 'success']);
    }
}

5. Template Migration

Template files need to be converted from Shopware 5's Smarty syntax to Shopware 6's Twig template engine. The syntax changes are significant but provide better performance and maintainability:

{# Shopware 5 Smarty #}
{if $product.price}
    <span class="price">{$product.price}</span>
{/if}

{# Shopware 6 Twig #}
{% if product.price %}
    <span class="price">{{ product.price }}</span>
{% endif %}

Advanced Technical Considerations

Event System Changes

Shopware 6's event system has been significantly enhanced. Instead of using the old Shopware_Events constants, you now work with Symfony events:

<?php declare(strict_types=1);

namespace YourPlugin\Subscriber;

use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;

class ExampleSubscriber implements EventSubscriberInterface
{
    public static function getSubscribedEvents(): array
    {
        return [
            'product.written' => 'onProductWritten',
        ];
    }

    public function onProductWritten(EntityWrittenEvent $event): void
    {
        // Handle product written event
    }
}

Plugin Lifecycle Management

Shopware 6 introduces new lifecycle methods that need to be implemented in your plugin class:

<?php declare(strict_types=1);

namespace YourPlugin;

use Shopware\Core\Framework\Plugin;
use Symfony\Component\DependencyInjection\ContainerBuilder;

class YourPlugin extends Plugin
{
    public function build(ContainerBuilder $container): void
    {
        parent::build($container);
        // Custom container building logic
    }

    public function install(): void
    {
        parent::install();
        // Installation logic
    }

    public function uninstall(): void
    {
        parent::uninstall();
        // Uninstallation logic
    }
}

Performance Optimization Strategies

Caching Implementation

Shopware 6 provides enhanced caching mechanisms. You should leverage the Symfony cache component for better performance:

<?php declare(strict_types=1);

namespace YourPlugin\Service;

use Psr\Cache\CacheItemPoolInterface;
use Shopware\Core\Framework\Context;

class CacheService
{
    private CacheItemPoolInterface $cache;

    public function __construct(CacheItemPoolInterface $cache)
    {
        $this->cache = $cache;
    }

    public function getCachedData(string $key, callable $callback): mixed
    {
        $item = $this->cache->getItem($key);
        
        if ($item->isHit()) {
            return $item->get();
        }
        
        $data = $callback();
        $item->set($data);
        $item->expiresAfter(3600); // 1 hour
        
        $this->cache->save($item);
        return $data;
    }
}

Database Query Optimization

The new database layer in Shopware 6 offers better query optimization. Utilize the Criteria class for efficient data fetching:

<?php declare(strict_types=1);

use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;

$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('active', true));
$criteria->setLimit(10);
$criteria->addAssociation('categories');

$result = $productRepository->search($criteria, $context);

Testing and Quality Assurance

Unit Testing Framework

Shopware 6 provides comprehensive testing capabilities. Create proper unit tests for your services:

<?php declare(strict_types=1);

namespace YourPlugin\Test\Unit\Service;

use PHPUnit\Framework\TestCase;
use YourPlugin\Service\ExampleService;

class ExampleServiceTest extends TestCase
{
    public function testExampleMethod(): void
    {
        $service = new ExampleService();
        
        $result = $service->exampleMethod();
        
        $this->assertEquals('expected_result', $result);
    }
}

Integration Testing

For more complex scenarios, implement integration tests that work with the actual database:

<?php declare(strict_types=1);

namespace YourPlugin\Test\Integration;

use PHPUnit\Framework\TestCase;
use Shopware\Core\Test\Helper\ContainerHelper;

class ExampleIntegrationTest extends TestCase
{
    public function testServiceIntegration(): void
    {
        $container = ContainerHelper::createContainer();
        
        $service = $container->get(ExampleService::class);
        
        // Test integration scenarios
    }
}

Common Migration Pitfalls and Solutions

Namespace Conflicts

One common issue during migration is namespace conflicts. Ensure your plugin uses unique namespaces to avoid conflicts with core Shopware components.

Service Availability

Some services that were available in Shopware 5 may have been moved or renamed. Always check the official documentation for service availability and proper usage.

Template Variable Access

Template variables in Shopware 6 follow different conventions. Make sure to test all template outputs thoroughly after migration.

Conclusion

Migrating from Shopware 5 to Shopware 6 is a substantial technical undertaking that requires understanding of modern PHP practices, Symfony components, and Shopware's new architecture. The investment in this migration pays dividends through improved performance, better maintainability, and access to the latest features.

By following the structured approach outlined in this guide, focusing on dependency injection, proper service registration, and leveraging Shopware 6's enhanced capabilities, your plugin will not only function correctly but also be future-proofed against upcoming changes. The key is to embrace the new patterns rather than trying to retrofit old approaches, ensuring your plugin remains competitive in the evolving e-commerce landscape.

Remember that this migration process requires thorough testing across different environments and careful attention to edge cases. Take advantage of Shopware 6's extensive documentation and community resources to ensure a smooth transition to the modern platform architecture.