Search by

wmtm / storage-api-sdk

WM-Pryhodya

Laravel SDK for Storage API integration

Package info

bitbucket.org/wmtm/storage-api-sdk

pkg:composer/wmtm/storage-api-sdk

Statistics

Installs: 83

Dependents: 0

Suggesters: 0

2.12.0 2026-09-17 12:16 UTC

README

Цей пакет надає зручний та потужний інструмент (SDK) для інтеграції з Storage API у Laravel-додатках. Він побудований з використанням ресурсно-орієнтованої архітектури (Resource-Oriented Architecture), підтримує fluent-білдери запитів, DTO та асинхронні пули запитів.

Встановлення

Встановіть пакет за допомогою Composer:

composer require wmtm/storage-api-sdk

Опублікуйте конфігураційний файл:

php artisan vendor:publish --tag="storage-api-config"

Конфігурація

Після публікації конфігурації у вас з'явиться файл config/storage-api.php. Ви можете налаштувати базовий URL, токен авторизації та інші параметри, використовуючи .env:

STORAGE_API_BASE_URL=https://api.example.com
STORAGE_API_TOKEN=your_oauth2_token_here
STORAGE_API_LOCALE=pl

Використання

Пакет надає фасад StorageApi, який дає доступ до різних ресурсів API.

Ресурси API

Доступні наступні ресурси:

  • products() — робота з конкретними товарами.
  • catalog() — пошук та отримання товарів з каталогу.
  • categories() — робота з категоріями та підкатегоріями.
  • stock() — перевірка кількостей товарів в каталозі та черги оновлень.

Робота з Каталогом (CatalogQueryBuilder)

Для зручного пошуку та фільтрації по каталогу реалізовано Fluent Builder. Він дозволяє формувати складні запити в читабельному вигляді.

use StorageApi\Facades\StorageApi;

$results = StorageApi::catalog()->query()
    // Основні параметри
    ->inCategory(['3', '5']) // Масив або рядок через кому
    ->search('iphone 15')
    ->priceBetween(from: 20000, to: 45000)
    
    // Пагінація
    ->paginate(page: 2, perPage: 50) 
    
    // Сортування (через хелпери або класичним методом)
    ->sortByPriceDesc() 
    // доступні: sortByPriceAsc(), sortByPriceDesc(), sortByPopularity(), 
    // sortByRelevance(), sortByUpdateTime(), sortByStartTime()
    // класичний спосіб: ->sortBy('-price')
    
    // Фільтрація
    ->withFilters(['11323_1', 'offerHas_1'])
    ->exclude(categories: [123, 456], sellers: ['bad_seller_id'])
    
    // Вузькоспеціалізовані налаштування (Solr фасети, фід-мод, фічі)
    ->facets(categoryChildren: true, allFilters: true)
    ->advancedSearch(operator: 'and', mmPercentage: 80)
    ->feedMode(isFeed: true)
    ->withFeatures(['product_properties', 'product_deliveries', 'product_text']) 
    // Доступні фічі:
    // - product_properties (характеристики товару)
    // - product_deliveries (інформація про доставку)
    // - product_text (HTML-опис товару)
    
    // Виконання запиту
    ->get(); // Повертає об'єкт CatalogResponse

Мапінг товарів та DTO (CatalogResponse)

Каталог завжди повертає об'єкт StorageApi\DTO\CatalogResponse. Завдяки імплементації інтерфейсу Countable, ви можете викликати count($response), щоб отримати кількість завантажених товарів.

Ви можете налаштувати власне перетворення товарів (мапінг) у свій DTO клас, реалізувавши інтерфейс StorageApi\Contracts\CatalogProductMapper:

use StorageApi\Contracts\CatalogProductMapper;
use StorageApi\Builders\CatalogQueryBuilder;

class MyProductMapper implements CatalogProductMapper {
    public function mapProduct(array $product, ?CatalogQueryBuilder $builder = null): mixed {
        // Якщо повернути null, товар буде виключено з результатів
        if (empty($product['price'])) {
            return null;
        }
        return new MyProductDto($product);
    }
}

Ви можете призначити мапер для конкретного запиту, передавши назву класу у метод withProductMapper():

$response = StorageApi::catalog()->query()
    ->search('iphone 15')
    ->withProductMapper(MyProductMapper::class) // Передаємо назву класу
    ->get();

Перевірка наявності в каталозі через Builder

Ви можете використовувати той самий білдер запитів, щоб перевірити наявність товарів (stock check), не формуючи запит з нуля. Для цього замість get() викличте check() або checkAsync().

// Отримає CatalogAvailabilityDto
$availability = StorageApi::catalog()->query()
    ->inCategory('3')
    ->priceBetween(20000, 45000)
    ->withFilters('brand:apple')
    ->check();

