Adding New Attribute to an Entity¶
In the following example, we will add the extId (alias "external ID") field to the Product entity.
It is a common modification when you need your e-commerce application and ERP system to co-work smoothly.
Extend framework Product entity¶
Tip
"How does the entity extension work?"
Find it out in the separate article.
Most common entities (including Product) are already extended in project-base to ease your development.
However, when extending any other entity, there are few more steps that need to be done.
Add new extId field with Doctrine ORM annotations and a getter for the field into App\Model\Product\Product class.
Overwrite setData method for setting entity data from a data object.
namespace App\Model\Product;
use Doctrine\ORM\Mapping as ORM;
use Shopsys\FrameworkBundle\Model\Product\Product as BaseProduct;
use Shopsys\FrameworkBundle\Model\Product\ProductData as BaseProductData;
/**
* @ORM\Table(name="products")
* @ORM\Entity
*/
class Product extends BaseProduct
{
/**
* @var int
*
* @ORM\Column(type="integer")
*/
protected $extId;
/**
* @param \App\Model\Product\ProductData $productData
*/
protected function setData(BaseProductData $productData): void
{
parent::setData($productData);
$this->extId = $productData->extId ?? 0;
}
/**
* @return int
*/
public function getExtId(): int
{
return $this->extId;
}
}
Notice that type hints and annotations of the methods do not match. This is on purpose - extended class must respect the interface of its parent while annotation ensures proper IDE autocomplete.
Database migrations¶
Generate a database migration creating a new column for the field by running:
php phing db-migrations-generate
The command prints a file name the migration was generated into:
Checking database schema...
Database schema is not satisfying ORM, a new migration was generated!
Migration file ".../src/Migrations/Version20180503133713.php" was saved (525 B).
As you are adding a not nullable field, you need to modify the generated migration manually and add a default value for already existing entries:
$this->sql('ALTER TABLE products ADD ext_id INT NOT NULL DEFAULT 0');
$this->sql('ALTER TABLE products ALTER ext_id DROP DEFAULT');
Hint
In this step, you were using Phing target db-migrations-generate.
More information about what Phing targets are and how they work can be found in Console Commands for Application Management (Phing Targets).
Run the migration to create the column in your database:
php phing db-migrations
ProductData class¶
Add public extId field into App\Model\Product\ProductData class.
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductData as BaseProductData;
class ProductData extends BaseProductData
{
/**
* @var int
*/
public $extId;
}
ProductDataFactory class¶
In the following steps, we will overwrite all services that are responsible
for Product and ProductData instantiation to make them take our new attribute into account.
Edit App\Model\Product\ProductDataFactory - overwrite create() and createFromProduct() methods.
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\Product as BaseProduct;
use Shopsys\FrameworkBundle\Model\Product\ProductData as BaseProductData;
use Shopsys\FrameworkBundle\Model\Product\ProductDataFactory as BaseProductDataFactory;
class ProductDataFactory extends BaseProductDataFactory
{
/**
* @return \App\Model\Product\ProductData
*/
protected function createInstance(): BaseProductData
{
return new ProductData();
}
/**
* @param \App\Model\Product\Product $product
* @return \App\Model\Product\ProductData
*/
public function createFromProduct(BaseProduct $product): BaseProductData
{
$productData = $this->createInstance();
$this->fillFromProduct($productData, $product);
$productData->extId = $product->getExtId() ?? 0;
return $productData;
}
/**
* @return \App\Model\Product\ProductData
*/
public function create(): BaseProductData
{
$productData = $this->createInstance();
$this->fillNew($productData);
$productData->extId = 0;
return $productData;
}
}
Your ProductDataFactory is already registered in services.yaml
as an alias for the original class.
Shopsys\FrameworkBundle\Model\Product\ProductDataFactory:
alias: App\Model\Product\ProductDataFactory
Enable an administrator to edit the extId field¶
Add your extId field into the form by editing ProductFormTypeExtension in App\Form\Admin namespace.
The original ProductFormType is set as the extended type by the implementation of getExtendedType() method.
namespace App\Form\Admin;
use Shopsys\FrameworkBundle\Form\Admin\Product\ProductFormType;
use Symfony\Component\Form\AbstractTypeExtension;
use Symfony\Component\Form\Extension\Core\Type\IntegerType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Validator\Constraints;
final class ProductFormTypeExtension extends AbstractTypeExtension
{
/**
* {@inheritdoc}
*/
public function buildForm(FormBuilderInterface $builder, array $options)
{
// "basicInformationGroup" is defined in ProductFormType
$basicInformationGroup = $builder->get('basicInformationGroup');
$basicInformationGroup->add('extId', IntegerType::class, [
'required' => true,
'constraints' => [
new Constraints\NotBlank(['message' => 'Please enter external ID']),
],
'label' => 'External ID',
]);
}
/**
* {@inheritdoc}
*/
public function getExtendedType()
{
return ProductFormType::class;
}
}
Tip
If you want to change the order for your newly created field, please look at the section Changing order of groups and fields
Overwrite the setData() method in your Product class.
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductData as BaseProductData;
// ...
/**
* @param \App\Model\Product\ProductData $productData
*/
public function setData(BaseProductData $productData) {
parent::setData($productData);
$this->extId = $productData->extId;
}
In your ProductDataFactory class, update the createFromProduct() method so it sets your new extId field.
namespace App\Model\Product;
use Shopsys\FrameworkBundle\Model\Product\Product;
use Shopsys\FrameworkBundle\Model\Product\ProductData as BaseProductData;
// ...
class ProductDataFactory extends BaseProductDataFactory
{
/**
* @param \App\Model\Product\Product $product
* @return \App\Model\Product\ProductData
*/
public function createFromProduct(BaseProduct $product): BaseProductData
{
$productData = $this->createInstance();
$this->fillFromProduct($productData, $product);
$productData->extId = $product->getExtId();
return $productData;
}
// ...
}
GraphQL Frontend API¶
To add your new attribute into the frontend API, so it can be displayed on the storefront, you need to modify the API definition, which is
already a part of your open-box project base: Product.types.yaml.
Product:
# ...
config:
fields:
# ...
extId:
type: 'String'
description: 'External ID of the product'
The corresponding product data are then fetched automatically from the Product entity or from the elasticsearch index.
For more information about the API, refer to the frontend-api docs.
Export data to elasticsearch index¶
To export your new extId field to the elasticsearch index, you need to modify the ProductExportRepository class:
protected function getExportedFieldValue(int $domainId, BaseProduct $product, string $locale, string $field): mixed
{
// ...
+ 'extId' => $product->getExtId(),
default => parent::getExportedFieldValue($domainId, $product, $locale, $field),
}
For more information about the elasticsearch usage in Shopsys Platform, check the Elasticsearch documentation section.
Storefront¶
Now, we will display the extId in the storefront on the product detail page. First, we need to extend the GraphQL query to include the extId field.
Edit ProductDetailInterfaceFragment file in your project:
fragment ProductDetailInterfaceFragment on Product {
// ...
+ extId
}
Finally, you can use the extId within a product detail component:
{
product.extId && (
<div>
{t('External ID')}: {product.extId}
</div>
);
}
Data fixtures¶
You can modify data fixtures in src/DataFixtures/ of your project.
Random extId¶
If you want to add a unique random extId for products from data fixtures, you can add it in the createProduct method of ProductDataFixture.php.
You can use Faker to generate random numbers like this:
+ use Faker\Generator as Faker;
//...
+ /**
+ * @var \Faker\Generator
+ */
+ protected $faker;
/**
* @param \Shopsys\FrameworkBundle\Model\Product\ProductFacade $productFacade
* @param \Shopsys\FrameworkBundle\Model\Product\ProductVariantFacade $productVariantFacade
* @param \Shopsys\FrameworkBundle\Component\Domain\Domain $domain
* @param \Shopsys\FrameworkBundle\Model\Pricing\Group\PricingGroupFacade $pricingGroupFacade
* @param \App\Model\Product\ProductDataFactory $productDataFactory
* @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ProductParameterValueDataFactory $productParameterValueDataFactory
* @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterValueDataFactory $parameterValueDataFactory
* @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterFacade $parameterFacade
* @param \Shopsys\FrameworkBundle\Model\Product\Parameter\ParameterDataFactory $parameterDataFactory
* @param \Shopsys\FrameworkBundle\Model\Pricing\PriceConverter $priceConverter
+ * @param \Faker\Generator $faker
*/
public function __construct(
ProductFacade $productFacade,
ProductVariantFacade $productVariantFacade,
Domain $domain,
PricingGroupFacade $pricingGroupFacade,
ProductDataFactory $productDataFactory,
ProductParameterValueDataFactory $productParameterValueDataFactory,
ParameterValueDataFactory $parameterValueDataFactory,
ParameterFacade $parameterFacade,
ParameterDataFactory $parameterDataFactory,
- PriceConverter $priceConverter
+ PriceConverter $priceConverter,
+ Faker $faker
) {
$this->productFacade = $productFacade;
$this->productVariantFacade = $productVariantFacade;
$this->domain = $domain;
$this->pricingGroupFacade = $pricingGroupFacade;
$this->productDataFactory = $productDataFactory;
$this->productParameterValueDataFactory = $productParameterValueDataFactory;
$this->parameterValueDataFactory = $parameterValueDataFactory;
$this->parameterFacade = $parameterFacade;
$this->parameterDataFactory = $parameterDataFactory;
$this->priceConverter = $priceConverter;
+ $this->faker = $faker;
}
//...
/**
* @param \App\Model\Product\ProductData $productData
* @return \App\Model\Product\Product
*/
protected function createProduct(ProductData $productData): Product
{
+ $productData->extId = $this->faker->unique()->numberBetween(1, 10000);
/** @var \App\Model\Product\Product $product */
$product = $this->productFacade->create($productData);
$this->addProductReference($product);
return $product;
}
Specific extId¶
If you need to add specific extId to products in the data fixture, you will have to update the creation of products in ProductDataFixture::load.
//...
$productData = $this->productDataFactory->create();
$productData->catnum = '9184440';
$productData->partno = '8328B006';
$productData->ean = '8845781245936';
+ $productData->extId = 1;
//...