Shopware 6 hat sich bewusst gegen Doctrine ORM entschieden und einen eigenen Data Abstraction Layer (DAL) entwickelt. Doctrine bietet zwar ebenfalls Repositories, Lifecycle-Events und Query-Mechanismen — der entscheidende Unterschied liegt aber in der Runtime-Erweiterbarkeit: Der DAL erlaubt es Plugins, bestehende Entity-Definitionen zur Laufzeit um eigene Felder und Assoziationen zu erweitern, ohne den Original-Code zu verändern. Doctrine-Entities sind dagegen statisch an ihre PHP-Klasse gebunden und lassen sich nicht von außen um Felder ergänzen. Dazu kommen DAL-Features wie eingebaute Versionierung, automatische API-Anbindung (ApiAware), mehrsprachige Felder (TranslatedField) und Field Inheritance für Produktvarianten — alles Funktionen, die Doctrine nicht nativ mitbringt.
Grundlegende Konzepte
Der DAL besteht aus drei Hauptkomponenten:
Entity: Eine PHP-Klasse, die einen Datenbankdatensatz repräsentiert. EntityDefinition: Beschreibt die Felder und Assoziationen der Entity — analog zu Doctrine-Mapping. EntityRepository: Wird für alle Datenbankoperationen genutzt, nie direktes SQL.
Eine eigene Entity anlegen
Man erstellt ein Beispiel: Eine Testimonial-Entity für Kundenbewertungen.
EntityDefinition
<?php
declare(strict_types=1);
namespace MyPlugin\Core\Content\Testimonial;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\BoolField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
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\IntField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\LongTextField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\ManyToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\StringField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
use Shopware\Core\System\SalesChannel\SalesChannelDefinition;
class TestimonialDefinition extends EntityDefinition
{
public const ENTITY_NAME = 'my_plugin_testimonial';
public function getEntityName(): string
{
return self::ENTITY_NAME;
}
public function getEntityClass(): string
{
return TestimonialEntity::class;
}
public function getCollectionClass(): string
{
return TestimonialCollection::class;
}
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey(), new ApiAware()),
(new StringField('author_name', 'authorName'))->addFlags(new Required(), new ApiAware()),
(new LongTextField('content', 'content'))->addFlags(new Required(), new ApiAware()),
(new IntField('rating', 'rating'))->addFlags(new Required(), new ApiAware()),
(new BoolField('is_active', 'isActive'))->addFlags(new ApiAware()),
new ManyToOneAssociationField('salesChannel', 'sales_channel_id', SalesChannelDefinition::class),
]);
}
}
Entity-Klasse
<?php
declare(strict_types=1);
namespace MyPlugin\Core\Content\Testimonial;
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityIdTrait;
class TestimonialEntity extends Entity
{
use EntityIdTrait;
protected string $authorName;
protected string $content;
protected int $rating;
protected bool $isActive = true;
public function getAuthorName(): string
{
return $this->authorName;
}
public function setAuthorName(string $authorName): void
{
$this->authorName = $authorName;
}
public function getContent(): string
{
return $this->content;
}
public function setContent(string $content): void
{
$this->content = $content;
}
public function getRating(): int
{
return $this->rating;
}
public function setRating(int $rating): void
{
$this->rating = $rating;
}
public function isActive(): bool
{
return $this->isActive;
}
public function setIsActive(bool $isActive): void
{
$this->isActive = $isActive;
}
}
Collection-Klasse
<?php
declare(strict_types=1);
namespace MyPlugin\Core\Content\Testimonial;
use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;
/**
* @extends EntityCollection<TestimonialEntity>
*/
class TestimonialCollection extends EntityCollection
{
protected function getExpectedClass(): string
{
return TestimonialEntity::class;
}
public function getActive(): self
{
return $this->filter(fn (TestimonialEntity $t): bool => $t->isActive());
}
}
Service-Registration in services.xml
<service id="MyPlugin\Core\Content\Testimonial\TestimonialDefinition">
<tag name="shopware.entity.definition" entity="my_plugin_testimonial"/>
</service>
Migration für die Datenbanktabelle
<?php
declare(strict_types=1);
namespace MyPlugin\Migration;
use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;
class Migration1712500000CreateTestimonialTable extends MigrationStep
{
public function getCreationTimestamp(): int
{
return 1712500000;
}
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `my_plugin_testimonial` (
`id` BINARY(16) NOT NULL,
`sales_channel_id` BINARY(16) NULL,
`author_name` CHAR(255) NOT NULL,
`content` LONGTEXT NOT NULL,
`rating` INT(11) NOT NULL,
`is_active` TINYINT(1) NOT NULL DEFAULT 1,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
KEY `fk_testimonial_sales_channel` (`sales_channel_id`),
CONSTRAINT `fk_testimonial_sales_channel`
FOREIGN KEY (`sales_channel_id`)
REFERENCES `sales_channel` (`id`)
ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
}
public function updateDestructive(Connection $connection): void
{
}
}
CRUD mit dem Repository
Das Repository wird per Dependency Injection eingebunden:
<service id="MyPlugin\Service\TestimonialService">
<argument type="service" id="my_plugin_testimonial.repository"/>
</service>
<?php
declare(strict_types=1);
namespace MyPlugin\Service;
use MyPlugin\Core\Content\Testimonial\TestimonialCollection;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Sorting\FieldSorting;
use Shopware\Core\Framework\Uuid\Uuid;
class TestimonialService
{
public function __construct(
private readonly EntityRepository $testimonialRepository,
) {
}
public function findActive(Context $context): TestimonialCollection
{
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('isActive', true));
$criteria->addSorting(new FieldSorting('createdAt', FieldSorting::DESCENDING));
$criteria->setLimit(10);
/** @var TestimonialCollection $result */
$result = $this->testimonialRepository->search($criteria, $context)->getEntities();
return $result;
}
public function create(string $authorName, string $content, int $rating, Context $context): string
{
$id = Uuid::randomHex();
$this->testimonialRepository->create([
[
'id' => $id,
'authorName' => $authorName,
'content' => $content,
'rating' => max(1, min(5, $rating)),
'isActive' => true,
],
], $context);
return $id;
}
public function deactivate(string $id, Context $context): void
{
$this->testimonialRepository->update([
['id' => $id, 'isActive' => false],
], $context);
}
public function delete(string $id, Context $context): void
{
$this->testimonialRepository->delete([['id' => $id]], $context);
}
}
Criteria und Filter
Der DAL bietet eine umfangreiche Criteria-API:
<?php
declare(strict_types=1);
use Shopware\Core\Framework\DataAbstractionLayer\Search\Criteria;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\AndFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\EqualsFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Filter\RangeFilter;
use Shopware\Core\Framework\DataAbstractionLayer\Search\Aggregation\Metric\AvgAggregation;
$criteria = new Criteria();
// Kombinierte Filter (AND-Logik ist Standard)
$criteria->addFilter(
new AndFilter([
new EqualsFilter('isActive', true),
new RangeFilter('rating', [
RangeFilter::GTE => 4,
]),
])
);
// Durchschnittsbewertung berechnen
$criteria->addAggregation(new AvgAggregation('avg_rating', 'rating'));
// Paginierung
$criteria->setLimit(20);
$criteria->setOffset(0);
$result = $this->testimonialRepository->search($criteria, $context);
$avgRating = $result->getAggregations()->get('avg_rating')?->getAvg();
Migrationen per Konsolen-Befehl erstellen
Shopware bietet zwei Konsolen-Befehle, um Migrationen zu generieren, statt sie komplett von Hand zu schreiben.
Leeres Migrations-Skelett erzeugen:
bin/console database:create-migration -p MeinPlugin --name CreateTestimonialTable
Der Befehl erstellt eine leere Klasse mit update() und updateDestructive() — den SQL-Code schreibt man selbst. Der -p-Parameter gibt den Plugin-Namen an, --name fügt einen beschreibenden Suffix an den Timestamp an.
Migration automatisch aus der EntityDefinition generieren:
bin/console dal:migration:create --bundle=MeinPlugin --entities=my_plugin_testimonial
Dieser Befehl vergleicht die EntityDefinition mit dem aktuellen Datenbankschema und generiert automatisch CREATE TABLE oder ALTER TABLE Statements. Das Plugin muss dafür aktiviert sein, damit die Definition gefunden wird.
Migrationen ausführen:
# Nicht-destruktive Änderungen (update-Methode)
bin/console database:migrate MeinPlugin --all
# Destruktive Änderungen (updateDestructive-Methode)
bin/console database:migrate-destructive MeinPlugin --all
Wichtig: Migrationen werden automatisch bei Plugin-Installation und -Update ausgeführt. Bereits ausgeführte Migrationen dürfen nicht mehr geändert werden. update() enthält nur umkehrbare Änderungen, updateDestructive() irreversible (Spalten oder Tabellen löschen).
Entity Extensions: Entities von außen erweitern
Der eigentliche Vorteil des DAL zeigt sich bei der Erweiterbarkeit. Andere Plugins können bestehende Entities um eigene Felder und Assoziationen erweitern — ohne die Original-Entity zu verändern.
Beispiel: Produkt um eine 1:1-Beziehung erweitern
Schritt 1: Eigene EntityDefinition für die Extension-Daten:
<?php
declare(strict_types=1);
namespace MeinPlugin\Core\Content\ProductRating;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
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\IntField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
class ProductRatingDefinition extends EntityDefinition
{
public const ENTITY_NAME = 'my_plugin_product_rating';
public function getEntityName(): string
{
return self::ENTITY_NAME;
}
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->addFlags(new Required(), new PrimaryKey(), new ApiAware()),
(new FkField('product_id', 'productId', ProductDefinition::class))->addFlags(new Required()),
(new IntField('expert_score', 'expertScore'))->addFlags(new ApiAware()),
new OneToOneAssociationField('product', 'product_id', 'id', ProductDefinition::class, false),
]);
}
}
Schritt 2: EntityExtension-Klasse, die die Assoziation am Produkt registriert:
<?php
declare(strict_types=1);
namespace MeinPlugin\Extension\Content\Product;
use MeinPlugin\Core\Content\ProductRating\ProductRatingDefinition;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityExtension;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
class ProductRatingExtension extends EntityExtension
{
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new OneToOneAssociationField(
'productRating',
'id',
'product_id',
ProductRatingDefinition::class,
true
))->addFlags(new ApiAware())
);
}
public function getDefinitionClass(): string
{
return ProductDefinition::class;
}
}
Schritt 3: Service-Registrierung in services.xml:
<service id="MeinPlugin\Core\Content\ProductRating\ProductRatingDefinition">
<tag name="shopware.entity.definition" entity="my_plugin_product_rating"/>
</service>
<service id="MeinPlugin\Extension\Content\Product\ProductRatingExtension">
<tag name="shopware.entity.extension"/>
</service>
Field Inheritance: Vererbung für Produktvarianten
Wenn eine Extension auch auf Varianten vererbt werden soll (die Variante erbt Daten vom Elternprodukt), braucht man den Inherited-Flag und das InheritanceUpdaterTrait in der Migration:
<?php
declare(strict_types=1);
namespace MeinPlugin\Extension\Content\Product;
use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityExtension;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\ApiAware;
use Shopware\Core\Framework\DataAbstractionLayer\Field\Flag\Inherited;
use Shopware\Core\Framework\DataAbstractionLayer\Field\OneToOneAssociationField;
use Shopware\Core\Framework\DataAbstractionLayer\FieldCollection;
class ProductRatingExtension extends EntityExtension
{
public const EXTENSION_NAME = 'productRating';
public function extendFields(FieldCollection $collection): void
{
$collection->add(
(new OneToOneAssociationField(
self::EXTENSION_NAME,
'id',
'product_id',
ProductRatingDefinition::class,
true
))->addFlags(new ApiAware(), new Inherited())
);
}
public function getDefinitionClass(): string
{
return ProductDefinition::class;
}
}
In der Migration erstellt updateInheritance() automatisch die nötige Vererbungsspalte in der product-Tabelle:
<?php
declare(strict_types=1);
namespace MeinPlugin\Migration;
use Doctrine\DBAL\Connection;
use MeinPlugin\Extension\Content\Product\ProductRatingExtension;
use Shopware\Core\Framework\Migration\InheritanceUpdaterTrait;
use Shopware\Core\Framework\Migration\MigrationStep;
class Migration1712600000CreateProductRatingTable extends MigrationStep
{
use InheritanceUpdaterTrait;
public function getCreationTimestamp(): int
{
return 1712600000;
}
public function update(Connection $connection): void
{
$connection->executeStatement('
CREATE TABLE IF NOT EXISTS `my_plugin_product_rating` (
`id` BINARY(16) NOT NULL,
`product_id` BINARY(16) NOT NULL,
`product_version_id` BINARY(16) NOT NULL,
`expert_score` INT(11) NULL,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uniq_product` (`product_id`, `product_version_id`),
CONSTRAINT `fk_product_rating_product`
FOREIGN KEY (`product_id`, `product_version_id`)
REFERENCES `product` (`id`, `version_id`) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
');
$this->updateInheritance($connection, 'product', ProductRatingExtension::EXTENSION_NAME);
}
public function updateDestructive(Connection $connection): void
{
}
}
Voraussetzungen für Field Inheritance: Alle vererbbaren Felder müssen nullable sein. Die Entity braucht ParentFkField, ParentAssociationField und ChildrenAssociationField (bei ProductDefinition bereits vorhanden). Felder müssen mit dem Inherited-Flag markiert werden.
Eigene Entity erweiterbar gestalten
Damit andere Plugins die eigene Entity per EntityExtension erweitern können, sind keine besonderen Maßnahmen nötig — jede registrierte EntityDefinition ist automatisch erweiterbar. Trotzdem gibt es Best Practices:
CustomFields-Feld einbauen — ermöglicht einfache Erweiterungen ohne eigene Migration- Entity-Name als Konstante (
public const ENTITY_NAME) — andere Plugins können darauf referenzieren - Verzeichnisstruktur nach Domäne (
Core/Content/EntityName/) — folgt der Shopware-Core-Konvention
Ab Shopware 6.6.10 gibt es zusätzlich die BulkEntityExtension, mit der man mehrere Entities in einer einzigen Klasse erweitern kann — statt für jede Entity eine separate Extension-Klasse.
Fazit
Der Shopware DAL hat eine steile Lernkurve für Entwickler, die von Doctrine kommen, aber das System ist konsequent durchdacht. Doctrine kennt zwar Repositories, Events und Vererbungsstrategien (Mapped Superclass, Single Table Inheritance) — aber diese müssen zur Entwicklungszeit in der Entity-Klasse definiert werden. Der entscheidende Vorteil des DAL ist die Runtime-Erweiterbarkeit: Über den EntityExtension-Mechanismus können Plugins beliebige Entity-Definitionen um Felder und Assoziationen erweitern, ohne den Original-Code anzufassen. Zusammen mit Field Inheritance für Produktvarianten, eingebauter Versionierung und automatischem Event-Dispatching für alle CRUD-Operationen bietet Shopware die Werkzeuge, um auch komplexe Plugin-Ökosysteme sauber zu verwalten.
Kommentare
Kommentare werden von Remark42 bereitgestellt. Beim Laden werden Daten an unseren Kommentar-Server übertragen.