Skip to content

marko/filesystem-s3

S3 filesystem driver --- stores files in Amazon S3 or any S3-compatible service with URL generation and pre-signed URLs. Supports key prefixing, visibility via ACLs, MIME type detection, public URL generation, and temporary pre-signed URLs for private files. Works with Amazon S3, MinIO, DigitalOcean Spaces, Cloudflare R2, and any S3-compatible service. Uses the AWS SDK for PHP.

Implements FilesystemInterface from marko/filesystem.

Terminal window
composer require marko/filesystem-s3

This automatically installs marko/filesystem and aws/aws-sdk-php.

Add an S3 disk to your filesystem config:

config/filesystem.php
return [
'default' => 'local',
'disks' => [
's3' => [
'driver' => 's3',
'bucket' => $_ENV['AWS_BUCKET'],
'region' => $_ENV['AWS_DEFAULT_REGION'],
'key' => $_ENV['AWS_ACCESS_KEY_ID'],
'secret' => $_ENV['AWS_SECRET_ACCESS_KEY'],
'prefix' => '',
],
],
];

For S3-compatible services, add endpoint configuration:

config/filesystem.php
's3' => [
'driver' => 's3',
'bucket' => $_ENV['S3_BUCKET'],
'region' => $_ENV['S3_REGION'],
'key' => $_ENV['S3_KEY'],
'secret' => $_ENV['S3_SECRET'],
'endpoint' => $_ENV['S3_ENDPOINT'],
'path_style_endpoint' => true,
],

The factory builds an S3Config value object from your disk configuration. The following keys are supported:

KeyRequiredDefaultDescription
bucketYes---S3 bucket name
regionYes---AWS region (e.g., us-east-1)
keyYes---AWS access key ID
secretYes---AWS secret access key
prefixNo''Key prefix applied to all paths
endpointNonullCustom endpoint URL for S3-compatible services
urlNonullCustom base URL for public URL generation
path_style_endpointNofalseUse path-style URLs instead of virtual-hosted

Use FilesystemManager to access the S3 disk:

use Marko\Filesystem\Manager\FilesystemManager;
class MediaService
{
public function __construct(
private FilesystemManager $filesystemManager,
) {}
public function upload(
string $path,
string $contents,
): void {
$this->filesystemManager->disk('s3')->write(
$path,
$contents,
['visibility' => 'public'],
);
}
public function download(
string $path,
): string {
return $this->filesystemManager->disk('s3')->read($path);
}
}

The S3 driver provides URL generation for stored files:

use Marko\Filesystem\S3\Filesystem\S3Filesystem;
/** @var S3Filesystem $s3 */
$s3 = $this->filesystemManager->disk('s3');
// Public URL
$url = $s3->url('images/photo.jpg');
// Temporary pre-signed URL (default: 1 hour)
$tempUrl = $s3->temporaryUrl('private/report.pdf', expiration: 3600);

Public URLs are constructed based on your configuration:

  • Custom url --- uses the configured base URL directly.
  • Custom endpoint with path-style --- uses the endpoint with the bucket in the path.
  • Default --- uses the standard S3 virtual-hosted URL format: https://{bucket}.s3.{region}.amazonaws.com/{key}.

All keys are automatically prefixed when a prefix is configured, keeping your S3 bucket organized without changing application paths:

// With prefix 'uploads':
// Application path: 'images/photo.jpg'
// S3 key: 'uploads/images/photo.jpg'

Visibility is managed via S3 ACLs. Pass 'visibility' in the options array when writing, or use setVisibility() to change it after the fact:

use Marko\Filesystem\S3\Filesystem\S3Filesystem;
/** @var S3Filesystem $s3 */
$s3 = $this->filesystemManager->disk('s3');
// Write with public visibility
$s3->write('images/photo.jpg', $contents, ['visibility' => 'public']);
// Change visibility later
$s3->setVisibility('images/photo.jpg', 'private');
// Check current visibility
$visibility = $s3->visibility('images/photo.jpg'); // 'public' or 'private'

Replace the S3 filesystem with a Preference for custom behavior:

use Marko\Core\Attributes\Preference;
use Marko\Filesystem\S3\Filesystem\S3Filesystem;
#[Preference(replaces: S3Filesystem::class)]
class CdnS3Filesystem extends S3Filesystem
{
public function url(
string $path,
): string {
// Return CDN URL instead of direct S3 URL
return 'https://cdn.example.com/' . ltrim($path, '/');
}
}

Implements all methods from FilesystemInterface. See marko/filesystem for the full contract.

MethodDescription
url(string $path): stringGenerate a public URL for the given path
temporaryUrl(string $path, int $expiration = 3600): stringGenerate a temporary pre-signed URL (default: 1 hour)
MethodDescription
exists(string $path): boolCheck if a file exists
isFile(string $path): boolCheck if the path is a file
isDirectory(string $path): boolCheck if the path is a directory
info(string $path): FileInfoGet file metadata (size, last modified, MIME type)
read(string $path): stringRead file contents as a string
readStream(string $path): mixedRead file contents as a stream resource
write(string $path, string $contents, array $options = []): boolWrite contents to a file
writeStream(string $path, mixed $resource, array $options = []): boolWrite a stream resource to a file
append(string $path, string $contents): boolAppend contents to a file (reads + rewrites in S3)
delete(string $path): boolDelete a file
copy(string $source, string $destination): boolCopy a file
move(string $source, string $destination): boolMove a file (copy + delete)
size(string $path): intGet the file size in bytes
lastModified(string $path): intGet the last modified timestamp
mimeType(string $path): stringGet the MIME type
listDirectory(string $path = '/'): DirectoryListingInterfaceList files and directories
makeDirectory(string $path): boolCreate a directory marker
deleteDirectory(string $path): boolDelete a directory and all its contents
setVisibility(string $path, string $visibility): boolSet file visibility via ACL (public or private)
visibility(string $path): stringGet the current visibility (public or private)

The driver automatically detects MIME types from file extensions when writing. Over 40 common types are supported, including images, documents, audio, video, fonts, and archives. Unrecognized extensions default to application/octet-stream. You can override detection by passing content_type in the options array:

$s3->write('data.bin', $contents, ['content_type' => 'application/custom']);