Formularios: Coleccion no mapeada en la entity usando un DTO

Si no has leido mi anterior “trick” acerca del uso de los DTOs en los formularios, deberias visitar antes este link.

En este ejemplo, necesitaba varios campos que no estan mapeados en la entity de la cual nace el formulario. Para ello, no he tenido mas remedio que añadir un nuevo campo “No mapeado”. Este campo es una coleccion, por lo que se puede añadir o eliminar elementos.

De acuerdo con esto, en mi formulario añado ese campo asi:

public function buildForm(FormBuilderInterface $builder, array $options)
{
    $builder
        //OTROS CAMPOS DE LA ENTIDAD
        //...
        ->add('enrolUserToOrg', CollectionType::class, array(
             'label' => false,
             'entry_type' => new EnrolUserToOrgType(),
             'mapped' => false,
             'allow_add' => true,
             'attr' => array(
                     'class' => 'enrolUserOrgWrapper'
             )
      ));
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'efor\AppBundle\Entity\Organization',
    ));
}

Os he marcado en color aquello que hay que tener en cuenta.

  • CollectionType::class: Definimos el campo de tipo coleccion para indicarle a Symfony que podremos añadir/quitar elementos
  • ‘entry_type’ => new EnrolUserToOrgType() : Instanciamos el sub-formulario que contendra los campos para cada fila de la coleccion
  • ‘mapped’ => false : Le indicamos que no esta mapeado en la entity para que no valide nada
  • ‘allow_add’ => true : Indicamos que podremos añadir elementos

Ahora lo que haremos sera crear un DTO(en otros sitios lo llaman ‘Domain Model’) que contendra los elementos del “subformulario” el cual hablaba antes y que representaran una fila de cada coleccion. Este DTO contendra 2 campos: usuario y rol, y quedaria asi:

<?php
namespace efor\AppBundle\Model;

use efor\AppBundle\Entity\Role;
use efor\UsuarioBundle\Entity\User;

class EnrolUserToOrg
{
    /** @var User $user */
    private $user;

    /** @var Role $role */
    private $role;

    /**
     * @return User
     */
    public function getUser()
    {
        return $this->user;
    }

    /**
     * @param User $user
     */
    public function setUser($user)
    {
        $this->user = $user;
    }

    /**
     * @return Role
     */
    public function getRole()
    {
        return $this->role;
    }

    /**
     * @param Role $role
     */
    public function setRole($role)
    {
        $this->role = $role;
    }
}

Ahora creamos el subformulario y le indicamos que la clase en la que se tiene que basar es el anterior DTO:

public function buildForm(FormBuilderInterface $builder, array $options)
{
        $builder
            ->add('user', EntityType::class, array(
                'label' => false,
                'class' => 'UsuarioBundle:User',
                'empty_value' => 'Selecciona el/los responsable/s',
                'query_builder' => function (EntityRepository $er) {
                    return $er->createQueryBuilder('users');
                },
                'attr' => array(
                    'class' => 'col-md-5 select-user'
                )
            ))
            ->add('role', ChoiceType::class, array(
                'label' => false,
                'empty_value' => 'Seleccionar un Rol',
                'choices' => $this->roleChoices,
                'attr' => array(
                    'class' => 'col-md-5 select-role'
                )
            ))
            ->add('Quitar', ButtonType::class, array(
                'attr' => array(
                    'class' => 'col-md-2 btn-danger button-remove-enrol'
                )
            ))
       ;
}

public function configureOptions(OptionsResolver $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'efor\AppBundle\Model\EnrolUserToOrg'
    ));
}

Llegados a este punto, vamos a por la vista. Como es una coleccion he seguido la guia de Symfony para pintar esos campos en el formulario y la he ajustado a mi gusto, y quedaria asi:

<ul class="enrolUserToOrg" 
    data-prototype="{{ form_widget(form.enrolUserToOrg.vars.prototype) | e }}">
</ul>

Despues añadiriamos la logica necesaria en jQuery para poder añadir y eliminar filas de esa coleccion:

var $collectionHolder;

// setup an "add a enrolUserToOrg" link
var $buttonAdd = '<span class="input-group-btn">'+
    '<a class="btn btn-success btn-add add_enrol_link"><span class="glyphicon glyphicon-plus"></span> ' +
    '<span>Añadir nuevo responsable</span></a>'+
    '</span>';
var $addLink = $($buttonAdd);
var $newLinkLi = $('<li class="text-center"></li>').append($addLink);

