---
title: Add Data to Your Product | Shopware Community Hub
description: >-
  Learn how to extend products with custom data programmatically using custom
  fields and understand when to use properties or custom entities.
canonical_url: 'https://hub.shopware.com/learn/unit/add-data-to-your-product'
---

# Add Data to Your Product

<LearningObjectives>

- Understand the different ways to add custom data to a product.
- Learn how to add custom fields to a product programmatically.
- Understand the advantages and disadvantages of product properties, custom fields, and custom entities.
  
</LearningObjectives>

# Add Data to Your Product

You have a working plugin and storefront controller from the previous units. Now you extend **product data**—the core entity of every Shopware shop.

The product is at the front and center of every Shopware shop. It is the most important entity and can be extended with custom data.

In this learning unit, we will take a look at ways in which we can programmatically add custom data to a product.

## Deciding on the Implementation

Before we start, we need to decide how we want to add custom data to our product. There are three main ways to do this:

1. **Product properties**: The easiest way to add custom data to a product. You can add properties like color, size, or any other custom property you need.

2. **Custom fields**: Custom fields are more flexible than product properties and can be used to store any kind of data.

3. **Custom entities**: If more complex data structures are required, you can create custom entities in Shopware. Custom entities are custom database tables, which can be used to store any kind of data.

| Feature              | Product Properties | Custom Fields | Entity Extensions |
|----------------------|--------------------|---------------|-------------------|
| CRUD via Admin       | Yes                | Yes           | Not by default    |
| Category filtering   | Yes                | No            | No                |
| Variant generation   | Yes                | No            | No                |
| Hide in Admin        | No                 | Yes           | Yes               |
| Migration needed     | No                 | No            | Yes               |
| Association possible | No                 | No            | Yes               |
| Custom Entities      | No                 | No            | Yes               |

As shown, each method has its own advantages and disadvantages. Let's break down the use cases:

### Use Case: Product Properties

Use **product properties** when you need to filter products in your category listing or generate variants based on the property.

**Example:** `color: red, blue, green`: Generates variants for each color. Needed for filtering and variant generation.

### Use Case: Custom Fields

Use **custom fields** when you need to store additional, simple information that is not part of the standard attributes and does not require filtering or variant generation. They can be added to almost all entities (e.g., product entity, customer entity, order entity).

**Example:** `dangerous-goods: true`: Displays a warning on the product page if set to true. Not necessarily needed for filtering or variant generation.

<Callout title="Custom Fields" type="info">

In this learning unit, we will focus on adding custom fields to a **product**. Custom fields are a good compromise between flexibility and ease of use.

</Callout>

### Use Case: Custom Entities

Use **custom entities** if you need to store complex data structures that are not related to the product itself.

**Example:** `customize-product: COMPLEX DATABASE STRUCTURE`: Stores complex data structures in separate database tables. For example a ring product configurator (material, size, engraving, etc.).

