Extension Development Guide
This guide explains how to build paid extension plugins for the AventDropshippingSuite Core.
Creating an Extension Plugin
1. composer.json
{
"name": "avent/dropshipping-supplier-connect",
"type": "shopware-platform-plugin",
"require": {
"shopware/core": "~6.7.0",
"avent/dropshipping-suite": "~1.0"
},
"autoload": {
"psr-4": {
"Avent\\DropshippingSupplierConnect\\": "src/"
}
},
"extra": {
"shopware-plugin-class": "Avent\\DropshippingSupplierConnect\\AventDropshippingSupplierConnect"
}
}
2. Decorating Core Interfaces
Extensions modify Core behavior by decorating interfaces:
<!-- services.xml -->
<service id="Avent\DropshippingAutoOrder\Service\AutoOrderProcessor"
decorates="Avent\DropshippingSuite\Interface\DropshipOrderProcessorInterface"
decoration-priority="100">
<argument type="service" id=".inner"/>
<argument type="service" id="event_dispatcher"/>
</service>
class AutoOrderProcessor implements DropshipOrderProcessorInterface
{
public function __construct(
private readonly DropshipOrderProcessorInterface $innerProcessor,
private readonly EventDispatcherInterface $eventDispatcher,
) {}
public function process(OrderEntity $order, Context $context): array
{
// Call the core processor first
$dropshipOrders = $this->innerProcessor->process($order, $context);
// Add extension logic (e.g., send notifications)
foreach ($dropshipOrders as $dropshipOrder) {
$this->sendToSupplier($dropshipOrder, $context);
}
return $dropshipOrders;
}
}
3. Listening to Core Events
<service id="Avent\DropshippingTracking\Subscriber\DropshipOrderSubscriber">
<argument type="service" id="avent_dropship_order.repository"/>
<tag name="kernel.event_subscriber"/>
</service>
class DropshipOrderSubscriber implements EventSubscriberInterface
{
public static function getSubscribedEvents(): array
{
return [
DropshipOrderCreatedEvent::class => 'onCreated',
DropshipOrderStatusChangedEvent::class => 'onStatusChanged',
SupplierSelectedEvent::class => 'onSupplierSelected',
];
}
public function onCreated(DropshipOrderCreatedEvent $event): void
{
// Initialize tracking entry for new dropship order
}
public function onStatusChanged(DropshipOrderStatusChangedEvent $event): void
{
if ($event->getNewStatus() === 'shipped') {
// Send tracking notification to customer
}
}
public function onSupplierSelected(SupplierSelectedEvent $event): void
{
// Override supplier selection based on stock availability
$supplier = $this->checkStockAndSelectBest($event->getProductId());
if ($supplier) {
$event->setSupplier($supplier);
}
}
}
4. Optional Dependencies Between Extensions
Use on-invalid="null" for optional dependencies:
<!-- AutoOrder can optionally use SupplierConnect's adapter -->
<service id="Avent\DropshippingAutoOrder\Service\NotificationService">
<argument type="service"
id="Avent\DropshippingSupplierConnect\Adapter\AdapterRegistry"
on-invalid="null"/>
</service>
class NotificationService
{
public function __construct(
private readonly ?AdapterRegistry $adapterRegistry,
) {}
public function notify(SupplierEntity $supplier, DropshipOrderEntity $order): void
{
if ($this->adapterRegistry !== null) {
// Use API adapter from SupplierConnect
$adapter = $this->adapterRegistry->getAdapter($supplier->getId());
$adapter->sendOrder($order);
} else {
// Fallback to email
$this->sendEmailNotification($supplier, $order);
}
}
}
5. Registering CSV Mapping Types
Extensions register their mapping types by providing target field definitions:
// In SupplierConnect: register 'product_import' type
$targetFields = [
['value' => 'supplier_sku', 'label' => 'Supplier SKU'],
['value' => 'purchase_price', 'label' => 'Purchase Price'],
['value' => 'stock', 'label' => 'Stock Quantity'],
['value' => 'ean', 'label' => 'EAN'],
['value' => 'name', 'label' => 'Product Name'],
];
6. Using Core CSV Components in Extension Admin UI
<!-- In extension admin component template -->
<avent-csv-upload @file-loaded="onFileLoaded" />
<avent-csv-column-mapper
v-if="csvHeaders.length > 0"
:source-columns="csvHeaders"
:target-fields="productImportFields"
:mapping="currentMapping"
@mapping-changed="onMappingChanged"
/>
<avent-csv-transformer
v-if="currentMapping.length > 0"
:fields="mappedFields"
:transformations="transformations"
@transformations-changed="onTransformationsChanged"
/>
<avent-csv-preview
v-if="csvRows.length > 0"
:rows="csvRows"
:mapping="currentMapping"
/>
<avent-csv-profile-manager
:supplier-id="supplierId"
profile-type="product_import"
@profile-loaded="onProfileLoaded"
@save-profile="onSaveProfile"
/>
Extension Architecture
Decorator Chain (Service Decoration)
DropshipOrderProcessorInterface
+-- AutoOrder Decorator (priority 100) -- Splitting + Notification
+-- Tracking Decorator (priority 50) -- Status Management
+-- Core Default Implementation (DropshipOrderService)
Higher priority decorators run first (outermost). Each decorator calls $this->innerProcessor->process() to delegate to the next in the chain.
Event Flow
Customer places order
|
v
OrderPlacedSubscriber (Core)
|
v
DropshipOrderProcessorInterface::process()
| (decorated by AutoOrder, Tracking)
|
v
SupplierSelectionService::selectSupplier()
|
v
SupplierSelectedEvent <-- Extensions can override supplier
|
v
DropshipOrder created in DB
|
v
DropshipOrderCreatedEvent
|-- AutoOrder listens: sends to supplier
+-- Tracking listens: initializes tracking status
Example Extension: AventDropshippingSupplierConnect
Entities
avent_ds_import_profile- Adapter-specific configuration (REST auth, endpoints)avent_ds_import_log- Import log with status and error tracking
Key Services
ImportService- ImplementsSupplierImportInterfaceAdapterRegistry- Manages REST API adapters per supplierStockSyncService- Scheduled stock synchronization
Admin UI
- Import profile management per supplier
- Sync log viewer
- Navigation under "Dropshipping > Lieferanten-Anbindung"
Testing Extensions
// In PHPUnit tests, mock Core interfaces:
$selectionStrategy = $this->createMock(SupplierSelectionStrategyInterface::class);
$selectionStrategy->method('selectSupplier')
->willReturn($mockSupplier);
$orderProcessor = new YourExtensionProcessor(
$coreProcessor,
$eventDispatcher,
$selectionStrategy,
);
$result = $orderProcessor->process($mockOrder, Context::createDefaultContext());