Winter integration
The SDK does not depend on the framework, but in a Winter application it is most convenient as a bean: the container builds the client once per worker and injects it wherever it is needed. Recipes for one bucket, several buckets and logging.
One bucket
Declare the bean in a class marked #[Configuration]. A method with #[Bean] is a factory: the
container caches its result and hands it to everyone who asks for 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…The client is injected with #[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');
}
}One instance per worker is right
The client keeps no state between calls: no open sessions, no caches. So a singleton bean (the
default for #[Bean]) is safe for concurrent requests of one worker too.
Settings from the environment
Everything S5wConfig takes can live in .env. The second argument of #[Value] is the default.
#[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'),
);
}What each parameter means, its default and limits — in the
S5wConfig reference.
Several buckets
One client works with one bucket because the key decides the bucket. For several buckets declare several beans and give each a name:
#[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'));
}
}A named bean is injected by name with #[Inject]:
use Flytachi\Winter\DI\Attribute\Inject;
final class BackupService
{
#[Inject('s5w.media')]
private S5w $media;
#[Inject('s5w.backups')]
private S5w $backups;
}Two named beans of one type
With two or more named S5w beans, injecting by type alone (#[Autowired] S5w) becomes an
error — the container cannot choose and names both candidates. Give the name with #[Inject('…')].
If the buckets live on different S5W servers, each bean also gets its own address — the mechanics are the same.
Logging
Pass any PSR-3 logger to the client. With winter-logger it is LoggerFactory::getLogger(); the
name you pass becomes the channel in the log line.
new S5w($config, logger: LoggerFactory::getLogger('S5W'));What reaches the log:
[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- one line per request at
debug: method, path, status, time; - chunked-upload events —
debugfor the start, recovery and completion,warningfor retries,errorfor a final failure; - the key never reaches the log — headers are not written.
The SDK fills the values into the message text itself, so the lines read right with any logger,
even one that does not substitute {placeholder}s. The same values are also passed in the
record’s context. Why, and which lines exist — in
HTTP and logging.
No logger — not a line
Without a logger the SDK writes nothing. In production at info and above the debug lines
do not appear either — only the warnings about chunk retries remain.
Long uploads in async tasks
Uploading a large file can take minutes, and there is no reason to keep the user’s HTTP request
waiting all that time. In Winter such work fits a method with #[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);
}
}For async methods to actually run asynchronously, the application needs the #[EnableAsync]
attribute — without it #[Async] runs synchronously. More on progress and resuming after a
failure — in the Large files guide.
See also
- Installation — requirements and keys
S5wConfig— every client parameter- HTTP and logging — what is logged and why