jQuery(document).ready(function() {
    // Get the ul that holds the collection of authorized
    $collectionHolder = $('ul.enrolUserToOrg');

    // add the add li object to the tags ul
    $collectionHolder.append($newLinkLi);

    // count the current form inputs we have (e.g. 2), use that as the new
    // index when inserting a new item (e.g. 2)
    $collectionHolder.data('index', $collectionHolder.find(':input').length);

    $addLink.on('click', function(e) {
        // prevent the link from creating a "#" on the URL
        e.preventDefault();

        // add a new tag form (see next code block)
        addAuthorizedUserForm($collectionHolder, $newLinkLi);
    });

    //initialize the collection with one element
    if($collectionHolder.find('li').length == 1){
        $addLink.click();
    }

    $(document).on('click', '.button-remove-enrol', function () {
        var contElems = $collectionHolder.find('li').length - 1;

        if(contElems > 1 ){
            $(this).closest('li').remove();
        }
    });

});

function addAuthorizedUserForm($collectionHolder, $newLinkLi)
{
    // Get the data-prototype explained earlier
    var prototype = $collectionHolder.data('prototype');

    // get the new index
    var index = $collectionHolder.data('index');

    // Replace '__name__' in the prototype's HTML to
    // instead be a number based on how many items we have
    var newForm = prototype.replace(/__name__/g, index);

    // increase the index with one for the next item
    $collectionHolder.data('index', index + 1);

    // Display the form in the page in an li, before the "Add a tag" link li
    var $newFormLi = $('<li></li>').append(newForm);
    $newLinkLi.before($newFormLi);
}

Y para la parte del controlador, podremos recoger los datos de la coleccion del campo no mapeado de la siguiente forma:

/**
 * @Route("/create", name="organization_create")
 */
public function createOrganizationAction(Request $request)
{
    $organization = new Organization();
    $form = $this->createForm(new OrganizationType(), $organization, array(
        'action' => $this->generateUrl('organization_create'),
    ));

    $form
        ->add('ok', 'submit', array(
            'attr' => array('class' => 'btn btn-primary pull-right'),
            'label' => 'Crear',
        ))
        ->add('cancel', 'button', array(
            'attr' => array('class' => 'return-button pull-right'),
            'label' => 'Volver atrás',
        ));

    $form->handleRequest($request);
    if ($form->isValid()) {
        $usersEnroled = $form->get('enrolUserToOrg')->getData();

        //DO SOMETHING YOU NEED WITH THIS ARRAY
        //...example
        // /** @var EnrolUserToOrg $usersEnroled */
        //foreach($usersEnroled as $userEnroled){
             //$user = $userEnroled->getUser();
             //$role = $userEnroled->getRole();
             //...
        //}  
        
       $this->addFlashMessage(CoreController::FLASH_TYPE_SUCCESS, 'La organización ha sido creada con exito'); return $this->redirect($this->generateUrl('organization_list')); } return $this->render('AppBundle:Organization:createOrganization.html.twig', array( 'form' => $form->createView() )); }

De esta forma, nuestra accion sera la que inicialice y procese el formulario a la vez, y asi podremos recoger la coleccion no mapeada y hacer lo que necesitemos.

Os dejo un pequeño video de como quedaria y el efecto de añadir / quitar elementos de la coleccion.

Coleccion no mapeada en Symfony 2

Espero que os guste y por favor, compartid este “trick”.

Cualquier duda, podeis consultarme o mirar en la documentación que os he puesto en los links de arriba y aqui.

Saludos Sf-Devs¡¡

Formularios: Sobrescribir template de un widget – Personalización

En esta ocasión os voy a hablar sobre como sobrescribir un form_widget de los que se usa en los formularios para personalizarlo a nuestro gusto.

En mi caso, tengo configurado para que Symfony coja las plantillas definidas en sus Form Themes,las cuales meten elementos HTML definidos para que se ajusten a un estilo concreto.

Eso se puede configurar en el app/config/config.yml:

twig:
   //...
    form_themes:
        - 'bootstrap_3_layout.html.twig'

Aqui podemos elegir entre varios form_themes que Symfony trae pre-definidos:

Symfony comes with some built-in form themes that define each and every fragment needed to render every part of a form:

form_div_layout.html.twig, wraps each form field inside a element.

form_table_layout.html.twig, wraps the entire form inside a element and each form field inside a element.

bootstrap_3_layout.html.twig, wraps each form field inside a element with the appropriate CSS classes to apply the default Bootstrap 3 CSS framework styles.

bootstrap_3_horizontal_layout.html.twig, it’s similar to the previous theme, but the CSS classes applied are the ones used to display the forms horizontally (i.e. the label and the widget in the same row).

foundation_5_layout.html.twig, wraps each form field inside a element with the appropriate CSS classes to apply the default Foundation CSS framework styles.

En mi aplicacion me ocurre que esto me inserta una etiqueta ‘div class=form-control’ al insertar un elemento de mi FormType y justamente necesito que ese “div” no exista para que se alinee todo horizontalmente, sino la clase form-control me coge todo el ancho y no permite elementos contiguos.

Para ello, no me queda mas remedio que buscar el form_theme y en concreto el widget que “pinta” ese div y “sobrescribirlo”.

vendor/symfony/symfony/src/Symfony/Bridge/Twig/Resources/views/Form/bootstrap_3_layout.html.twig

