Search by

vadage / presigned-uploader-bundle

vadage

Presigned direct-to-storage uploads for Symfony, with entity mapping, validation and webhooks

Package info

github.com/vadage/presigned-uploader-bundle

Type:symfony-bundle

pkg:composer/vadage/presigned-uploader-bundle

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.1 2026-10-09 11:37 UTC

This package is auto-updated.

Last update: 2026-10-09 11:39:46 UTC


README

CI Packagist Version PHP Version License

Direct-to-storage uploads for Symfony with presigned URLs. The browser uploads straight to an S3-compatible storage (AWS S3, Cloudflare R2, SeaweedFS, ...) and the file never passes through PHP: your application validates and signs the upload, verifies the stored file, and attaches it to your entity or DTO.

Constraints are declared next to the property and checked before a URL is handed out. Size, content type and an optional SHA-256 checksum are part of the signature, and a presigned URL can only write once. Before a file is attached, it is checked again, including the type detected from its content. With Doctrine, claims and deletions follow your transaction, so a rollback leaves no orphaned files and no referenced file gets deleted.

The bundle comes with a form type and Stimulus controller, a framework-agnostic JavaScript client, and serializer support for APIs (API Platform, #[MapRequestPayload]). It handles multiple storages, staging with promotion on claim, Flysystem targets, storage event webhooks (R2, S3) and the cleanup of unclaimed uploads.

Requirements

  • PHP 8.2 or higher, with ext-fileinfo
  • Symfony 7.4 LTS or 8.x
  • async-aws/s3 (installed with the bundle)
  • Doctrine ORM 3 / DBAL 4 with DoctrineBundle (optional, needed for the default setup)

Installation

composer require vadage/presigned-uploader-bundle async-aws/async-aws-bundle doctrine/orm doctrine/doctrine-bundle

With Symfony Flex, the recipe enables the bundle, imports the routes under /uploads and creates config/packages/vadage_presigned_uploader.yaml. See Installation for setups without Flex (or its recipe), the database tables, the frontend and the bucket's CORS rule.

Configure an S3 client and a storage:

# config/packages/async_aws.yaml
async_aws:
    clients:
        r2:
            type: s3
            config:
                endpoint: 'https://%env(R2_ACCOUNT_ID)%.r2.cloudflarestorage.com'
                region: auto
                pathStyleEndpoint: true
                accessKeyId: '%env(R2_ACCESS_KEY_ID)%'
                accessKeySecret: '%env(R2_SECRET_ACCESS_KEY)%'

# config/packages/vadage_presigned_uploader.yaml
vadage_presigned_uploader:
    storages:
        media:
            client: async_aws.client.r2
            bucket: media

Map a property and declare its constraints:

use Doctrine\ORM\Mapping as ORM;
use Vadage\PresignedUploaderBundle\Attribute\Uploadable;
use Vadage\PresignedUploaderBundle\Attribute\UploadableField;
use Vadage\PresignedUploaderBundle\Doctrine\StoredObjectType;
use Vadage\PresignedUploaderBundle\Model\StoredObject;
use Vadage\PresignedUploaderBundle\Validator\PresignedFile;

#[Uploadable]
#[ORM\Entity]
class User
{
    #[UploadableField(name: 'user_avatar', storage: 'media', prefix: 'avatars/')]
    #[PresignedFile(maxSize: '5M', mimeTypes: ['image/png', 'image/jpeg', 'image/webp'])]
    #[ORM\Column(type: StoredObjectType::NAME, nullable: true)]
    private ?StoredObject $avatar = null;

    // getter and setter
}

Add it to a form. The type guesser picks PresignedUploadType; the Stimulus controller uploads the file and the form submits only the upload id:

$builder->add('avatar');

The upload is verified when the form is submitted, and claimed in the transaction that flushes the entity. Create the database tables, set up the frontend and allow PUT requests in the bucket's CORS rule as described in Installation, and schedule the cleanup command. The upload endpoints are public until you restrict them, see Security.

Documentation

Security issues

If you think you have found a security issue, please do not open a public issue. Follow the security policy instead.

Backward compatibility

From 1.0 on, the bundle follows Semantic Versioning; code marked @internal and the markup of the form theme are not covered. Until then, minor versions may contain breaking changes, listed in the changelog.

AI assistance

The initial version of this bundle was built with heavy assistance from AI coding tools (Claude), directed and reviewed by its author, who set the design, the public API and the scope. Later AI-assisted changes are marked with an Assisted-by: trailer in their commit message, see Contributing.

All code, generated or not, is held to the same checks: PHPStan at level 10 with strict rules, the Symfony coding standard, Rector, and PHPUnit against the supported PHP versions with highest and lowest dependencies and against SQLite, MySQL and PostgreSQL.

The maintainers are responsible for the bundle: bugs and security issues are ours to fix, see the security policy.

License

Released under the MIT License.