Пакет · sdk-s5w

Интеграция с Winter

SDK не зависит от фреймворка, но в приложении Winter его удобнее всего держать бином: контейнер создаст клиент один раз на воркер и внедрит туда, где он нужен. Здесь — рецепты для одного бакета, нескольких бакетов и логирования.

Один бакет

Объявите бин в классе с атрибутом #[Configuration]. Метод с #[Bean] — фабрика: его результат контейнер кеширует и отдаёт всем, кто попросит S5w.

main/Configuration/S5wConfiguration.php
<?php

declare(strict_types=1);

namespace Main\Configuration;

use Flytachi\Winter\Kernel\App\Attribute\Bean;
use Flytachi\Winter\Kernel\App\Attribute\Configuration;
use Flytachi\Winter\Kernel\App\Attribute\Value;
use Flytachi\Winter\Logger\LoggerFactory;
use Flytachi\Winter\SDK\S5w\S5w;
use Flytachi\Winter\SDK\S5w\S5wConfig;

#[Configuration]
final class S5wConfiguration
{
  #[Bean]
  public function s5w(
      #[Value('S5W_URL')] string $url,
      #[Value('S5W_KEY')] string $key,
  ): S5w {
      return new S5w(
          new S5wConfig($url, $key),
          logger: LoggerFactory::getLogger('S5W'),
      );
  }
}
.env
S5W_URL=https://s5.example.com
S5W_KEY=s5w_9f3c…

Клиент внедряется атрибутом #[Autowired]:

main/Services/ReportService.php
use Flytachi\Winter\DI\Attribute\Autowired;
use Flytachi\Winter\SDK\S5w\Entity\File;
use Flytachi\Winter\SDK\S5w\S5w;

final class ReportService
{
  #[Autowired]
  private S5w $s5w;

  public function publish(string $path): File
  {
      return $this->s5w->files()->upload($path, folder: 'reports');
  }
}

Один экземпляр на воркер — это правильно

