background

March 19, 2024

Extending Shopware 6 in unwanted ways

Yireo Blog Post

When you are told to extend Shopware entities, the usual way to do this is by adding a custom entity extension. But this proofs to be a lot of work if you just want to add a single column. Here's the result of my hacking.

The baseline: Adding entity extensions

This blog is not meant as a tutorial. So refer to the Shopware documentation if you want to learn how to extend upon Shopware the official way. When extending a product entity, for instance, the official path is to add your own entity (example) and then to load this entity via an association ($criteria->addAssociation('example')) where needed (usually subscribable events like ProductEvents::PRODUCT_LOADED_EVENT). Creating an entity means adding your own definition class, entity class, collection class and entity extension class - plus the require definitions. And you will need a migration to create your own custom table.

That's a lot of work for just adding a single field to the product entity.

The Magento way

I hate to bring this up, but with Magento, the same solution exists: You can create an Extension Attribute, which involves an entity (model, resource model, collection, table) which then is merged together by adding an XML file which leads to a generated ExtensionAttributes class. And you will still need to add your entity where needed, usually via DI plugins. It's equally complex as entity extensions in Shopware.

However, Magento also allows you to use EAV attributes for entities (as long as the entity is an EAV entity). Hack the table and you're done.

Unwanted ways

So that's the experiment I setup for myself: What other ways are there to extend upon entities, other than associations? The journey starts with the entity extension class itself. It allows you to extend the fields of an entity definition (extendFields($collection)) via a call $collection->add($field) where your $field is an instance of Field. With an association, there are various subclasses available OneToOneAssociationField. But what if you simply want to refer to a simple StringField which is already added by you to the entity table?

$collection->add((new StringField('foobar', 'foobar')));

Unfortunately, this gives an exception Only AssociationFields, FkFields/ReferenceVersionFields for a ManyToOneAssociationField or fields flagged as Runtime can be added as Extension. And this usually translated by developers that you either need to go for an association or to add the field with the runtime flag ($field->addFlags(new Runtime())).

Runtime values

However, a runtime field is not loaded by default either. And it only is able to have a value, if - in runtime - that value is added to it. An acceptable workaround that I've found is to subscribe to an event like ProductEvents::PRODUCT_LOADED_EVENT and then to manipulate each entity with the addTranslated() method:

$product->addTranslated('foobar', 'Hello World');

Next, somewhere else, you can then retrieve the value with $product->getTranslation('foobar'). If the value is indeed a translatable value, then this feels ok. However, if the field actually contains other data (boolean, integer, etc), this feels weird. Using entity methods like get(), has() or set() require the property to be hard-coded into the entity class (PHP property_exists()) so this is not extensible. And I haven't found another extensible thing in the entity except for translations (and of course, custom extensions if you want to create one).

Decorating things

Because it seemed that the decision of Shopware to not prefer adding columns to core tables is also present in the PHP code (entity classes are not open for adding new fields, you have to use associations or runtime extensions), I decided to try to rewrite those entity classes instead. And the preferred way to do so is by adding a service decorator.

Fail. Once you have decorated a product entity class, the Shopware\Core\Framework\DataAbstractionLayer\EntityDefinition::$registry is not initialized properly yet and therefore you will get a PHP error. Also, the constructor EntityDefinition::__construct is final so can't be extended. As soon as I found out about these issues, I let go of a decorator.

Rewriting things

Next in line is to rewrite the entire service. In a service definition file (services.xml) I rewrote the product entity definition class to my own. Unfortunately, this didn't work because the definitions seemingly are picked up by compiler passes (?). So I wrote my own compiler pass and I was able to rewrite the product entity definition class:

$productDefinition = $container->getDefinition(ProductDefinition::class);
$productDefinition->setClass(ExtendedProductDefinition::class);

And next, within the new definition class (extending ProductDefinition), I was able to add the field of my choice:

    protected function defineFields(): FieldCollection
    {
        $fields = parent::defineFields();
        $fields->add(new StringField('foobar', 'foobar'));
        return $fields;
    }

Unfortunately, the entity class still lacks a property for this field. So you will also need to rewrite the entity class and the collection class:

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

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

And that's it. Easy. But do not do this. This type of override only works once - you can't have multiple plugins overriding the same entity twice. And it also hooks into parts of the code that might change in the near future. And it is not supported.

GitHub experiment

I have published all of my code so far on the following repository: github.com/yireo-training/SwagTrainingProductEntityExtension Feel free to take a look (or even see if you can improve it). There is still an issue with the override-option where data is actually not being loaded - but hey, it is an experiment.

Conclusion

If you want to extend upon an entity, it is best to add an association field for this - even though this requires you to come up with your own entity/definition/collection classes, a custom table and an entity extension.

Posted on March 19, 2024

About the author

Author Jisse Reitsma

Jisse Reitsma is the founder of Yireo, extension developer, developer trainer and 3x Magento Master. His passion is for technology and open source. And he loves talking as well.

Sponsor Yireo

Looking for a training in-house?

Let's get to it!

We don't write too commercial stuff, we focus on the technology (which we love) and we regularly come up with innovative solutions. Via our newsletter, you can keep yourself up to date on all of this coolness. Subscribing only takes seconds.

Do not miss out on what we say

This will be the most interesting spam you have ever read

We don't write too commercial stuff, we focus on the technology (which we love) and we regularly come up with innovative solutions. Via our newsletter, you can keep yourself up to date on all of this coolness. Subscribing only takes seconds.