tomatom / depoto-php-client
Depoto API client
Requires
- php: >=7.4
- ext-json: *
- himik/simple-graphql-query-builder: ^0.2.4
- psr/http-client: ^1.0
- psr/http-client-implementation: *
- psr/http-factory: ^1.0
- psr/http-factory-implementation: *
- psr/http-message: ^1.0
- psr/http-message-implementation: *
- psr/log: >=1.1
- psr/log-implementation: *
- psr/simple-cache: ^1.0
- psr/simple-cache-implementation: *
Requires (Dev)
- monolog/monolog: ^2.3
- nyholm/psr7: ^1.4
- phpunit/phpunit: ^9
- symfony/cache: ^5.3
- symfony/http-client: ^5.3
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-10-14 15:06:58 UTC
README
Depoto
Depoto je skladový, expediční a pokladní systém poskytující GraphQl API s OAuth2 authentifikací. Tato knihovna má za cíl práci s API zpříjemnit;)
Instalace
Doporučujeme instalaci pomocí Composer:
composer require tomatom/depoto-php-client
Použití
Čtěte best practices ve WIKI !!!:
use Depoto\Client; use Monolog\Handler\StreamHandler; use Monolog\Logger; use Nyholm\Psr7\Factory\Psr17Factory; use Symfony\Component\Cache\Adapter\ArrayAdapter; use Symfony\Component\Cache\Psr16Cache; use Symfony\Component\HttpClient\Psr18Client; $httpClient = new Psr18Client(); // PSR-18 Http client $psr17Factory = new Psr17Factory(); // PSR-17 HTTP Factories, PSR-7 HTTP message $cache = new Psr16Cache(new ArrayAdapter()); // PSR-16 Simple cache $logger = new Logger('Depoto', [new StreamHandler('depoto.log', Logger::DEBUG)]); // PSR-3 Logger $depoto = new Client($httpClient, $psr17Factory, $psr17Factory, $cache, $logger); $depoto ->setBaseUrl('https://server1.depoto.cz') // Pro testování: https://server1.depoto.cz.tomatomstage.cz ->setUsername('username') ->setPassword('password');
Pokud se z vaší aplikace potřebujete připojovat k různým účtů, možná vašemu service containeru příjde vhod továrna:
use Depoto\ClientFactory; $depotoFactory = new ClientFactory($httpClient, $psr17Factory, $psr17Factory, $cache, $logger); $depoto = $depotoFactory->createClient('username', 'password', 'https://server1.depoto.cz');
GraphQl
- Query pro čtení a Mutation pro czápis.
- Definuji data (stromovou strukturu), která chci vrátit.
- GraphQL dokumentace
$result = $depoto->query('queryName', ['arg1' => $arg1, 'arg2' => $arg2], ['returnSchema' => ['field', 'object' => ['field']]]); $result = $depoto->mutation('mutationName', ['arg1' => $arg1, 'arg2' => $arg2], ['returnSchema' => ['field', 'object' => ['field']]]);
Pro testování queries a procházení kompletního aktuálního schématu použijte GraphQL Explorer / GraphiQL konzoli.
Omezení počtu požadavků (HTTP 429)
Depoto server omezuje zátěž API pro každého uživatele: počet souběžných požadavků a čas, který server zpracováním požadavků stráví.
Požadavek nad limit odmítne odpovědí 429 Too Many Requests s hlavičkou Retry-After – počtem sekund, po kterých má smysl požadavek poslat znovu
(při příliš mnoha souběžných požadavcích 1 s, při vyčerpání časového rozpočtu 2 až 300 s).
Limit se vyhodnocuje hned po ověření uživatele, ještě než server začne GraphQL požadavek zpracovávat. Odmítnutý požadavek, včetně mutace, tedy vůbec neproběhl a je bezpečné ho poslat znovu.
Server zatím limity jen měří a nevynucuje. Vynucování se nezapne dřív, než s odpovědí 429 budou umět pracovat všechny konektory (viz níže).
Co dělá klient
query(),mutation(),batchQuery()abatchMutation()na odpověď 429 od Depoto serveru počkajíRetry-Aftersekund (chybějící, nečitelná nebo nulová hlavička = 1 s) a požadavek pošlou znovu, nejvýše 2× v rámci jednoho volání.- Automaticky se opakuje jen odmítnutí Depoto serverem, tj. odpověď 429 s JSON tělem, v jehož poli
errorsje chyba s"code": "RATE_LIMITED"({"errors":[{"message":"Too many concurrent requests.","code":"RATE_LIMITED"}]}). U jiné odpovědi 429 (např. od proxy nebo load balanceru před serverem, s jiným či neznámým tělem) nelze vyloučit, že požadavek už proběhl. Klient ji proto neopakuje a rovnou vyhodíRateLimitException. - K čekání klient přičte náhodný rozptyl 0 až 1000 ms, aby workery odmítnuté ve stejnou chvíli (např. 10–15 replik jednoho konzumenta pod jedním uživatelem)
neposlaly požadavky znovu všechny ve stejné sekundě. Rozptyl se do limitu
setMaxRateLimitWait()nepočítá, porovnává se jenRetry-After. - Pokud server žádá delší čekání než 5 s (
getMaxRateLimitWait()), klient nečeká a výjimku vyhodí hned – zbytečně by blokoval worker. - Pokud je i poslední odpověď 429, vyhodí
Depoto\Exception\RateLimitException. Je potomkemServerException, takže ji zachytí i stávajícícatch (ServerException $e).getRetryAfter()vrací počet sekund z hlavičkyRetry-After(u data převedený na zbývající sekundy), nebonull, pokud hlavička chybí či je nečitelná. authenticate()(žádost o token) se automaticky neopakuje, na odpověď 429 rovnou vyhodíRateLimitException.- Každé opakování se zaloguje na úrovni
info, konečné odmítnutí na úrovniwarning.
Nastavení (výchozí hodnoty; ClientFactory vytváří klienta s výchozími hodnotami, případně je nastavte na vráceném klientovi):
$depoto ->setRateLimitRetries(2) // kolikrát nejvýše opakovat jedno volání; 0 = neopakovat, výjimka hned ->setMaxRateLimitWait(5); // nejdelší Retry-After v sekundách, na který klient před opakováním počká; 0 = nikdy nečekat = neopakovat
Nejkratší čekání před opakováním je 1 s, takže setMaxRateLimitWait(0) automatické opakování vypne stejně jako setRateLimitRetries(0).
Jedno volání tak může blokovat až getRateLimitRetries() × (getMaxRateLimitWait() + 1) sekund (výchozí 2 × (5 s + nejvýše 1 s rozptylu) = 12 s), a to u každého volání zvlášť.
V synchronním kontextu (webhook nebo jiný HTTP controller, API proxy), kde volající čeká na odpověď, nastavte
setRateLimitRetries(0) (výjimka hned), nebo setMaxRateLimitWait(1) (klient vyčká jen krátké odmítnutí kvůli souběžným požadavkům),
a při RateLimitException práci odložte do fronty nebo vraťte chybu, místo aby požadavek visel.
Co musí udělat aplikace (konektor)
RateLimitExceptionnikdy nepolykejte. Handler zprávy, který výjimku jen zaloguje (catch (\Exception $e) { $logger->error(...); }), by zprávu zahodil – např. objednávka by se tiše nenaimportovala. Zprávu je nutné zpracovat znovu později, ideálně až zagetRetryAfter()sekund, nejméně však za 1 s –getRetryAfter()může vrátit 0 (hlavičkaRetry-After: 0nebo datum v minulosti) a okamžité opakování by limit jen dál zatěžovalo.null(hlavička chybí) nahraďte výchozí prodlevou.- Symfony Messenger: výjimku z handleru propusťte ven, nejlépe zabalenou do
RecoverableMessageHandlingExceptions prodlevou podlegetRetryAfter():
use Depoto\Exception\RateLimitException; use Symfony\Component\Messenger\Exception\RecoverableMessageHandlingException; try { $result = $depoto->mutation('createOrder', $arguments, ['data' => ['id'], 'errors']); } catch (RateLimitException $e) { // Zprávu zpracovat znovu za Retry-After sekund (bez hlavičky za 60 s), nejméně za 1 s – Retry-After 0 by znamenal okamžité opakování bez konce. // Náhodný rozptyl 0–1000 ms, aby se workery odmítnuté ve stejnou chvíli nevrátily ve stejné sekundě. // Messenger počítá prodlevu v milisekundách. $retryDelay = max(1, $e->getRetryAfter() ?? 60) * 1000 + mt_rand(0, 1000); throw new RecoverableMessageHandlingException($e->getMessage(), 0, $e, $retryDelay); } catch (\Exception $e) { $this->logger->error($e->getMessage()); }
- Čtvrtý parametr konstruktoru
$retryDelay(v milisekundách) máRecoverableMessageHandlingExceptionod symfony/messenger 7.2 (ověřeno ve verzích 7.3.6 a 8.0.14). Ve verzi 6.4 (ověřeno v 6.4.19) ho konstruktor nemá – tam vyhoďtenew RecoverableMessageHandlingException($e->getMessage(), 0, $e)a prodlevu určíretry_strategytransportu. RecoverableMessageHandlingExceptionvynutí opakování bez ohledu namax_retries– zpráva se zkouší, dokud neprojde.- Pokud výjimku jen propustíte ven (
throw $e;), opakuje se podleretry_strategytransportu. Při nastavenímax_retries: 3, multiplier: 2a výchozí prodlevě 1 s jsou to pokusy po 1, 2 a 4 sekundách, což přiRetry-Afteraž 300 s nemusí stačit a zpráva pak skončí ve failure transportu (nebo se zahodí, pokud žádný není). - Prodlevu (
$retryDelayiretry_strategy) použijí jen transporty, které respektujíDelayStamp(Doctrine, Redis, AMQP). Google Pub/Sub (gps://, petitpress/gps-messenger-bundle) zprávu k opakování znovu neodesílá, jen ji odmítne a Pub/Sub ji doručí znovu podle retry policy subscription – tu nastavte (minimumBackoff/maximumBackoff) tak, aby pokrylaRetry-After. - Znovu se zpracuje celá zpráva, ne jen odmítnutý požadavek. Kroky handleru, které proběhly před výjimkou (např. založení adresy nebo zákazníka
před
createOrder), proběhnou znovu. Handler opakovaný tímto způsobem musí být idempotentní – před založením ověřit, zda záznam už neexistuje, a přeskočit hotové kroky. Idempotence je nutná i proto, žeRateLimitExceptionz jiné odpovědi 429 než od Depoto serveru (viz výše) nezaručuje, že odmítnutý požadavek neproběhl. U handleru, který idempotentní není, raději zvolte omezený počet opakování (výjimku jen propustit aretry_strategynastavit na prodlevy v řáduRetry-After) než neomezené opakování přesRecoverableMessageHandlingException. - Na
RateLimitExceptionnereagujte náhradním pokusem s jinými daty (např. „zkusit znovu bez balíků“) – výjimku propusťte ven. Náhradní volání by po uvolnění limitu prošlo a část původních dat by se tiše ztratila. - Mimo Messenger (cron, fronta úloh) nechte úlohu selhat a naplánujte ji znovu (za
getRetryAfter()sekund, nejméně za 1 s), neoznačujte ji jako hotovou.
Ouery
Detail produktu
$result = $depoto->query('product', ['id' => $id], ['data' => [ 'id', 'name', 'ean', 'code', 'quantities' => [ 'depot' => ['id', 'name'], // Sklad 'quantityStock', // Množství na skladě 'quantityReservation', // Množství v rezervaci 'quantityAvailable', // Dostupné množství ], // a další viz GraphQL Explorer ], 'errors']);
Výpis produktů
$result = $depoto->query('products', ['filters' => ['fulltext' => $search]], // Filtry ['items' => [ 'id', 'name', 'ean', 'code', 'quantities' => [ 'depot' => ['id', 'name'], // Sklad 'quantityStock', // Množství na skladě 'quantityReservation', // Množství v rezervaci 'quantityAvailable', // Dostupné množství ] ]]);
Detail objednávky
$result = $depoto->query('order', ['id' => $id], ['data' => [ 'id', 'reservationNumber' 'processStatus' => ['id', 'name', 'note', 'created'], // Aktuální procesní stav, 'items' => ['name', 'amount'], 'paymentItems' => ['name', 'amount', 'currency'], 'carrier' => ['id', 'name'], 'externalId' ], 'errors']);
Výpis objednávek
$result = $depoto->query('orders', ['filters' => ['fulltext' => $search]], ['items' => [ 'id', 'reservationNumber' 'processStatus' => ['id', 'name', 'note', 'created'], 'items' => ['name', 'amount'], 'paymentItems' => ['name', 'amount', 'currency'], 'carrier' => ['id', 'name'], 'externalId' ]]);
Mutation
Vytvoření produktu
$result = $depoto->mutation('createProduct', [ 'name' => 'Testovací produkt', 'ean' => '123456987123', // EAN musí být unikátní 'code' => 'kod', // Nemusí být unikátní 'sellPrice' => 99.9, 'purchasePrice' => 99.9, // Nepovinné, výchozí nákupní cena 'externalId' => 'vas-identifikator-123', // Nepovinné, externí identifikátor ], ['data' => ['id'], 'errors']);
Úprava produktu
$result = $depoto->mutation('updateProduct', [ 'id' => $id, // ID produktu v Depotu 'name' => 'Nový název', ], ['data' => ['id'], 'errors']);
Vytvoření souboru/obrázku
$result = $depoto->mutation('createFile', [ 'text' => 'Popis souboru', 'originalFilename' => 'původní název souboru.jpg', 'mimeType' => 'image/jpeg', 'product' => $productId, // ID produktu v Depotu 'base64Data' => '...', // jen base64 data bez mimeType na začátku ], ['data' => ['id'], 'errors']);
Vytvoření zákazníka
$resultCustomer = $depoto->mutation('createCustomer', [ 'firstName' => 'Jméno', 'lastName' => 'Příjmení', 'email' => 'email@email.cz', 'phone' => '777123456', ], ['data' => ['id'], 'errors']);
Vytvoření adresy
$resultAddress = $depoto->mutation('createAddress', [ 'customer' => $resultCustomer['data']['id'], // Nepovinné 'firstName' => 'Jméno', 'lastName' => 'Příjmení', 'email' => 'email@email.cz', 'phone' => '777123456', 'street' => 'Ulice 12', 'city' => 'Město', 'zip' => '28903', 'country' => 'CZ', // ISO 3166-1 alpha-2 country code 'branchId' => 123456, // Nepovinné, identifikátor pobočky např. výdejny pro zásilkovnu, balíkovnu a ostatní výdejny. 'isStored' => 0, // 0/1 ... 0 pro jednorázové adresy určené pro createOrder. 1 pokud zakládáte adresu pro zákazníka (spolu s createCustomer) a chcete, aby šlo o výchozí adresu, se kterou se bude pracovat na kartě zákaznníka = půjde vybrat do dopravy či platby. V takovém případě je nutné vyplnit pole customer 'isBilling' => 0, // 0/1 ... 0 = doručovací adresa, 1 = fakturační adresa ], ['data' => ['id'], 'errors']);
Vytvoření objednávky
$result = $depoto->mutation('createOrder', [ 'status' => 'reservation', 'customer' => $resultCustomer['data']['id'], // Nepovinné 'invoiceAddress' => $resultAddress['data']['id']], 'shippingAddress' => $resultAddress['data']['id']], 'checkout' => 123, // ID pokladny daného eshopu 'currency' => 'CZK', 'carrier' => 'ppl', 'items' => [ [ 'product' => 123, 'code' => 'ABCD', 'name' => 'Název produktu', 'type' => 'product', 'quantity' => 2, 'price' => 123.45, // cena s DPH 'vat' => 12345 ], // name na produktu je nepovinný, Depoto si jej dotáhne. Je však doporučené jej posílat. [ 'product' => 456, 'code' => 'XYZ', 'name' => 'Název zlevněného produktu', 'type' => 'product', 'quantity' => 2, 'sale' => 20, // 20% sleva 'price' => 123.45, // základní cena s DPH před slevou 'vat' => 12345 ], // name na produktu je nepovinný, Depoto si jej dotáhne. [ 'code' => 'volna1', 'name' => 'Volná položka servisní poplatek', 'type' => 'product', 'quantity' => 1, 'price' => 666, 'vat' => 12345 ], // Volná položka [ 'code' => 'doprava', 'name' => 'Doprava za stovku', 'type' => 'shipping', 'quantity' => 1, 'price' => 100, 'vat' => 12345 ], [ 'code' => 'platba', 'name' => 'Dobírka za dvacku', 'type' => 'payment', 'quantity' => 1, 'price' => 20, 'vat' => 12345 ], ], 'paymentItems' => [ [ 'payment' => 789, 'amount' => 589.5, 'isPaid' => true // Zaplaceno - true/false ], ], ] ['data' => ['id'], 'errors']);
Přidání položky do objednávky
$result = $depoto->mutation('createOrderItem', [ 'order' => 123, 'product' => 123, 'quantity' => 5, 'price' => 31.5, 'sale' => 10, // 10% z výše uvedené ceny s DPH 'vat' => 666, ], ['data' => ['id'], 'errors']);
Úprava položky objednávky
$result = $depoto->mutation('updateOrderItem', [ 'id' => $id, 'quantity' => 5, ], ['data' => ['id'], 'errors']);
Smazání položky objednávky
$result = $depoto->mutation('deleteOrderItem', ['id' => $id], ['errors']);
Úprava objednávky
$result = $depoto->mutation('updateOrder', [ 'id' => $id, 'shippingAddress' => 753951, // Změna doručovací adresy 'items' => [ ['product' => 123, 'quantity' => 2, 'price' => 123.45], // Nové položky // items a typy obdobně viz createOrder ], 'paymentItems' => [ [ 'payment' => 789, 'amount' => 589.5, 'isPaid' => true // Zaplaceno - ano/ne ], ], ], ['data' => ['id'], 'errors']);
Zrušení rezervace
Zručit jde jen objednávky ve stavu rezervace (status="reservation").
$result = $depoto->mutation('deleteReservation', ['id' => $id], ['errors']]);
Vytvoření skladového pohybu
$result = $depoto->mutation('createProductMovePack', [ 'type' => "in", // "out" pro výdej, resp. "transfer" pro převodku 'moves' => [ [ 'product' => 123, 'depotTo' => 666, // ID skladu. Pokud jde o type "out", použijte "depotFrom" 'amount' => 1, 'supplier' => 123, // ID dodavatele 'purchasePrice' => 10.01, 'purchaseCurrency' => 'CZK', 'note' => 'Sync' ], // moves pro další produkty v pohybu ], ], ['data' => ['id'], 'errors']);