Клиент не хранит состояния между вызовами: ни открытых сессий, ни кешей. Поэтому бин в области «singleton» (по умолчанию для #[Bean]) безопасен и для параллельных запросов одного воркера.

Настройки из окружения

Всё, что задаётся в S5wConfig, можно вынести в .env. У #[Value] второй аргумент — значение по умолчанию.

main/Configuration/S5wConfiguration.php
#[Bean]
public function s5w(
  #[Value('S5W_URL')] string $url,
  #[Value('S5W_KEY')] string $key,
  #[Value('S5W_TIMEOUT', 15)] int $timeout,
  #[Value('S5W_TRANSFER_TIMEOUT', 300)] int $transferTimeout,
  #[Value('S5W_RETRIES', 1)] int $retries,
): S5w {
  return new S5w(
      new S5wConfig(
          $url,
          $key,
          timeout: $timeout,
          retries: $retries,
          transferTimeout: $transferTimeout,
      ),
      logger: LoggerFactory::getLogger('S5W'),
  );
}

Что означает каждый параметр, какие у него значения по умолчанию и пределы — в справочнике S5wConfig.

Несколько бакетов

Один клиент работает с одним бакетом, потому что бакет определяется ключом. Для нескольких бакетов объявите несколько бинов и дайте каждому имя:

main/Configuration/S5wConfiguration.php
#[Configuration]
final class S5wConfiguration
{
  #[Bean(name: 's5w.media')]
  public function media(
      #[Value('S5W_URL')] string $url,
      #[Value('S5W_MEDIA_KEY')] string $key,
  ): S5w {
      return new S5w(new S5wConfig($url, $key), logger: LoggerFactory::getLogger('S5W'));
  }

  #[Bean(name: 's5w.backups')]
  public function backups(
      #[Value('S5W_URL')] string $url,
      #[Value('S5W_BACKUPS_KEY')] string $key,
  ): S5w {
      return new S5w(new S5wConfig($url, $key), logger: LoggerFactory::getLogger('S5W'));
  }
}

Именованный бин внедряется по имени атрибутом #[Inject]:

php
use Flytachi\Winter\DI\Attribute\Inject;

final class BackupService
{
  #[Inject('s5w.media')]
  private S5w $media;

  #[Inject('s5w.backups')]
  private S5w $backups;
}

Два именованных бина одного типа

Когда бинов S5w с именами два и больше, внедрение просто по типу (#[Autowired] S5w) становится ошибкой — контейнер не знает, какой выбрать, и называет оба варианта. Указывайте имя через #[Inject('…')].

Если бакеты живут на разных серверах S5W, у каждого бина будет и свой адрес — механика та же.

Логирование

Передайте в клиент любой логгер PSR-3. С winter-logger это LoggerFactory::getLogger(); имя, которое вы передадите, станет каналом в строке лога.

php
new S5w($config, logger: LoggerFactory::getLogger('S5W'));

Что попадает в лог:

text
[DEBUG] (S5W): GET v1/check → 200 (21 ms)
[DEBUG] (S5W): POST v1/uploads → 201 (34 ms)
[DEBUG] (S5W): upload b3b53a23-… started: backup.sql, 100725660 bytes in chunks of 8388608
[DEBUG] (S5W): PATCH v1/uploads/b3b53a23-… → 200 (869 ms)
[WARNING] (S5W): upload b3b53a23-…: chunk at 16777216 failed (attempt 1/3), retry in 1000 ms: …
[DEBUG] (S5W): POST v1/uploads/b3b53a23-…/complete → 201 (1733 ms)
[DEBUG] (S5W): upload b3b53a23-… completed: backup.sql → file VVWEoDe7rh0FGbCx
  • одна строка на каждый запрос — уровень debug: метод, путь, статус, время;
  • события загрузки частями — debug для начала, восстановления и завершения, warning для повторов, error для окончательного сбоя;
  • ключ в лог не попадает никогда — заголовки не записываются.

Значения подставлены в текст сообщения самим SDK, поэтому строки читаются с любым логгером, даже если он не подставляет {placeholder}. Те же значения дополнительно передаются в контексте записи. Почему так и какие строки бывают — в разделе HTTP и логирование.

Без логгера — ни строки

Если logger не передан, SDK не пишет ничего. В продакшене с уровнем info и выше строки debug тоже не появятся — останутся только предупреждения о повторах при загрузке частями.

Долгие загрузки в асинхронных задачах

Загрузка большого файла может занимать минуты, и держать HTTP-запрос пользователя всё это время незачем. В Winter такую работу удобно вынести в метод с #[Async]:

main/Services/BigService.php
use Flytachi\Winter\DI\Attribute\Autowired;
use Flytachi\Winter\Kernel\Concurrent\Async\Async;
use Flytachi\Winter\Kernel\Concurrent\CompletableFuture;
use Flytachi\Winter\Kernel\Concurrent\Future;
use Flytachi\Winter\SDK\S5w\Entity\File;
use Flytachi\Winter\SDK\S5w\S5w;
use Psr\Log\LoggerInterface;

class BigService
{
  #[Autowired]
  private S5w $s5w;

  #[Autowired]
  private LoggerInterface $logger;

  /**
   * @return Future<File>
   */
  #[Async]
  public function upload(string $path): Future
  {
      $file = $this->s5w->uploads()->send(
          $path,
          folder: 'backups',
          onProgress: fn(int $sent, int $total) => $this->logger->info(
              sprintf('%s: %d%%', basename($path), $sent * 100 / $total),
          ),
      );

      return CompletableFuture::completedFuture($file);
  }
}

Чтобы асинхронные методы действительно выполнялись асинхронно, у приложения должен быть атрибут #[EnableAsync] — без него #[Async] выполняется синхронно. Подробнее о прогрессе и продолжении после сбоя — в руководстве Большие файлы.

См. также