---
title: Creating Custom Entities | Shopware Community Hub
description: >-
  Learn how to create, register and persist custom entities in Shopware using
  the Data Abstraction Layer.
canonical_url: 'https://hub.shopware.com/learn/unit/creating-custom-entities'
---

# Creating Custom Entities

<LearningObjectives>

- Create custom entities using Shopware's Data Abstraction Layer.
- Define entity properties with appropriate data types and constraints.
- Implement entity relationships to existing Shopware entities.
- Register custom entities so they are recognized by Shopware.
- Create and manage database migrations for custom entities.
- Follow Shopware's entity naming and structure conventions.
  
</LearningObjectives>

# Creating Custom Entities

Now that you understand the fundamentals of the Data Abstraction Layer, let's dive into the practical aspect of creating custom entities.

In this learning unit, we'll focus specifically on the mechanics of entity creation, from defining the structure to making it available in your Shopware application.

## Understanding Entity Creation

Creating a custom entity involves several key steps:

1. **Entity Definition** – Creating the PHP class that defines your entity
2. **Property Configuration** – Defining fields, types, and constraints
3. **Relationship Mapping** – Connecting to existing Shopware entities
4. **Plugin Registration** – Making Shopware aware of your entity
5. **Database Migration** – Creating the actual database table

## Our Practical Example: Product Notes

We'll implement the `product_note` entity for our AcademyProductNotes plugin. This entity will store admin notes about products, allowing colleagues to share interesting facts, specialities, and important information about products. This demonstrates:

- Basic entity structure
- Property definitions
- Relationships to products and admin users
- Proper registration and migration

A typical folder structure for creating a custom entity could look like this:

```txt
[shop_root]
 └── custom/plugins
     └── AcademyProductNotes
         └── src
             ├── Core
             │   └── Content
             │       └── ProductNote
             │           ├── ProductNoteCollection.php
             │           ├── ProductNoteDefinition.php
             │           └── ProductNoteEntity.php
             ├── Migration
             │   ├── Migration1769158002CreateProductNoteTable.php // Generated by Shopware CLI; our example migration
             │   └── // ... Other migration files
             ├── Resources
             │   └── config
             │       └── services.xml
             ├── AcademyProductNotes.php
             └── composer.json
```

## Step 1: Create the Entity Class

In Shopware, an entity represents a specific type of data stored in the database, such as a `product`, `customer`, or in our case a `product note`.

Entities define the structure, properties, and relationships of this data, making it accessible and manageable through Shopware's Data Abstraction Layer (DAL).

Let's start by creating the `ProductNoteEntity` class, which will describe the structure and properties of our custom product note entity.

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Core\Content\ProductNote;

use Shopware\Core\Content\Product\ProductEntity;
use Shopware\Core\Framework\DataAbstractionLayer\Entity;
use Shopware\Core\Framework\DataAbstractionLayer\EntityIdTrait;

class ProductNoteEntity extends Entity
{
    use EntityIdTrait;

    protected string $productId;
    protected string $productVersionId;
    protected string $userName;
    protected string $note;
    protected bool   $solved;

    // Relationships
    protected ?ProductEntity $product = null;

    // Getters and Setters
    public function getProductId(): string
    {
        return $this->productId;
    }

    public function setProductId(string $productId): void
    {
        $this->productId = $productId;
    }

    public function getProductVersionId(): string
    {
        return $this->productVersionId;
    }

    public function setProductVersionId(string $productVersionId): void
    {
        $this->productVersionId = $productVersionId;
    }

    public function getNote(): string
    {
        return $this->note;
    }

    public function setNote(string $note): void
    {
        $this->note = $note;
    }

    public function isSolved(): bool
    {
        return $this->solved;
    }

    public function setSolved(bool $solved): void
    {
        $this->solved = $solved;
    }

    // Relationship getters and setters
    public function getProduct(): ?ProductEntity
    {
        return $this->product;
    }

