Subir multiples ficheros a la vez en Symfony 2

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:

Captura de pantalla 2015-11-05 a las 11.57.05

<?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>.

vista

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 ¡

Anuncios

2 comentarios en “Subir multiples ficheros a la vez en Symfony 2

  1. 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

    Le gusta a 1 persona

Responder

Introduce tus datos o haz clic en un icono para iniciar sesión:

Logo de WordPress.com

Estás comentando usando tu cuenta de WordPress.com. Cerrar sesión / Cambiar )

Imagen de Twitter

Estás comentando usando tu cuenta de Twitter. Cerrar sesión / Cambiar )

Foto de Facebook

Estás comentando usando tu cuenta de Facebook. Cerrar sesión / Cambiar )

Google+ photo

Estás comentando usando tu cuenta de Google+. Cerrar sesión / Cambiar )

Conectando a %s