Або налаштувати глобальний мапер (наприклад, у AppServiceProvider), який може бути як назвою класу, так і функцією-замиканням:

use StorageApi\Facades\StorageApi;

StorageApi::bindCatalogProductMapper(MyProductMapper::class);
// Або через замикання:
// StorageApi::bindCatalogProductMapper(fn($prod, $b) => new MyProductDto($prod));

Серіалізація та Кешування Каталогу

CatalogResponse за замовчуванням зберігає в собі оригінальні (сирі) дані та серіалізований стан CatalogQueryBuilder, при цьому видаляючи вже змаплені об'єкти. Це дозволяє безпечно кешувати відповіді (наприклад, Cache::put()) без помилок, пов'язаних із серіалізацією складних об'єктів (Closure, PDO, Guzzle клієнти).

При десеріалізації DTO автоматично наново запустить процес мапінгу (mapProduct) для збережених сирих даних, використовуючи параметри оригінального білдера. Якщо необхідно, ви можете примусово перемапити дані після десеріалізації:

$dto = Cache::get('catalog_page_1'); // Сирі дані автоматично перетворяться на MyProductDto
$newDto = $dto->remap(new AnotherMapper()); // Повертає новий DTO з іншим мапінгом

Оптимізоване кешування (Optimized Serialization)

Якщо ви впевнені, що моделі/DTO, які повертає ваш мапер, можна безпечно серіалізувати, ви можете увімкнути оптимізований режим у вашому AppServiceProvider. Це дозволить зберігати в кеші вже готові об'єкти, значно пришвидшивши процес десеріалізації (CPU не буде витрачатися на повторний мапінг) та зменшивши об'єм кешу (сирі дані не зберігатимуться).

use StorageApi\Facades\StorageApi;

// Вмикаємо збереження готових об'єктів в кеш
StorageApi::serializeMappedProducts(true);

Увага: в оптимізованому режимі метод remap() буде недоступний, оскільки сирі дані більше не зберігаються.

Мета-дані в білдері (Metadata)

Для зручної передачі додаткового контексту (наприклад, глобальних націнок, курсів валют, кореневих категорій) всередину мапера або при паралельних (асинхронних) запитах, ви можете зберігати мета-дані в самому екземплярі CatalogQueryBuilder:

StorageApi::catalog()->query()
    ->withMeta('currency', 'USD')
    ->withMeta(['markup' => 1.2])
    ->getAsync('catalog_usd');
    
// Всередині мапера ви можете зчитати ці дані:
// $builder->getMeta('currency');
// $builder->hasMeta('markup');

Налаштування за замовчуванням (Presets)

Для гнучкого та зручного управління налаштуваннями за замовчуванням (наприклад, дефолтна пагінація, сортування тощо) білдер підтримує пресети. Ви можете застосовувати їх як локально для запиту, так і глобально для всього додатку.

Локальне застосування:

Ви можете реалізувати інтерфейс CatalogQueryPreset або використовувати замикання (Closure).

use StorageApi\Contracts\CatalogQueryPreset;
use StorageApi\Builders\CatalogQueryBuilder;

class DefaultSitePreset implements CatalogQueryPreset
{
    public function apply(CatalogQueryBuilder $builder): void
    {
        $builder->sortByRelevance()
                ->paginate(1, 20)
                ->feedMode(false);
    }
}

// Застосування через клас
StorageApi::catalog()->query()
    ->preset(new DefaultSitePreset())
    ->search('iphone')
    ->get();
    
// Застосування через замикання
StorageApi::catalog()->query()
    ->preset(fn($b) => $b->inCategory('default_cat_id'))
    ->get();

Глобальне застосування (наприклад, у ServiceProvider):

Якщо ви хочете задати базові налаштування в одному місці (наприклад, у методі boot вашого провайдера), використовуйте addGlobalPreset(). Ці налаштування автоматично застосовуватимуться до кожного нового запиту каталогу.

use StorageApi\Builders\CatalogQueryBuilder;

public function boot(): void
{
    CatalogQueryBuilder::addGlobalPreset(new DefaultSitePreset());
    
    // Або через замикання
    CatalogQueryBuilder::addGlobalPreset(function (CatalogQueryBuilder $builder) {
        $builder->paginate(1, 40);
    }, 'pagination_preset');
}

Також білдер підтримує трейти Conditionable (when(), unless()) та Macroable з Laravel. Ви можете легко розширити білдер власними шорткатами:

// Реєстрація макроса
CatalogQueryBuilder::macro('onlyInStock', function () {
    return $this->withFilters(['in_stock' => true]);
});

// Використання
StorageApi::catalog()->query()->onlyInStock()->get();

Робота з Товарами

Отримання даних про конкретний товар:

$product = StorageApi::products()->get('15230115011');

