<?php
declare(strict_types=1);
require_once __DIR__ . '/../../../backend/_lib/Validator.php';
require_once __DIR__ . '/../../../backend/_lib/JsonResponse.php';
require_once __DIR__ . '/../../../backend/_lib/Storage.php';
use DemoBuilder\JsonResponse;
use DemoBuilder\Storage;
use DemoBuilder\Validator;
$file = Validator::file('file')
->maxBytes(10 * 1024 * 1024)
->allowedMimes(['image/jpeg','image/png','image/webp','image/gif','image/svg+xml'])
->allowedExtensions(['jpg','jpeg','png','webp','gif','svg'])
->maxDimensions(8000, 8000)
->validateOrFail();
// ─── BUYER CONFIG ─────────────────────────────────────────────────
$useS3 = (getenv('USE_S3') ?: '1') !== '0';
$s3Config = [
'endpoint' => getenv('S3_ENDPOINT') ?: 'http://localhost:9000',
'bucket' => getenv('S3_BUCKET') ?: 'builderjs-assets',
'region' => getenv('S3_REGION') ?: 'us-east-1',
'key' => getenv('S3_KEY') ?: 'minioadmin',
'secret' => getenv('S3_SECRET') ?: 'minioadmin',
'public_url' => getenv('S3_PUBLIC_URL') ?: 'http://localhost:9000/builderjs-assets',
];
// ─────────────────────────────────────────────────────────────────
$key = sprintf('%s/%s.%s', date('Y/m/d'),
bin2hex(random_bytes(8)), $file['extension']);
$storage = $useS3
? Storage::s3($s3Config)
: Storage::local(__DIR__ . '/uploads', '/examples/backend/2-s3/uploads');
$url = $storage->put($key, $file['tmp_name']);
JsonResponse::success([
'url' => $url,
'key' => $key,
'mime' => $file['mime'] ?? '',
'size' => (int) $file['size'],
]);
Save Assets to S3
Stream uploaded images to any S3-compatible store. Ships with a one-command MinIO docker-compose so dev runs without an AWS account.
Walkthrough
-
Spin up MinIO
One-command local stack — MinIO at
:9000(S3 API) +:9001(web console). The companionbucket-createservice provisionsbuilderjs-assetson first run, so your first upload lands in a bucket that already exists. Login:minioadmin / minioadmin.Bashcd demo/examples/backend/2-s3 docker-compose up -d # → Bucket builderjs-assets ready. # Verify: curl -s http://localhost:9001 | grep -o '<title>.*</title>' # → <title>MinIO Console</title> -
Upload via the SigV4-signed handler
The handler hashes the body, signs an AWS SigV4 PUT, and streams to your bucket. No
composer install. The SigV4 signing lives inbackend/_lib/Storage.php— buyers who already pullaws/aws-sdk-phpcan replace one method and keep everything else.Bashcurl -F file=@/path/to/logo.png \ http://localhost:8000/examples/backend/2-s3/s3-upload.php # → {"status":"success", # "url":"http://localhost:9000/builderjs-assets/2026/05/07/<hash>.png", # "key":"2026/05/07/<hash>.png", # "mime":"image/png","size":12345} -
Wire the builder
In
demo/builder.php, change one line under theendpointsblock:endpoints.assetUpload→ your S3 upload handler URL. Reload the builder; every image upload now lands in your bucket. The returnedurlis what the canvas references — same shape as the W2 handler, just hosted somewhere else.PHP// demo/builder.php — top of file $builderConfig = [ 'themesRoot' => __DIR__ . '/themes', 'endpoints' => [ 'save' => '/backend/save.php', 'assetUpload' => '/examples/backend/2-s3/s3-upload.php', // ← here // … ], // … ]; -
Swap MinIO for any S3-compatible store
Same handler, six env vars. AWS S3 → real region + bucket; Backblaze B2 → their endpoint URL; Cloudflare R2 → R2 endpoint. The handler reads the env vars first, then falls back to MinIO defaults — production deploy is one
setenvstep.Bash# AWS S3 export S3_ENDPOINT='https://s3.us-east-1.amazonaws.com' export S3_BUCKET='your-bucket' export S3_REGION='us-east-1' export S3_KEY='AKIA…' export S3_SECRET='…' export S3_PUBLIC_URL='https://your-bucket.s3.amazonaws.com' # Cloudflare R2 (drop-in) export S3_ENDPOINT='https://<account-id>.r2.cloudflarestorage.com' export S3_REGION='auto' export S3_PUBLIC_URL='https://your-public-domain.com'
The whole snippet
Click Copy on any file to grab it byte-identical, or Expand to read inline (capped at ~520 px so the page stays scannable). Multi-file snippets surface a side-by-side grid — copy only the files you need.
services:
minio:
image: minio/minio:latest
container_name: builderjs-minio
ports:
- "9000:9000" # S3 API
- "9001:9001" # console
environment:
MINIO_ROOT_USER: minioadmin
MINIO_ROOT_PASSWORD: minioadmin
command: server /data --console-address ":9001"
volumes:
- minio-data:/data
healthcheck:
test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
interval: 5s
timeout: 3s
retries: 5
bucket-create:
image: minio/mc:latest
depends_on:
minio:
condition: service_healthy
entrypoint: >
/bin/sh -c "
mc alias set local http://minio:9000 minioadmin minioadmin &&
mc mb --ignore-existing local/builderjs-assets &&
mc anonymous set download local/builderjs-assets
"
volumes:
minio-data:
Notes
No composer dependency. The SigV4 signing lives in _lib/Storage.php — pure PHP + cURL. Total cost: ~30 lines, fully audited. If you'd rather use aws/aws-sdk-php, replace S3StorageDriver::put() with $client->putObject([...]); the surrounding handler is contract-stable.
Skip Docker? Set USE_S3=0 and the handler falls back to local-disk under uploads/. Same JSON response shape; the URL becomes /examples/backend/2-s3/uploads/<key> served by your PHP host.
S3-compatible matrix.
- AWS S3.
S3_ENDPOINT='https://s3.us-east-1.amazonaws.com'; rotate IAM keys per the AWS IAM best practices. - Cloudflare R2.
S3_ENDPOINT='https://<account-id>.r2.cloudflarestorage.com',S3_REGION='auto'. Public URL = your custom-domain or the public R2 bucket URL. - Backblaze B2.
S3_ENDPOINT='https://s3.<region>.backblazeb2.com'. The bucket-id-as-region quirk doesn't apply to the S3-compatible API. - Wasabi / DigitalOcean Spaces / Linode Object Storage. All speak S3; only the endpoint URL changes.
Production hardening. Enable bucket-level encryption at rest, lock the bucket policy to deny public PUT, and rotate access keys with short-lived STS credentials in production. The public_url config is the one your CDN serves to end-users — never echo the signed PUT endpoint to the canvas.
Deletion. The example only ships put(). S3StorageDriver is intentionally minimal — buyers who need delete() + list() can extend the driver class with two more SigV4-signed methods following the same pattern, or swap to the AWS SDK in one method.