Since Shopware 6.4, there are officially two ways to extend the shop: the classic plugin and the app system. Both can add functionality, manage data and automate processes -- but they do so in fundamentally different ways. When planning a project, you need to decide early which path to take, as the architectures are not compatible.
The Plugin Model
Plugins are PHP packages that are loaded directly into the Shopware process. They have access to all Symfony services, the dependency injection container, the database and all Shopware internals.
Typical Plugin Structure
MyPlugin/
├── composer.json
├── src/
│ ├── MyPlugin.php # Plugin class
│ ├── Core/
│ │ └── Content/ # Custom Entities, DAL extensions
│ ├── Service/ # Business Logic
│ └── Resources/
│ ├── config/
│ │ └── services.xml # Service definitions
│ └── views/ # Twig templates
└── Migration/ # Database migrations
Plugin Class
<?php
declare(strict_types=1);
namespace MyPlugin;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;
class MyPlugin extends Plugin
{
public function install(InstallContext $installContext): void
{
// One-time initialization during installation
}
public function uninstall(UninstallContext $uninstallContext): void
{
if ($uninstallContext->keepUserData()) {
return;
}
// Database cleanup
}
}
What Plugins Can Do
- Create custom entities and DAL definitions
- Extend existing entities with custom fields
- Register and override Symfony services
- Event subscribers for all Shopware events
- Integrate custom admin components (Vue.js) into the backend
- Custom storefront templates with template inheritance
- Direct database operations with full access
Plugins run in the same PHP process as Shopware. This means: direct access to all resources, but also the risk of bringing down the entire shop with a single error.
The App System
The app system was introduced to enable extensions that run outside the Shopware process. An app is an external service that communicates via webhooks and the admin API.
Typical App Structure
MyApp/
├── manifest.xml # App manifest (no PHP!)
├── Resources/
│ ├── views/ # Twig templates (for storefront blocks)
│ └── app/
│ └── administration/ # Admin extensions
└── (external web server) # Your own service
manifest.xml
The central file of an app defines everything declaratively:
<?xml version="1.0" encoding="UTF-8"?>
<manifest xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/shopware/shopware/trunk/src/Core/Framework/App/Manifest/Schema/manifest-2.0.xsd">
<meta>
<name>MyApp</name>
<label>Meine App</label>
<description>Eine Demo-App</description>
<author>Wunner Software</author>
<copyright>(c) 2025 Wunner Software</copyright>
<version>1.0.0</version>
<license>proprietary</license>
</meta>
<permissions>
<read>order</read>
<read>customer</read>
<create>order_tag</create>
</permissions>
<webhooks>
<webhook name="order-placed"
url="https://my-app.example.com/webhook/order-placed"
event="checkout.order.placed"/>
</webhooks>
<setup>
<registrationUrl>https://my-app.example.com/register</registrationUrl>
<!-- Hardcoded secrets are for local development only.
In production/Store apps, the secret is exchanged automatically during the registration handshake. -->
<secret>my-secret-for-hmac-validation</secret>
</setup>
</manifest>
Webhook Handler (External Service, e.g. Symfony)
<?php
declare(strict_types=1);
namespace App\Controller;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\Routing\Attribute\Route;
class WebhookController extends AbstractController
{
#[Route('/webhook/order-placed', methods: ['POST'])]
public function orderPlaced(Request $request): Response
{
// shopware-shop-signature is the correct header for incoming webhooks.
// (shopware-app-signature is used for responses FROM the app, not incoming requests.)
$signature = $request->headers->get('shopware-shop-signature');
$body = $request->getContent();
if (!$this->validateSignature($signature, $body)) {
return new Response('Unauthorized', Response::HTTP_UNAUTHORIZED);
}
$data = json_decode($body, true);
$orderId = $data['data']['payload']['id'] ?? null;
if ($orderId !== null) {
// Custom business logic: e.g. ERP integration
$this->erpService->syncOrder($orderId);
}
return new Response('', Response::HTTP_OK);
}
private function validateSignature(?string $signature, string $body): bool
{
if ($signature === null) {
return false;
}
$expected = hash_hmac('sha256', $body, 'my-secret-for-hmac-validation');
return hash_equals($expected, $signature);
}
}
Admin API Calls from the App
The app receives an API key during the registration process and can then use the Shopware admin API:
<?php
declare(strict_types=1);
namespace App\Service;
use Symfony\Contracts\HttpClient\HttpClientInterface;
class ShopwareApiClient
{
public function __construct(
private readonly HttpClientInterface $httpClient,
private readonly string $shopUrl,
private readonly string $accessToken,
) {
}
public function getOrder(string $orderId): array
{
$response = $this->httpClient->request('GET', sprintf('%s/api/order/%s', $this->shopUrl, $orderId), [
'headers' => [
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
],
]);
return $response->toArray();
}
public function addTagToOrder(string $orderId, string $tagId): void
{
$this->httpClient->request('POST', sprintf('%s/api/order/%s/tags', $this->shopUrl, $orderId), [
'headers' => [
'Authorization' => 'Bearer ' . $this->accessToken,
'Content-Type' => 'application/json',
],
'json' => [['id' => $tagId]],
]);
}
}
Direct Comparison
| Criterion | Plugin | App |
|---|---|---|
| Runtime environment | In the Shopware process | External service |
| Programming language | PHP (enforced) | Any (PHP, Node.js, Python...) |
| Database access | Direct (DAL/SQL) | Only via admin API |
| Performance impact | Directly on the shop | Asynchronous via webhooks |
| Deployment | On the shop server | Own server/cloud |
| Shopware Cloud compatible | No | Yes |
| Template overrides | Full | Limited (blocks) |
| Admin extensions | Full Vue.js components | Limited (iFrame, modules) |
| Error risk for shop | High (same process) | Low (isolated) |
| Development complexity | Medium-high | Medium (more infrastructure) |
When to Choose Plugin vs. App?
Plugin is the right choice when...
- deep intervention in Shopware core logic is needed (e.g., price calculation, checkout flow)
- custom DAL entities are required that Shopware should manage directly
- the shop runs on a self-managed server
- extensive storefront customizations (template inheritance) are needed
- no external server should be operated
App is the right choice when...
- the shop runs on Shopware Cloud or with a host that does not allow plugins
- the extension communicates with external systems (ERP, CRM, warehouse management)
- the logic runs in another language or an existing service
- isolation and fault tolerance are important (an app error does not slow down the shop)
- the product is sold as SaaS to multiple shop operators
Hybrid Approaches
In practice, both models are often combined:
- A lightweight plugin registers custom fields or flow actions
- The actual business service runs as an app on its own server
- Communication runs via the admin API and webhooks
This is particularly useful for complex B2B integration projects where Shopware serves as the frontend and ordering system, but ERP and warehouse are managed externally.
Conclusion
The plugin model remains the more powerful option for local Shopware installations because it has direct access to all Shopware internals. The app system is more flexible in technology choice, more isolated and cloud-compatible -- but limited to the admin API. When planning new projects, you should seriously evaluate the app system, especially if an external service architecture is already being considered.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.