За замовчуванням метод повертає DTO StorageApi\DTO\Product. Ви також можете передати власний мапер (ProductMapper) для автоматичного перетворення даних товару у ваш кастомний DTO:

use StorageApi\Contracts\ProductMapper;

/**
 * @implements ProductMapper<MyProductDto>
 */
class MyProductMapper implements ProductMapper {
    public function mapProduct(array $product): \StorageApi\Contracts\DTO\ProductDto {
        // MyProductDto повинно імплементувати \StorageApi\Contracts\DTO\ProductDto
        return new MyProductDto($product);
    }
}

// Завдяки підтримці Generics (@template), IDE автоматично зрозуміє, що $productDto - це об'єкт MyProductDto
$productDto = StorageApi::products()->get('15230115011', new MyProductMapper());

// Для асинхронних запитів:
StorageApi::products()->getAsync('15230115011', 'my_product', new MyProductMapper());

Робота з Категоріями (повертає DTO)

Для зручності роботи з категоріями, методи повертають спеціальні DTO класи (StorageApi\DTO\Category), а не звичайні масиви. Це забезпечує типізацію та автодоповнення в IDE.

// Отримання дерева категорій
$categories = StorageApi::categories()->get('317958');

echo $categories[0]->name;
if (!empty($categories[0]->children)) {
    echo $categories[0]->children[0]->id;
}

Перевірка залишків та оновлень (Stock & Availability)

Ці ендпоінти також використовують DTO для форматування відповідей (CatalogAvailability та ProductAvailability).

// Перевірка наявності товарів в каталозі
$catalogStock = StorageApi::stock()->check(['category_id' => '123']);
if ($catalogStock->totalCount > 0) {
    echo "Знайдено {$catalogStock->totalCount} товарів.";
}

// Статус оновлення конкретного товару
$productStatus = StorageApi::stock()->queueUpdate('15230115011');
if ($productStatus->status === 'recently_updated') {
     echo "Останнє оновлення: {$productStatus->updatedAt}";
}

Асинхронні запити (Async Pool)

SDK підтримує паралельне виконання запитів за допомогою Guzzle Promises. Замість того, щоб чекати кожен запит окремо, ви можете відправити їх в пул, а потім виконати одночасно.

// 1. Додаємо запити в пул, використовуючи методи з суфіксом `Async`
// Другим аргументом передаємо ключ для подальшої ідентифікації результату
StorageApi::products()->getAsync('12345', 'my_product');
StorageApi::catalog()->query()->search('macbook')->getAsync('search_results');
StorageApi::stock()->checkAsync([], 'stock_status');

// 2. Виконуємо всі запити паралельно
$responses = StorageApi::getAsyncResponses();

// 3. Звертаємось до результатів за ключами
$product = $responses['my_product']; // Об'єкт StorageApi\DTO\Product
$catalog = $responses['search_results']; // Об'єкт StorageApi\DTO\CatalogResponse
$stock = $responses['stock_status']; // Об'єкт StorageApi\DTO\CatalogAvailability

Автоматичний мапінг в DTO працює як для синхронних, так і для асинхронних запитів.

Приклад правильного використання у джобі (Background Jobs)

Якщо ви робите серію асинхронних запитів у фоновому джобі в безкінечному циклі (або обходите великий каталог), обов'язково викликайте flushAsync() в кінці кожної ітерації (батчу), щоб уникнути витоку пам'яті:

use StorageApi\Facades\StorageApi;

while ($shouldContinue) {
    // 1. Формуємо пул з запитів
    foreach ($pages as $page) {
        StorageApi::catalog()->query()
               ->inCategory($categoryId)
               ->paginate($page)
               ->getAsync("page_{$page}");
    }

    // 2. Виконуємо запити
    $responses = StorageApi::getAsyncResponses();
    
    // 3. Обробляємо результати
    // ...

    // 4. Очищуємо пул та накопичені результати
    StorageApi::flushAsync();
}

Розширені можливості (Advanced)

SDK надає глобальний клас конфігурації \StorageApi\StorageApi для тонкого налаштування клієнта, що дозволяє легко інтегруватися в існуючі проєкти. Зазвичай ці налаштування викликаються у boot методі вашого AppServiceProvider.

1. Кастомізація відповідей: Глобальні DTO (Closure Registry) vs Локальні Мапери (Mappers)

У SDK є два способи вплинути на те, в якому вигляді вам повертаються дані. Щоб уникнути плутанини, важливо розуміти різницю між ними:

  • DTO (Глобально): Використовується, коли ви хочете замінити стандартний клас DTO у всьому проєкті. SDK використовує патерн Closure Registry.
  • Mapper (Локально): Використовується, коли ви хочете для конкретного запиту (наприклад, у певному контролері) перетворити сирі дані у якусь специфічну структуру (чи масив).

