Noticias

Creando formularios personalizados en Sonata

En los inicios del proyecto Sonata la documentación no siempre estaba actualizada, para conocer algunas de las opciones del bundle era necesario consultar en foros o estudiar el código. Hoy en día la documentación es mucho más extensa y vemos cubiertos todos los aspectos necesarios para poner en marcha un proyecto de forma rápida y sencilla.

Una de las reticencias iniciales a la hora de elegir Sonata para resolver la gestión de nuestro backend son supuestos problemas para extender su funcionalidad o dudas sobre la dificultad en integrar nuestra lógica en el mismo, la gran mayoría de estos problemas se pueden resolver con las opciones que el propio bundle soporta.

Para aquellas opciones más personalizadas vamos a ver hoy la creación de un formulario personalizado.


SonataAdminBundle permite modificar el comportamiento de los formularios en múltiples formas:

  • Permite sobreescribir las plantillas de forma global o a nivel de formulario.

  • Permite sobreescribir los managers y servicios.

  • Permite crear controladores y formularios propios.

  • Etc...

En este post vamos crear una ruta nueva dentro de SonataAdmin, que será procesada por una método dentro de un controlador que generaremos para la ocasión y que procesará un formulario custom.


Preliminares
Cuando creamos un servicio para exponer una entidad en el backend de Sonata, se le pasan al constructor 3 argumentos. El primero va a indicar el código interno que utilizará Sonata para identificar el servicio, el segundo es la entidad en sí que queremos administrar con Sonata, finalmente el tercer parámetro indica que controlador va a gestionar el CRUD de la entidad. El último parámetro no es obligatorio, no obstante debemos especificarlo para poder crear nuestras propias acciones.

Aquí tienes un ejemplo extraido de la doumentación de Sonata, declarando una entidad propia

# Acme/DemoBundle/Resources/config/admin.yml
services:
    sonata.admin.post:
        class: Acme\DemoBundle\Admin\PostAdmin
        tags:
            - { name: sonata.admin, manager_type: orm, group: "Content", label: "Post" }
        arguments:
            - ~
            - Acme\DemoBundle\Entity\Post
            - AcmeDemoBundle:PostAdmin
        calls:
            - [ setTranslationDomain, [AcmeDemoBundle]]


Como ves el tercer parámetro apunta a PostAdmin, Sonata buscará un controlador llamado PostAdminController en la carpeta Controller del bundle AcmeDemoBundle.


Seguridad
Lo primero vamos a crear un rol específico que permita mostrar la opción de importación sólo a aquellos que dispongan del mismo. Para ello agrega el rol ROLE_SONATA_USER_ADMIN_USER_IMPORT a los contenidos en ROLE_SUPER_ADMIN:

// app/config/security.yml

    role_hierarchy:
        ROLE_ADMIN: [ROLE_USER, ROLE_SONATA_ADMIN]
        ROLE_SUPER_ADMIN: [ROLE_ADMIN, ROLE_ALLOWED_TO_SWITCH, ROLE_SONATA_USER_ADMIN_USER_IMPORT]
        SONATA:
            - ROLE_SONATA_PAGE_ADMIN_PAGE_EDIT  # if you are using acl then this line must be commented

 

Una vez creado el rol este debe aparecer en la gestión de usuarios del backend. Crea un usuario test y otórgale al menos los permisos de acceso al la gestión de usuarios ROLE_SONATA_USER_ADMIN_USER* incluido el recién creado rol ROLE_SONATA_USER_ADMIN_USER_IMPORT además del rol ROLE_ADMIN para dar acceso al backend.

 

Configuración del servicio
Nosotros vamos a partir del proyecto anterior en el que realizamos una instalación de Sonata básica. En este caso el servicio de Sonata ya está definido por lo que no es posible directamente indicarle una clase propia como controlador. Por suerte Sonata nos indica en su propia documentación la manera de sobreescribir este servicio.

Vamos a crear un fichero de configuración para el bundle de Sonata User. Para ello crea un fichero sonata_user.yml dentro de la carpeta Sonata en app/config:

sonata_user:
    admin:                  # Admin Classes
        user:
            class:          Application\Sonata\UserBundle\Admin\UserAdmin
            controller:     ApplicationSonataUserBundle:UserAdmin

 

Como verás hemos sobrescrito tanto la clase admin, como el controlador que manejará el CRUD de la aplicación. Ahora es necesario importar este fichero en la configuración de Symfony y enseguida crear las clases correspondientes.

// app/config/config.yml

imports:
 // ...
 - { resource: sonata/sonata_user.yml }
    

