Package · sdk-s5w

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.

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…

The client is injected with #[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');
  }
}

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.

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'),
  );
}

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:

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'));
  }
}

A named bean is injected by name with #[Inject]:

php
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.

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

What reaches the log:

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
  • one line per request at debug: method, path, status, time;
  • chunked-upload events — debug for the start, recovery and completion, warning for retries, error for 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]:

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);
  }
}

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