martes, 20 de diciembre de 2011

[Symfony2] Subir varios archivos al servidor

Tratando de subir imagenes a mi servidor con Symfony2, me tope con varios Bundles muy interesantes; el mas completo es SonataMediaBundle.

Lastimosamente para lo que tenia pensado era demasiado...

Lo que deseaba hacer es subir varias imágenes cuando se enviaba el formulario, para esto se necesita un campo de tipo "file", pero solo se puede subir uno a la vez como explica el Cookbook 

En este caso se necesita un array de campos "file". Para esto en nuestro Entity se define un atributo de tipo array:

<?php

//////Unidad.php

    /**
     * @var array
     */
    public $imagenes;
 //////

?>

Ahora para el Type:


///////UnidadType.php

public function buildForm(FormBuilder $builder, array $options)
    {
        $builder
               ->add('imagenes', 'collection', array(
                'type'      => 'file',
                'allow_add' => true,
                'allow_delete' => true,
                'prototype' => true,
                'options'=>array(
                    'required'  => false,
                    'attr'  => array('class' => 'unidades'),
                )));
    }


Como se puede ver, se define como 'collection' el tipo de datos para la variable $imagenes. La opcion prototype nos permite tener en el formulario el texto HTML del campo generado (en este caso el campo file); se genera algo como esto:


<div><label class="unidades" for="unidad_imagenes_$$name$$">$$name$$</label><input type="file" id="unidad_imagenes_$$name$$" name="unidad[imagenes][$$name$$]"    class="unidades" /></div>

 De este modo, mediante javascript (preferiblemente jQuery), podemos crear mas campos 'file' e ir agregándolos al formulario con la siguiente función:


    jQuery('#agregar_campo_file').click(function(e) {
        e.preventDefault();
        var files = jQuery('#unidad_imagenes');
        var newWidget = files.attr('data-prototype');
        newWidget = newWidget.replace(/\$\$name\$\$/g, files);
        $('#unidad_imagenes').append(newWidget);
        files++;
        return false;
    });


Dentro del HTML debemos tener un link:

<a href="#" id="agregar_campo_file">Agregar Imagen</a>

Al hacer clic sobre el enlace se iran agregando campos 'file' al formulario.

En la plantilla TWIG para el formulario de envio se debe incluir la etiqueta "form_enctype" para poder enviar los archivos.


Hasta aquí podemos enviar los archivos al servidor, pero estos archivos no están siendo almacenados. Es necesario utilizar las funciones para el tipo 'file' como "move". Con esta función se puede almacenar los archivos subidos en alguna carpeta dentro del servidor.

Esta gestión es realizada por una función dentro de la Entidad y debe ser llamada antes de persistir el objeto:



        if ($editForm->isValid()) {
           
            $entity->uploadVarios();
            $em->persist($entity);
            $em->flush();

            return $this->redirect(/* Generar tu URL */);
        }

Como se puede ver, se hace un llamado a la función uploadVarios desde la entidad antes de persistirla.

Para tener una relacion entre las imagenes subidas y la entidad, definí un campo en la base de datos de tipo 'text' y es una array serializado que contiene el nombre de las imágenes almacenadas. Opté por esto (dato serializado) para no definir una nueva tabla en la base de datos que almacene un registro por cada imagen guardada.


    /**
     * @ORM\Column(type="text", nullable=true)
     */
    public $path;


Este campo se actualiza dentro de la funcion uploadVarios de la Entidad. Al ser un dato serializado, se puede "des-serializar" y obtener un array con los nombres de las imagenes almacenadas anteriormente. De igual manera se puede agregar mas nombres y mantener los anteriores