En la clase admin vamos a crear una nueva ruta que apuntará al controlador extendido y vamos a modificar la plantilla que se carga en el listado de usuarios, para ello vamos a sobreescribir la función getTemplate:

// src/Application/Sonata/Admin/UserAdmin.php
<?php

namespace Application\Sonata\UserBundle\Admin;

use Sonata\UserBundle\Admin\Model\UserAdmin as BaseUserAdmin;
use Sonata\AdminBundle\Route\RouteCollection;

class UserAdmin extends BaseUserAdmin
{
    /**
     * @param RouteCollection $collection
     * @return Response|void
     */
    protected function configureRoutes(RouteCollection $collection)
    {
        $collection->add('user-import', 'import');
    }

    /**
     * @param string $name
     * @return null|string
     */
    public function getTemplate($name)
    {
        switch ($name) {
            case 'list':
                return 'ApplicationSonataUserBundle:CRUD:user_base_list.html.twig';
                break;
            default:
                return parent::getTemplate($name); // TODO: Change the autogenerated stub
                break;
        }
    }

}


Ahora crearemos la plantilla user_base_list.html.twig en la que situaremos un botón nuevo al lado de las opciones de exportación en el listado de usuarios::

// src/Application/Sonata/Userbundle/Resources/views/CRUD/user_base_list.html.twig

{% extends "SonataAdminBundle:CRUD:base_list.html.twig" %}

 {% block table_footer %}
  <tfoot>
   <tr>
    <th colspan="{{ admin.list.elements|length - (app.request.isXmlHttpRequest ? (admin.list.has('_action') + admin.list.has('batch')) : 0) }}">
     <div class="form-inline">
      {% if not app.request.isXmlHttpRequest %}
       {% if admin.hasRoute('batch') and batchactions|length > 0  %}
        {% block batch %}
         <script>
          {% block batch_javascript %}
           jQuery(document).ready(function ($) {
            $('#list_batch_checkbox').on('ifChanged', function () {
             $(this)
              .closest('table')
              .find('td.sonata-ba-list-field-batch input[type="checkbox"]')
              .iCheck($(this).is(':checked') ? 'check' : 'uncheck')
             ;
            });

            $('td.sonata-ba-list-field-batch input[type="checkbox"]')
             .on('ifChanged', function () {
              $(this)
               .closest('tr')
               .toggleClass('sonata-ba-list-row-selected', $(this).is(':checked'))
              ;
             })
             .trigger('ifChanged')
            ;
           });
          {% endblock %}
         </script>

         {% block batch_actions %}
          <label class="checkbox" for="{{ admin.uniqid }}_all_elements">
           <input type="checkbox" name="all_elements" id="{{ admin.uniqid }}_all_elements">
           {{ 'all_elements'|trans({}, 'SonataAdminBundle') }}
            ({{ admin.datagrid.pager.nbresults }})
          </label>

          <select name="action" style="width: auto; height: auto" class="form-control">
           {% for action, options in batchactions %}
            <option value="{{ action }}">{{ options.label }}</option>
           {% endfor %}
          </select>
         {% endblock %}

         <input type="submit" class="btn btn-small btn-primary" value="{{ 'btn_batch'|trans({}, 'SonataAdminBundle') }}">
        {% endblock %}
       {% endif %}

       <div class="pull-right">


        {% if admin.isGranted("ROLE_SONATA_USER_ADMIN_USER_IMPORT") %}
        <div class="btn-group">
         <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
          <i class="glyphicon glyphicon-export"></i>
          Import options
          <span class="caret"></span>
         </button>
         <ul class="dropdown-menu">
          <li>
           <a href="{{ admin.generateUrl('user-import') }}">
            <i class="glyphicon glyphicon-download"></i>
            {{ "label_import"|trans({}, "SonataAdminBundle") }}
           </a>
          <li>
         </ul>
        </div>
        {% endif %}


        {% if admin.hasRoute('export') and admin.isGranted("EXPORT") and admin.getExportFormats()|length %}
         <div class="btn-group">
          <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
           <i class="glyphicon glyphicon-export"></i>
           {{ "label_export_download"|trans({}, "SonataAdminBundle") }}
           <span class="caret"></span>
          </button>
          <ul class="dropdown-menu">
           {% for format in admin.getExportFormats() %}
            <li>
             <a href="{{ admin.generateUrl('export', admin.modelmanager.paginationparameters(admin.datagrid, 0) + {'format' : format}) }}">
              <i class="glyphicon glyphicon-download"></i>
              {{ format|upper }}
             </a>
            <li>
           {% endfor %}
          </ul>
         </div>

          - 
        {% endif %}

        {% block pager_results %}
         {% include admin.getTemplate('pager_results') %}
        {% endblock %}
       </div>
      {% endif %}
     </div>
    </th>
   </tr>

   {% block pager_links %}
    {% if admin.datagrid.pager.haveToPaginate() %}
     {% include admin.getTemplate('pager_links') %}
    {% endif %}
   {% endblock %}

  </tfoot>
 {% endblock %}

 

