Implementing Custom Entities in Shopware 6 using the Data Abstraction Layer
Learn how to implement custom entities in Shopware 6 using the Data Abstraction Layer (DAL), including migrations, EntityDefinitions, and Criteria-based repository queries.
31 May 2026, 09:36 UTC

The Core Requirement for Custom Data
When you need to store data that doesn't fit into existing Shopware tables—such as a custom loyalty program, a specialized product note system, or external shipment tracking—the Data Abstraction Layer (DAL) is the required mechanism. The takeaway is that a custom entity is not just a database table; it is a trio of a Migration (the physical table), an EntityDefinition (the logical map), and an EntityCollection (the result set).
By using the DAL instead of raw SQL, your data automatically integrates with Shopware's event system, allowing other plugins to react to EntityWrittenEvent or EntityLoadedEvent without modifying your code.
Defining the Entity Structure
To implement a custom entity, you must first create the database schema. In Shopware, this is handled via a Migration class. For this example, we are creating a swag_demo_note entity to store simple notes linked to products.
// src/Migration/Migration1670000000Note.php
public function update(Connection $connection): void
{
$connection->executeStatement("
CREATE TABLE IF NOT EXISTS `swag_demo_note` (
`id` BINARY(16) NOT NULL,
`product_id` BINARY(16) NOT NULL,
`title` VARCHAR(255) NOT NULL,
`done` TINYINT(1) NOT NULL DEFAULT 0,
`created_at` DATETIME(3) NOT NULL,
`updated_at` DATETIME(3) NULL,
PRIMARY KEY (`id`),
CONSTRAINT `fk.demo_note.product_id` FOREIGN KEY (`product_id`)
REFERENCES `product` (`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
");
}
Next, you define the EntityDefinition. This class tells the DAL how to treat the columns. Fields like IdField and CreatedAtField are mandatory for standard DAL behavior.
// src/Core/Content/Note/NoteDefinition.php
class NoteDefinition extends EntityDefinition
{
public function getEntityName(): string
{
return 'swag_demo_note';
}
protected function defineFields(): FieldCollection
{
return new FieldCollection([
(new IdField('id', 'id'))->isRequired(),
(new StringField('title', 'title'))->isRequired(),
new BoolField('done', 'done'),
new FkField('product_id', 'productId', ProductDefinition::class),
new ManyToOneAssociationField('product', 'product_id', ProductDefinition::class, 'id', false),
new CreatedAtField(),
new UpdatedAtField(),
]);
}
}
Finally, create a NoteCollection class that extends EntityCollection. This ensures that when you fetch multiple notes, they are returned as a typed collection rather than a generic array.
Interacting with Data via the Repository
Once the definition is registered (via Symfony autoconfiguration), Shopware generates a repository service. You do not write SQL to read or write; you use the Criteria object.
Writing Data
To create a record, use the create method. Note that IDs must be generated as binary UUIDs using Uuid::randomHex().
// Run within a service/controller with access to NoteRepository
$this->noteRepository->create([[
'id' => Uuid::randomHex(),
'productId' => $productId,
'title' => 'Review shipping dimensions',
'done' => false,
]], $context);
Reading Data with Criteria
The Criteria DSL allows for complex filtering and association loading without manual joins.
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('done', false));
$criteria->addAssociation('product'); // Eager load the linked product
$criteria->addSorting(new FieldSorting('createdAt', FieldSorting::DESCENDING));
$criteria->setLimit(10);
$result = $this->noteRepository->search($criteria, $context);
$notes = $result->getEntities(); // Returns NoteCollection
Design Limits and Common Pitfalls
The DAL provides immense power but has specific boundaries that often trip up developers:
- API Exposure: Custom entities are not automatically exposed to the Store API or the Admin API. You must create a custom API route or a dedicated Admin module to make this data accessible to the frontend or management panel.
- Search Indexing: If your entity needs to be searchable via the storefront search, it will not work by default. You must implement a custom
EntityIndexerto push data into Elasticsearch/OpenSearch. - Translatability: If the
titlefield needs to be different for English and German, you cannot simply use aStringField. You must create a separateNoteTranslationDefinitionand link it via aTranslationsAssociationField. - Context Misuse: Using
Context::createDefaultContext()in storefront code is a risk. It bypasses the current user's language and currency settings. Always use the injectedSalesChannelContextwhen working in the storefront.
Verification and Testing
To verify the implementation, follow these steps:
- Database Check: Run
bin/console database:migrateand verify the table exists in your SQL client. - Container Check: Run
bin/console debug:container | grep note.repositoryto ensure the repository service was correctly autowired. - Integration Test: Create a KernelTest that uses the repository to
create()a record and thensearch()for it using aCriteriaobject, asserting that the returnedEntityResultcontains the expected object.
Rollback Procedure
If the entity implementation causes instability, create a new migration to drop the table. Never edit a migration that has already been deployed to other environments.
// src/Migration/Migration1670000001RemoveNote.php
public function update(Connection $connection): void
{
$connection->executeStatement('DROP TABLE IF EXISTS `swag_demo_note`;');
}