{% block form_row -%}
    {{- form_label(form) -}} 
    {{- form_widget(form) -}} 
    {{- form_errors(form) -}}
{%- endblock form_row %}
 Ese es el bloque que mete el div que necesito quitar. Justamente el widget que pinta un row del formulario.

1- Creamos un template Twig en nuestro proyecto y lo llamamos ‘formRowCustom.html.twig’ y en el metemos lo siguiente:

{% block form_row -%}
        {{- form_label(form) -}}
        {{- form_widget(form) -}}
        {{- form_errors(form) -}}
{%- endblock form_row %}

2- A continuacion para poder usar nuestro “widget theme”, nos vamos al formulario donde lo queremos y lo usaremos asi:

{% form_theme form.user 'AppBundle:Form:formRowCustom.html.twig' %}
{{ form_widget(form.user) }}

De esta forma, cuando se vaya a ejecutar la funcion de Twig llamada “form_widget()”, en lugar de “pintar” el estilo del Form Theme, nos mostrara  nuestro estilo personalizado.

Con este “trick” podemos personalizar todos los elementos de los formularios para que se ajusten a nuestro diseño.

Para más información os dejo el enlace de la documentación oficial para que veais mas ejemplos o formas de sobrescribir los elementos de los form themes.

Si te a gustado y lo encuentras util, no dudes en compartir este truco en las redes sociales.

Hasta la proxima Sf dev¡¡

 

Uso del operador IN en consultas DQL de Doctrine y otros operadores SQL

A menudo tengo la necesidad de usar el operador IN dentro del WHERE de una consulta DQL con Doctrine y casi siempre se me olvida como usarlo.

Para ello voy a explicar como usarlo a continuación con un ejemplo:

– Imaginamos que tenemos un array con los posibles valores que queremos seleccionar en la parte del where de la consulta tal que asi:

$filters = array(1, 2, 3, 4, 10, 15, 25);

-Ahora, hacemos nuestra consulta de la siguiente forma:

$users = $qb
    ->addSelect('user')
    ->from('UsuarioBundle:User', 'user')
    ->join('user.userGroup', 'userGroup')
    ->where($qb->expr()->in('userGroup.id', $filters))
    ->getQuery()
    ->getResult();

 De esta forma, usando el expr() del QueryBuilder podemos usar los operadores IN, NOT IN, BETWEEN, MAX, MIN, etc. Podeis ver toda la documentacion y los operadores disponibles en la documentacion oficial de Doctrine sobre el querybuilder:

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/query-builder.html#high-level-api-methods

Ale, espero que os sirva de ayuda.

Saludos Devs¡

Herencia entre entidades y consultas DQL relacionadas

herencia entre entidades y consultas DQL
Herencia entre entidades

Una de las características que tiene Doctrine es que se puede usar la herencia de clases para representar aquellas tablas que heredan de una tabla “base”.