La funcion uploadVarios se define como sigue:


    public function uploadVarios()
    {       
        $mypath = unserialize($this->path);

        foreach ($this->imagenes as $key => $value) {
           
            if ($value){
           
                //Definir un nombre valido para el archivo
                //Gedmo es una de las extensiones de Doctrine para Sluggable, Timestampable, etc
                $nombre = \Gedmo\Sluggable\Util\Urlizer::urlize($value->getClientOriginalName(), '-');

                //Verificar la extension para guardar la imagen
                $extension = $value->guessExtension();
               
                $extvalidas = array('JPG','JPEG','PNG','GIF');
               
                if ( !in_array(strtoupper($extension), $extvalidas)){
                    return;
                }
               
                //Quitar la extension del nombre generado
                //caso contrario el nombre queda algo como:  miimagen-jpg
                $nombre = str_replace('-'.$extension, '', $nombre);
               
                //Nombre final con extension
                //Queda algo como: miimagen.jpg
                $nombreFinal = $nombre.'.'.$extension;
               
                //Verificar si la imagen ya esta almacenada
                if (@in_array($nombreFinal, $mypath)){
                    //si la imagen ya esta almacenada, se continua con el siguiente item   
                    continue;
                }
               
                //Almacenar la imagen en el servidor
                $value->move($this->getUploadRootDir(), $nombreFinal);
               
            //Agregar el nuevo nombre al final del Array
                $mypath[]= $nombreFinal;
            }
        }
        $this->path = serialize($mypath);
        $this->imagenes = array();
    } 



Las siguientes funciones se explican en el Cookbook y se utilizan para definir los directorios donde se almacenan los arhivos (van dentro del Entity):


    protected function getUploadRootDir()
    {
        return __DIR__.'/../../../../web/'.$this->getUploadDir();
    }

    protected function getUploadDir()
    {
        return 'uploads/documents';
    }

Ahora cuando se suben varios archivos, la variable $path de mi base de datos queda algo asi:


a:3:{i:0;s:25:"claymore-wallpaper-09.jpg";i:1;s:11:"2lw20ro.jpg";i:2;s:38:"kawapaper-selain-0000096-1920x1200.jpg";}


Ahora ya se almacenan las imagenes en el servidor :)



Visualizar las Imágenes 

Si queremos ver las imagenes almacenadas se necesita pasar un array con el nombre de las imagenes a la plantilla TWIG:


    public function editAction($id)
    {
        $em = $this->getDoctrine()->getEntityManager();

        $entity = $em->getRepository(/*Reposritorio de tu Entity*/)->find($id);

        return array(
            'entity'      => $entity,
            'arrayimagenes' => unserialize($entity->getPath()),
        );
    }


Y en la plantilla se los visualiza de la siguiente manera:




    {% for imagen in arrayimagenes%}
    <img
         src="/uploads/documents/{{imagen}} "
         title="{{imagen}}">
   
    {% endfor %}



Imágenes Finales


Pagina inicial

 

Archivos listos para enviar
Archivos almacenados en el Servidor


Eso es todo ;)

Sugerencias y comentarios son bienvenidos.



