Despues de romperme la cabeza buscando entre infinidad de paginas y lectura de la escueta documentacion de Symfony para este tema, asi como probar bundles que me añadian demasiada complejidad para lo que necesitaba, he encontrado la solucion a la subida multiple de ficheros en Symfony 2.
Empezamos ¡¡
La entity
Comenzamos creando una entidad llamada Album la cual contiene lo siguiente:
namespace AppBundle\Entity;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity
* @ORM\Table(name="album")
*/
class Album
{
/**
* @ORM\Column(name="id", type="integer")
* @ORM\Id
* @ORM\GeneratedValue(strategy="AUTO")
*/
protected $id;
/**
* @ORM\Column(type="string", length=250, nullable=true)
*/
protected $name;
/**
* @var array
*
* @ORM\Column(name="images", type="array", nullable=true)
*/
protected $picture;
Esta entidad contiene una columna que sera un array de los nombres de las imagenes que vamos a subir mediante el formulario. Deberemos generar los metodos set y get con el comando:
>php app/console doctrine:generate:entities AppBundle –no-backup
El parametro –no-backup es para que no genere clases temporales que luego tengamos que borrar a mano.
El FormType
Nuestro formulario sera de la siguiente forma
<?php namespace AppBundle\Form; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolverInterface; class AlbumType extends AbstractType { /** * @param FormBuilderInterface $builder * @param array $options */ public function buildForm(FormBuilderInterface $builder, array $options) { $builder ->add('name') ->add('picture', 'file', array( 'attr' => array( 'accept' => 'image/*', 'multiple' => 'multiple' ), 'data_class' => null ) ); } /** * @param OptionsResolverInterface $resolver */ public function setDefaultOptions(OptionsResolverInterface $resolver) { $resolver->setDefaults(array( 'data_class' => 'AppBundle\Entity\Album' )); } /** * @return string */ public function getName() { return 'appbundle_album'; } }
Creamos el formulario con los campos de nuestra Entity y le especificamos que el atributo picture(nuestro array con los nombres de las imágenes) es un tipo File y acepta multiples ficheros seleccionados en el cuadro de dialogo. En mi caso, Symfony me tiraba un error de que necesitaba especificar el ‘data_class’ a null, así que le hice caso sin más y funcionó a la primera.
El controlador
Aunque nuestro controlador tenga muchas cosas, lo pongo como ejemplo, debería refactorizarse y separar la lógica en funciones mas pequeñas.
/** * @Route("/album-uploader", name="album-uploader") */ public function createAction(Request $request) { $album = new Album(); $form = $this->createForm(new AlbumType(), $album); $form->handleRequest($request); if ($form->isValid()) { // Handle the uploaded images $files = $form->getData()->getPicture(); // If there are images uploaded if($files != null) { $constraints = array('maxSize'=>'10M', 'mimeTypes' => array('image/*')); $uploadFiles = $this->get('app.fileuploader')->create($files, $constraints); if($uploadFiles->upload()) { $album->setPicture($uploadFiles->getFilePaths()); $em = $this->getDoctrine()->getEntityManager(); $em->persist($album); $em->flush(); $this->get('session')->getFlashBag()->add('notice', 'Las imagenes se han subido con éxito.'); } // If there are file constraint validation issues else { // Check for errors foreach($uploadFiles->getErrors() as $error) { $this->get('session')->getFlashBag()->add('error', $error); } return $this->render('AppBundle:AlbumOld:uploadAlbum.html.twig', array( 'entity' => $album, 'form' => $form->createView(), )); } } } return $this->render('AppBundle:AlbumOld:uploadAlbum.html.twig', array( 'form' => $form->createView() )); // ... persist, flush, success message, redirect, other functionality }
Como has observado en color rojo, hemos usado un servicio para la subida de los ficheros. A continuación explico como crearlo.
Configuración del servicio
Para ello creamos la carpeta «config» dentro de «Resources» y dentro de ella creamos el fichero services.xml con la siguiente configuración, y le inyectamos el EntityManager, el RequestStack, Validator y Kernel:
<?xml version="1.0" ?> <container xmlns="http://symfony.com/schema/dic/services" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd"> <services> <service id="app.fileuploader" class="AppBundle\Service\FileUploader"> <argument type="service" id="doctrine.orm.entity_manager" /> <argument type="service" id="request_stack" /> <argument type="service" id="validator" /> <argument type="service" id="kernel" /> </service> </services> </container>
En YAML seria algo parecido a esto:
services: your_namespace.fileuploader: class: Namespace\YourBundle\Services\FileUploader arguments: [ @doctrine.orm.entity_manager, @request_stack, @validator, @kernel ]
Una vez tenemos el servicio creado, debemos crear la clase que atenderá a ese servicio.
Creando la clase FileUploader del Servicio
<?php
namespace AppBundle\Service;
use Symfony\Component\HttpFoundation\File\UploadedFile;
use Symfony\Component\HttpFoundation\RequestStack;
use Symfony\Component\HttpFoundation\File\Exception\FileException;
use Symfony\Component\Validator\Constraints\File;
use Doctrine\ORM\EntityManager;
use Symfony\Component\Validator\Validator;
use Symfony\Component\HttpKernel\Kernel;
class FileUploader
{
// Entity Manager
private $em;
// The request
private $request;
// Validator Service
private $validator;
// Kernel
private $kernel;
// The files from the upload
private $files;
// Directory for the uploads
private $directory;
// File pathes array
private $paths;
// Constraint array
private $constraints;
// Array of file constraint object
private $fileConstraints;
// Error array
private $errors;
public function __construct(EntityManager $em, RequestStack $requestStack, Validator\ValidatorInterface $validator, Kernel $kernel)
{
$this->em = $em;
$this->request = $requestStack->getCurrentRequest();
$this->validator = $validator;
$this->kernel = $kernel;
$this->directory = 'web/bundles/pictures';
$this->paths = array();
$this->errors = array();
}
// Create FileUploader object with constraints
public function create($files, $constraints = NULL)
{
$this->files = $files;
$this->constraints = $constraints;
if($this->constraints)
{
$this->fileConstraints = $this->createFileConstraint($this->constraints);
}
return $this;
}
// Upload the file / handle errors
// Returns boolean
public function upload()
{
if(!$this->files)
{
return true;
}
/** @var UploadedFile $file */
foreach($this->files as $file)
{
if( isset($file) )
{
if($this->fileConstraints)
{
$this->errors[] = $this->validator->validateValue($file, $this->fileConstraints);
}
$fileName = $file->getClientOriginalName();
$this->paths[] = $fileName;
if(!$this->hasErrors())
{
$file->move($this->getUploadRootDir(), $fileName);
}
else
{
foreach($this->paths as $path)
{
$fullpath = $this->kernel->getRootDir() . '/../' . $path;
if(file_exists($fullpath))
{
unlink($fullpath);
}
}
$this->paths = null;
return false;
}
}
}
return true;
}
// Get array of relative file paths
public function getFilePaths()
{
return $this->paths;
}
// Get array of error messages
public function getErrors()
{
$errors = array();
foreach($this->errors as $errorListItem)
{
foreach($errorListItem as $error)
{
$errors[] = $error->getMessage();
}
}
return $errors;
}
// Get full file path
private function getUploadRootDir()
{
return $this->kernel->getRootDir() . '/../'. $this->directory;
}
// Create array of file constraint objects
private function createFileConstraint($constraints)
{
$fileConstraints = array();
foreach($constraints as $constraintKey => $constraint)
{
$fileConstraint = new File();
$fileConstraint->$constraintKey = $constraint;
if($constraintKey == "mimeTypes")
{
$fileConstraint->mimeTypesMessage = "The file type you tried to upload is invalid.";
}
$fileConstraints[] = $fileConstraint;
}
return $fileConstraints;
}
// Check if there are constraint violations
private function hasErrors()
{
if(count($this->errors) > 0)
{
foreach($this->errors as $error)
{
if($error->__toString())
{
return true;
}
}
}
return false;
}
}
Esta clase se encarga de guardar en el directorio que le digamos los ficheros.
La vista
Es una vista como otra cualquiera, que contiene el formulario con la particularidad de que debemos añadir [] en el nombre del campo para poder enviarlo como un array de ficheros y ademas, añadirle el form_enctype para que nos añada el atributo enctype=»multipart/form-data» a la etiqueta <form>.
Y voilà ¡¡ Ya podemos seleccionar varios ficheros a la vez y subirlos a nuestra carpeta.
Fuente: http://www.keganv.com/upload-multiple-images-in-symfony2-with-validation-on-a-single-entity-property/
NOTAS
- Puede que nos de error al subir si la carpeta destino no tenga los permisos necesarios para poder escribir. Revisad el usuario:grupo de la carpeta y los permisos.
- Puede que no nos permita subir ficheros de mas de 2M. Para ello solo teneis que cambiar el php.ini, la directiva para que admita un tamaño mayor. En mi caso, lo puse a 10M.
upload_max_filesize = 2M
Y eso es todo. Se que habrán mejores formas y más optimas de hacerlo, pero yo no he encontrado nada que sea más sencillo y rapido que esto. Si tienes alguna sugerencia, no dudes en añadir un comentario para intentar optimizarlo mejor, y que sirva de ayuda a la comunidad.
Saludos ¡
Reblogueó esto en DominandoPHPy comentado:
Un excelente tutorial,
Me gustaLe gusta a 1 persona
Le he dado mil vueltas pero no tengo forma de quitar este error que no entiendo. Me ocurre cuando llamo a la ruta /album, para ver el resultado:
Error: Uncaught TypeError: Argument 1 passed to Symfony\Component\Debug\ErrorHandler::handleException() must be an instance of Exception, instance of Error given in /Applications/MAMP/htdocs/simplex/vendor/symfony/symfony/src/Symfony/Component/Debug/ErrorHandler.php:436
Stack trace:
#0 [internal function]: Symfony\Component\Debug\ErrorHandler->handleException(Object(Error))
#1 {main}
thrown
Me gustaLe gusta a 1 persona