English
English
Appearance
English
English
Appearance
The official library for the Search API and the Catalog API. It handles batching, partial failures, retries and a safe full sync.
The client has no framework dependency: HTTP goes through a PSR-18 client and PSR-17 factories — either the ones you pass in, or the ones discovery finds in your project — and logging is optional through PSR-3. It requires PHP 8.1 or newer.
composer require getmetty/metty-phpIf your project does not have a PSR-18 client yet, install any implementation:
composer require symfony/http-client nyholm/psr7use Metty\Client\MettyClient;
$client = MettyClient::create(
publicKey: '<PUBLIC_API_KEY>', // reads
secretKey: '<SECRET_API_KEY>', // catalog writes; server side only
);The addresses of both APIs are defaults, so you do not have to state them. Pass only the key you actually need: a client with pk_ can only read, a client with sk_ can only write. Swapped keys are rejected at construction time so that a secret cannot end up in a URL.
create() finds the PSR-18 client and the PSR-17 factories through discovery, and discovery does not guarantee a timeout. In an application that already has them in its container, wire them in yourself — then nothing depends on what discovery happens to find, and a PSR-3 logger can be passed at the same time:
use Metty\Client\Configuration;
use Metty\Client\MettyClient;
$client = new MettyClient(
new Configuration(
publicKey: '<PUBLIC_API_KEY>',
secretKey: '<SECRET_API_KEY>',
maxRetries: 3,
),
$httpClient, // Psr\Http\Client\ClientInterface
$requestFactory, // Psr\Http\Message\RequestFactoryInterface
$streamFactory, // Psr\Http\Message\StreamFactoryInterface
$logger, // Psr\Log\LoggerInterface, optional
);Every argument after the configuration is optional and falls back to discovery, so you can hand over only the logger. Without a logger the client logs nothing.
Autodiscovery (php-http/discovery) picks whatever client it finds, and none of the implementations guarantee a timeout — with the wrong one a request can hang until the PHP process is killed. Configure the connect and total timeouts yourself and pass the ready-made client in:
use Metty\Client\Configuration;
use Metty\Client\MettyClient;
use Nyholm\Psr7\Factory\Psr17Factory;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\HttpClient\Psr18Client;
$psr17 = new Psr17Factory();
$httpClient = new Psr18Client(
HttpClient::create(['timeout' => 10, 'max_duration' => 30]),
$psr17,
$psr17,
);
$client = new MettyClient(
new Configuration('<PUBLIC_API_KEY>', '<SECRET_API_KEY>'),
$httpClient,
$psr17,
$psr17,
);timeout is the idle timeout of the connection, max_duration the ceiling for the whole request; Guzzle calls the same pair connect_timeout and timeout. They bound a single attempt — a retried call also waits between the attempts, at most 60 s per retry when the server sends Retry-After, otherwise a short backoff. search() and suggest() never retry a 429; they throw it at once.
use Metty\Client\Search\SearchQuery;
$response = $client->search()->search(
SearchQuery::for('vŕtačka')
->facet('farba', 'modrá')
->facet('farba', 'čierna') // the same field twice means OR
->category('Náradie > Vŕtačky')
->priceRange(100, 200)
->sortBy('price_asc')
->withSections('facets', 'categories', 'suggestions')
->perPage(24)
->page(2),
);
foreach ($response->products as $product) {
echo $product->id, ' ', $product->name, ' ', $product->price, PHP_EOL;
}
echo $response->total, ' results across ', $response->pages, ' pages';facets, categories, priceRange and suggestions are populated only when withSections() is used. The highlight field arrives from the server including the [] markers; the client does not compute highlighting.
The client knows about the 200 result window: hasNextPage() respects it and a query outside the window fails before the request is sent rather than as a 422.
foreach ($client->search()->searchAll(SearchQuery::for('vŕtačka')) as $product) {
echo $product->name, PHP_EOL;
}Autocomplete:
$suggest = $client->search()->suggest('vŕta', limit: 8);
$suggest->suggestions; // [['query' => 'vŕtačka', 'count' => 41], …]
$suggest->products; // at most 5 compact productsuse Metty\Client\Catalog\CatalogProduct;
$result = $client->catalog()->replace([
CatalogProduct::create('sku-1', 'Príklepová vŕtačka', 'https://eshop.sk/vrtacka',
price: 129.9, inStock: true, brand: 'Bosch', category: 'Náradie > Vŕtačky',
params: ['farba' => 'modrá', 'príkon' => '800 W']),
CatalogProduct::create('sku-2', 'Uhlová brúska', 'https://eshop.sk/bruska', price: 89.5),
]);
if ($result->hasFailures()) {
foreach ($result->failures() as $failure) {
echo $failure->id, ': ', $failure->error, ' — ', $failure->message, PHP_EOL;
}
}You can hand over a catalog of any size at once — the client splits it into batches of 100 products and merges the results.
replace() replaces a product entirely, patch() changes only the fields you send, and delete(['sku-1']) removes products. With patch() an omitted field differs from a field set to null, so clearing a value is written explicitly:
$client->catalog()->patch([
new CatalogProduct('sku-1', ['price' => 99.0, 'brand' => null]),
]);$outcome = $client->catalog()->synchronize($products);
echo $outcome['commit']['removed']; // how many stale products were droppedThe client opens a sync, uploads the whole catalog under it and only then commits. If any product fails to upload, the sync is not committed — the commit would delete exactly what failed — and a SyncIncompleteException is thrown. The sync stays open, so it can be finished:
use Metty\Client\Exception\SyncIncompleteException;
try {
$client->catalog()->synchronize($products);
} catch (SyncIncompleteException $exception) {
$retry = $client->catalog()->replace($products, $exception->syncId);
if ($retry->hasFailures()) {
foreach ($retry->failures() as $failure) {
echo $failure->id, ': ', $failure->error, ' — ', $failure->message, PHP_EOL;
}
throw $exception;
}
$client->catalog()->commit($exception->syncId);
}replace() never throws over a single product, so a repeat has to be checked exactly like the first upload: commit only a WriteResult without failures. A commit of a sync in which something failed again would delete precisely the products that failed to upload. An uncommitted sync deletes nothing, so leaving it open and finishing it in the next run is the safe outcome.
Resending the whole snapshot is the simplest repeat, because a product written twice under the same sync stays a single product. It does assume $products can be iterated a second time — a generator that synchronize() has already consumed yields nothing, so read the source again for the repeat. To resend only what failed, take the ids from $exception->result->failures() — every ItemResult carries id, error and message.
An empty snapshot is always rejected by the client. The force: true parameter applies solely to the server-side safeguard that rejects a snapshot covering less than half of the catalog.
foreach ($client->catalog()->export() as $product) {
echo $product['id'], PHP_EOL;
}The client pages automatically, so the loop walks the whole catalog.
429 for catalog calls, honouring Retry-After; search() and suggest() throw a 429 at once (ApiException::isRateLimited()); a server or network error only for methods that are safe to repeat; other 4xx neverAuthorization header reaches neither the log nor an exceptionNo idempotency key is needed: the server writes by id, so a repeated batch cannot create duplicates.
| exception | when |
|---|---|
ConfigurationException | invalid configuration, or a query the server would reject |
ApiException | the server returned an error; carries statusCode and errorCode |
SyncIncompleteException | a full sync did not complete and was not committed |
TransportException | a network error, or a response that cannot be parsed |
All of them implement Metty\Client\Exception\MettyException, so they can be caught together.
Complete usage examples are in the Examples section.