Shopware 6 has established itself as one of the most robust e-commerce platforms available today. Its modular architecture and extensibility make it an ideal choice for developers looking to create custom solutions tailored to specific business needs. In this technical deep dive, we'll explore how to build a custom Shopware 6 plugin from scratch, focusing on proper structure, coding standards, and best practices that will ensure maintainability and performance.
Plugin Architecture Overview
Before diving into implementation details, understanding Shopware 6's plugin architecture is crucial. A plugin in Shopware 6 follows a specific directory structure that the framework expects:
custom/plugins/
└── MyPlugin/
├── src/
│ ├── Command/
│ ├── Controller/
│ ├── Entity/
│ ├── Event/
│ ├── Migration/
│ ├── Resources/
│ ├── Service/
│ └── Subscriber/
├── config.xml
├── manifest.xml
├── composer.json
└── MyPlugin.php
Setting Up the Plugin Structure
The foundation of any Shopware 6 plugin begins with the manifest.xml file, which defines basic plugin metadata:
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/master/src/Core/Framework/App/Manifest/Schema/manifest-1.0.xsd">
<meta>
<name>MyCustomPlugin</name>
<label>My Custom Plugin</label>
<description>Custom plugin for specialized functionality</description>
<version>1.0.0</version>
<author>Your Company</author>
<license>MIT</license>
</meta>
</manifest>
The composer.json file ensures proper dependency management and autoloading:
{
"name": "your-company/my-custom-plugin",
"type": "shopware-platform-plugin",
"autoload": {
"psr-4": {
"MyPlugin\\": "src/"
}
},
"extra": {
"shopware-plugin-class": "MyPlugin\\MyPlugin",
"label": {
"de-DE": "Mein Custom Plugin",
"en-GB": "My Custom Plugin"
}
}
}
Core Plugin Class Implementation
The main plugin class, MyPlugin.php, serves as the entry point for your plugin's initialization:
<?php declare(strict_types=1);
namespace MyPlugin;
use Shopware\Core\Framework\Plugin;
use Symfony\Component\DependencyInjection\ContainerBuilder;
class MyPlugin extends Plugin
{
public function build(ContainerBuilder $container): void
{
parent::build($container);
// Add custom services and configurations here
$container->setParameter('my_plugin.config', $this->getConfig());
}
public function getMigrationNamespace(): string
{
return 'MyPlugin\Migration';
}
}
Database Migration Strategy
Shopware 6 uses migrations for database schema changes. Create your migration files in the src/Migration directory:
<?php declare(strict_types=1);
namespace MyPlugin\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
class Migration1600000000CreateMyTable extends MigrationStep
{
public function getCreationTimestamp(): int
{
return 1600000000;
}
public function update(Connection $connection): void
{
$sql = <<<SQL
CREATE TABLE IF NOT EXISTS `my_plugin_data` (
`id` BINARY(16) NOT NULL,
`name` VARCHAR(255) NOT NULL,
`created_at` DATETIME NOT NULL,
`updated_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL;
$connection->executeStatement($sql);
}
public function updateDestructive(Connection $connection): void
{
// Destructive operations go here
}
}
Entity Definition and Repository Pattern
Shopware 6 follows the repository pattern for data access. Define your entity in src/Entity/MyEntityDefinition.php:
<?php declare(strict_types=1);
namespace MyPlugin\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\PrimaryKey;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Required;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
class MyEntityDefinition extends EntityDefinition
{
public function getEntityName(): string
{
return 'my_plugin_data';
}
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->setFlags(new PrimaryKey(), new Required()),
new StringField('name', 'name'),
]);
}
}
Service Layer Implementation
Services form the core of your plugin's business logic. Create a service in src/Service/MyService.php:
<?php declare(strict_types=1);
namespace MyPlugin\Service;
use MyPlugin\Entity\MyEntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepositoryInterface;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
class MyService
{
private EntityRepositoryInterface $myRepository;
public function __construct(EntityRepositoryInterface $myRepository)
{
$this->myRepository = $myRepository;
}
public function findByName(string $name): ?array
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('name', $name));
return $this->myRepository->search($criteria)->first();
}
public function create(array $data): string
{
$id = Uuid::randomHex();
$data['id'] = $id;
$this->myRepository->create([$data], Context::createDefaultContext());
return $id;
}
}
Controller Implementation
Controllers handle HTTP requests and responses. Create a controller in src/Controller/MyController.php:
<?php declare(strict_types=1);
namespace MyPlugin\Controller;
use MyPlugin\Service\MyService;
use Shopware\Core\Framework\Routing\Annotation\RouteScope;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Routing\Annotation\Route;
/**
* @RouteScope(scopes={"api"})
*/
class MyController extends AbstractController
{
private MyService $myService;
public function __construct(MyService $myService)
{
$this->myService = $myService;
}
/**
* @Route("/api/my-plugin/data", methods={"GET"})
*/
public function getData(Request $request): JsonResponse
{
$name = $request->query->get('name');
if ($name) {
$data = $this->myService->findByName($name);
return new JsonResponse($data);
}
return new JsonResponse(['error' => 'Name parameter required']);
}
}
Event Subscriber Pattern
Shopware 6's event system allows you to hook into various points in the application lifecycle. Create an event subscriber in src/Subscriber/MyEventSubscriber.php:
<?php declare(strict_types=1);
namespace MyPlugin\Subscriber;
use Shopware\Core\Framework\DataAbstractionLayer\Event\EntityWrittenEvent;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class MyEventSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
'my_plugin.data.written' => 'onDataWritten',
];
}
public function onDataWritten(EntityWrittenEvent $event): void
{
// Custom logic when data is written
$ids = $event->getIds();
foreach ($ids as $id) {
// Process each ID
}
}
}
Configuration Management
Proper configuration management ensures your plugin can be customized without code changes. Create a configuration file in src/Resources/config/config.xml:
<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/platform/master/src/Core/System/Config/Schema/config-1.0.xsd">
<card>
<title>My Plugin Configuration</title>
<input-field type="text">
<name>apiUrl</name>
<label>API URL</label>
<help-text>Enter the API endpoint URL</help-text>
</input-field>
</card>
</config>
Performance Optimization Techniques
When building custom plugins, performance should be a primary concern. Implement caching strategies using Shopware's cache service:
use Psr\Cache\CacheItemPoolInterface;
public function __construct(
private CacheItemPoolInterface $cache,
private MyService $myService
) {
}
public function getCachedData(string $key): ?array
{
$item = $this->cache->getItem($key);
if ($item->isHit()) {
return $item->get();
}
$data = $this->myService->findAll();
$item->set($data);
$item->expiresAfter(3600); // 1 hour
$this->cache->save($item);
return $data;
}
Testing Best Practices
Thorough testing ensures your plugin works correctly across different scenarios. Create unit tests in tests/Unit directory:
<?php declare(strict_types=1);
namespace MyPlugin\Tests\Unit;
use MyPlugin\Service\MyService;
use PHPUnit\Framework\TestCase;
class MyServiceTest extends TestCase
{
public function testFindByNameReturnsData(): void
{
$repository = $this->createMock(EntityRepositoryInterface::class);
$service = new MyService($repository);
$result = $service->findByName('test');
$this->assertNull($result);
}
}
Security Considerations
Security is paramount in e-commerce applications. Always validate input data and implement proper authentication checks:
public function createData(Request $request): JsonResponse
{
// Validate input
$data = json_decode($request->getContent(), true);
if (!isset($data['name']) || empty($data['name'])) {
return new JsonResponse(['error' => 'Name is required'], 400);
}
// Sanitize data
$sanitizedData = [
'name' => filter_var($data['name'], FILTER_SANITIZE_STRING)
];
try {
$id = $this->myService->create($sanitizedData);
return new JsonResponse(['id' => $id], 201);
} catch (\Exception $e) {
return new JsonResponse(['error' => 'Creation failed'], 500);
}
}
Deployment and Maintenance
When deploying your plugin, ensure proper versioning and migration handling. Always test migrations on staging environments before production deployment:
# Run migrations
bin/console database:migrate --all
# Clear cache
bin/console cache:clear
# Warm up cache
bin/console cache:warmup
Conclusion
Building custom Shopware 6 plugins requires understanding of the framework's architecture and adherence to established best practices. By following proper directory structures, implementing clean service layers, utilizing events, and considering performance and security aspects, you can create robust, maintainable plugins that enhance your e-commerce platform functionality.
Remember that Shopware 6's extensibility model provides powerful tools for customization while maintaining the platform's stability and performance standards. Regular updates, thorough testing, and careful consideration of edge cases will ensure your plugin remains reliable and valuable to users throughout its lifecycle.
The key to successful plugin development lies in balancing functionality with maintainability, following established patterns, and leveraging Shopware 6's powerful ecosystem of services, events, and tools for building enterprise-grade solutions.