Es el momento de crear el método y la plantilla que va a manejar nuestro formulario, entra en acción la clase UserAdminController:

// Application/Sonata/Userbundle/Controller/UserAdminController.php

<?php
namespace Application\Sonata\UserBundle\Controller;

use Sonata\AdminBundle\Controller\CRUDController as SonataCRUDController;
use Symfony\Component\HttpFoundation\Request;

/**
 * Class UserAdminController
 * @package Application\Sonata\UserBundle\Controller
 */
class UserAdminController extends SonataCRUDController
{
 /**
  * User Import
  */
 public function userImportAction(Request $request)
 {
   if (false === $this->admin->isGranted('IMPORT')) {
       throw new AccessDeniedException();
   }

  $form = $this->createFormBuilder([])
   ->add('csvFile', 'file', [
     'mapped' => false,
     'label' => 'File'
    ])
   ->getForm();

  $form->handleRequest($request);
  if ($form->isValid()) {
   $csvFile = $form->get('csvFile')->getData();
   $count = $this->importCustomersCsv($csvFile->getRealPath());
   $this->addFlash('sonata_flash_success', sprintf('(%s) Registros importados', $count));

   return $this->redirect($this->generateUrl('admin_sonata_user_user_list'));
  }

  return $this->render('ApplicationSonataUserBundle:CRUD:user_import.html.twig', array(
    'base_template' => $this->getBaseTemplate(),
    'action' => 'user_import',
    'form'   => $form->createView()
   ));
 }

 /**
  * @param $filePath
  * @return int
  */
 private function importCustomersCsv($filePath)
 {
  $importedRows = 0;
  if (($file = fopen($filePath, "r")) !== FALSE) {
   while (($row = fgetcsv($file, 1000, ";")) !== FALSE) {

    $customerName = array_shift($row);
    $customerPw = array_shift($row);

    // Insert to db
    //@todo import rows to db

    $importedRows++;
   }
   fclose($file);
  }

  return $importedRows;
 }
}

// src/Application/Sonata/UserBundle/Resources/views/CRUD/user_import.html.twig

{% extends base_template %}

{% block actions %}
 <li>{% include 'SonataAdminBundle:Button:list_button.html.twig' %}</li>
{% endblock %}

{#{% block tab_menu %}{{ knp_menu_render(admin.sidemenu(action), {'currentClass' : 'active', 'template': admin_pool.getTemplate('tab_menu_template')}, 'twig') }}{% endblock %}#}

{% block show %}

 {% include 'SonataCoreBundle:FlashMessage:render.html.twig' %}

 <div class="sonata-ba-view">

  {#{{ form(form) }}#}

  <div class="row">
   <div class="col-md-6">Ejemplo </div>
   <div class="col-md-6">

    {{ form_start(form, {'action': admin.generateUrl('user-import'), 'method': 'POST'}) }}
     <fieldset>

      <!-- Form Name -->
      <legend>Importación de clientes</legend>

      <!-- File Button -->
      <div class="control-group">
       {{ form_label(form.csvFile) }}
       {{ form_errors(form.csvFile) }}
       {{ form_widget(form.csvFile) }}
      </div>

      <!-- Button -->
      <div class="control-group">
       <label class="control-label" for="singlebutton">Single Button</label>
       <div class="controls">
        <button id="singlebutton" name="singlebutton" class="btn btn-primary">Button</button>
       </div>
      </div>

      {{ form(form) }}

     </fieldset>
    </form>

   </div>
  </div>

 </div>
{% endblock %}

 

En este punto debes poder visualizar un nuevo botón en el listado de usuarios, junto a las opciones de exportación. Si lo despliegas y seleccionas la única opción disponible accederas a nuestro formulario custom, puedes probar el formulario con algún fichero de ejemplo. Te queda el trabajo de realizar el procesamiento en sí del fichero que quieras importar.


Conclusiones

Como has podido ver Sonata nos facilita y dá la flexibilidad suficiente como para poder integrar nuestros propios formularios en el ciclo lógico propuesto por el bundle. Desde ahora ya no hay excusa para no poder utilizar Sonata con la seguridad de poder integrar nuestros propios formularios.

En próximas entregas seguiremos viendo algunas de las opciones que nos dá Sonata y Symfony para extender sus servicios y poder crear servicios de manera más rápida y profesional.

 

 


Symfony2 php Sonata Project CMS


Compartir mola!!