Интеграция с Winter
SDK не зависит от фреймворка, но в приложении Winter его удобнее всего держать бином: контейнер создаст клиент один раз на воркер и внедрит туда, где он нужен. Здесь — рецепты для одного бакета, нескольких бакетов и логирования.
Один бакет
Объявите бин в классе с атрибутом #[Configuration]. Метод с #[Bean] — фабрика: его
результат контейнер кеширует и отдаёт всем, кто попросит S5w.
<?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'),
);
}
}S5W_URL=https://s5.example.com
S5W_KEY=s5w_9f3c…Клиент внедряется атрибутом #[Autowired]:
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] второй аргумент —
значение по умолчанию.
#[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.
Несколько бакетов
Один клиент работает с одним бакетом, потому что бакет определяется ключом. Для нескольких бакетов объявите несколько бинов и дайте каждому имя:
#[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]:
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();
имя, которое вы передадите, станет каналом в строке лога.
new S5w($config, logger: LoggerFactory::getLogger('S5W'));Что попадает в лог:
[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]:
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] выполняется синхронно. Подробнее о
прогрессе и продолжении после сбоя — в руководстве Большие файлы.
См. также
- Установка — требования и ключи
S5wConfig— все параметры клиента- HTTP и логирование — что и почему пишется в лог