-
Notifications
You must be signed in to change notification settings - Fork 2
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
Install AWS SDK v3 in your project:
composer require aws/aws-sdk-php:^3The service validates dependencies at runtime and throws PairException with ErrorCodes::AMAZON_S3_ERROR if missing.
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 exampleeu-west-1) -
bucket: bucket name
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-bucketuse 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')
);Returns configured bucket name.
$bucket = $s3->bucket();Uploads a local file to S3.
Behavior:
- validates local file readability
- streams upload (
fopen(..., 'rb')) - tries MIME detection with
mime_content_type()and setsContentType
$s3->put(APPLICATION_PATH . '/tmp/report.pdf', 'reports/2026/report.pdf');Checks object existence via headObject.
Behavior:
- returns
truewhen object is accessible - returns
falsefor403/404 - throws
PairExceptionfor other S3/AWS errors
if ($s3->exists('reports/2026/report.pdf')) {
// object is available
}Reads remote object and returns full body content as string.
$content = $s3->read('configs/app.json');Downloads remote object to local filesystem.
Behavior:
- returns
falseif object does not exist - returns
truewhen file was written locally
$ok = $s3->get('reports/2026/report.pdf', APPLICATION_PATH . '/tmp/report.pdf');Deletes one object.
Behavior:
- returns
falseif object does not exist - returns
trueif delete call is executed
$deleted = $s3->delete('reports/2026/report.pdf');Deletes objects under a prefix.
Behavior:
- normalizes prefix (
rtrim(..., '/') . '/') - lists objects under prefix
- deletes listed keys with
deleteObjects - returns
falsewhen no objects are found
$s3->deleteDir('reports/2026');Generates a temporary signed URL for an existing object.
Behavior:
- clamps expiration to S3 allowed range
1..604800seconds - checks existence first with
headObject - returns
nullif object is not available (403/404)
$url = $s3->presignedUrl('reports/2026/report.pdf', 900); // 15 minutes
if ($url) {
// expose URL to frontend/client
}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);
}Returns underlying AWS client for advanced operations.
$client = $s3->rawClient();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);$key = 'invoices/2026/INV-1001.pdf';
$target = APPLICATION_PATH . '/tmp/INV-1001.pdf';
if ($s3->exists($key)) {
$s3->get($key, $target);
}use Pair\Helpers\Upload;
$upload = new Upload('attachment');
$upload->saveS3('tickets/42/attachment.pdf', $s3);$removed = $s3->deleteDir('exports/legacy/');
if ($removed) {
// at least one object deleted
}$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'] ?? []
);AmazonS3 may throw:
-
PairException(constructor dependency/init failures, some exists() errors) -
Exceptionfor 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;
}-
read()loads full object body in memory; avoid it for very large files. -
get()internally usesread(), so it is also memory-heavy for large objects. -
deleteDir()currently uses onelistObjectsV2call and does not iterate continuation tokens. For very large prefixes, userawClient()pagination. -
validUrl()fallback HEAD check can returnfalsefor private resources that are otherwise valid. - Presigned URLs are time-bound and should not be stored long term.
- 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.