Shopware 6 represents a significant leap forward in e-commerce platform development, offering developers a modern, flexible architecture built on Symfony components and PHP 8.1+. One of the most important aspects of working with Shopware 6 is understanding how to create and manage database migrations. This technical guide will walk you through the process of writing your first Shopware 6 migration from scratch.

Understanding Shopware 6 Migration Architecture

Before diving into code, it's crucial to understand that Shopware 6 uses a sophisticated migration system based on Doctrine Migrations. Unlike previous versions, Shopware 6 migrations are organized in a specific directory structure and follow strict naming conventions. The migration system ensures database consistency across different environments and allows for safe version upgrades.

Migrations in Shopware 6 are PHP classes that extend Shopware\Core\Framework\Migration\MigrationStep. Each migration class must implement the update method, which contains the actual database schema changes, and optionally the updateDestructive method for irreversible operations.

Setting Up Your Migration Environment

To begin writing your first migration, you'll need a properly configured Shopware 6 development environment. Ensure you have:

  1. A working Shopware 6 installation
  2. Composer installed with proper dependencies
  3. Access to the command line interface
  4. Database access permissions

First, navigate to your Shopware 6 root directory and create a new migration file using the console command:

php bin/console database:migration:create YourMigrationName

This command generates a skeleton migration file in the appropriate directory structure under src/Core/Framework/Migration/.

Creating Your First Migration Class

Let's walk through creating a practical example - a migration that adds a new custom field to the product entity. Here's a complete migration class:

<?php declare(strict_types=1);

namespace Shopware\Core\Framework\Migration;

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

class Migration1678901234AddCustomProductField extends MigrationStep
{
    public function getCreationTimestamp(): int
    {
        return 1678901234;
    }

    public function update(Connection $connection): void
    {
        // Create custom field table
        $connection->executeStatement('
            CREATE TABLE IF NOT EXISTS `product_custom_field` (
                `id` BINARY(16) NOT NULL,
                `product_id` BINARY(16) NOT NULL,
                `custom_text` VARCHAR(255) DEFAULT NULL,
                `custom_number` INT DEFAULT NULL,
                `created_at` DATETIME(3) NOT NULL,
                `updated_at` DATETIME(3) DEFAULT NULL,
                PRIMARY KEY (`id`),
                KEY `idx_product_id` (`product_id`),
                CONSTRAINT `fk.product_custom_field.product_id` 
                    FOREIGN KEY (`product_id`) REFERENCES `product` (`id`) ON DELETE CASCADE
            ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
        ');

        // Add custom field to product entity
        $connection->executeStatement('
            ALTER TABLE `product` 
            ADD COLUMN IF NOT EXISTS `custom_field_enabled` TINYINT(1) DEFAULT 0,
            ADD COLUMN IF NOT EXISTS `custom_field_version` VARCHAR(36) DEFAULT NULL;
        ');
    }

    public function updateDestructive(Connection $connection): void
    {
        // Remove custom field from product entity
        $connection->executeStatement('
            ALTER TABLE `product` 
            DROP COLUMN IF EXISTS `custom_field_enabled`,
            DROP COLUMN IF EXISTS `custom_field_version`;
        ');

        // Drop custom field table
        $connection->executeStatement('
            DROP TABLE IF EXISTS `product_custom_field`;
        ');
    }
}

Key Migration Concepts

Timestamp Management

The getCreationTimestamp() method is crucial as it determines the migration order. Shopware 6 uses Unix timestamps, and migrations are executed in chronological order. For new migrations, always use a timestamp that's higher than any existing migration in your project.

Connection Handling

The migration receives a Doctrine DBAL Connection object. This connection provides all necessary methods for executing raw SQL statements while maintaining proper transaction handling. Always use the provided connection rather than creating new ones.

Transaction Safety

Shopware 6 migrations execute within transactions, ensuring that if any part fails, the entire migration rolls back. This is particularly important for destructive operations in the updateDestructive method.

Advanced Migration Patterns

Conditional Schema Changes

Modern migrations often need to handle different database states. Here's an example of conditional field addition:

public function update(Connection $connection): void
{
    // Check if column exists before adding it
    $columns = $connection->executeQuery('
        SELECT COLUMN_NAME 
        FROM INFORMATION_SCHEMA.COLUMNS 
        WHERE TABLE_SCHEMA = DATABASE() 
        AND TABLE_NAME = "product" 
        AND COLUMN_NAME = "custom_attribute"
    ')->fetchAllAssociative();

    if (empty($columns)) {
        $connection->executeStatement('
            ALTER TABLE `product` 
            ADD COLUMN `custom_attribute` VARCHAR(255) DEFAULT NULL;
        ');
    }
}

Data Migration

Beyond schema changes, migrations can also handle data transformations:

public function update(Connection $connection): void
{
    // Migrate existing product data to new structure
    $connection->executeStatement('
        INSERT INTO `product_custom_field` (`id`, `product_id`, `custom_text`, `created_at`)
        SELECT 
            UUID() as id,
            p.id as product_id,
            p.manufacturer_name as custom_text,
            NOW() as created_at
        FROM product p 
        WHERE p.manufacturer_name IS NOT NULL;
    ');
}

Migration Best Practices

Naming Conventions

Follow Shopware's naming conventions strictly:

  • Use descriptive names that reflect the migration's purpose
  • Include a timestamp prefix for chronological ordering
  • Use camelCase for class names
  • Keep migrations focused on single logical changes

Testing Migrations

Always test your migrations in a development environment before applying them to production. Create a backup of your database and verify that:

  1. The migration executes without errors
  2. Database schema is correctly updated
  3. Existing data remains intact
  4. No foreign key constraints are broken

Version Control Integration

Migrations should be version-controlled alongside your codebase. Each migration file represents a specific point in your application's evolution, making it crucial for deployment consistency across environments.

Common Pitfalls to Avoid

  1. Using deprecated methods: Shopware 6's migration system has evolved significantly. Always use the latest APIs available.
  2. Ignoring foreign key constraints: When modifying tables with relationships, ensure proper constraint handling.
  3. Not testing destructive operations: The updateDestructive method should be thoroughly tested as it cannot be rolled back.
  4. Overcomplicating migrations: Keep migrations focused and simple. Complex logic should be handled in separate services.

Deployment Considerations

When deploying migrations to production, always:

  • Run migrations in order using the console command
  • Monitor for errors during execution
  • Verify database integrity after migration
  • Document changes for future reference
# Execute all pending migrations
php bin/console database:migration:migrate

# Execute specific migration
php bin/console database:migration:migrate --version=Migration1678901234AddCustomProductField

Conclusion

Writing your first Shopware 6 migration might seem daunting at first, but understanding the underlying architecture and following established patterns makes the process straightforward. The key is to start simple, test thoroughly, and maintain good practices throughout your migration lifecycle.

As you progress in your Shopware development journey, you'll find that migrations become an essential tool for managing database evolution alongside your application's feature development. Remember that well-written migrations not only ensure data integrity but also make your system more maintainable and deployable across different environments.

The migration system in Shopware 6 represents a significant improvement over previous versions, providing developers with robust tools to manage complex database changes while maintaining backward compatibility and ensuring smooth deployments across all environments.