Products
The product entity in Magento 2.4.9: SKU, attribute sets, website and store view scope, and loading one through the repository.
ProductRepositoryInterface::get($sku, $editMode = false, $storeId = null, $forceReload = false): ProductInterfaceA product in Magento is one row in catalog_product_entity plus a value stored in a separate table for every attribute it has. That storage pattern is EAV: entity, attribute, value. The entity row carries only sku, type_id, attribute_set_id and a few bookkeeping columns. Everything a merchant edits lives elsewhere.
SKU, type and attribute set
The sku is the business key and is unique across the whole installation. The attribute set is the list of attributes this product is allowed to have: a product in the Bag set has no color field unless color was added to that set. type_id selects the behaviour class that governs price and stock.
Scope
Every attribute has a scope: global, website, or store view. The same product can have a different name in store view 1 and store view 2. ProductRepositoryInterface::get() takes the store id as its third argument. Omit it and no store_id is set on the product at all, so Product::getStoreId() falls back to whatever store the current request is in: store 0, the admin values, in an admin or CLI context, but the current store view on the storefront. Pass the id explicitly whenever the scope matters.
Three ways to read one value
$product->getColor(); // shorthand
$product->getData('color'); // what the shorthand calls
$product->getCustomAttribute('color')->getValue(); // the API form
The first two are the same call: __call turns getColor() into getData('color'). The third is the form that works on a plain ProductInterface from a web API payload, where only the codes listed in ProductInterface::ATTRIBUTES are top-level fields and everything else is a custom attribute.
After saving a product from code, run bin/magento indexer:reindex and bin/magento cache:flush before judging the storefront.
Example
<?php
// app/code/Vendor/Catalog/Console/Command/ShowProductCommand.php
declare(strict_types=1);
namespace Vendor\Catalog\Console\Command;
use Magento\Catalog\Api\ProductRepositoryInterface;
use Magento\Framework\Exception\NoSuchEntityException;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
class ShowProductCommand extends Command
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository,
?string $name = null
) {
parent::__construct($name);
}
protected function configure(): void
{
$this->setName('vendor:product:show')
->setDescription('Show one product as a given store view sees it')
->addArgument('sku', InputArgument::REQUIRED, 'Product SKU')
->addArgument('store', InputArgument::OPTIONAL, 'Store view id', '0');
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
try {
$product = $this->productRepository->get(
(string)$input->getArgument('sku'),
false,
(int)$input->getArgument('store')
);
} catch (NoSuchEntityException $e) {
$output->writeln('<error>' . $e->getMessage() . '</error>');
return Command::FAILURE;
}
$output->writeln('name: ' . $product->getName());
$output->writeln('type: ' . $product->getTypeId());
$output->writeln('set: ' . $product->getAttributeSetId());
return Command::SUCCESS;
}
}
// Register the command in app/code/Vendor/Catalog/etc/di.xml under
// Magento\Framework\Console\CommandList, then run bin/magento setup:di:compile.
//
// $ bin/magento vendor:product:show 24-MB01 1
// name: Joust Duffle Bag
// type: simple
// set: 4When to use it
- Read a product's store-view name and price for an export without loading a whole collection.
- Check whether a SKU already exists before creating it in an import script.
- Find out which attribute set a product uses, so you know which attributes you are allowed to set.
- Make an existing product visible on a second website after a multi-store rollout.
More examples
Writing one attribute for a single store view
Reach for this when a merchant wants a different product name or description for one store view only. The store id argument is what makes the save scoped; without it the new name lands on the global value and every store view changes. Afterwards run bin/magento cache:flush, and bin/magento indexer:reindex catalogsearch_fulltext if the attribute is searchable.
<?php
// app/code/Vendor/Catalog/Model/StoreScopedName.php
declare(strict_types=1);
namespace Vendor\Catalog\Model;
use Magento\Catalog\Api\ProductRepositoryInterface;
class StoreScopedName
{
public function __construct(
private readonly ProductRepositoryInterface $productRepository
) {
}
public function rename(string $sku, int $storeId, string $name): void
{
// Passing $storeId sets store_id on the loaded product, so the save
// writes a store-view value instead of overwriting the global one.
// $editMode = true skips storefront-only data preparation.
$product = $this->productRepository->get($sku, true, $storeId, true);
$product->setName($name);
$this->productRepository->save($product);
}
}The same read in longhand and shorthand
Use this to read other people's code. Core templates and older modules use the magic getter, newer API code uses getCustomAttribute(). Knowing they resolve to the same stored value stops you hunting for a getColor() method that was never written. The API form is the one to use when the object came from a web API request or an extension attribute, because there the magic getters may not be present at all.
<?php
// app/code/Vendor/Catalog/Model/ColorReader.php
declare(strict_types=1);
namespace Vendor\Catalog\Model;
use Magento\Catalog\Api\Data\ProductInterface;
class ColorReader
{
public function fromModel(\Magento\Catalog\Model\Product $product): ?string
{
// These two lines are the same thing. getColor() does not exist as a
// method; __call() rewrites it to getData('color').
$shorthand = $product->getColor();
$longhand = $product->getData('color');
return $shorthand === $longhand ? $shorthand : null;
}
public function fromApiObject(ProductInterface $product): ?string
{
// On a ProductInterface there are no magic getters you can rely on.
// Anything outside ProductInterface::ATTRIBUTES is a custom attribute.
$attribute = $product->getCustomAttribute('color');
return $attribute === null ? null : (string)$attribute->getValue();
}
}Assigning an existing product to a second website
A product that exists but shows nowhere is usually a product with no website assignment. This is the supported way to add one. Note the factory: ProductWebsiteLinkInterface is a data object, not injectable, so it is created through its generated factory rather than the object manager. Run bin/magento indexer:reindex and bin/magento cache:flush afterwards, otherwise the product still will not appear in listings.
<?php
// app/code/Vendor/Catalog/Model/WebsiteAssigner.php
declare(strict_types=1);
namespace Vendor\Catalog\Model;
use Magento\Catalog\Api\Data\ProductWebsiteLinkInterfaceFactory;
use Magento\Catalog\Api\ProductWebsiteLinkRepositoryInterface;
class WebsiteAssigner
{
public function __construct(
private readonly ProductWebsiteLinkInterfaceFactory $linkFactory,
private readonly ProductWebsiteLinkRepositoryInterface $linkRepository
) {
}
public function assign(string $sku, int $websiteId): void
{
$link = $this->linkFactory->create();
$link->setSku($sku);
$link->setWebsiteId($websiteId);
$this->linkRepository->save($link);
}
}
Discussion