Upon my symfony application, I try to configure s3 as a service:
services:
Aws\S3\S3Client:
public: true
arguments:
$args:
version: 'latest'
region: '%env(AWS_REGION)%'
endpoint: 'http://s3:9000'
use_path_style_endpoint: true
credentials:
key: '%env(S3_CLIENT_KEY)%'
secret: '%env(S3_CLIENT_SECRET)%'
And I made a simple command that is Aws\S3\S3Client
is injected upon. It does a simple thing it generates a txt file and uploads it into Aws s3:
namespace App\Command;
use Aws\S3\S3Client;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
#[AsCommand(
name: 'test:s3',
description: 'Add a short description for your command',
)]
class TestS3Command extends Command
{
public function __construct(private S3Client $s3Client)
{
parent::__construct();
}
protected function configure(): void
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$bucketName = 'test'; // Replace with your actual bucket name
$objectKey = 'example.txt'; // The key (file name) in S3
$text = "Hello";
try {
$this->s3Client->putObject([
'Bucket' => $bucketName,
'Key' => $objectKey,
'Body' => $text,
'ContentType' => 'text/plain',
]);
$output->writeln("File uploaded successfully to s3://{$bucketName}/{$objectKey}");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Upload failed: {$e->getMessage()}</error>");
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
But Once I run the command in console:
php bin/console test:s3
I got:
TypeError {#108
#message: "Aws\S3\S3Client::__construct(): Argument #1 ($args) must be of type array, null given, called in /var/www/html/var/cache/dev/Container9KJ5J9E/getTestS3CommandService.php on line 31"
#code: 0
#file: "./vendor/aws/aws-sdk-php/src/S3/S3Client.php"
#line: 412
trace: {
./vendor/aws/aws-sdk-php/src/S3/S3Client.php:412 { …}
./var/cache/dev/Container9KJ5J9E/getTestS3CommandService.php:31 {
Container9KJ5J9E\getTestS3CommandService::do($container, $lazyLoad = true)^
›
› $container->privates['App\\Command\\TestS3Command'] = $instance = new \App\Command\TestS3Command(new \Aws\S3\S3Client(NULL));
›
arguments: {
$args: null
}
}
./var/cache/dev/Container9KJ5J9E/App_KernelDevDebugContainer.php:169 { …}
./var/cache/dev/Container9KJ5J9E/getTestS3Command_LazyService.php:24 { …}
./vendor/symfony/console/Command/LazyCommand.php:189 { …}
./vendor/symfony/console/Application.php:338 { …}
./vendor/symfony/framework-bundle/Console/Application.php:77 { …}
./vendor/symfony/console/Application.php:193 { …}
./vendor/symfony/runtime/Runner/Symfony/ConsoleApplicationRunner.php:49 { …}
./vendor/autoload_runtime.php:29 { …}
./bin/console:15 { …}
}
}
Why ??? Another approach I tried was:
Aws\S3\S3Client:
public: true
arguments:
-
version: 'latest'
region: '%env(AWS_REGION)%'
endpoint: 'http://s3:9000'
use_path_style_endpoint: true
credentials:
key: '%env(S3_CLIENT_KEY)%'
secret: '%env(S3_CLIENT_SECRET)%'
But I still fail to initialize the S3 object upon service container.
As @Bademeister mentions it is not recommended, and as many times I tried to inject the S3 directly to Symfony Service container I failed.
But there are cases that using S3 directly is far more preferable such as:
Therefore, I made a proxy object (applying Proxy Pattern) using the magic method __call
in order to forward any method call towards to actual S3:
namespace App\Services;
use Aws\S3\S3Client;
class S3Service
{
private S3Client $s3Client;
public function __construct(array $config=[])
{
$this->s3Client = new S3Client($config);
}
public function __call($method, $arguments)
{
return call_user_func_array([$this->s3Client, $method], $arguments);
}
}
Then upon services.yaml you can do:
App\Services\S3Service:
public: true
arguments:
- version: 'latest'
region: '%env(AWS_REGION)%'
endpoint: 'http://s3:9000'
use_path_style_endpoint: true
credentials:
key: '%env(S3_CLIENT_KEY)%'
secret: '%env(S3_CLIENT_SECRET)%'
Afterward, upon the command you can use the App\Services\S3Service
as you would use the Aws\S3\S3Client
:
namespace App\Command;
use App\Services\S3Service;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
#[AsCommand(
name: 'test:s3',
description: 'Add a short description for your command',
)]
class TestS3Command extends Command
{
public function __construct(private S3Service $s3Client)
{
parent::__construct();
}
protected function configure(): void
{
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$bucketName = 'test'; // Replace with your actual bucket name
$objectKey = 'example.txt'; // The key (file name) in S3
$text = "Hello";
try {
$this->s3Client->putObject([
'Bucket' => $bucketName,
'Key' => $objectKey,
'Body' => $text,
'ContentType' => 'text/plain',
]);
$output->writeln("File uploaded successfully to s3://{$bucketName}/{$objectKey}");
return Command::SUCCESS;
} catch (\Exception $e) {
$output->writeln("<error>Upload failed: {$e->getMessage()}</error>");
return Command::FAILURE;
}
return Command::SUCCESS;
}
}
That allows me to use the underlying S3 methods and bypassing the error that Symfony's service container throws.
But there's a downside that your IDE may not type hint any method compared on using the S3Client directly.
If you want the methdods from the S3Client to be typehinted in your IDE, you can make S3Service
implement the Aws\S3\S3ClientInterface
:
namespace App\Services;
use Aws\CommandInterface;
use Aws\S3\S3Client;
use Aws\S3\S3ClientInterface;
class S3Service implements S3ClientInterface
{
private S3Client $s3Client;
public function __construct(array $config = [])
{
$this->s3Client = new S3Client($config);
}
public function __call($name, $arguments)
{
return call_user_func_array([$this->s3Client, $name], $arguments);
}
public function getConfig($option = null)
{
return $this->s3Client->getConfig($option);
}
public function getHandlerList()
{
return $this->s3Client->getHandlerList();
}
public function getIterator($name, array $args = [])
{
return $this->s3Client->getIterator($name, $args);
}
public function getPaginator($name, array $args = [])
{
return $this->s3Client->getPaginator($name, $args);
}
public function waitUntil($name, array $args = [])
{
return $this->s3Client->waitUntil($name, $args);
}
public function getWaiter($name, array $args = [])
{
return $this->s3Client->getWaiter($name, $args);
}
public function createPresignedRequest(CommandInterface $command, $expires, array $options = [])
{
return $this->s3Client->createPresignedRequest($command, $expires, $options);
}
public function getObjectUrl($bucket, $key)
{
return $this->s3Client->getObjectUrl($bucket, $key);
}
public function doesBucketExist($bucket)
{
return $this->s3Client->doesBucketExist($bucket);
}
public function doesBucketExistV2($bucket, $accept403)
{
return $this->s3Client->doesBucketExistV2($bucket, $accept403);
}
public function doesObjectExist($bucket, $key, array $options = [])
{
return $this->s3Client->doesObjectExist($bucket, $key, $options);
}
public function doesObjectExistV2($bucket, $key, $includeDeleteMarkers = false, array $options = [])
{
return $this->s3Client->doesObjectExistV2($bucket, $key, $includeDeleteMarkers, $options);
}
public function registerStreamWrapper()
{
$this->s3Client->registerStreamWrapper();
}
public function registerStreamWrapperV2()
{
$this->s3Client->registerStreamWrapperV2();
}
public function deleteMatchingObjects($bucket, $prefix = '', $regex = '', array $options = [])
{
$this->s3Client->deleteMatchingObjects($bucket, $prefix, $regex, $options);
}
public function deleteMatchingObjectsAsync($bucket, $prefix = '', $regex = '', array $options = [])
{
return $this->s3Client->deleteMatchingObjectsAsync($bucket, $prefix, $regex, $options);
}
public function upload($bucket, $key, $body, $acl = 'private', array $options = [])
{
return $this->s3Client->upload($bucket, $key, $body, $acl, $options);
}
public function uploadAsync($bucket, $key, $body, $acl = 'private', array $options = [])
{
return $this->s3Client->uploadAsync($bucket, $key, $body, $acl, $options);
}
public function copy($fromBucket, $fromKey, $destBucket, $destKey, $acl = 'private', array $options = [])
{
return $this->s3Client->copy($fromBucket, $fromKey, $destBucket, $destKey, $acl, $options);
}
public function copyAsync($fromBucket, $fromKey, $destBucket, $destKey, $acl = 'private', array $options = [])
{
return $this->s3Client->copyAsync($fromBucket, $fromKey, $destBucket, $destKey, $acl, $options);
}
public function uploadDirectory($directory, $bucket, $keyPrefix = null, array $options = [])
{
$this->s3Client->uploadDirectory($directory, $bucket, $keyPrefix, $options);
}
public function uploadDirectoryAsync($directory, $bucket, $keyPrefix = null, array $options = [])
{
return $this->s3Client->uploadDirectoryAsync($directory, $bucket, $keyPrefix, $options);
}
public function downloadBucket($directory, $bucket, $keyPrefix = '', array $options = [])
{
$this->s3Client->downloadBucket($directory, $bucket, $keyPrefix, $options);
}
public function downloadBucketAsync($directory, $bucket, $keyPrefix = '', array $options = [])
{
return $this->s3Client->downloadBucketAsync($directory, $bucket, $keyPrefix, $options);
}
public function determineBucketRegion($bucketName)
{
return $this->s3Client->determineBucketRegion($bucketName);
}
public function determineBucketRegionAsync($bucketName)
{
return $this->s3Client->determineBucketRegionAsync($bucketName);
}
public function getCommand($name, array $args = [])
{
return $this->s3Client->getCommand($name,$args);
}
public function execute(CommandInterface $command)
{
return $this->s3Client->execute($command);
}
public function executeAsync(CommandInterface $command)
{
return $this->s3Client->executeAsync($command);
}
public function getCredentials()
{
return $this->s3Client->getCredentials();
}
public function getRegion()
{
return $this->s3Client->getRegion();
}
public function getEndpoint()
{
return $this->s3Client->getEndpoint();
}
public function getApi()
{
return $this->s3Client->getApi();
}
}
Then upon services.yaml
set an alias for the Aws\S3\S3ClientInterface
:
services:
# ALIAS interface as a Service
Aws\S3\S3ClientInterface:
alias: App\Services\S3Service
public: true
App\Services\S3Service:
public: true
arguments:
- version: 'latest'
region: '%env(AWS_REGION)%'
endpoint: 'http://s3:9000'
use_path_style_endpoint: true
credentials:
key: '%env(S3_CLIENT_KEY)%'
secret: '%env(S3_CLIENT_SECRET)%'