Gdy sklep ma kilka tysięcy produktów z dziesiątkami atrybutów w kilku językach, zarządzanie danymi produktowymi bezpośrednio w Magento staje się prawdziwym bólem. Akeneo PIM rozwiązuje ten problem – centralizuje dane produktowe i dostarcza je do Magento (i innych kanałów) przez API. Pokazuję po co w ogóle PIM i jak wygląda integracja technicznie.
Po co PIM, skoro Magento ma atrybuty?
Magento ma system atrybutów, attribute sets i kategorii – to prawda. Ale jest zoptymalizowany pod kątem sprzedaży, nie zarządzania treścią. Problemy pojawiają się gdy:
- Masz kilka kanałów sprzedaży (Magento, Amazon, allegro, własna aplikacja mobilna) i chcesz jednego źródła prawdy dla danych produktowych
- Treści produktowe tworzy dział marketingu, a nie developer czy administrator Magento
- Obsługujesz kilka języków i tłumaczenia wymagają workflow z akceptacją
- Atrybutów jest kilkaset i ich struktura zmienia się często
- Dane produktowe przychodzą z ERP i wymagają walidacji przed publikacją
Akeneo Community Edition jest darmowe i open source. Wersja Enterprise dodaje workflow, uprawnienia i zaawansowane reguły jakości danych.
Architektura integracji
Akeneo udostępnia REST API. Integracja z Magento 2 może działać na dwa sposoby:
- Push z Akeneo – Akeneo wysyła dane do Magento przez webhook lub cron po każdej zmianie
- Pull z Magento – Magento cyklicznie odpytuje Akeneo API i importuje zmiany
Najczęściej stosuje się podejście Pull z Magento, przez dedykowany moduł importu uruchamiany cronem. Dostępne są gotowe konektory – oficjalny od Akeneo oraz open-source’owy od Flagbit.
Akeneo REST API – podstawowe operacje
<?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);
}
public function getProduct(string $sku): array
{
if (empty($this->accessToken)) {
$this->authenticate();
}
$response = $this->httpClient->get(
$this->baseUrl . '/api/rest/v1/products/' . urlencode($sku),
['headers' => ['Authorization' => 'Bearer ' . $this->accessToken]]
);
return json_decode($response->getBody()->getContents(), true, 512, JSON_THROW_ON_ERROR);
}
}
Mapowanie danych Akeneo na Magento 2
Dane z Akeneo mają specyficzną strukturę – atrybuty to tablica z wartościami per locale i per scope. Potrzebujesz warstwy mapującej:
<?php
declare(strict_types=1);
namespace Vendor\AkeneoConnector\Model;
class ProductMapper
{
// Mapowanie atrybutów Akeneo na atrybuty Magento
private array $attributeMap = [
'name' => 'name',
'description' => 'description',
'short_description' => 'short_description',
'price' => 'price',
'weight' => 'weight',
'ean' => 'ean',
];
public function map(array $akeneoProduct, string $locale = 'pl_PL', string $scope = 'ecommerce'): array
{
$magentoData = [
'sku' => $akeneoProduct['identifier'],
'type_id' => 'simple',
'visibility' => 4,
'status' => $this->mapStatus($akeneoProduct['enabled']),
];
// Atrybuty zlokalizowane i per-scope
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
{
// Wartość zlokalizowana i per-scope
foreach ($values as $value) {
if ($value['locale'] === $locale && $value['scope'] === $scope) {
return $value['data'];
}
}
// Wartość per-scope bez locale
foreach ($values as $value) {
if ($value['locale'] === null && $value['scope'] === $scope) {
return $value['data'];
}
}
// Wartość globalna (bez locale i scope)
foreach ($values as $value) {
if ($value['locale'] === null && $value['scope'] === null) {
return $value['data'];
}
}
return null;
}
private function mapStatus(bool $enabled): int
{
return $enabled ? 1 : 2;
}
}
Import cykliczny przez Magento cron
<?php
namespace Vendor\AkeneoConnector\Cron;
use Vendor\AkeneoConnector\Api\AkeneoClient;
use Vendor\AkeneoConnector\Model\ProductMapper;
use Magento\Catalog\Api\ProductRepositoryInterface;
class SyncProducts
{
public function __construct(
private AkeneoClient $akeneoClient,
private ProductMapper $mapper,
private ProductRepositoryInterface $productRepository,
private \Psr\Log\LoggerInterface $logger
) {}
public function execute(): void
{
$page = 1;
do {
$response = $this->akeneoClient->getProducts($page, 100);
$products = $response['_embedded']['items'] ?? [];
foreach ($products as $akeneoProduct) {
try {
$this->syncProduct($akeneoProduct);
} catch (\Exception $e) {
$this->logger->error('Sync failed for SKU: ' . $akeneoProduct['identifier'], [
'error' => $e->getMessage(),
]);
}
}
$page++;
$totalPages = ceil($response['items_count'] / 100);
} while ($page <= $totalPages);
}
private function syncProduct(array $akeneoProduct): void
{
$data = $this->mapper->map($akeneoProduct);
try {
$product = $this->productRepository->get($data['sku']);
} catch (\Magento\Framework\Exception\NoSuchEntityException $e) {
$product = null;
}
// Aktualizacja lub tworzenie - dalsza logika importu
$this->logger->info('Synced product: ' . $data['sku']);
}
}
Podsumowanie
Akeneo PIM ma sens od pewnej skali – kilka tysięcy produktów, kilka języków, kilka kanałów sprzedaży. Poniżej tej granicy zarządzanie danymi bezpośrednio w Magento jest prostsze. Integracja techniczna przez REST API jest dobrze udokumentowana, a warstwa mapowania atrybutów to najważniejszy element całego rozwiązania – tutaj spędza się najwięcej czasu przy realnych wdrożeniach.
