While working on a client project in Ibexa Commerce, I needed to add a discriminating attribute on a product type to contain a simple text. Unfortunately, Ibexa Commerce does not natively include this type of attribute.
If the attribute concerned the product and not the variants, I could have simply replaced the attribute with a content field, since products are contents. For variants, however, I am limited by the existing attribute types.
So I started to examine the Ibexa Commerce code to understand how attribute types are managed, and to be able to create my own. In the end, it consists of a table to store the data, a few PHP classes, and a bit of service configuration.
First, a bit of configuration to declare the attribute type. In your services configuration:
services:
app.commerce.attribute_type.string:
class: Ibexa\ProductCatalog\Local\Repository\Attribute\AttributeType
arguments:
$identifier: string
tags:
- name: ibexa.product_catalog.attribute_type
alias: string
We simply declare a new attribute type with a string identifier. This identifier will be reused to link to other services used by our attribute type.
Ibexa Commerce stores product attributes in ibexa_product_specification_attribute_* tables, one per attribute type. So I'll do the same and create an ibexa_product_specification_attribute_string table.
Here is the associated Doctrine migration:
<?php
declare(strict_types=1);
namespace DoctrineMigrations;
use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;
final class Version20240122141124 extends AbstractMigration
{
public function up(Schema $schema): void
{
$this->addSql("CREATE TABLE ibexa_product_specification_attribute_string (id INT NOT NULL PRIMARY KEY, value VARCHAR(255) NULL, CONSTRAINT ibexa_product_specification_attribute_string_fk FOREIGN KEY (id) REFERENCES ibexa_product_specification_attribute (id) ON UPDATE CASCADE ON DELETE CASCADE);");
$this->addSql("CREATE INDEX ibexa_product_specification_attribute_string_value_idx ON ibexa_product_specification_attribute_string (value);");
}
public function down(Schema $schema): void
{
$this->addSql("DROP TABLE ibexa_product_specification_attribute_string");
}
}
Now that I have my table, I need to tell Ibexa how to store my values. First, I create a class to represent the table structure:
<?php
declare(strict_types=1);
namespace App\Commerce\Attribute\String;
use Doctrine\DBAL\Types\Types;
use Ibexa\Contracts\ProductCatalog\Local\Attribute\StorageDefinitionInterface;
class StringStorageDefinition implements StorageDefinitionInterface
{
// Liste de colonnes (hors id) avec leur type
public function getColumns(): array
{
return [
'value' => Types::TEXT,
];
}
// Nom de la table
public function getTableName(): string
{
return 'ibexa_product_specification_attribute_string';
}
}
Then a class to convert the table data into application data and vice versa:
<?php
declare(strict_types=1);
namespace App\Commerce\Attribute\String;
use Ibexa\Contracts\ProductCatalog\Local\Attribute\StorageConverterInterface;
class StringStorageConverter implements StorageConverterInterface
{
// On lit simplement le contenu de la colonne value
public function fromPersistence(array $data)
{
return $data['value'];
}
// On met la valeur applicative dans la colonne value
public function toPersistence($value): array
{
return [
'value' => $value,
];
}
}
And finally, a bit of configuration:
services:
App\Commerce\Attribute\String\StringStorageConverter:
tags:
- name: ibexa.product_catalog.attribute.storage_converter
type: string # The identifier of our type
App\Commerce\Attribute\String\StringStorageDefinition:
tags:
- name: ibexa.product_catalog.attribute.storage_definition
type: string # The identifier of our type
I create a class to build the edit form:
<?php
declare(strict_types=1);
namespace App\Commerce\Attribute\String;
use Ibexa\Bundle\ProductCatalog\Validator\Constraints\AttributeValue;
use Ibexa\Contracts\ProductCatalog\Local\Attribute\ValueFormMapperInterface;
use Ibexa\Contracts\ProductCatalog\Values\AttributeDefinitionAssignmentInterface;
use Symfony\Component\Form\Extension\Core\Type\TextType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints as Assert;
class StringValueFormMapper implements ValueFormMapperInterface
{
public function createValueForm(
string $name,
FormBuilderInterface $builder,
AttributeDefinitionAssignmentInterface $assignment,
array $context = []
): void {
$definition = $assignment->getAttributeDefinition();
// Les options sont presque copiées/collées telles qu'elles de la classe
// native IntegerValueFormMapper
$options = [
'disabled' => $context['translation_mode'] ?? false,
'label' => $definition->getName(),
'block_prefix' => 'string_attribute_value',
'required' => $assignment->isRequired(),
'constraints' => [
new AttributeValue([
'definition' => $definition,
]),
],
];
if ($assignment->isRequired()) {
$options['constraints'][] = new Assert\NotBlank();
}
// J'ajoute un champ texte, comme ma valeur est une simple string
$builder->add($name, TextType::class, $options);
}
}
And again, a bit of setup:
services:
App\Commerce\Attribute\String\StringValueFormMapper:
tags:
- name: ibexa.product_catalog.attribute.form_mapper.value
type: string # The identifier of our type
And finally the last step, I create a class to tell Ibexa how to format this value for display:
<?php
declare(strict_types=1);
namespace App\Commerce\Attribute\String;
use Ibexa\Contracts\ProductCatalog\Local\Attribute\ValueFormatterInterface;
use Ibexa\Contracts\ProductCatalog\Values\AttributeInterface;
class StringValueFormatter implements ValueFormatterInterface
{
public function formatValue(AttributeInterface $attribute, array $parameters = []): ?string
{
// Since my value is already a string, I return it as is
return $attribute->getValue();
}
}
And the last piece of configuration:
services:
App\Commerce\Attribute\String\StringValueFormatter:
tags:
- name: ibexa.product_catalog.attribute.formatter.value
type: string # The identifier of our type
Despite the lack of documentation on the subject, the associated code is simple and clear. In my case, I simply copied/pasted the code associated with the Integer type and adapted it for a string. For a more complex type, I could have taken inspiration from the SingleMeasurement type, which stores both a numeric value and a unit.
To celebrate the release of Dataflow 5 for Symfony 7, here is some feedback on ...
🎯 Nous relevons le défi de lancer un grand concours : Vous faire gagner un ...
How to dynamically calculate shipping costs? The official documentation that explains how to create shipping ...
How to create a new product attribute type in Ibexa Commerce
A la recherche d'un poste de travail temporaire ou permanent ? Vous recherchez un environnement ...
Après une découverte de surface d'Ibexa Commerce, entrons plus dans le détail pour comprendre son ...
Ibexa DXP propose un module pour gérer des produits pour la réalisation d'un site e-commerce. ...
Voici une présentation d'IbexaMailing, un module qui ajoute la gestion des newsletters à Ibexa. IbexaMailing est ...
C'est la dernière occasion de vous souhaitez le meilleur pour cette année 2024 et surtout ...
To celebrate the release of Dataflow 5 for Symfony 7, here is some feedback on ...
🎯 Nous relevons le défi de lancer un grand concours : Vous faire gagner un ...
How to dynamically calculate shipping costs? The official documentation that explains how to create shipping ...
How to create a new product attribute type in Ibexa Commerce
A la recherche d'un poste de travail temporaire ou permanent ? Vous recherchez un environnement ...
Après une découverte de surface d'Ibexa Commerce, entrons plus dans le détail pour comprendre son ...
Ibexa DXP propose un module pour gérer des produits pour la réalisation d'un site e-commerce. ...
Voici une présentation d'IbexaMailing, un module qui ajoute la gestion des newsletters à Ibexa. IbexaMailing est ...
C'est la dernière occasion de vous souhaitez le meilleur pour cette année 2024 et surtout ...