When a shop has several thousand products with dozens of attributes in several languages, managing product data directly in Magento becomes a real pain. Akeneo PIM solves this – it centralises product data and delivers it to Magento (and other channels) through an API. I show why you would want a PIM and what the technical integration looks like.
Why a PIM when Magento has attributes?
Magento has an attribute system, attribute sets and categories – that is true. But it is optimised for selling, not content management. Problems arise when:
- You have multiple sales channels (Magento, Amazon, Allegro, mobile app) and want a single source of truth for product data
- Product content is created by the marketing department, not a developer or Magento admin
- You support multiple languages and translations require an approval workflow
- You have several hundred attributes and their structure changes frequently
- Product data comes from an ERP and requires validation before publishing
Akeneo Community Edition is free and open source. The Enterprise version adds workflows, permissions and advanced data quality rules.
Integration architecture
Akeneo exposes a REST API. Integration with Magento 2 can work two ways:
- Push from Akeneo – Akeneo sends data to Magento via webhook or cron after each change
- Pull from Magento – Magento periodically polls the Akeneo API and imports changes
The Pull approach is most common – a dedicated import module triggered by a cron job. Ready-made connectors are available: the official one from Akeneo and an open-source one from Flagbit.
Akeneo REST API – basic operations
<?php
declare(strict_types=1);
namespace Vendor\AkeneoConnector\Api;
class AkeneoClient
{
private string $accessToken = '';
public function __construct(
private string $baseUrl,
private string $clientId,
private string $clientSecret,
private string $username,
private string $password,
private \GuzzleHttp\Client $httpClient
) {}
private function authenticate(): void
{
$response = $this->httpClient->post($this->baseUrl . '/api/oauth/v1/token', [
'auth' => [$this->clientId, $this->clientSecret],
'json' => [
'grant_type' => 'password',
'username' => $this->username,
'password' => $this->password,
],
]);
$data = json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
$this->accessToken = $data['access_token'];
}
public function getProducts(int $page = 1, int $limit = 100): array
{
if (empty($this->accessToken)) $this->authenticate();
$response = $this->httpClient->get($this->baseUrl . '/api/rest/v1/products', [
'headers' => ['Authorization' => 'Bearer ' . $this->accessToken],
'query' => [
'page' => $page,
'limit' => $limit,
'with_count' => true,
'locales' => 'pl_PL,en_US',
'scope' => 'ecommerce',
],
]);
return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
}
}
Mapping Akeneo data to Magento 2
Data from Akeneo has a specific structure – attributes are an array of values per locale and per scope. You need a mapping layer:
<?php
declare(strict_types=1);
class ProductMapper
{
private array $attributeMap = [
'name' => 'name',
'description' => 'description',
'short_description' => 'short_description',
'price' => 'price',
'weight' => 'weight',
];
public function map(array $akeneoProduct, string $locale = 'pl_PL', string $scope = 'ecommerce'): array
{
$magentoData = [
'sku' => $akeneoProduct['identifier'],
'type_id' => 'simple',
'visibility' => 4,
'status' => $akeneoProduct['enabled'] ? 1 : 2,
];
foreach ($akeneoProduct['values'] as $attributeCode => $values) {
$magentoCode = $this->attributeMap[$attributeCode] ?? $attributeCode;
$value = $this->extractValue($values, $locale, $scope);
if ($value !== null) {
$magentoData[$magentoCode] = $value;
}
}
return $magentoData;
}
private function extractValue(array $values, string $locale, string $scope): mixed
{
// Locale + scope specific
foreach ($values as $value) {
if ($value['locale'] === $locale && $value['scope'] === $scope) return $value['data'];
}
// Scope only
foreach ($values as $value) {
if ($value['locale'] === null && $value['scope'] === $scope) return $value['data'];
}
// Global (no locale, no scope)
foreach ($values as $value) {
if ($value['locale'] === null && $value['scope'] === null) return $value['data'];
}
return null;
}
}
Periodic import via Magento cron
<?php
namespace Vendor\AkeneoConnector\Cron;
class SyncProducts
{
public function __construct(
private \Vendor\AkeneoConnector\Api\AkeneoClient $akeneoClient,
private \Vendor\AkeneoConnector\Model\ProductMapper $mapper,
private \Magento\Catalog\Api\ProductRepositoryInterface $productRepository,
private \Psr\Log\LoggerInterface $logger
) {}
public function execute(): void
{
$page = 1;
do {
$response = $this->akeneoClient->getProducts($page, 100);
$products = $response['_embedded']['items'] ?? [];
$totalPages = (int) ceil($response['items_count'] / 100);
foreach ($products as $akeneoProduct) {
try {
$this->syncProduct($akeneoProduct);
} catch (\Exception $e) {
$this->logger->error('Sync failed: ' . $akeneoProduct['identifier'], [
'error' => $e->getMessage(),
]);
}
}
$page++;
} while ($page <= $totalPages);
}
private function syncProduct(array $akeneoProduct): void
{
$data = $this->mapper->map($akeneoProduct);
$this->logger->info('Synced: ' . $data['sku']);
}
}
Summary
Akeneo PIM makes sense from a certain scale – a few thousand products, multiple languages, multiple sales channels. Below that threshold managing data directly in Magento is simpler. The technical integration through the REST API is well documented, and the attribute mapping layer is the most important part of the whole solution – that is where most time is spent in real implementations.