Pongamos un ejemplo:

  • Tenemos una tabla Rol (“padre”) y de ella heredan 3 tipos de roles distintos(RolPlataforma, RolOrganizacion, RolEdicion), pero que comparten los mismos atributos que la tabla Rol “padre”, excepto que se cada “entidad” hija se relacionan con entidades diferentes. Aqui el diagrama UML que representa el caso:
  • umlAhora la parte de las entidades:
    • Entity Padre Role
  • /**
     * @ORM\Table(name="role")
     * @ORM\InheritanceType("JOINED")
     * @ORM\DiscriminatorColumn(name="discr", type="string")
     * @ORM\DiscriminatorMap({
     *     "role" = "Role",
     *     "role_edition" = "RoleEdition",
     *     "role_org" = "RoleOrganization",
     *     "role_plat" = "RolePlatform"
     * })
     */
    class Role 
    {
     const ROLE_EDITION = 'RoleEdition';
     const ROLE_ORGANIZATION = 'RoleOrganization';
     const ROLE_PLATFORM = 'RolePlatform';
     const ROLE = 'Role';
    
     /**
     * @ORM\Id
     * @ORM\Column(type="integer", nullable=false)
     * @ORM\GeneratedValue()
     */
     protected $id;
    
     /**
     * @ORM\Column(name="rol_borrado", type="boolean", nullable=false)
     */
     private $deleted = false;
    
     /**
     * @ORM\ManyToMany(targetEntity="efor\UsuarioBundle\Entity\User", mappedBy="roleUser")
     */
     private $user;
    
    //...
    
    //OTROS CAMPOS NO RELEVANTES PARA EL EJEMPLO
    //SETTERS Y GETTERS Y OTROS METODOS.
  • Con la anotacion estamos indicando que se va a hacer herencia de tablas y que los tipos de discriminador van a ser los que pone en el “DiscriminatorMap”. De esta entidad nunca se van a instanciar objetos, siempre se instanciaran objetos de las entidades hijas, por lo que podriamos definirla como Abstracta.
  • Entity descendiente RolePlatform
    • /**
       * @ORM\Table(name="rol_plataforma")
       * @ORM\Entity
       */
      class RolePlatform extends Role
      {
          protected $discr = self::ROLE_PLATFORM;
      
          /**
           * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Platform", inversedBy="rolePlatform")
           */
          private $platform;
      
          //otros campos
      }
  • Entity descendiente RoleOrganization
  • /**
     * @ORM\Table(name="rol_organizacion")
     * @ORM\Entity
     */
    class RoleOrganization extends Role
    {
        protected $discr = self::ROLE_ORGANIZATION;
    
        /**
         * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Organization", inversedBy="roleOrganization", cascade={"persist"})
         */
        private $organization;
    
        //otros campos
    }
  • Entity descendiente RoleEdition
  • /**
     * @ORM\Table(name="rol_edicion")
     * @ORM\Entity
     */
    class RoleEdition extends Role
    {
        protected $discr = self::ROLE_EDITION;
    
        /**
         * @ORM\ManyToOne(targetEntity="efor\AppBundle\Entity\Edition", inversedBy="roleEdition")
         */
        private $edition;
    
        //otros campos
    }

Si os fijas cada entidad hija tiene una relacion distinta a otra entidad, por eso necesitabamos usar la herencia entre entidades, porque podemos crear objetos que se relacionan con entidades distintas en funcion del tipo de relacion que queramos tener.

En la documentación de Doctrine explica los tipos de herencia entre entidades y algunas consideraciones a tener en cuenta cuando se usa la herencia. Os dejo aqui la documentacion:

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#class-table-inheritance

-http://doctrine-orm.readthedocs.org/projects/doctrine-orm/en/latest/reference/inheritance-mapping.html#single-table-inheritance

-http://stackoverflow.com/questions/7504680/doctrine-2-how-to-write-a-dql-select-statement-to-search-some-but-not-all-the

Consultas DQL filtradas por el campo discriminador:

Una de las dudas que surgen al trabajar con la herencia en Doctrine es saber como puedes filtrar las consultas por el campo discriminatorio. En mi caso, necesitaba filtrar los resultados por aquellos objetos que eran de una determinada clase del discriminador(RoleOrganization) y despues de buscar, encontre que se puede hacer algo asi:

public function findUsersFilteredByRoleOrganization(User $loggedUser, Role $selectedRole)
{
    $qb = $this->getEntityManager()->createQueryBuilder();
    $usersResult = $qb
    ->select('user')
    ->from('UsuarioBundle:User', 'user')
    ->join('user.roleUser', 'roleUser')
    ->where('roleUser INSTANCE OF AppBundle:RoleOrganization')
    ->getQuery();

    return $usersResult->getResult();
}

Si os dais cuenta, hemos hecho uso del INSTANCE OF dentro del Where de la consulta DQL, la cual se traduce por esto en SQL:

SELECT t0_.*
INNER JOIN rol_usuario t2_ ON t0_.id = t2_.user_id
INNER JOIN rol t1_ ON t1_.id = t2_.role_id
LEFT JOIN rol_edition t3_ ON t1_.id = t3_.id
LEFT JOIN rol_org t4_ ON t1_.id = t4_.id
LEFT JOIN rol_plataforma t5_ ON t1_.id = t5_.id
WHERE t1_.discr IN (‘role_org’)

De esta forma solo obtenemos los objetos del tipo “RoleOrganization” que queremos.

Espero que os sirve de ayuda, ya que la documentacion de Doctrine no es muy explicita en este aspecto y no lo explica muy bien que digamos.

Recordad compartir este articulo para que llegue al maximo de amigos de Symfony.

Saludos ¡¡

 

 

Uso de Data Transfer Objects (DTO) en formularios

Uno de los problemas que a veces nos encontramos en los formularios a menudo, es que necesitamos mostrar campos que no existen en nuestra entidad directamente ya que puede que necesitemos esos campos para construir un determinado objeto en funcion de las opciones que haya elegido.

En mi caso, tengo un formulario en el que aparecen 4 campos de tipo <select> y que son dependientes entre sí, y en funcion de las opciones el usuario tendra asignado unas cosas o otras, y 3 de estos campos no estan mapeados en la Entity.

Para ello, debemos hacer uso de los DTO (Data Transfer Object) que no son mas que “Objetos de Transferencia de Datos” o objetos “pasarela” para recoger la informacion del formulario y hacer a posteriori nuestras cosas.

Enctonces, imaginamos que tenemos una entidad Organizacion pero necesitamos 4 campos mas que no estan en nuestra entity pero que los necesitamos para el formulario:

<?php
namespace myApp\AppBundle\Model;

use Symfony\Component\Validator\Constraints as Assert;

class Enrol
{
    protected $organization;

    protected $plan;

    protected $edition;

    protected $group;

    /**
     * @Assert\NotBlank()
     */
    protected $roleUser;

    /**
     * Enrol constructor.
     */
    public function __construct()
    {
    }

    /**
     * @return mixed
     */
    public function getOrganization()
    {
        return $this->organization;
    }

    /**
     * @param mixed $organization
     */
    public function setOrganization($organization)
    {
        $this->organization = $organization;
    }

    /**
     * @return mixed
     */
    public function getPlan()
    {
        return $this->plan;
    }

    /**
     * @param mixed $plan
     */
    public function setPlan($plan)
    {
        $this->plan = $plan;
    }

    /**
     * @return mixed
     */
    public function getEdition()
    {
        return $this->edition;
    }

    /**
     * @param mixed $edition
     */
    public function setEdition($edition)
    {
        $this->edition = $edition;
    }

    /**
     * @return mixed
     */
    public function getGroup()
    {
        return $this->group;
    }

    /**
     * @param mixed $group
     */
    public function setGroup($group)
    {
        $this->group = $group;
    }

    /**
     * @return mixed
     */
    public function getRoleUser()
    {
        return $this->roleUser;
    }

    /**
     * @param mixed $roleUser
     */
    public function setRoleUser($roleUser)
    {
        $this->roleUser = $roleUser;
    }
}

Este es el modelo en el que se basara nuestro formulario para “hidratar” los campos del objeto que le pasemos.

Nuestro formulario quedaría así:

<?php

namespace myApp\AppBundle\Form;

use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolverInterface;

class EnrolType extends AbstractType
{
    public function buildForm(FormBuilderInterface $builder, array $options)
    {
        $builder
            ->add('organization', 'entity', array(
                'label' => 'Seleccionar una Organización',
                'class' => 'AppBundle:Organization',
                'required' => false
            ))
            ->add('plan', 'entity', array(
                'label' => 'Seleccionar un Plan',
                'class' => 'AppBundle:Plan',
                'required' => false
            ))
            ->add('edition', 'entity', array(
                'label' => 'Seleccionar una Edición',
                'class' => 'AppBundle:Edition',
                'required' => false
            ))
            ->add('group', 'entity', array(
                'label' => 'Seleccionar un Grupo',
                'class' => 'AppBundle:Group',
                'required' => false
            ))
            ->add('roleUser', 'entity', array(
                'label' => 'Seleccionar un Rol',
                'class' => 'AppBundle:Role',
                'required' => false
            ));
    }

    public function setDefaultOptions(OptionsResolverInterface $resolver)
    {
        $resolver->setDefaults(array(
            'data_class' => 'myApp\AppBundle\Model\Enrol'
        ));
    }

    public function getName()
    {
        return 'myapp_bundle_enrol_type';
    }
}

Creamos el action:

/**
 * @Route("/organizations-form", name="org_form")
 */
public function orgFormAction(Request $request)
{
    $enrol = new Enrol();
    $form = $this->createForm(new EnrolType(), $enrol, array());
    $form->add('submit', 'submit');

    $form->handleRequest($request);
    if($form->isValid()){
        //Aqui harás lo que necesites con los datos del formulario
        //ya que tienes el DTO $enroll y con sus métodos puedes
        //recoger la informacion seleccionada en el formulario.
    }

    return $this->render('UserBundle::orgForm.html.twig', array(
        'form' => $form->createView()
    ));
}

Ya tenemos nuestro formulario, ahora la vista(puedes hacerla mas bonita si quieres):

{{ form_start(form) }}
{{ form_row(form.organization) }}
{{ form_row(form.plan) }}
{{ form_row(form.edition) }}
{{ form_row(form.group) }}
{{ form_row(form.roleUser) }}
{{ form_end(form) }}

Y con esto ya tendrias un formulario que no se a basado en una Entity y que contiene los campos que a ti te interesan y que despues procesaras en el Action de la forma que creas conveniente.

Espero que te haya servido, y si es asi, puedes compartir este articulo en redes sociales pulsando en ellas.

En otro articulo, explicare como hacer selects dependientes o anidados a traves de AJAX, otra de las necesidades que todo tenemos hoy en dia para los formularios. Pero eso sera en otro capitulo 🙂

Gracias y saludos a todos.

Uso de Voters para el control del acceso de usuarios en zonas

Acceso denegado - 403
Acceso denegado – 403

Los voters son un elemento de Symfony para darle una vuelta de tuerca al las listas de acceso – ACL (Access Control List) de Symfony donde podemos validar con mas profundidad si un recurso esta disponible para un usuario.

Aqui la documentacion oficial.

A continuacion explicare un ejemplo mas concreto donde lo he usado.

La problematica era la siguiente:

-Un usuario puede realizar ciertas acciones en las zonas, segun el tipo de rol y los permisos definidos para cada rol y zona. Es decir que para mi zona de “Gestion de Usuarios” existen las opciones de listar, crear, editar, visualizar y eliminar usuarios. Por ejemplo para un usuario con “rol admin” podra hacerlas todas, pero un usuario con “rol guest” solo podra listar y visualizar información de los usuarios.

Todo esto va gestionado a traves de relaciones entre enitdades que representan tablas en la BD. Esto lo explicare en otro post con mas detalle para que veais como he resuelto la gestion del acceso de usuarios con roles, permisos y zonas en una aplicacion con FOSUserBundle de por medio.

Empezamos:

1- He definido en el firewall que el rol minimo para poder acceder a esa ruta debe ser algo parecido a esto, de esa forma otros usuarios que no tengan ese rol “minimo” no podran acceder a esa/s ruta/s:

(security.yml)
access_control:
    - { path: ^/login$, roles: IS_AUTHENTICATED_ANONYMOUSLY }
    - ... //seguridad de acceso en otras rutas
    - { path: ^/users, roles: ROLE_GUEST }

2 – En mi bundle he creado una carpeta llamada “Security” donde metere los Voters para revisar la seguridad de las acciones sobre las entidades.

3 – Como en la gran mayoria de mis Voters van a hacerse las mismas (o parecidas) comprobaciones respecto a si puede acceder a ese recurso o no, me he hecho un “GenericVoter” abstracto donde se incluye la logica de la seguridad, el cual herederan el resto de Voters de mis zonas:

<?php
namespace efor\AppBundle\Security;

use Doctrine\Common\Collections\Criteria;
use efor\AppBundle\Entity\Role;
use efor\AppBundle\Entity\RoleZone;
use efor\UsuarioBundle\Entity\User;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\Security\Core\Authorization\Voter\AbstractVoter;
use Symfony\Component\Security\Core\User\UserInterface;

// La clase AbstractVoter está disponible a partir de Symfony 2.6
abstract class GenericVoter extends AbstractVoter
{
    const READ = 'read';
    const CREATE = 'create';
    const EDIT = 'edit';
    const DELETE = 'delete';
    
    protected function getSupportedAttributes()
    {
        return array(self::CREATE, self::EDIT, self::DELETE, self::READ);
    }

    abstract protected function getSecureZone();

    /**
     * @param string $action
     * @param object $object
     * @param null $user
     *
     * @return bool
     */
    protected function isGranted($action, $object, $user = null)
    {
        if(!$user instanceof User){
            return false;
        }

        //Buscamos todos los roles del usuario(>= 1 role)
        $roleUser = $user->getRoleUser();

        //recorremos los posibles roles que tenga el usuario
        /** @var Role $role */
        foreach($roleUser as $role){ 
            //buscamos las zonas que tenga disponibles ese role
            $securedZones = $role->getRoleZone(); 

            /** @var RoleZone $securedZone */
            foreach($securedZones as $securedZone)
            {
                if($securedZone->getZone()->getName() === $this->getSecureZone()){
                    switch($action){
                        case self::READ:
                                return $securedZone->getAllowedToRead();
                            break;
                        case self::CREATE:
                                return $securedZone->getAllowedToAdd();
                            break;
                        case self::EDIT:
                                return $securedZone->getAllowedToEdit();
                            break;
                        case self::DELETE:
                                return $securedZone->getAllowedToDelete();
                            break;
                        default: return false;
                            break;
                    }
                }
            }
        }

        return true; //dejamos pasar siempre que se pueda
    }
}

En esta clase hay que destacar que hereda de AbstractVoter que definde una serie de metodos publicos implementados, y otros abstractos. De esa forma, asi nosotros añadimos las posibles acciones sobre un “objeto (entidad)” y una funcion abstracta getSecureZone() donde cada Voter especifico definira la “zona” donde se revisara la seguridad. Dentro de la funcion isGranted() comprobamos que esa zona este definida en alguno de los roles del usuario que la intenta acceder para ver si puede leer, crear, editar o eliminar un objeto de ese tipo.

4 – Definimos nuestro Voter especifico para comprobar la seguridad cuando se accede a los usuarios tal que así:

class UserVoter extends GenericVoter
{
    protected function getSupportedClasses()
    {
        return array('efor\UsuarioBundle\Entity\User');
    }

    protected function getSecureZone()
    {
        //en mi caso apunta a una clase donde tengo definidas 
        //mis rutas seguras - user
        return ZoneMapping::USERS_ROUTE_NAME;
    }
}

Este Voter comprobara para la entidad User (es la clase donde se podran hacer las 4 operaciones posibles – CRUD) cuando acceda a la zona ‘user’ si es posible conceder el acceso.

5 – Definimos un servicio en nuestro ‘services.yml’ que atendera para garantizar la seguridad:

services:
    ...
    user_voter:
        class: efor\AppBundle\Security\UserVoter
        public: false
        tags:
            - { name: security.voter }

6 – Ahora tenemos que usar las funciones que proporciona Symfony de chequeo del acceso para que este Voter se ejecute como responsable. Para ello en nuestro UserController deberemos añadir la/s siguiente/s comprobaciones a traves de anotaciones(cuando se pueda) o condicionales:

class UserController
 {
/**
 * @Route("/create", name="user_create")
 */
public function createUserAction(Request $request)
{
     $newUser = new User();

     if(!$this->isGranted(GenericVoter::CREATE, $newUser)){
         throw $this->createAccessDeniedException();
     }
     $form = ...
}
/**
 * @Route("/list", name="user_list")
 */
public function listUserAction()
{
    if(!$this->isGranted(GenericVoter::READ, new User())){
        throw $this->createAccessDeniedException();
    }
    ...
}

/**
* @Route("/show/{id}", name="user_show")
* @ParamConverter("user", class="efor\UsuarioBundle\Entity\User")
* @Security("is_granted('read', user)")
*/
public function showUserAction(User $user)
{
    if($user === $this->getUser()){
        return $this->redirectToRoute('profile_show');
    }

    return $this->render('UsuarioBundle:User:showUser.html.twig', array(
        'user' => $user
    ));
}

//IDEM con el resto de acciones del CRUD o Actions 
//que queramos proteger

Por una parte destacamos la anotacion @Security donde le decimos que ejecute la funcion isGranted y le pasamos la accion y un objeto(que se obtiene con el @ParamConverter previo).

De la misma forma pero sin usar anotacion, es llamando dentro de nuestro Action a $this->isGranted() pasandole la misma informacion ( a veces necesitamos instanciar un nuevo objeto para que se pueda comprobar) y lanzando una excepcion en caso negativo.

7 – Si queremos revisar la seguridad en las vistas para mostrar u ocultar botones podemos hacer uso de la funcion is_granted() en TWIG de la siguiente forma:

{% set editAction = constant('efor\\AppBundle\\Security\\GenericVoter::EDIT') %}
{% if is_granted(editAction, user) %}
    <a href={{ url('user_edit', { 'id': user.id } }}>Editar</a>
{% endif %}

De esta forma mostrar un link a la ruta de editar usuario solo si mi Voter lo permite validando los permisos sobre ese objeto.


 

Y eso es todo, podeis encontrar mas informacion sobre los Voters en el link que os deje al principio del post para que se ajuste a vuestras necesidades.

Si te ha gustado el articulo, no dudes en compartirlo en las redes sociales para que llegue a toda la comunidad Symfony Hispana. Si tienes otras formas de usarlo, o crees que harias algo distinto y que aportara valor a los demas, no dudes en dejar tu comentario al respecto, y lo revisare lo antes que pueda.

Saludos ¡

 

 

 

Crear un bundle distribuible para Symfony 2

vendorsHoy voy a explicar como se puede hacer un bundle para Symfony 2 distribuible y descargable para otros proyectos tuyos o de tipo Open Source y que cualquiera lo pueda descargar y usar en su proyecto.

Después de buscar y buscar documentación acerca de crear tus propios bundles y poder distribuirlos para la comunidad, por fin encontré una mini-guia que explica por encima como se puede hacer. Os voy a hacer yo una pequeña explicación de como lo he hecho yo para que podais ir practicando con vuestros bundles.

Empezamos ¡

1 – Crear un nuevo proyecto

Lo primero es crearse un proyecto nuevo vacío de Symfony en el cual vamos a crear un bundle y meteremos ahí todo el código que necesitemos, limpito de cualquier cosa para que no de problemas. Para ello ejecutamos:

*Si tenemos el instalador de Symfony:

> symfony new distributable-project

*Si no, siempre nos quedara composer(global):

> composer create-project symfony/framework-standard-edition distributable-project

Esto nos creara una nueva carpeta con la estructura del proyecto en Symfony. A continuación debemos crearnos un nuevo bundle.

> php app/console generate:bundle

Nos saltara el asistente donde deberemos completar todas las preguntas que nos hace:

-Bundle namespace: Company/YourBundle

Bundle name [CompanyYourBundle]: YourBundle

Target Directory [src/]: (Pulsamos Intro)

Configuration format (annotation, yml, xml, php) [xml]: annotation (Elegid la que mas os guste)

Y listo, ya hemos creado nuestro nuevo bundle.

2 – Crear el repositorio

Lo siguiente que haremos sera crear un repositorio en Github dentro de nuestra cuenta para ir subiendo los cambios que vayamos metiendo y además poder descargar en otros proyectos el bundle desde allí.

2.1 – Github

Accedemos con nuestro usuario en Github y creamos un nuevo proyecto dandole un nombre.

2.2 – Inicializamos nuestro repositorio Git en local

Ahora viene la pieza clave de todo esto. Como lo que queremos es distribuir un bundle, nuestro repositorio Git deberá ser inicializado dentro de ese bundle:

> cd src/Company/YourBundle

>git init

> git add -A

> git commit -m “First commit”

> git remote add origin https://github.com/YourAccount/YourBundle.git

> git push -u origin master

De esta forma hemos metido todo el bundle dentro de Github.

2.3 – Crear el composer.json

Ahora otra pieza clave es crear el archivo composer.json dentro del bundle que queremos distribuir.

> sudo nano src/Company/YourBundle/composer.json

{
    "name" : "company/your-bundle",
    "version": "v1.0.0",
    "description" : "Fill with your description",
    "type" : "symfony-bundle",
    "authors" : [{
        "name" : "Your name",
        "email" : "Your email",
        "homepage": "Your site"
    }],
    "keywords" : [
        "put",
        "your",
        "keywords",
        "here"
    ],
    "license" : [
        "MIT"
    ],
    "require" : {
        "php": ">=5.4",
        "symfony/framework-bundle": ">=2.2",
        "symfony/symfony": ">=2.2",
        //Add your require libraries here...
    },
    "autoload" : {
        "psr-4" : {
            "Company\\NameBundle\\" : ""
        }
    }
}

Este es un ejemplo de como podría ser el composer.json. Podeis ajustarlo a vuestras necesidades según los requisitos de vuestro bundle.

Por descontado, tenemos que subir este fichero al repositorio en Github.

> git add -A

> git commit -m “Added composer json file”

> git push origin master

3 – Probar a instalar dicho bundle en otro proyecto

Ahora solo nos queda usar dicho bundle dentro de otro proyecto, ya sea nuevo o uno que ya estemos desarrollando. Para ello tenemos que añadir en nuestro composer.json del proyecto donde vayamos a usar nuestro bundle distribuido lo siguiente:

 

“require”: {

…,

“company/your-bundle”: “dev-master” //o la rama que queramos

},

repositories”: [{

“type”: “vcs”,

“url”: “https://github.com/YourAccount/YourBundle.git

}]

Ahora solo nos queda actualizar nuestros vendors y ver si efectivamente se ha instalado nuestro bundle como un vendor dentro de nuestro proyecto:

>> composer update company/your-bundle

> … Installing company/yourbundle (dev-master)

Si todo a ido bien, deberíamos tener dentro de /vendor/Company/YourBundle nuestro bundle ya instalado.

Ahora debemos inicializarlo en nuestro archivo appKernel.php para poder hacer uso de el:

$bundles = [

… ,

new Company\YourBundle\YourBundle()

];

Y ya esta ¡ Con esto ya podremos empezar a usar nuestro bundle en cualquier otro proyecto.

En symfony recomiendan seguir una serie de buenas practicas a la hora de hacer un bundle ditribuible y ademas ojear otros proyectos famosos para ver como lo han resuelto ellos. Os dejo aquí el enlace de las buenas practicas para que le pegueis un vistazo.

4 – Resumen

Eso es todo por hoy.

Por supuesto que el composer.json es un pequeño ejemplo y debereis buscar mas información acerca de todas las opciones que os ofrece Composer para ese fichero.

Tampoco cabe decir que, esto son algunas pruebas que voy haciendo yo y que me sirve de recordatorio para en un futuro acordarme como lo hice y poder hacerlo en otros proyectos. Este articulo esta sujeto a errores y fallos como todo en este mundo, asi que se comprensible y no me castigues muy duro. Es más, ayudame a mejorar y si ves algun fallo o sugerencia siempre seran bienvenidos.

Uno de mis objetivos a corto-medio plazo es el de crear un bundle que hice para uno de los proyectos en los que trabaje y distribuirlo ya que no encontre nada sencillo que se pudiera acoplar a lo que necesitaba, pero eso sera mas adelante.

Gracias por leer, y si me haces un favor, comparte este post con tu equipo, o en redes sociales y ayudame a difundir el conocimiento y aprendizaje con Symfony2.

Nos vemos pronto ¡

Fuentes:

-http://stackoverflow.com/questions/21523481/symfony2-creating-own-vendor-bundle-project-and-git-strategy
-https://github.com/LilaConcepts/LilaConceptsBestPracticeBundle/blob/master/composer.json

Actualización(29/02/2016):

Puede ocurrir que si el bundle no es “publico” quizas no encuentre el repositorio de donde descargarse el bundle para nuestro proyecto y composer nos arroje un error en consola al intentar descargase ese “vendor”.

En ese caso, habria que rellenar lo siguiente para decirle la url del repositorio en nuestro composer.json:

"repositories": [{
    "type": "package",
    "package": {
        "name": "shtumi/useful-bundle",
        "version": "2.0",
        "source": {
            "url": "https://github.com/shtumi/ShtumiUsefulBundle.git",
            "type": "git",
            "reference": "2.0"
        }
    }
}],
"require": {
    ...
    "shtumi/useful-bundle": "2.0"
},

Despues hacemos >>composer update -o y listo.

Saludos ¡¡