I am currently working on a PHP v5.5.9 project in which I am using some standalone components (I do not use the full-stack framework), currently the following:
psr/log 1.0.0 Common interface for logging libraries
sonata-project/intl-bundle 2.2.1 Symfony SonataIntlBundle
symfony/debug v2.4.2 Symfony Debug Component
symfony/dependency-injection v2.4.2 Symfony DependencyInjection Component
symfony/event-dispatcher v2.4.2 Symfony EventDispatcher Component
symfony/form v2.4.2 Symfony Form Component
symfony/http-foundation v2.4.2 Symfony HttpFoundation Component
symfony/http-kernel v2.4.2 Symfony HttpKernel Component
symfony/icu v1.2.0 Contains an excerpt of the ICU data and classes to load it.
symfony/intl v2.4.2 A PHP replacement layer for the C intl extension that includes additional data from the ICU library.
symfony/locale v2.4.2 Symfony Locale Component
symfony/options-resolver v2.4.2 Symfony OptionsResolver Component
symfony/property-access v2.4.2 Symfony PropertyAccess Component
symfony/security-core v2.4.2 Symfony Security Component - Core Library
symfony/security-csrf v2.4.2 Symfony Security Component - CSRF Library
symfony/templating v2.4.2 Symfony Templating Component
symfony/translation v2.4.2 Symfony Translation Component
symfony/twig-bridge v2.4.2 Symfony Twig Bridge
symfony/validator v2.4.2 Symfony Validator Component
twig/extensions v1.0.1 Common additional features for Twig that do not directly belong in core
twig/twig v1.15.1 Twig, the flexible, fast, and secure template language for PHP
I have one Entity class, one FormType class and one Controller and I am able to persist the Entity via a "Twig" template View. So the full stack is working correctly.
Then I added a File
attribute to my Entity, in order to add an image file as described in the official documentation]1. I've also extended my FormType with the following code:
$builder->add('pictureFile', 'file', array('label' => 'Image File'));
After running the updated code (with an image file selected), the following error is shown:
Catchable fatal error: Argument 1 passed to MyEntity::setPictureFile() must be an instance of Symfony\Component\HttpFoundation\File\File, array given, called in [.]\vendor\symfony\property-access\Symfony\Component\PropertyAccess\PropertyAccessor.php on line [.] and defined in MyEntity.php on line [.]
So, Symfony does not automatically create a UploadedFile
from the form, but binds the $_FILE
data to an array
. I've noticed, that other people do also have this problem:
At the moment I am using the following code in my Controller, but I would like to use the auto-binding feature (because at the moment I cannot validate the uploaded file in the Entity).
$formType = new MyType;
$entity = new MyEntity;
$form = $this->formFactory->create($formType, $entity);
$form->handleRequest();
if (true === $form->isSubmitted()) {
if (true === $form->isValid()) {
// TODO Replace ugly workaround to partially solve the problem of the file upload.
$request = Request::createFromGlobals();
$file = $request->files->get('myentity')['pictureFile'];
// Persist entity, upload file to server and redirect.
}
}
// Load View template.
Edit (2014-03-03): Here's the relevant code of the Entity:
<?php
use Symfony\Component\HttpFoundation\File\File;
use Symfony\Component\Validator\Constraints as Assert;
use Symfony\Component\Validator\Mapping\ClassMetadata;
class MyEntity
{
private $id;
private $name;
private $picture;
public function getId()
{
return $this->id;
}
public function setId($id)
{
$this->id = $id;
}
public function getName()
{
return $this->name;
}
public function setName($name)
{
$this->name = $name;
}
public function getPictureFile()
{
return $this->pictureFile;
}
public function setPictureFile(File $pictureFile = null)
{
$this->pictureFile = $pictureFile;
}
public static function loadValidatorMetadata(ClassMetadata $metadata)
{
$metadata->addPropertyConstraint(
'name',
new Assert\NotBlank
);
$metadata->addPropertyConstraint(
'name',
new Assert\Type('string'));
$metadata->addPropertyConstraint(
'pictureFile',
new Assert\Image(
array(
'maxSize' => '2048k',
'minWidth' => 640,
'maxWidth' => 4096,
'minHeight' => 480,
'maxHeight' => 4096
)
)
);
}
}
And the source code of the FormType:
<?php
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text')
->add(
'pictureFile',
'file',
array('label' => 'Image File')
)
->add('submit', 'submit', array('label' => 'Save'));
}
public function getName()
{
return 'myentity';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => __NAMESPACE__ . '\MyEntity'));
}
}
Any suggestions how-to get this work?
Edit (2014-03-07): Simplified source code in preparation for my answer.
I've found a solution for the problem described in the question. Since this seems to be a problem many people have, I describe my solution in this answer.
During my research I've come across a issue on GitHub, which contains a dummy DataTransformer.
After reading the content of that page, I came up with the idea to try it with a custom DataTransformer which converts the array
into an instance of UploadedFile
.
Here is the step-by-step solution:
Create a DataTransformer named UploadedFileTransformer
with the following source code:
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\TransformationFailedException;
use Symfony\Component\HttpFoundation\File\Exception\FileNotFoundException;
use Symfony\Component\HttpFoundation\File\UploadedFile;
class UploadedFileTransformer implements DataTransformerInterface
{
/**
* {@inheritdoc}
*
* @param array $data The array to transform to an uploaded file.
*
* @return \Symfony\Component\HttpFoundation\File\UploadedFile|null The
* uploaded file or `null` if no file has been uploaded.
*/
public function reverseTransform($data)
{
if (!$data) {
return null;
}
$path = $data['tmp_name'];
$pathinfo = pathinfo($path);
$basename = $pathinfo['basename'];
try {
$uploadedFile = new UploadedFile(
$path,
$basename,
$data['type'],
$data['size'],
$data['error']
);
} catch (FileNotFoundException $ex) {
throw new TransformationFailedException($ex->getMessage());
}
return $uploadedFile;
}
/**
* {@inheritdoc}
*
* @param \Symfony\Component\HttpFoundation\File\UploadedFile|null $file The
* uploaded file to transform to an `array`.
*
* @return \Symfony\Component\HttpFoundation\File\UploadedFile|null The
* argument `$file`.
*/
public function transform($file)
{
return $file;
}
}
Update the FormType to use the UploadedFileTransformer
for a file
type input field (compare with the code in the question):
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;
class MyType extends AbstractType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
$builder->add('name', 'text')
->add(
$builder->create(
'pictureFile',
'file',
array('label' => 'Image File')
)->addModelTransformer(new \UploadedFileTransformer)
)
->add('submit', 'submit', array('label' => 'Save'));
}
public function getName()
{
return 'myentity';
}
public function setDefaultOptions(OptionsResolverInterface $resolver)
{
$resolver->setDefaults(array('data_class' => __NAMESPACE__ . '\MyEntity'));
}
}
Remove crap from the Controller, in my example the access to the Request
object to obtain the uploaded file (compare with the code in the question).
$formType = new MyType;
$entity = new MyEntity;
$form = $this->formFactory->create($formType, $entity);
$form->handleRequest();
if (true === $form->isSubmitted()) {
if (true === $form->isValid()) {
$file = $entity->getPictureFile();
// Persist entity, upload file to server and redirect.
}
}
// Load View template.
And that's it. No fiddling with attributes in the FormType and the validation in the Entity is also working.
I really don't know, why this isn't the default behavior of the Symfony components? Maybe I missed to register something in my startup code?