The [Custom Products extension](https://docs.shopware.com/en/shopware-6-en/extensions/customproducts) is a good example of the implementation of custom entities.

## Real-World Example

In our case, we want to add a custom field to our product that stores a "cargo" flag. If the flag is set to true, a warning should be displayed on the product page.

This information is not needed for filtering or variant generation, so we will use a custom field for this. It will also be used in the email notification for the warehouse.

## Getting Started

To add a custom field to a product, we need to create a new custom fieldset and add a custom field to it. We can do this via the Shopware administration, or we can do it programmatically.

<Callout title="Important" type="warning">

As of Shopware 6.7.0.0, custom field names and field set names must be valid Twig variable names. This means **hyphens (-)** and **dots (.)** are no longer allowed. Existing custom fields will continue to work. The validation is only enforced when creating new custom fields.

</Callout>

In our case, it makes sense to define custom fields programmatically because we want to automate the process of adding the custom field to our products. This plugin will be rolled out to all shops in the network, and to avoid human error on creation, we will automate the process.

If you feel exploratory, you can create a very basic plugin with the following command:

```shell
bin/console plugin:create AcademyDemoProductCustomField
```

In the interactive setup, you can choose the `example custom fieldset` option if you want Shopware to generate the basic file structure for you. In this learning unit, you will replace the generated example content before installing the plugin.

```shell
Do you want to create an example custom fieldset? (yes/no)
```

<Callout title="Use Unique Custom Field Names" type="warning">

The generated Shopware example custom fieldset uses generic technical names such as `swag_example_set` and `swag_example_size`. Custom field names must be unique in the database. If the generated example was installed before, installing another plugin with the same names can cause a duplicate-entry error.

Before you install the plugin, replace the generated example content with the code shown below. This learning unit uses its own custom field name, `academy_demo_product_cargo`. If you compare your code with the Academy reference plugin, remember that the reference plugin uses different names, such as `academy_product_set` and `academy_product_cargo`.

</Callout>

Keep this command for later. After you have added the code in this learning unit, install and activate the plugin with:

```bash
bin/console plugin:refresh
bin/console plugin:install AcademyDemoProductCustomField --activate
```

Or you can use our [AcademyProductCustomField reference plugin](https://github.com/ShopwareAcademy/AcademyProductCustomField). The reference plugin follows the same pattern, but it uses the `AcademyProductCustomField` namespace and `academy_*` technical names.

## Add a Custom Field Programmatically

The custom fields integration in this example is a combination of different parts:

| Part                      | Description                                      |
|---------------------------|--------------------------------------------------|
| The CustomFieldsInstaller | A PHP class that creates and removes the field set, relation, and custom field |
| The plugin lifecycle      | Calls the installer during install and uninstall |
| The template              | A Twig file that renders the custom field        |

In our case, we do not need to create a migration to add the custom field set, because we will use an entity (product) that already exists.

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

If you are more versed and have a custom entity, please take a look at the [custom entity documentation](https://developer.shopware.com/docs/guides/plugins/plugins/framework/custom-field/add-custom-field.html#supporting-custom-fields-with-your-entity).

</Callout>

The **custom fieldset** is a collection of custom fields that can be added to a product. In this example, the installer creates the field set, links it to the product entity, and adds the custom field in one nested write.

Replace the content of your generated `CustomFieldsInstaller.php` file with the following content:

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

namespace ProductDemoPlugin\Service;

use Shopware\Core\Content\Product\ProductDefinition;
use Shopware\Core\Defaults;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\DataAbstractionLayer\EntityRepository;
use Shopware\Core\Framework\Uuid\Uuid;
use Shopware\Core\System\CustomField\CustomFieldTypes;

class CustomFieldsInstaller
{
    private const string CUSTOM_FIELD_SET_NAME = 'Academy Product Custom Field Set';
    private const string CUSTOM_FIELD_NAME = 'academy_demo_product_cargo';
    private const string CUSTOM_FIELD_TECHNICAL_NAME = 'Academy_Demo_Product_Cargo';

    public function __construct(
        private readonly EntityRepository $customFieldSetRepository
    ) {
    }

    public function install(Context $context): void
    {
        $this->customFieldSetRepository->upsert([
            [
                'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME),
                'name' => self::CUSTOM_FIELD_TECHNICAL_NAME,
                'position' => 0,
                'config' => [
                    'label' => [
                        'en-GB' => self::CUSTOM_FIELD_SET_NAME,
                        'de-DE' => self::CUSTOM_FIELD_SET_NAME,
                    ],
                ],
                'relations' => [
                    [
                        'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME . '_product_relation'),
                        'entityName' => ProductDefinition::ENTITY_NAME,
                    ],
                ],
                'customFields' => [
                    [
                        'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_NAME),
                        'name' => self::CUSTOM_FIELD_NAME,
                        'type' => CustomFieldTypes::BOOL,
                        'config' => [
                            'componentName' => 'mt-switch',
                            'type' => 'checkbox',
                            'label' => [
                                Defaults::LANGUAGE_SYSTEM => 'Cargo Flag',
                                'en-GB' => 'Cargo Flag',
                                'de-DE' => 'Cargo Flag',
                            ],
                        ],
                    ],
                ],
            ],
        ], $context);
    }

    public function uninstall(Context $context): void
    {
        $this->customFieldSetRepository->delete([
            [
                'id' => Uuid::fromStringToHex(self::CUSTOM_FIELD_SET_NAME),
            ],
        ], $context);
    }
}
```

You do not need a separate `services.xml` registration for this example. The plugin lifecycle can create the installer and pass the `custom_field_set.repository` service directly.

Now we can call the installer from our plugin class:

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

namespace AcademyDemoProductCustomField;

use AcademyDemoProductCustomField\Service\CustomFieldsInstaller;
use Shopware\Core\Framework\Context;
use Shopware\Core\Framework\Plugin;
use Shopware\Core\Framework\Plugin\Context\InstallContext;
use Shopware\Core\Framework\Plugin\Context\UninstallContext;

class AcademyDemoProductCustomField extends Plugin
{
    public function install(InstallContext $installContext): void
    {
        parent::install($installContext);

        $this->installCustomFields($installContext->getContext());
    }

    public function uninstall(UninstallContext $uninstallContext): void
    {
        parent::uninstall($uninstallContext);

        if ($uninstallContext->keepUserData()) {
            return;
        }

        $this->uninstallCustomFields($uninstallContext->getContext());
    }

    private function installCustomFields(Context $context): void
    {
        $customFieldsInstaller = new CustomFieldsInstaller(
            $this->container->get('custom_field_set.repository')
        );

        $customFieldsInstaller->install($context);
    }

    private function uninstallCustomFields(Context $context): void
    {
        $customFieldsInstaller = new CustomFieldsInstaller(
            $this->container->get('custom_field_set.repository')
        );

        $customFieldsInstaller->uninstall($context);
    }
}
```

<Callout title="Note: Plugin Lifecycle" type="info">

Plugin lifecycle methods are commonly used to create or clean up plugin-owned setup data, such as custom field sets.

In this example, the plugin creates the custom field during installation and removes it during uninstallation only when the user does not keep plugin data.

</Callout>

### Explanation of the Code

- Our class CustomFieldsInstaller
  - Defines the label and technical names used for the custom field set and custom field.
  - Uses `Uuid::fromStringToHex()` to create stable IDs from these values.
  - The `install()` method creates or updates the custom field set, product relation, and `academy_demo_product_cargo` custom field in one nested write.
  - The `uninstall()` method removes the custom field set again when plugin data should be deleted.

- The plugin base class (`AcademyDemoProductCustomField`)
  - The `install()` method calls `installCustomFields()`.
    - Executed when you run `bin/console plugin:install <PluginName>`.
  - The `uninstall()` method calls `uninstallCustomFields()` only when the user does not keep plugin data.
    - Executed when you run `bin/console plugin:uninstall <PluginName>`.

**Flow:** When the plugin is installed, the custom field set is created, linked to the product entity, and filled with the `academy_demo_product_cargo` custom field. After that, the custom field is available in the product settings in the administration.

## Twig Template

To display the custom field on the product detail page, we need to extend the `buy-widget` template and include our custom template.

In this example, we extend the original `buy-widget` and include a new Twig file that renders the custom field:

```twig
{% sw_extends '@Storefront/storefront/component/buy-widget/buy-widget.html.twig' %}

{% block buy_widget_ordernumber_container %}
    {{ parent() }}
    {% include '@AcademyDemoProductCustomField/storefront/component/academy-demo-product-cargo.html.twig' %}
{% endblock %}
```

Here, we use `sw_extends` and `parent` to inherit the `buy-widget` template and extend it by using `include` to load our custom template.

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

In this learning unit, we use the standard Twig **`include`** syntax for simplicity. In advanced use cases, consider using Shopware's **`sw_include`** tag, which supports [multi inheritance](https://developer.shopware.com/docs/resources/references/storefront-reference/twig-function-reference.html#tags).

</Callout>

### Included Template

It is always good practice to use includes in Twig. This way you can keep your code clean and modular.

Also, remember to wrap your custom field in a block, so you can override it in your theme.

```twig
{% block academy_demo_product_cargo %}
    {% if product.translated.customFields.academy_demo_product_cargo %}
        <div class="academy-demo-product-cargo">
            <img 
              src="{{ asset('bundles/academydemoproductcustomfield/images/cargo.png') }}"
              alt="Cargo shipping"
              style="width: 50px; height: 35px;"
            />
        </div>
    {% endif %}
{% endblock %}
```

## Add an Image as a Public Asset

As some of you may be aware, you can add media to the custom field via `CustomFieldTypes::MEDIA`. Nonetheless, choosing to define our custom field in this way gives full control to the developer / plugin - a new release of our Plugin can contain a new image, and the rollout will change it in all shops. In contrast, the media field cannot be updated in this way.

So let's add an image to the file system. This is done via the [asset](https://developer.shopware.com/docs/guides/plugins/plugins/storefront/add-custom-assets.html#overview) structure in Shopware.

```bash
# PluginRoot

├── composer.json
└── src
    ├── Resources
    │   └── public
    │       └── images
    │           └── cargo.png <-- Asset file here
    └── AcademyDemoProductCustomField.php
```

<Callout title="Asset install" type="info">

If your image does not show up in the storefront, you can run the `bin/console assets:install` command to copy the assets to the public (PROJECT_ROOT/public/bundles) folder.

</Callout>

## Assigning the Custom Field to a Product

### Via the Shopware Administration

You can assign the custom field to a product via the Shopware administration. Go to the product detail page and click on the "Specifications" tab.

Here you can add the custom field to the product. In our case, we toggle the "Cargo flag" to true.

![Custom field](assets/images/administration-product-custom-field-set-cargo.jpg)

### Via the Admin API

You can also assign the custom field to a product via the Admin API. You can use the `POST /api/product/{productId}` endpoint to update the product with the custom field.

```http request
PATCH http://YOUR_SHOP_URL/api/product/YOUR_PRODUCT_UUID
Content-Type: application/json
Authorization: Bearer YOUR_ACCESS_TOKEN
```

With the JSON body:

```json
{
  "customFields": {
    "academy_demo_product_cargo": true
  }
}
```

Learn more about the [Admin API](https://shopware.stoplight.io/docs/admin-api/twpxvnspkg3yu-quick-start-guide) in our Stoplight documentation.

## Result

The result of our work is a product that has a custom field "Cargo flag" that can be toggled in the Shopware administration. If the flag is set to true, an image of a truck is displayed on the product page, indicating that our XXL washing machine is shipped via cargo.

![Product page](assets/images/cargo-truck-image-on-product-detail-page.jpg)

<Callout title="Expected result" type="success">

**Prove it works:** Enable **Cargo flag** on a product in the administration. The truck image appears on the product detail page. If not, run `cache:clear`, `assets:install`, and `theme:compile`.

</Callout>

<ArticleQuestionnaire>
  <ArticleQuestionnaireQuestion>When should you use custom fields instead of product properties?</ArticleQuestionnaireQuestion>
  <ArticleQuestionnaireAnswer>When you need variant generation from the value</ArticleQuestionnaireAnswer>
  <ArticleQuestionnaireAnswer correct>When you need simple extra data without filtering or variants</ArticleQuestionnaireAnswer>
  <ArticleQuestionnaireAnswer>When you need a separate database table with associations</ArticleQuestionnaireAnswer>
</ArticleQuestionnaire>

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

If you did everything correctly but don't see the cargo flag on the product page, run the following commands in your shop's root directory:

- **`bin/console cache:clear`**: Clears old cache and reindex compiled Twig templates.
- **`bin/console assets:install`**: Copies your plugin's assets into the public `bundles` folder (`[shop_root]/public/bundles`). At this point, you may have already done it.
- **`bin/console theme:compile`**: Rebuilds your storefront theme so that the changes (theme configurations, assets, SCSS changes) become visible.

</Callout>

## Summary

In this learning unit, you have learned:

- How to decide between **product properties**, **custom fields**, and **custom entities** based on filtering, variant generation, complexity, and associations.
- How to add a custom field programmatically using a **`CustomFieldsInstaller`** and hook it into the **plugin lifecycle** (**`install`**, **`uninstall`**).
- How to render a custom field in the storefront using Twig and manage assets as public assets.
- How to assign the custom field to a product via the Shopware administration and via the Admin API.

With this, you have a solid foundation to extend products safely and consistently, from data modeling to storefront rendering and API updates.
