justbetter/laravel-akeneo-client
A Laravel-friendly wrapper around Akeneo’s PHP API client, offering configuration, service container bindings, and convenient access to PIM products, categories, families, and other resources from your application.
Install the client and publish its config:
composer require justbetter/laravel-akeneo-client
php artisan vendor:publish --tag=akeneo-config
Add credentials to .env (names may differ slightly per version — check the published config/akeneo.php):
AKENEO_BASE_URI=https://your-pim.example.com
AKENEO_CLIENT_ID=1_abc123
AKENEO_CLIENT_SECRET=secret
AKENEO_USERNAME=api_user
AKENEO_PASSWORD=api_pass
The service provider registers the Akeneo PIM client in the container and exposes a facade. Your first use case is almost always a read: pull a page of products to confirm connectivity.
use Akeneo\Pim\ApiClient\AkeneoPimClientInterface;
$client = app(AkeneoPimClientInterface::class);
$page = $client->getProductApi()->listPerPage(10);
foreach ($page->getItems() as $product) {
dump($product['identifier']);
}
Where to look first:
config/akeneo.php — connection/base URI, credentials, and any HTTP options.AkeneoPimClientInterface, and possibly the individual API classes).akeneo/api-php-client docs — this package is a thin Laravel binding, so most behaviour you'll debug lives there.Inject the interface, never instantiate the client. Constructor injection into jobs, actions, and console commands keeps tests swappable and lets Laravel resolve the configured client:
class SyncProduct
{
public function __construct(private AkeneoPimClientInterface $akeneo) {}
public function handle(string $sku): void
{
$product = $this->akeneo->getProductApi()->get($sku);
// map + persist locally
}
}
Use the facade for terse, one-off calls in controllers or tinker-style scripts:
use JustBetter\Akeneo\Facades\Akeneo;
Akeneo::getProductApi()->get('SKU-123');
Paginate with cursors, not offsets. Akeneo's search API is paginated by search_after. The client's cursor() helper handles the loop for you:
$api = $client->getProductApi();
$cursor = $api->all(100, [
'search' => ['updated' => [['operator' => '>', 'value' => $since]]],
'pagination_type' => 'search_after',
]);
foreach ($cursor as $product) {
// stream into your importer — do not collect() a whole catalog
}
Push work through the queue. API calls are I/O-bound and Akeneo has per-connection throttling. Wrap per-page or per-SKU work in jobs with ->onQueue('akeneo') and a RateLimited middleware so bursts don't get you 429s:
public function middleware(): array
{
return [new RateLimited('akeneo')];
}
Model delta syncs around updated. Store a high-water mark (last successful run timestamp) instead of pulling the full catalog. Use the search filter above and commit the timestamp only after the batch succeeds, so a failure replays cleanly.
Upsert with upsertList for bulk writes, chunked to ~100 items to stay under request-size limits. Check the per-item response for 422s rather than assuming the batch succeeded.
Test with a fake binding. In TestCase or per-test, bind a mock of the interface so nothing leaves your process:
$this->mock(AkeneoPimClientInterface::class, function ($mock) {
$mock->shouldReceive('getProductApi->get')->andReturn(['identifier' => 'SKU-1']);
});
If you need to swap the underlying client in local/dev environments, rebind AkeneoPimClientInterface in a service provider after the package's provider (order matters) and return a stub.
base_uri setting has no trailing slash mismatch — a malformed base URL silently produces wrong endpoint paths and confusing 404s. After upgrading, hit a single getProductApi()->get() in tinker to confirm the resolved URL before running any sync..env inside the package config will be baked into the cached config. Run php artisan config:clear after changing any Akeneo env value, or your rotated secret will appear to have no effect.AkeneoPimClientBuilder yourself with a second config block and bind it under a distinct key.try/catch on Akeneo\Pim\ApiClient\Exception\HttpException and inspect getResponse()->getBody() — Akeneo's validation messages are detailed and worth logging verbatim.search_after requires a deterministic sort order; without a proper sort on updated/identifier, you can silently skip or duplicate records.AppServiceProvider) and decorate it — add logging via a middleware handler on the Guzzle stack, or wrap API objects with your own retry/telemetry logic rather than forking the package.X-Request-Id-style header through the Guzzle client options if you debug sync failures often; Akeneo support will ask for it.How can I help you explore Laravel packages today?