devture / symfony-storer-bundle
Symfony bundle that lets you deal with uploaded files in a relatively sane and storage-provider-independent way
Installs: 58
Dependents: 0
Suggesters: 0
Security: 0
Stars: 0
Watchers: 3
Forks: 0
Open Issues: 0
Type:symfony-bundle
Requires
- php: >=7.1
- dflydev/apache-mime-types: 1.0.*
- doctrine/orm: ^2.6
- ramsey/uuid: 3.8.*
Suggests
- aws/aws-sdk-php: Allows using Amazon S3 or Minio storage
- imagine/imagine: To use the image-validation constraints
- microsoft/azure-storage-blob: Allows using Azure Blob Storage
- symfony/validator: To use the validation constraints
This package is auto-updated.
Last update: 2024-10-29 05:46:55 UTC
README
A Symfony bundle that lets you deal with uploaded files in a relatively sane and storage-provider-independent way.
The Gaufrette library is used for storage abstraction.
Currently, the following storage adapters can be used through the bundle:
- the local filesystem
- Amazon S3 or Minio
- Azure Blob storage
With minor code changes, all other Gaufrette-supported adapters (see Helper/AdapterFactory.php
) could be made to work.
Pull requests are welcome!
Installation
Install through composer (composer require --dev devture/symfony-web-command-bundle
).
Add to config/bundles.php
:
Devture\Bundle\StorerBundle\DevtureStorerBundle::class => ['all' => true],
Depending on the storage provider that you'd like to use, you may need to install additional libraries (e.g. aws/aws-sdk-php
, etc.)
Configuration
Drop the following config in config/packages/devture_storer.yaml
devture_storer: adapter_url: '%env(resolve:DEVTURE_STORER_ADAPTER_URL)%' validation_max_size_megabytes: 20
You then need to define an environment variable DEVTURE_STORER_ADAPTER_URL
, which would specify which storage adapter you'd like to use.
Example (.env
):
# Local and remote adapters are supported.
#
# Remote URLs usually require that their secrets be urlencoded and percentage-sign-escaped (`/` turned to `%%2F`).
# Reason: `/` found in secrets breaks URL-parsing, so we urlencode it. Symfony thinks % is a parameter, so we escape it (as %%).
#
# Example of S3-compatible Minio URL:
# DEVTURE_STORER_ADAPTER_URL=s3://access.key:key%%2Fwith%%2Furlencoded%%2Fslashes@us-east-1.localhost:9000/bucket.name
#
# Example of actual Amazon S3 URL:
# DEVTURE_STORER_ADAPTER_URL=s3://access.key:key%%2Fwith%%2Furlencoded%%2Fslashes@ap-northeast-1.s3.amazonaws.com/bucket.name
#
# Example of actual Azure Blob URL:
# DEVTURE_STORER_ADAPTER_URL=azure-blob://account-name:account-key%%2Fwith%%2Furlencoded%%2Fslashes@account.blob.core.windows.net/container
DEVTURE_STORER_ADAPTER_URL=file://%kernel.project_dir%/var/devture-storer
You also need to register a Doctrine type like this (in config/doctrine.yaml
):
doctrine: dbal: # Other stuff here ... types: devture_storer.file: Devture\Bundle\StorerBundle\Doctrine\StorerFileType
Usage example
Entity
/** * @ORM\Table(name="user") */ class User implements \Devture\Bundle\StorerBundle\Entity\StorerFilesContainerInterface { /** * @ORM\Id * @ORM\Column(type="integer") * @ORM\GeneratedValue(strategy="AUTO") */ private $id; /** * @ORM\Column(type="devture_storer.file", length=191, nullable=true) * @Devture\Bundle\StorerBundle\Validator\Constraints\Image() * @Devture\Bundle\StorerBundle\Validator\Constraints\MaximumSize(maxSizeMegabytes=10) */ private $photo; public function getPhoto(): ?\Devture\Bundle\StorerBundle\Entity\FileInterface { return $this->photo; } public function setPhoto(?\Devture\Bundle\StorerBundle\Entity\FileInterface $photo) { $this->photo = $photo; } /** * {@inheritDoc} * @see \Devture\Bundle\StorerBundle\Entity\StorerFilesContainerInterface::getContainedStorerFiles() */ public function getContainedStorerFiles(): array { $files = []; if ($this->photo !== null) { $files[] = $this->photo; } return $files; } }
Form Type
<?php namespace App\UserBundle\Form\Type; use Symfony\Component\Form\Extension\Core\Type\FileType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\Form\CallbackTransformer; use Symfony\Component\HttpFoundation\File\UploadedFile; use Devture\Bundle\StorerBundle\FileHandle\VolatileLocalFileHandle; class UserType extends AbstractType { public function buildForm(FormBuilderInterface $builder, array $options) { /** @var \App\UserBundle\Entity\User $entity */ $entity = $options['data']; $builder->add('photoUpload', FileType::class, [ 'label' => 'Photo', 'mapped' => false, 'required' => false, ]); $builder->get('photoUpload')->addModelTransformer(new CallbackTransformer( function ($whatever): ?\Devture\Bundle\StorerBundle\Entity\FileInterface { // No need to transform "to form". return $whatever; }, function (?UploadedFile $uploadedFile) use ($entity) { if ($uploadedFile === null) { // Nothing new. Not binding anything. return; } $file = new \Devture\Bundle\StorerBundle\Entity\File( \Devture\Bundle\StorerBundle\Util::generateFullStorageKey( 'user/photo', $uploadedFile->getClientOriginalName() ) ); $file->setStorerFileHandle(new VolatileLocalFileHandle($uploadedFile->getPathname())); if ($entity->getPhoto() !== null) { // Save the reference to the old photo, so we can delete it // (happens automatically when the new one is persisted). $file->setPreviousFile($entity->getPhoto()); } $entity->setPhoto($file); } )); } public function getBlockPrefix() { return 'user'; } }
Form theme
{% block _user_photoUpload_widget %} {% set user = form.parent.vars.value %} {% set photo = user.photo %} {% if photo is not none %} {% if photo_url_full is not none %} <img src="{{ photo|user_generate_photo_serving_link }}" class="img-fluid img-thumbnail" /> {% endif %} {# Some delete button could go here #} {% endif %} {{ form_widget(form) }} {% endblock %}
Serving
use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\Routing\Annotation\Route; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use App\UserBundle\Repository\UserRepository; /** * @Route("/user/{userId}/photo", requirements={"userId": "\d+"}) */ class UserPhotoController extends AbstractController { /** * @Route("/serve", name="user.photo.serve", methods={"GET"}) */ public function serve( Request $request, int $userId, UserRepository $userRepository ): Response { /** @var User|null $user */ $user = $userRepository->find($userId); if ($user === null || $user->getPhoto() === null) { throw $this->createNotFoundException('No photo'); } /** @var \Devture\Bundle\StorerBundle\Entity\FileInterface $photo **/ $photo = $user->getPhoto(); return new \Devture\Bundle\StorerBundle\Http\FileResponse($photo->getStorerFileHandle(), 200, [ 'Content-Type' => \Devture\Bundle\StorerBundle\Util::getContentTypeByFileName($photo->getStorerFileKey()), ]); } }
Deletion
$previousFile = $user->getPhoto(); if ($previousFile !== null) { // Setting a new special `NullFile` with a reference to the one we want to delete // (called "previous"), will ensure that `Storer` would delete it. $user->setPhoto(new \Devture\Bundle\StorerBundle\Entity\NullFile($previousFile)); $entityManager->flush($user); }