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.