    public function setProduct(?ProductEntity $product): void
    {
        $this->product = $product;
    }
}
```

### Why Do We Use EntityIdTrait?

Custom entities in Shopware must provide a unique identifier, so they can be managed by the Data Abstraction Layer.

The `EntityIdTrait` provides common ID-related functionality for entities, including the `getId` and `setId` methods.

This allows Shopware to uniquely identify, persist, and reference entity instances.

Without this trait, a custom entity would not expose its ID in a standardized way expected by the DAL.

### Why Are Association Properties Nullable?

In Shopware's DAL, associations are **not loaded automatically**. In this example, the `ProductEntity` is therefore **not available by default** when fetching a `ProductNoteEntity`.

To load the related entity (in this case, the `ProductEntity`), you must explicitly add the association to the criteria:

```php
$criteria = new Criteria();
$criteria->addAssociation('product');
```

Only then the `ProductEntity` will be available in the `ProductNoteEntity`. If the association is not added to the criteria, the `product` property will be `null`.

For this reason, association properties in custom entities must be defined as nullable.

## Step 2: Create the Entity Definition

The **entity definition** tells Shopware how to map your entity to the database and how to handle relationships.

By creating an `EntityDefinition`, you enable Shopware to automatically generate the database schema, provide API access, and integrate your entity into the admin and DAL.

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Core\Content\ProductNote;

use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition;
use Shopware\Core\Framework\DataAbstractionLayer\Field\DateTimeField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\FkField;
use Shopware\Core\Framework\DataAbstractionLayer\Field\IdField;
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;

class ProductNoteDefinition extends EntityDefinition
{
    public const string ENTITY_NAME = 'academy_product_note';

    public function getEntityName(): string
    {
        return self::ENTITY_NAME;
    }

    public function getEntityClass(): string
    {
        return ProductNoteEntity::class;
    }

    public function getCollectionClass(): string
    {
        return ProductNoteCollection::class;
    }

    protected function defineFields(): FieldCollection
    {
        return new FieldCollection([
            (new IdField('id', 'id'))->addFlags(new PrimaryKey(), new Required()),
            
            // Foreign key to the product (versioned)
            (new FkField('product_id', 'productId', ProductDefinition::class))->addFlags(new Required()),
            (new ReferenceVersionField(ProductDefinition::class, 'product_version_id'))->addFlags(new Required()),
            
            // Content fields
            (new LongTextField('note', 'note'))->addFlags(new Required()),
            (new BoolField('solved', 'solved'))->addFlags(new Required()),
            
            // Associations
            new ManyToOneAssociationField('product', 'product_id', ProductDefinition::class, 'id', false),
        ]);
    }
}
```

### Why Do We Need the ReferenceVersionField Field?

Products in Shopware are **versioned entities**. This means a product is uniquely identified by a combination of `id` and `version_id`.

The `id` identifies the logical product, while the `version_id` identifies a specific version of that product (e.g., the live version vs. a draft/staging version).

This is beneficial because Shopware can keep multiple versions of the same entity at the same time. So changes can be prepared and validated without affecting the live data.

Technically, this is reflected in the database/DAL model by using a composite key (`id`, `version_id`). Only the combination of both values uniquely identifies a specific version of a versioned entity.

When creating an association to a versioned entity, your custom entity must store **both values**. Otherwise, the DAL cannot reliably resolve the relationship.

Shopware validates this when it processes your entity definition (e.g., when running `dal:migration:create`). If the reference version field is missing, you may encounter an error like this:

```txt
[Shopware\Core\Framework\DataAbstractionLayer\DataAbstractionLayerException]  
  Field "product" is missing a reference version field 
```

## Step 3: Create the Entity Collection

Collections are used to handle multiple entities and provide useful methods for filtering and manipulation.

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Core\Content\ProductNote;

use Shopware\Core\Framework\DataAbstractionLayer\EntityCollection;

/**
 * @extends EntityCollection<ProductNoteEntity>
 */
class ProductNoteCollection extends EntityCollection
{
    protected function getExpectedClass(): string
    {
        return ProductNoteEntity::class;
    }
}
```

Via the DAL, you can search for entities such as `product`, `customer`, or your custom entity using a criteria object.

```php
$criteria = new Criteria();
$criteria->addFilter(new EqualsFilter('product_id', $productId));
$criteria->addAssociation('product');

