Skip to content

AmazonS3

Viames Marino edited this page Feb 26, 2026 · 2 revisions

Pair framework: AmazonS3

Pair\Services\AmazonS3 is a lightweight S3 wrapper built on AWS SDK v3.

It covers common storage operations for Pair apps:

  • upload files
  • check object existence
  • read/download objects
  • delete single objects or prefixes
  • generate presigned URLs
  • validate URL expiration

Requirements

Install AWS SDK v3 in your project:

composer require aws/aws-sdk-php:^3

The service validates dependencies at runtime and throws PairException with ErrorCodes::AMAZON_S3_ERROR if missing.

Constructor

new AmazonS3(string $key, string $secret, string $region, string $bucket)

Parameters:

  • key: AWS access key id
  • secret: AWS secret access key
  • region: bucket region (for example eu-west-1)
  • bucket: bucket name

Configuration pattern

AmazonS3 does not enforce specific .env variable names. Use your own keys and pass them to the constructor.

Example:

AWS_S3_KEY=
AWS_S3_SECRET=
AWS_S3_REGION=eu-west-1
AWS_S3_BUCKET=my-app-bucket
use Pair\Core\Env;
use Pair\Services\AmazonS3;

$s3 = new AmazonS3(
    (string)Env::get('AWS_S3_KEY'),
    (string)Env::get('AWS_S3_SECRET'),
    (string)Env::get('AWS_S3_REGION'),
    (string)Env::get('AWS_S3_BUCKET')
);

API reference

bucket(): string

Returns configured bucket name.

$bucket = $s3->bucket();

put(string $filePath, string $destination): void

Uploads a local file to S3.

Behavior:

  • validates local file readability
  • streams upload (fopen(..., 'rb'))
  • tries MIME detection with mime_content_type() and sets ContentType
$s3->put(APPLICATION_PATH . '/tmp/report.pdf', 'reports/2026/report.pdf');

exists(string $remoteFile): bool

Checks object existence via headObject.

Behavior:

  • returns true when object is accessible
  • returns false for 403/404
  • throws PairException for other S3/AWS errors
if ($s3->exists('reports/2026/report.pdf')) {
    // object is available
}

read(string $remoteFile): string

Reads remote object and returns full body content as string.

$content = $s3->read('configs/app.json');

get(string $remoteFile, string $localFilePath): bool

Downloads remote object to local filesystem.

Behavior:

  • returns false if object does not exist
  • returns true when file was written locally
$ok = $s3->get('reports/2026/report.pdf', APPLICATION_PATH . '/tmp/report.pdf');

delete(string $remoteFile): bool

Deletes one object.

Behavior:

  • returns false if object does not exist
  • returns true if delete call is executed
$deleted = $s3->delete('reports/2026/report.pdf');

deleteDir(string $remoteDir): bool

Deletes objects under a prefix.

Behavior:

  • normalizes prefix (rtrim(..., '/') . '/')
  • lists objects under prefix
  • deletes listed keys with deleteObjects
  • returns false when no objects are found
$s3->deleteDir('reports/2026');

presignedUrl(string $remoteFile, int $expiration = 3600): ?string

Generates a temporary signed URL for an existing object.

Behavior:

  • clamps expiration to S3 allowed range 1..604800 seconds
  • checks existence first with headObject
  • returns null if object is not available (403/404)
$url = $s3->presignedUrl('reports/2026/report.pdf', 900); // 15 minutes
if ($url) {
    // expose URL to frontend/client
}

validUrl(string $url, int $skew = 30): bool

Validates whether a URL appears still valid.

Supported paths:

  • S3 SigV4 URLs via X-Amz-Date + X-Amz-Expires
  • CloudFront-style signed URLs via Expires
  • fallback HEAD check for generic URLs
if (!$s3->validUrl($url)) {
    $url = $s3->presignedUrl('reports/2026/report.pdf', 900);
}

rawClient(): Aws\S3\S3Client

Returns underlying AWS client for advanced operations.

$client = $s3->rawClient();

End-to-end examples

Upload + presigned URL

use Pair\Services\AmazonS3;

$s3 = new AmazonS3($key, $secret, 'eu-west-1', 'my-bucket');

$s3->put(APPLICATION_PATH . '/tmp/invoice.pdf', 'invoices/2026/INV-1001.pdf');

$url = $s3->presignedUrl('invoices/2026/INV-1001.pdf', 600);

Safe download with existence check

$key = 'invoices/2026/INV-1001.pdf';
$target = APPLICATION_PATH . '/tmp/INV-1001.pdf';

if ($s3->exists($key)) {
    $s3->get($key, $target);
}

Integrate with Upload

use Pair\Helpers\Upload;

$upload = new Upload('attachment');
$upload->saveS3('tickets/42/attachment.pdf', $s3);

Cleanup old prefix

$removed = $s3->deleteDir('exports/legacy/');
if ($removed) {
    // at least one object deleted
}

Advanced use with raw client

$client = $s3->rawClient();

$result = $client->listObjectsV2([
    'Bucket' => $s3->bucket(),
    'Prefix' => 'reports/2026/',
    'MaxKeys' => 50,
]);

$keys = array_map(
    static fn(array $row): string => (string)$row['Key'],
    $result['Contents'] ?? []
);

Error handling pattern

AmazonS3 may throw:

  • PairException (constructor dependency/init failures, some exists() errors)
  • Exception for operation failures (put, read, get, delete, deleteDir, presignedUrl)

Recommended boundary pattern:

use Pair\Exceptions\PairException;

try {
    $s3->put($localPath, $remoteKey);
} catch (PairException $e) {
    // dependency/config/bootstrap failure
    throw $e;
} catch (\Throwable $e) {
    // runtime operation failure
    throw $e;
}

Operational notes and limitations

  • read() loads full object body in memory; avoid it for very large files.
  • get() internally uses read(), so it is also memory-heavy for large objects.
  • deleteDir() currently uses one listObjectsV2 call and does not iterate continuation tokens. For very large prefixes, use rawClient() pagination.
  • validUrl() fallback HEAD check can return false for private resources that are otherwise valid.
  • Presigned URLs are time-bound and should not be stored long term.

Security recommendations

  • Use IAM credentials with least privilege for the target bucket/prefix.
  • Keep credentials only in backend .env / secret manager.
  • Never expose AWS secret keys in frontend code.
  • Keep presigned URL expiration short for sensitive files.

Related pages

Clone this wiki locally