Sonata e sortable

Chi avesse l’esigenza di gestire in Sonata un’entità con applicato il behaviour sortable di Doctrine2 si troverebbe con la sgradita sorpresa di non poter operare le comuni operazioni necessarie in questi casi: tipicamente, spostare le righe in su, in giù o in cima alla lista.
Purtroppo tale behaviour non offre alcun metodo che astragga le operazioni di cui sopra: l’unico modo per spostare un oggetto è quello di assegnargli una nuova posizione, dopo di che gli altri oggetti saranno riordinati di conseguenza.
Vediamo dunque come ottenere il risultato desiderato in Sonata. Per farlo, supponiamo di avere un’entità chiamata Article. Supponiamo inoltre che la proprietà che indica la posizione in tale entità si chiami $position.
Per prima cosa, dobbiamo creare un controllore che implementi le nostre nuove azioni di spostamento.

<?php
 
namespace Acme\MioBundle\Controller;
 
use Sonata\AdminBundle\Controller\CRUDController;
use Symfony\Component\HttpFoundation\RedirectResponse;
 
class ArticleAdminController extends CRUDController
{
    /**
     * Move element up
     *
     * @param integer $id
     */
    public function moveupAction($id)
    {
        $object = $this->admin->getObject($id);
        if ($object->getPosition() > 0) {
            $object->setPosition($object->getPosition() - 1);
            $this->admin->update($object);
        }
        if ($this->isXmlHttpRequest()) {
            return $this->renderJson(array(
                'result'    => 'ok',
                'objectId'  => $this->admin->getNormalizedIdentifier($object)
            ));
        }
        $this->get('session')->setFlash('sonata_flash_info', 'Elemento spostato in su.');
 
        return new RedirectResponse($this->admin->generateUrl('list', $this->admin->getFilterParameters()));
    }
 
    /**
     * Move element top
     *
     * @param integer $id
     */
    public function movetopAction($id)
    {
        $object = $this->admin->getObject($id);
        if ($object->getPosition() > 0) {
            $object->setPosition(0);
            $this->admin->update($object);
        }
        if ($this->isXmlHttpRequest()) {
            return $this->renderJson(array(
                'result'    => 'ok',
                'objectId'  => $this->admin->getNormalizedIdentifier($object)
            ));
        }
        $this->get('session')->setFlash('sonata_flash_info', 'Elemento spostato in cima.');
 
        return new RedirectResponse($this->admin->generateUrl('list', $this->admin->getFilterParameters()));
    }
}

Le uniche parti da adattare in questo controllore sono il namespace e il nome della classe.
Ora occorre dire a Sonata di usare questo controllore: possiamo farlo nel file di configurazione dei servizi (services.xml o services.yml), semplicemente sostituendo SonataAdminBundle:CRUD con AcmeMioBundle:ArticleAdmin nel terzo argument.
L’ultima parte da modificare è la classe ArticleAdmin.
Innanzitutto sarebbe logico che gli elementi siano ordinati in base alla posizione: aggiungiamo quindi la seguente proprietà:

 protected $datagridValues = array(
    '_page'       => 1,
    '_sort_order' => 'ASC',
    '_sort_by'    => 'position',
 );

Modifichiamo quindi il metodo configureListFields() in questo modo (ovviamente campi e azioni possono variare):

public function configureListFields(ListMapper $listMapper)
{
    $listMapper
        ->add('title')
        ->add('abstract')
        ->add('position')
        ->add('_action', 'actions', array(
            'actions' => array(
              'edit'    => array(),
              'view'    => array(),
              'delete'  => array(),
              'moveUp'  => array('template' => 'AcmeMioBundle:Article:_moveup.html.twig'),
              'moveTop' => array('template' => 'AcmeMioBundle:Article:_movetop.html.twig'),
            )
        ))
    ;
}

Infine non resta che creare i template:

{# Acme/MioBundle/Resources/view/Article/_moveup.html.twig #}
{% if object.position > 0 %}
    <a class="moveup_link" href="{{ path('admin_article_moveup', {id: object.id}) }}" title="Sposta su">↑</a>
{% endif %}
 
{# Acme/MioBundle/Resources/view/Article/_movetop.html.twig #}
{% if object.position > 1 %}
    <a class="movetop_link" href="{{ path('admin_article_movetop', {id: object.id}) }}" title="Sposta in cima">↑↑</a>
{% endif %}

Purtroppo non ho potuto implementare lo spostamento in giù, non avendo ancora trovato il modo di verificare che la posizione non sia già l’ultima.

Symfony 2.1 validazione a cascata

In Symfony2 incorporare i form è davvero facile come mostrato  nella guida ufficiale di Symfony per la creazione e l’associazione di due form.

In questo tip parleremo della validazione in cascata di sottoform.

Supponiamo di avere un ProductForm che incorpora CategoryForm, entrambi con annotazioni che impostano i diversi vincoli di validazione per ciascuna classe. Seguendo la guida scriveremmo in ProductForm:

$builder->add('category', new CategoryForm());

Ci aspettiamo che CategoryForm sia inglobato in ProductForm e ciascun campo automaticamente validato in base ai vincoli di validazione di CategoryForm e ProductForm.

Noteremo con stupore, che dalla versione 2.1 di Symfony CategoryForm non viene validato automaticamente.

Questo è dovuto all’introduzione di un settaggio specifico dei form: cascade_validation

Il parametro, infatti, suggerisce al form builder se validare il form incorporato. Di default questo settaggio è impostato a false, quindi per validare il nostro sottoform con i vincoli di validazione specificati nella classe, c’è bisogno di esplicitare a true l’opzione nella classe CategoryForm, nella funzione setDefaultOptions :

public function setDefaultOptions(OptionsResolverInterface $resolver)
{
    $resolver->setDefaults(array(
        'data_class' => 'namespace\entita\correlata',
        'cascade_validation' => true,
    ));
}

Così facendo, ProductForm provvederà a validare anche tutti i campi di CategoryForm.