13 comentarios:

  1. Buenas, tengo una pregunta

    ¿donde esta el codigo donde enlazas el array de imagenes del formulario con tu entidad?

    Un saludo,
    Buen post

    ResponderEliminar
  2. Hola balasobrebroadway, al definir en tu Entity un atributo:

    /**
    * @var array
    */
    public $imagenes;

    y si en el Type lo defines como 'collection',

    ->add('imagenes', 'collection', .......

    esto es suficiente para que cualquier cosa que sea enviada dentro de este campo (collection) sea parte del Entity y este disponible como array. Es por esto que puedes manipular los datos como si fueran parte del Entity.

    No necesitas mas código para enlazar los archivos enviados con tu Entity.

    Espero que sea de tu ayuda.

    Gracias por tu visita.

    ResponderEliminar
  3. Estimado Gregorio,

    buena página, te felicito.

    Saludos

    ResponderEliminar
  4. Hola, estoy intentando seguir lo que haces y creo que lo tengo todo igual. Pero cuando pulso sobre el botón submit de mi formulario me sale lo siguiente:

    No se pudo encontrar el archivo

    ¿Qué puede ser?

    ResponderEliminar
    Respuestas
    1. Hola Juanma, esto puede suceder si la ruta al archivo no es valida:

      http://gitnacho.github.com/symfony-docs-es/reference/constraints/File.html#notfoundmessage

      Asegurate que la ruta a la imagen no contenga caracteres raros (espacios,"ñ", ").

      Ademas asegurate que tienes permiso (escritura y lectura) tanto a la imagen que intentas subir, como al directorio web/ de tu aplicación

      Eliminar
  5. Antes de nada muchísimas gracias por responder, era que tenía una validación en la variable path de la entidad, la quite y ya funciona, pero ahora tengo otro problemilla.En mi formulario, lo relleno, sale lo de agregar las fotos(agrego por ejemplo 2) y le doy al botón de submit, y no salta ningún error, pero en la BD, el campo path no queda como pone en el tutorial me queda así :
    Patch b:0;
    Imagenes a:0:{}

    ...

    No hace referencia a la foto, el cual tampoco se crea en
    /web/uploads/images ...

    y como pone aquí llamo antes de persistir a la función
    $incidencia->uploadVarios();
    $em = $this->getDoctrine()->getEntityManager();
    $em->persist($incidencia);
    $em->flush();
    y lo he puesto todo siguiente este magnifico tutorial.. ¿Alguna idea de que puede ser?

    ResponderEliminar
    Respuestas
    1. Hola Juanma, mira la variable "imagenes" es un array que contiene las imágenes que subes, no tiene que ser serializado (ni tiene relación con la Base de Datos) es como una variable temporal para iterar cada una de las imágenes que subes.

      Fijate en esto:

      /**
      * @var array
      */
      public $imagenes;


      A diferencia de la variable path:

      /**
      * @ORM\Column(type="text", nullable=true)
      */
      public $path;

      Cuando subes imagenes, cada una esta almacenada en la variable $imagenes, esto debido a que en el formulario se pasa este campo en el
      FormBuider del UnidadType:

      public function buildForm(FormBuilder $builder, array $options)
      {
      $builder
      ->add('imagenes', 'collection', array(
      'type' => 'file',
      'allow_add' => true,
      'allow_delete' => true,
      'prototype' => true,
      'options'=>array(
      'required' => false,
      'attr' => array('class' => 'unidades'),
      )));
      }

      Fijate que la variable que pasas en el FormBuilder sea "imagenes" o la variable que tengas asociada como array en tu entidad.

      Saludos.

      Eliminar
  6. Hola, hice el cambio que me comentaste, ahora ya en mi BD, no aparece el campo imagenes, solo path,pero sigue pasando lo mismo, no se guarda la imagen en web/images, ni tampoco en el campo path de la BD se guarda el nombre... solo se guarda b:0; , que no se que significa, nunca he serializado nada, e igual ahí esta el problema, aunque el hecho de que no se me guarde la foto en la carpeta... no tiene nada que ver con serializar...no? Una pregunta en mi entidad, tendría que poner que implementa a \Serializable ?? o alguna otra cosa, por si el fallo es de eso. Como siempre gracias por intentar ayudarme

    ResponderEliminar
  7. Para entenderlo mejor puedes verlo aqui...

    El atributo imagenes, lo he llamado archivos ..

    https://github.com/ReyDoz/Vecinos2.0/blob/master/src/Vecinos/IncidenciaBundle/Entity/Incidencia.php

    Aquí esta el build form, que es igual que el que pones ..

    https://github.com/ReyDoz/Vecinos2.0/blob/master/src/Vecinos/IncidenciaBundle/Form/Frontend/IncidenciaType.php

    Y este es el controlador donde se llama a uploadVarios()

    https://github.com/ReyDoz/Vecinos2.0/blob/master/src/Vecinos/IncidenciaBundle/Controller/DefaultController.php

    ResponderEliminar
  8. Grep Villalba me podrías mostrar tu vista twig del formulario? Tenía el mismo problema que el compañero de arriba y lo he solucionado siguiendo esto: http://gitnacho.github.com/symfony-docs-es/reference/forms/types/collection.html#agregando-y-quitando-elementos

    Pero me gustaría saber como tienes tu vista twig del formulario para que me quede más claro.

    ResponderEliminar
  9. Hola estoy haciendo pruebas pero me serviría mucho ver el Type y la entidad para guiarme por ello te pido el gran favor que me ayudes con eso! :)...

    ResponderEliminar