I’m migrating a legacy PHP application to Symfony, and that includes implementing file uploads to AWS S3. Symfony does not provide this feature out of the box, and many of the existing tutorials are outdated or do not work well, therefore I decided to document this in the hope that it will be useful to someone else.
To implement this, we will need to install four packages:
aws/aws-sdk-php
: the AWS PHP SDK.oneup/flysystem-bundle
: a bundle for Flysystem (league/flysystem
), a filesystem abstraction library.league/flysystem-aws-s3-v3
: a Flysystem adapter for AWS SDK.vich/uploader-bundle
: a bundle that eases file uploads that are attached to ORM entities.
These packages can be easily installed with Composer:
composer require aws/aws-sdk-php oneup/flysystem-bundle league/flysystem-aws-s3-v3 vich/uploader-bundle
Next, we need to add to AppKernel.php
our newly installed bundles:
<?php
class AppKernel extends Kernel
{
public function registerBundles()
{
$bundles = [
// other bundles...
new Vich\UploaderBundle\VichUploaderBundle(),
new Oneup\FlysystemBundle\OneupFlysystemBundle(),
new AppBundle\AppBundle()
];
// rest of the code
}
}
Now, we will start setting up the bundles on the configuration file app/config.yml
.
First, we will setup the Flysystem bundle. This bundle will expose a set of adapters and filesystems for the application to use. The configuration below creates an adapter called assets_adapter
and a filesystem called assets_fs
, which in turn uses the adapter assets_adapter
. The built-in awss3v3
Flysystem adapter takes two arguments: a service that’s nothing more than an instance of the AWS S3 SDK and the AWS bucket name (in this case, from the parameters.yml
variable called assets_bucket
).
oneup_flysystem:
adapters:
assets_adapter:
awss3v3:
client: app.assets.s3
bucket: %assets_bucket%
prefix: assets
filesystems:
assets_fs:
adapter: assets_adapter
mount: assets_fs
Next, we will setup the VichUploaderBundle
. The configuration below tells VichUploaderBundle
to use the orm
driver, which integrates with Doctrine, and to use the Flysystem storage adapter. It also creates a mapping, which will be hooked into our entity. A mapping tells VichUploaderBundle
where to upload a file, how to generate its URI, and what naming strategy to use. In this case, we are supplying a user defined URI prefix, which is the S3 bucket URI (it’s defined in parameters.yml
); we are telling it to upload the file the the assets_fs
filesystem; and we are telling it to use the naming strategy that generates unique IDs for each file.
vich_uploader:
db_driver: orm
storage: flysystem
mappings:
assets:
uri_prefix: %assets_uri%
upload_destination: assets_fs
namer: vich_uploader.namer_uniqid
Finally, on app/services.yml
, we will create the AWS S3 service that will be used to upload the files to S3. The configuration below defines a service named app.assets.s3
that is an instance of the Aws\S3\S3Client
class, which takes the arguments defined below. These arguments are best stored in the parameters.yml
file.
services:
app.assets.s3:
class: Aws\S3\S3Client
arguments:
-
version: 'latest'
region: %assets_region%
credentials:
key: %assets_key%
secret: %assets_secret%
Good! Now it’s time to plug everything into our model.
Let us suppose we have an entity called Product
, which represents a… product, ta-da! First, we will start by adding the @Vich\Uploadable
annotation to the class, which allows it to upload the files. Then, we will add two properties: $image
, which represents an image that was uploaded, and $imageFile
, which represents a image that may be uploaded. Finally, we will also add a few setters and getters. The complete class can be seen below:
<?php
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
use Symfony\Component\HttpFoundation\File\File;
use Vich\UploaderBundle\Mapping\Annotation as Vich;
/**
* Product
*
* @ORM\Table(name="product")
* @Vich\Uploadable
*/
class Product
{
/**
* @var integer
*
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="IDENTITY")
*/
protected $id;
/**
* @var string
*
* @ORM\Column(name="name", type="string", length=64, nullable=false)
*/
protected $name;
/**
* @ORM\Column(name="image", type="string", length=255, nullable=true)
*
* @var string
*/
protected $image = null;
/**
* @Vich\UploadableField(mapping="assets", fileNameProperty="image")
*
* @var File
*/
protected $imageFile;
/**
* Get id
*
* @return integer
*/
public function getId()
{
return $this->id;
}
/**
* Set name
*
* @param string $name
* @return Category
*/
public function setName($name)
{
$this->name = $name;
return $this;
}
/**
* Get name
*
* @return string
*/
public function getName()
{
return $this->name;
}
/**
* @param string $image
*
* @return Category
*/
public function setImage($image)
{
$this->image = $image;
return $this;
}
/**
* @return string|null
*/
public function getImage()
{
return $this->image;
}
/**
* If manually uploading a file (i.e. not using Symfony Form) ensure an instance
* of 'UploadedFile' is injected into this setter to trigger the update. If this
* bundle's configuration parameter 'inject_on_load' is set to 'true' this setter
* must be able to accept an instance of 'File' as the bundle will inject one here
* during Doctrine hydration.
*
* @param File|\Symfony\Component\HttpFoundation\File\UploadedFile $image
*
* @return Product
*/
public function setImageFile(File $image = null)
{
$this->imageFile = $image;
return $this;
}
/**
* Returns the image to be uploaded.
*
* @return File|null
*/
public function getImageFile()
{
return $this->imageFile;
}
}
Note that the $imageFile
property has the following annotation:
/**
* @Vich\UploadableField(mapping="assets", fileNameProperty="image")
*/
This annotation tells VichUploaderBundle
to upload the file using the assets
mapping we defined earlier, and to use the $image
property to hold the details of the uploaded file.
Now, how do we upload an image? We add an image field to the product creation form. VichUploaderBundle
provides a special form type VichImageType
that automatically handles the file download, file preview and deletion. We simply add it to our form and use the $imageFile
property - remember, this is the property that handles the image upload.
<?php
namespace AppBundle\Form\Type;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\{
AbstractType,
FormBuilderInterface,
};
use Symfony\Component\Form\Extension\Core\Type\{
SubmitType,
TextType
};
use Symfony\Component\OptionsResolver\OptionsResolver;
use Vich\UploaderBundle\Form\Type\VichImageType;
/**
* Form used to create products.
*/
class ProductCreateType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder
->add('name', TextType::class, [
'label' => 'Nome'
])
->add('imageFile', VichImageType::class, [
'label' => 'Image'
, 'required' => false
])
->add('save', SubmitType::class, [
'label' => 'Save'
]);
}
public function configureOptions(OptionsResolver $resolver)
{
$resolver->setDefaults([
'data_class' => 'AppBundle\Entity\Product'
]);
}
}
What about our controller? What do we need to change? Fortunately, we don’t need to change anything: VichUploaderBundle
is well implemented, and uploading is done behind the scenes when the forms are processed.
That’s all. You’re ready to start uploading. ;)
Notes
You probably do not want to upload your files to S3 on your development and test environments (maybe you do, but that’s not very smart, lol). You can get over this by using a local filesystem on dev and a in-memory filesystem on test.
// config_test.yml
oneup_flysystem:
adapters:
local_adapter:
memory: ~
vich_uploader:
db_driver: orm
storage: flysystem
mappings:
assets:
upload_destination: local_fs
namer: vich_uploader.namer_uniqid
// config_dev.yml
oneup_flysystem:
adapters:
local_adapter:
local:
directory: %kernel.root_dir%/../web/uploads
filesystems:
local_fs:
adapter: local_adapter
mount: local_fs
vich_uploader:
db_driver: orm
storage: flysystem
mappings:
assets:
uri_prefix: /uploads
upload_destination: local_fs
namer: vich_uploader.namer_uniqid