Shopware 6: Extend Products with Custom Fields via Migrations
Add domain-specific data to Shopware 6 products without overriding core classes by defining custom fields in a migration.
21 Apr 2026, 08:24 UTC

Problem: Core Entities Are Not Enough
Shopware's core Product entity covers name, price, stock, and standard attributes, but it does not include fields such as a manufacturer part number, warranty period, or internal compliance code. Overriding core classes or adding raw database columns breaks upgrades and bypasses the Data Abstraction Layer (DAL). The supported approach is the Custom Field system: define a field set and individual fields in a migration, then let Shopware join the values automatically when the DAL loads a product.
Useful takeaway: write one migration class that implements MigrationStepInterface, call addCustomFieldSet() for the product entity, then call addCustomField() for each attribute. Set apiAware and api flags to expose the data in the Store-API, and write to allow writes. After running the migration, the fields appear under Settings → Custom Fields and are available in Twig and API responses without touching core code.
Migration Example: Add Fields to Product
Create a migration file in your custom plugin or in the migrations directory. The class name and timestamp must be unique. This example assumes Shopware 6.5 or later.
addCustomFieldSet(
$context,
'product_compliance_fields',
'product',
'Product Compliance',
['apiAware' => true]
);
$this->addCustomField(
$context,
'product_compliance_fields',
'mpn',
'Manufacturer Part Number',
'text',
['api' => true, 'write' => true, 'grid' => true]
);
$this->addCustomField(
$context,
'product_compliance_fields',
'warranty_months',
'Warranty (Months)',
'int',
['api' => true, 'write' => true]
);
$this->addCustomField(
$context,
'product_compliance_fields',
'compliance_certificate',
'Compliance Certificate',
'media',
['entity' => 'media', 'api' => true]
);
}
public function updateDestructive(Context $context): void
{
}
}
MigrationStepInterface marks the class as a database migration executed by doctrine:migrations:migrate. The DAL is Shopware's Data Abstraction Layer; it loads entities and joins related data automatically. apiAware is a flag on a field set that makes the set eligible for Store-API responses. grid marks a field as a candidate for Admin data grid columns.
Key configuration details:
- addCustomFieldSet() registers the group under Settings → Custom Fields. The second argument is the internal set name; the third is the target entity, such as product.
- addCustomField() creates a field inside that set. The fourth argument is the type: text, int, bool, json, or media.
- The media type requires 'entity' => 'media' so the DAL can resolve a MediaEntity reference.
- 'apiAware' => true on the set and 'api' => true on each field are both required for Store-API visibility.
- 'write' => true permits POST and PATCH requests to set the field; without it, the API silently ignores the value.
- 'grid' => true marks the field for Admin grids, but you still need to add the column in the grid configuration.
Run the Migration and Verify
Run the command from the Shopware project root:
bin/console doctrine:migrations:migrate 1700000000000
Where to run: the project root, as the web server user (commonly www-data) or any user with write access to var/ and database credentials in .env. Permissions matter because the command writes migration metadata and may change the schema.
Expected check: the command reports the migration as executed. Inspect the intended SQL first with:
bin/console doctrine:migrations:execute --up 1700000000000 --dry-run
Risk: this changes the database schema. Run it on a staging environment before production, and back up the database first.
After migration, refresh the search index so the new fields are available for search and grid filtering:
bin/console dal:refresh:index
Verify in the Admin: go to Settings → Custom Fields → Product Compliance. The fields MPN, Warranty (Months), and Compliance Certificate should appear. Open a product detail page and confirm the fields render in the assigned section.
Verify in the Store-API: call GET /store-api/product/{productId} and inspect the customFields object:
{
"id": "...",
"customFields": {
"mpn": "ABC-12345",
"warranty_months": 24,
"compliance_certificate": {
"id": "...",
"url": "https://example.com/media/..."
}
}
}
Read Values in Twig and DAL
In a Storefront Twig template, read the field directly from the product entity:
{{ product.customFields.mpn }}
{% if product.customFields.compliance_certificate %}
Certificate
{% endif %}
In PHP via the DAL, load the product with the custom fields association:
$criteria = new Criteria();
$criteria->addAssociation('customFields');
$product = $productRepository->search($criteria, $context)->first();
$mpn = $product->getCustomFields()['mpn'] ?? null;
The DAL joins custom_field values automatically through the field set definition; you do not write manual SQL joins.
Common Mistakes and Limits
- Duplicate field keys. Keys must be unique across all sets for the same entity. Two sets defining mpn for product cause DAL hydration errors. Prefix keys, for example custom_mpn.
- Missing API flags. A field without 'api' => true does not appear in Store-API responses. A field without 'write' => true is ignored on POST/PATCH even when apiAware is set.
- Large JSON values. A json field over 64 KB slows DAL hydration. Split large payloads into separate fields or use a dedicated entity extension.
- Changing a field type. Do not alter the type of an existing field after data exists. The schema migration may truncate or fail. Create a new field and migrate values in a separate data migration.
- Forgetting the index refresh. New fields are not searchable in Admin grids until you run dal:refresh:index and add the grid column.
Remove a Field Set
Because a migration changes database state, provide a removal path. Create a new migration with a later timestamp and call removeCustomField() for each field, then removeCustomFieldSet():
public function update(Context $context): void
{
$this->removeCustomField($context, 'product_compliance_fields', 'mpn');
$this->removeCustomField($context, 'product_compliance_fields', 'warranty_months');
$this->removeCustomField($context, 'product_compliance_fields', 'compliance_certificate');
$this->removeCustomFieldSet($context, 'product_compliance_fields');
}
Run bin/console dal:refresh:index afterward to remove the fields from search indexes.
Limitations
- Custom fields are not automatically searchable in Admin grids. Set grid to true and configure the column in the grid definition.
- The DAL loads custom fields through joins. Heavy use on high-traffic listing pages can increase query time; test page load under realistic data volumes.
- Media fields return a full MediaEntity reference. Access url or thumbnails to get the image path rather than storing URLs directly.
- Field sets can be scoped to sales channels, but the relation must be configured and assigned in the Admin; otherwise the field may not appear for the expected channel.
Practical check: after deployment, load a product in the Admin, set a value in the new field, save, reload the product, and confirm the value persists. Then call the Store-API endpoint for that product and confirm the same value appears under customFields.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.