$result = $this->productNoteRepository->search($criteria, $context);

$notes = $result->getEntities(); // ProductNoteCollection
```

When querying data, repositories often return **multiple results**, for example, all product notes belonging to a specific product. In these cases, Shopware returns the results as a `ProductNoteCollection`.

By defining your own collection class, you ensure that the result set is **type-safe** and contains only instances of your custom entity.

If you don't define a custom collection class, Shopware will still return an `EntityCollection`, but it won't be specific to your custom entity type. You will get the generic `EntityCollection` instead of `ProductNoteCollection`.

### Differences Between Entity, Entity Definition, and Entity Collection

To summarize the roles of the different classes:

- **Entity**: Represents a single data record and is used as the data object in your business logic.
- **Entity Definition**: Describes how the entity is mapped, validated, and managed by Shopware's DAL.
- **Entity Collection**: Represents a set of entities returned by repository queries and provides type-safe access to multiple results.

## Step 4: Create the Database Migration

Defining an entity in PHP only describes its structure and relationships. To actually create the corresponding database table (`academy_product_note`), you need a database migration.

The migration updates the database schema so your data can be stored and retrieved.

To generate a migration file automatically, you can use the Shopware CLI command. In your plugin directory, run:

```shell
bin/console database:create-migration -p AcademyProductNotes --name="CreateProductNoteTable"
```

The parameter `-p` specifies the plugin name (in this case `AcademyProductNotes`) and `--name` specifies the migration name.

<Callout title="Note" type="info">

If you run the `database:create-migration` command without `-p YourPluginName`, it will generate a migration file in the `vendor/shopware/core/Migration/V6_X` directory instead of the `custom/plugins/YourPluginName/Migration` directory.

</Callout>

By executing this command, a new migration file is created in the `custom/plugins/AcademyProductNotes/Migration` directory.

At this stage, the migration file looks like this:

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Migration;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1769158002CreateProductNoteTable extends MigrationStep
{
    public function getCreationTimestamp(): int
    {
        return 1769158002;
    }
    
    public function update(Connection $connection): void
    {

    }
}
```

At this point, the migration file does not yet contain any SQL. Now let's add the SQL query to create the table.

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Migration;

use Doctrine\DBAL\Connection;
use Shopware\Core\Framework\Migration\MigrationStep;

class Migration1769158002CreateProductNoteTable extends MigrationStep
{
    public function getCreationTimestamp(): int
    {
        return 1769158002;
    }