Приклад: Глобальна підміна DTO через Closure Registry Якщо ви хочете, щоб SDK глобально повертав ваші власні класи DTO замість стандартних. Ваші класи повинні імплементувати відповідні інтерфейси з StorageApi\Contracts\DTO.

use StorageApi\StorageApi;
use App\DTO\MyCustomProductDto;

// У вашому ServiceProvider:
public function boot(): void
{
    StorageApi::bindProductFactory(function (array $data) {
        return new MyCustomProductDto($data);
    });
    
    // Доступні також:
    // StorageApi::bindCategoryFactory(...)
    // StorageApi::bindCatalogResponseFactory(...)
    // StorageApi::bindCatalogAvailabilityFactory(...)
    // StorageApi::bindProductAvailabilityFactory(...)
}

Після цього виклик StorageApi::products()->get('123') завжди повертатиме ваш MyCustomProductDto.

Приклад: Локальне використання Mapper Якщо в одному конкретному місці вам потрібно обійти глобальні налаштування і отримати, наприклад, свій кастомний DTO:

class SimpleCustomMapper implements \StorageApi\Contracts\ProductMapper 
{
    public function mapProduct(array $product): \StorageApi\Contracts\DTO\ProductDto 
    {
        return new SimpleProductDto($product['id'], $product['name']); // SimpleProductDto імплементує ProductDto
    }
}

// Передаємо мапер другим аргументом. SDK використає ваш мапер тільки для цього виклику:
$simpleDto = StorageApi::products()->get('123', new SimpleCustomMapper());

2. Кастомні правила повторних запитів (Retry Policies)

За замовчуванням SDK автоматично повторює запити лише при помилках 500+, 429 та обривах з'єднання. Ви можете додати свою логіку (наприклад, щоб повторювати запит при 404):

use StorageApi\StorageApi;

StorageApi::retryWhen(function ($retries, $request, $response, $exception) {
    if ($exception instanceof \StorageApi\Exceptions\ProductArchivedException) {
        return false;
    }
    if ($exception instanceof \StorageApi\Exceptions\ProductNotFoundException) {
        return true;
    }
    return null; // Повернути null, щоб передати рішення стандартному механізму SDK
});

3. Interceptors / Middleware (Логування)

Якщо вам потрібно логувати запити (або їх час виконання), ви можете додати власні Guzzle Middleware:

use StorageApi\StorageApi;
use GuzzleHttp\Middleware;

StorageApi::pushMiddleware(Middleware::tap(
    function ($request, $options) {
        logger('Відправляємо запит', ['url' => $request->getUri()]);
    }
), 'my_logger');

4. Кастомні заголовки для запитів

Ви можете динамічно додавати глобальні заголовки до всіх запитів, використовуючи resolveHeadersUsing:

use StorageApi\StorageApi;

StorageApi::resolveHeadersUsing(function () {
    return [
        'X-Project-Id' => config('app.project_id'),
    ];
});

5. Аліаси параметрів запиту (Query Aliases)

Якщо у вашому проєкті використовуються специфічні ключі для фільтрів (наприклад, category.id замість categoryId), ви можете вказати білдеру автоматично їх перейменовувати при передачі сирого масиву:

use StorageApi\Builders\CatalogQueryBuilder;

// Рекомендуємо додавати це в глобальний пресет (наприклад, в AppServiceProvider)
CatalogQueryBuilder::addGlobalPreset(function (CatalogQueryBuilder $builder) {
    $builder->withQueryAliases([
        'category.id' => 'categoryId',
        'phrase' => 'search',
    ]);
});

// Тепер ви можете передати старий масив, і білдер автоматично переведе ключі у правильні:
StorageApi::catalog()->query()
    ->withParameters(['category.id' => 123, 'phrase' => 'iphone'])
    ->get(); // Під капотом відправить ?categoryId=123&search=iphone

6. Події після кожного виконаного запиту RequestFinished

Якщо вам потрібно отримати більше деталей про запит і відповідь, час виконання ви можете отримати їх підписавшись на подію RequestFinished

use StorageApi\Events\RequestFinished;

Event::listen(RequestFinished::class, static function (RequestFinished $event) {
    if ($event->resource === 'catalog') {
        parse_str($event->request->getUri()->getQuery(), $output);
        logger()->debug('catalog', $output); # всі get параметри запиту в каталозі
    } elseif ($event->resource === 'product') {
        logger()->debug('product', ['id' => $event->resourceId]); # на який товар був запит
    }
});

7. Обробка архівованих товарів

Якщо API повертає 404 з тілом {"status": "ARCHIVED"}, SDK автоматично викине виняток StorageApi\Exceptions\ProductArchivedException. Це дозволяє вашій бізнес-логіці відрізняти "товар тимчасово не знайдено" від "товар назавжди знято з публікації".

Вимоги

  • PHP 8.1+
  • Laravel 10.0+
  • GuzzleHttp 7.8+