Documentazione italiana di Symfony

Purtroppo è stato deciso di rimuovere qualsiasi traduzione della documentazione ufficiale di Symfony. Al momento della decisione le uniche traduzioni superstiti, delle quattro indicate, erano la nostra e quella Francese. Uno scarno comunicato di una settimana fa, pervenuto da un membro di SensioLabs al responsabile della traduzione francese e a chi vi scrive, ha anticipato tale decisione, che è stata messa in pratica pochi giorni fa.
Nel rispetto della licenza CC BY-SA 3.0, la traduzione (non più ufficiale) italiana della documentazione di Symfony è al momento presente su docs.symfony.it. I contributi sono sempre ben accetti, sull’apposito repository github.com/garak/symfony-docs-it.

Fixture con contenitore di servizi nei test funzionali

A volte può essere utile avere a disposizione il contenitore di servizi nelle fixture usate nei test funzionali. Un caso tipico è l’uso di FOSUserBundle, che mette a disposizione un servizio UserManager per creare utenti, utilizzabile quindi anche per crearli all’interno delle fixture.
Purtroppo la documentazione a riguardo è alquanto avara di informazioni, per cui condivido qui questa soluzione, mostrando un esempio.
Ecco un possibile file di fixture per gli utenti:

<?php
 
namespace Acme\PippoBundle\DataFixtures\ORM;
 
 
use Doctrine\Common\DataFixtures\AbstractFixture;
use Doctrine\Common\DataFixtures\OrderedFixtureInterface;
use Doctrine\Common\Persistence\ObjectManager;
use Symfony\Component\DependencyInjection\ContainerAwareInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
 
class LoadUserData extends AbstractFixture implements OrderedFixtureInterface, ContainerAwareInterface
{
    private $container;
 
    public function setContainer(ContainerInterface $container = null)
    {
        $this->container = $container;
    }
 
    public function load(ObjectManager $manager)
    {
        $userManager = $this->container->get('fos_user.user_manager');
 
        $user1 = $userManager->createUser();
        $user1
            ->setUsername('pippo')
            ->setEmail('pippo@example.org')
            ->setFirstName('Mallo')
            ->setLastName('Di Noce')
            ->setBirthday(new \DateTime('1977-07-07'))
            ->setEnabled(true)
            ->setPlainPassword('mallodinoce')
        ;
        $userManager->updateUser($user1, false);
        $manager->persist($user1);
        $this->addReference('user1', $user1);
 
        $manager->flush();
    }
 
    public function getOrder()
    {
        return 1;
    }
}

Questa fixture si può usare in un test in questo modo:

<?php
 
namespace Acme\PippoBundle\Tests\Controller;
 
use Acme\PippoBundle\DataFixtures\ORM\LoadUserData;
use Doctrine\Common\DataFixtures\Executor\ORMExecutor;
use Doctrine\Common\DataFixtures\Purger\ORMPurger;
use Symfony\Bridge\Doctrine\DataFixtures\ContainerAwareLoader as Loader; // il trucco è qui..
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase
 
class DefaultControllerTest extends WebTestCase
{
    public function setUp()
    {
        $kernel = static::createKernel();
        $kernel->boot();
        $container = $kernel->getContainer();
        $loader = new Loader($container);  // ... e qui
        $loader->addFixture(new LoadUserData);
        $purger = new ORMPurger();
        $executor = new ORMExecutor($this->em, $purger);
        $executor->execute($loader->getFixtures());
    } 
 
    public function testIndex()
    {
        $client = static::createClient();
        $crawler = $client->request('GET', '/');
        $this->assertTrue($client->getResponse()->isSuccessful());
    }
}

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.

velocizzare il deploy con capifony

Come probabilmente saprete, Capifony (per chi non lo conoscesse, è lo standard de facto per gestire i deploy su Symfony2) purtroppo ha un’impostazione predefinita che reinstalla tutti i vendor a ogni deploy. Questo vuol dire che ogni volta che facciamo un deploy dobbiamo aspettare che il server di produzione (o comunque il server su cui stiamo eseguendo il deploy stesso) scarichi da github tutte le librerie necessarie. Ogni volta, anche se molto probabilmente le librerie dei vendor non sono state modificate dal deploy precedente. In realtà, quando anche fossero state modificate, per esempio per un aggiornamento di Symfony, non sarebbe più efficiente scaricare solo le modifiche, piuttosto che tutto quanto? Dopotutto, è esattamente ciò che facciamo sulle nostre macchine di sviluppo.
Ecco una possibile soluzione, da riportare nel proprio file di configurazione deploy.rb.

# app/config/deploy.rb
# [...]
set :vendors_mode, "install"
set :update_vendors, true
before "symfony:vendors:install", "symfony:copy_vendors"
 
namespace :symfony do
  desc "Copy vendors from previous release"
  task :copy_vendors, :except => { :no_release => true } do
    pretty_print "--> Copying vendors from previous release"
    run "cp -a #{previous_release}/vendor/* #{latest_release}/vendor/"
    puts_ok
  end
end

Aggiornamento: il codice, ampliato e migliorato, è stato pubblicato come ricetta sul sito di Capifony.