    /**
     * Applies the database schema changes for this migration
     * This method is executed once when the migration is new.
     *
     * @throws Exception
     */
    public function update(Connection $connection): void
    {
        $sql = <<<SQL
CREATE TABLE IF NOT EXISTS `academy_product_note` (
    `id` BINARY(16) NOT NULL,
    `product_id` BINARY(16) NOT NULL,
    `product_version_id` BINARY(16) NOT NULL, 
    `note` LONGTEXT NOT NULL,
    `solved` TINYINT(1) DEFAULT 0 NOT NULL,
    `created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
    `updated_at` DATETIME(3) NULL ON UPDATE CURRENT_TIMESTAMP(3),
    PRIMARY KEY (`id`),
    CONSTRAINT `fk.academy_product_note.product`
      FOREIGN KEY (`product_id`, `product_version_id`)
      REFERENCES `product` (`id`, `version_id`) 
      ON DELETE CASCADE
      ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
SQL;
        $connection->executeStatement($sql);
    }
}
```

**What the query is doing:**

- Creates a new table named `academy_product_note`
- Adds a primary key column (`id`)
- Adds a foreign key constraint to the `product` table
- Stores both `product_id` and `product_version_id` to reference a versioned product
- Automatically deletes product notes when the related product is deleted (`ON DELETE CASCADE`)
- Keeps the database references consistent (`ON UPDATE CASCADE`)

After updating your plugin, the `academy_product_note` table should be created in your database.

<Callout title="Tip" type="info">

If you need to change the database schema later (for example, add a new column to an existing custom entity), **do not edit an already executed migration**.

Instead:

- Update the related entity and entity definition
- Create a **new** migration file using the CLI command to apply the schema change

</Callout>

<Callout title="Note" type="info">

Most of the time, you only need the `update` method.

You may come across the `updateDestructive` method as well. This method is primarily used **internally** by Shopware for major updates that introduce breaking, **irreversible** changes.

Such changes are not backwards compatible, which means a rollback is not possible.

</Callout>

## Step 5: Register the Entity in Services

This step is necessary so that Shopware recognizes and manages your custom entity. If you don't register your entity definition in the dependency injection container, Shopware won't be able to find it, and your entity won't be available for use in the system. This means you wouldn't be able to query, persist, or relate data for your new entity.

```xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
        <service id="Swag\AcademyProductNotes\Content\ProductNote\ProductNoteDefinition">
            <tag name="shopware.entity.definition" />
        </service>
    </services>
</container>
```

### Putting Everything Together

At this point, all required building blocks for a custom entity are in place.

- The **Entity**, **EntityDefinition**, and **EntityCollection** classes define how the data is represented in code.
- The **Database Migration** creates the actual database table for storing your entity data.
- The **Service Registration** registers your entity definition in the dependency injection container, making it known to Shopware.

With these steps completed, your custom entity is fully registered in Shopware and can now be used.

## Step 6: Test Your Entity

Now you can test your entity by creating, reading, updating, and deleting product notes using the DAL.

Install the plugin and test; you can use our [example plugin,](https://github.com/ShopwareAcademy/AcademyProductNotes) which already contains all necessary files.

<Callout title="Note" type="info">

Custom Entities are **not automatically available in the administration UI**. To manage them in the administration, a dedicated administration module must be implemented.

</Callout>

### Creating a Product Note

The following example shows a method which creates a product note. The method is defined in a related service class.

```php
<?php declare(strict_types=1);

namespace AcademyProductNotes\Service;

use Shopware\Core\Defaults;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\Context;

class ProductNoteService
{
    public function __construct(
        private readonly EntityRepository $productNoteRepository
    ) {}

    public function createProductNote(string $productId, string $note, Context $context): void
    {
        $this->productNoteRepository->create([
        [
              'productId' => $productId,
              'productVersionId' => Defaults::LIVE_VERSION,
              'note' => $note,
              'solved' => false,
          ]
      ], $context);
  }
}
```

It doesn't matter whether an entity definition is provided by Shopware or you created by your plugin – the DAL's `EntityRepository` handles all registered entity definitions.

<Callout title="Note" type="info">

The DAL automatically handles the fields `createdAt` and `updatedAt`, so you usually don't need to set them manually.

</Callout>

### Accessing the Custom Entity Repository

Once an entity definition is registered, Shopware automatically provides a repository service for it. The service id follows the pattern:

- `<entity_name>.repository`

For our example entity, the repository service is `academy_product_note.repository`.

You can inject this repository into your service (or other places) via dependency injection:

```xml
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
           xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
           xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">

    <services>
      <service id="AcademyProductNotes\Service\ProductNoteService">
        <argument type="service" id="academy_product_note.repository"/>
        <!-- other arguments -->
      </service>
    </services>
</container>
```

Repository usage for reading and writing entities is identical to any other Shopware entity.

## Summary

Good Job! In this learning unit, you learned how to create a fully functional custom entity in Shopware. You learned:

- **Entity Structure**: Custom Entities always extend the base `Entity` class and use `EntityIdTrait`.
- **Field Types**: Choose appropriate field types (`StringField`, `LongTextField`, `DateTimeField`, etc.).
- **Relationships**: Model relations using `FkField` for foreign keys and `ManyToOneAssociationField` for associations.
- **Validation**: Add required flags where appropriate.
- **Naming Conventions**: Follow Shopware's naming conventions for tables and fields.
- **Registration**: Register entity definitions in the `services.xml` file so they are recognized by Shopware.
- **Migrations**: Generate and manage database migrations to persist your entity data in the database.

With these building blocks in place, your custom entity can be queried, persisted, and related to other entities using the DAL.
