vadage / presigned-uploader-bundle
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
Requires
- php: >=8.2
- ext-fileinfo: *
- async-aws/core: ^1.27
- async-aws/s3: ^3.5
- psr/clock: ^1.0
- psr/container: ^2.0
- psr/log: ^3.0
- symfony/clock: ^7.4 || ^8.0
- symfony/config: ^7.4 || ^8.0
- symfony/dependency-injection: ^7.4 || ^8.0
- symfony/event-dispatcher-contracts: ^3.6
- symfony/framework-bundle: ^7.4 || ^8.0
- symfony/http-foundation: ^7.4 || ^8.0
- symfony/http-kernel: ^7.4 || ^8.0
- symfony/mime: ^7.4 || ^8.0
- symfony/routing: ^7.4 || ^8.0
- symfony/uid: ^7.4 || ^8.0
- symfony/validator: ^7.4 || ^8.0
Requires (Dev)
- api-platform/doctrine-orm: ^4.4 || ^5.0
- api-platform/graphql: ^4.4 || ^5.0
- api-platform/symfony: ^4.4 || ^5.0
- doctrine/dbal: ^4.3
- doctrine/doctrine-bundle: ^2.13 || ^3.0
- doctrine/orm: ^3.3
- doctrine/persistence: ^3.1 || ^4.0
- friendsofphp/php-cs-fixer: ^3.95
- league/flysystem: ^3.29
- league/flysystem-memory: ^3.29
- phpstan/extension-installer: ^1.4
- phpstan/phpstan: ^2.3
- phpstan/phpstan-doctrine: ^2.0
- phpstan/phpstan-phpunit: ^2.1
- phpstan/phpstan-strict-rules: ^2.1
- phpstan/phpstan-symfony: ^2.1
- phpunit/phpunit: ^11.5 || ^12.4 || ^13.4
- rector/rector: ^2.7
- symfony/browser-kit: ^7.4 || ^8.0
- symfony/console: ^7.4 || ^8.0
- symfony/css-selector: ^7.4 || ^8.0
- symfony/expression-language: ^7.4 || ^8.0
- symfony/form: ^7.4 || ^8.0
- symfony/http-client: ^7.4 || ^8.0
- symfony/messenger: ^7.4 || ^8.0
- symfony/options-resolver: ^7.4 || ^8.0
- symfony/property-access: ^7.4 || ^8.0
- symfony/remote-event: ^7.4 || ^8.0
- symfony/security-core: ^7.4 || ^8.0
- symfony/security-csrf: ^7.4 || ^8.0
- symfony/serializer: ^7.4 || ^8.0
- symfony/translation: ^7.4 || ^8.0
- symfony/twig-bundle: ^7.4 || ^8.0
- symfony/webhook: ^7.4 || ^8.0
Suggests
- async-aws/async-aws-bundle: To configure the S3 clients
- doctrine/doctrine-bundle: To store pending uploads and map StoredObject columns (with doctrine/orm)
- league/flysystem-bundle: To read, write and delete objects through Flysystem, and to promote uploads into non-S3 storages
- symfony/console: For the cleanup command
- symfony/expression-language: For the "security" option of #[UploadableField] (with symfony/security-bundle)
- symfony/form: For PresignedUploadType
- symfony/security-csrf: To require CSRF tokens on the upload endpoints
- symfony/serializer: To read and write StoredObject properties in APIs, e.g. with API Platform
- symfony/stimulus-bundle: For the upload Stimulus controller
- symfony/translation: To translate validation and widget messages (English, German and French are included)
- symfony/twig-bundle: To render the upload form widget
- symfony/webhook: To receive storage event notifications (with symfony/remote-event and symfony/messenger)
Provides
None
Conflicts
- api-platform/core: <4.4
- api-platform/json-schema: <4.4
- api-platform/metadata: <4.4
- doctrine/dbal: <4.3
- doctrine/doctrine-bundle: <2.13
- doctrine/orm: <3.3
- league/flysystem: <3.29
- symfony/console: <7.4
- symfony/expression-language: <7.4
- symfony/form: <7.4
- symfony/messenger: <7.4
- symfony/remote-event: <7.4
- symfony/security-core: <7.4
- symfony/security-csrf: <7.4
- symfony/serializer: <7.4
- symfony/translation: <7.4
- symfony/twig-bundle: <7.4
- symfony/webhook: <7.4
Replaces
None
README
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
- Installation
- Mapping and validation
- Forms and the Stimulus controller
- JavaScript client and HTTP endpoints
- APIs and API Platform
- Storages, staging and Flysystem
- Upload lifecycle, transactions and cleanup
- Storage events and webhooks
- Security
- Events and extension points
- Translations
- Configuration reference
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.