Curso de Symfony completo con ejercicios resueltos gratis

Durante estos días he estado impartiendo un curso de Symfony en el trabajo. He confeccionado el material basándome en la documentación de Symfony que he encontrado en su página oficial, asi como en tutoriales de otras páginas. Trata las bases de Symfony, asumiendo que sabes programar en PHP.

El curso está estructurado asi:

  • Día 1: Introducción
    • Introducción a modelo-vista-controlador (MVC) y al framework Symfony
    • Instalación de Symfony en WAMP
    • Introducción al framework
    • Routing: bases (placeholders, defaults, requirements, debug, generación de URLs)
  • Día 2: Rutas y Controlador
    • Controlador: teoría (mensajes flash, manejo de errores, redirección, clase BaseController, sesiones, cookies) y ejercicios
    • Routing: ejercicios
  • Día 3: Vista
    • PHP+HTML vs TWIG
    • Herencia de plantillas
    • Filters y tags
    • Assets (CSS, JS, IMGs) sin Assetic y con Assetic
    • Internal linking
    • Debugging
    • Ejercicios
  • Día 4: Forms
    • Entity (objeto del formulario)
    • Tipos de campos
    • Renderizado simple y avanzado
    • Recogida de resultados
    • Validación (seguridad)
    • Reutilización: creación de clase asociada al formulario
    • Form embedding
    • Ejercicios de formularios
  • Día 5: Doctrine + Seguridad
    • Qué es Doctrine
    • Configuración de Doctrine en Symfony (una o varias BD)
    • Creación de la BD
    • Entity: mappeado de BD-Entidad
    • Generación automática de getters y setters
    • Generación de tablas
    • Inserción, modificación, recuperación y borrado de datos en la BD (introducción a DQL)
    • Ejercicios de Doctrine
    • Seguridad: basic HTTP authentication
    • Seguridad: autenticación y autorización (por formulario, IP, SSL, ACLs)
    • Seguridad:
  • A lo largo de los temas planteo varios ejercicios. Para que podáis probar, en el RAR del curso incluyo además mi carpeta de Symfony, que contiene las soluciones a todos los ejercicios, para colocarla en el WWWROOT de tu servidor web.

    Podéis DESCARGA el CURSO de SYMFONY completamente gratis (RAR) pulsando aqui

Symfony: disable the web debug toolbar in PROD environment [SOLVED]

According to Symfony documentation, the debug toolbar is disabled in production environment (this is, /Symfony/web/app.php/WHATEVS).

Well, mine (Symfony 2.0.13 running on top of a WAMP2.2 server) was not. It was active no matter the url I was using (app.php or app_dev.php). Irritating.

To disable it, edit web/app.php and change from:

$kernel = new AppKernel('prod', false);

To

$kernel = new AppKernel('prod', true);

Why is that?

AppKernel class extends Kernel class, and take a look at its constructor, more precisely to the second parameter:

public __construct(string $environment, Boolean $debug)
 
Constructor.
 
Parameters
string 	$environment 	The environment
Boolean 	$debug 	Whether to enable debugging or not

FOSJsRoutingBundle: ejemplo paso por paso

Ayer os contaba cómo instalar FOSJsRoutingBundle en WAMP (Windows) y hoy veremos un ejemplo de cómo usar FOSJsRoutingBundle en nuestras plantillas TWIG.

Hoy os traigo un nuevo minitutorial sobre el tema de moda estos días, Symfony.

Propósito

Aprovecharé el gestor de tareas que construimos con Symfony ayer para incluir una nueva funcionalidad, que será borrar tareas.

Sin FOSJsRoutingBundle

Realmente, podríamos proceder sin utilizar FOSJsRoutingBundle. Recordemos que TWIG nos permite generar rutas con parámetros.

Veamos cómo hacerlo SIN FOSJsRoutingBundle.

Sería tan sencillo como crear una nueva ruta en routing.yml:

cursoTaskBundle_task_delete:
    pattern: /{id}/delete
    defaults: { _controller: cursoTaskBundle:Default:delete, id: 0}
    requirements:
        id: \d+

La acción necesaria en el controlador (DefaultController.php), que contiene la invocación a la función de Doctrine que borra un elemento de la BD.

public function deleteAction($id){
// borrado de tarea de la BD
	if (!$id){
		throw $this->createNotFoundException('No se puede borrar una tarea si no se especifica su ID');
	}
	$em = $this->getDoctrine()->getEntityManager();
	$task = $em->getRepository('cursoTaskBundle:Task2')->find($id);
 
	if (!$task){
		throw $this->createNotFoundException('No se ha encontrado la tarea '.$id.' en la BD');
	}
 
	// task exists
	// so, delete it
	$em->remove($task); 
	$em->flush(); 
 
	return $this->render('cursoTaskBundle:Default:success.html.twig', array(
		'message' => "Tarea borrada correctamente"
	));
}

Y crear las rutas en TWIG (editaremos la plantilla show.html.twig que construimos en el ejemplo mencionado anteriormente). Atención a la línea 16:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
{% block message %}
	{% if tasks is defined %}
		<table style="border-spacing: 5px; margin-right: 5px; border-collapse: separate;">
			<tr style="font-weight: bold;">
				<td></td><!-- editar -->
				<td></td><!-- borrar -->
				<td>NOMBRE</td>
				<td>FECHA</td>
				<td>DESCRIPCI&Oacute;N</td>
				<td>URL</td>
 
			</tr>
		{% for tarea in tasks %}
			<tr style="font-size: 0.8em">
				<td><a href=" {{ path('cursoTaskBundle_task_edit', { 'id' : tarea.id } ) }}" >edit</a></td><!-- editar -->
				<td><a href=" {{ path('cursoTaskBundle_task_delete', { 'id' : tarea.id } ) }}" >edit</a></td><!-- borrar -->
				<td>{{ tarea.task }}</td>
				<td>{{ tarea.dueDate|date("m/d/Y")  }}</td>
				<td>{{ tarea.description }}</td>
				<td>{{ tarea.url }}</td>
 
			</tr>
		{% endfor %}
		</table>
	{% else %}
		<p>No hay tareas. Puedes <a href="{{ path('cursoTaskBundle_task_new2') }}">Insertar una nueva tarea</a></p>
	{% endif %}
 
{% endblock message %}

Y ya estaría. El usuario, al pulsar sobre “delete”, sería redirigido a curso/task/(id)/delete, que borraría la tarea identificada por (id). Si vuestro usuario es de click fácil, como yo, esto podría causar el borrado involuntario de una tarea. Nunca está de más pedir una confirmación al borrado. En este caso vamos a construirla en Javascript y el ejemplo servirá para ver cómo utilizar FOSJsRoutingBundle.

Con FOSJsRoutingBundle

Deseamos que, cuando el usuario pulse sobre el link de “borrar tarea”, se le pregunte si está seguro de que desea hacerlo.

El primer paso será modificar nuestra plantilla en TWIG añadiendo los javascripts necesarios para que FOSJsRoutingBundle funcione, para lo cual reescribiremos el block script que definimos en nuestro primer ejemplo.

Además de incluir los JS (líneas 5 y 6), debemos crear una nueva función que genere un cuádro de diálogo tipo Sí/No. En caso de que el usuario pulse sí, habrá que redirigirle a la página que borra esa tarea (url_borrar). Es en la generación de esta url_borrar donde interviene FOSJsRoutingBundle (líneas 13 a 19)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
{% block script %}
		{% javascripts
			'@cursoe5Bundle/Resources/public/js/*'
		%}
		<script type="text/javascript" src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
		<script type="text/javascript" src="{{ path('fos_js_routing_js', {"callback": "fos.Router.setData"}) }}"></script>
		{% endjavascripts %}
 
	<script type="text/javascript">
	//<![CDATA[	
 
		// Sacar un dialogo de confirmacion y redireccionar al borrado, si procede.
		function estasSeguro(id){
			var url_borrar = Routing.generate('cursoTaskBundle_task_delete',{ "id": id });
			//alert(url_borrar);
			if (confirm("Seguro que deseas borrar?")){
				window.location = url_borrar;
			}
		}
 
	//]]> 
	</script>
{% endblock script %}

La ruta que borra las tareas es cursoTaskBundle_task_delete. Para poder acceder a una ruta vía FOSJsRoutingBundle tenemos que indicarlo de forma explícita en el fichero de rutas (routing.yml). Esto se conoce como exponer una ruta. Atención a las líneas 4 y 5:

1
2
3
4
5
6
7
cursoTaskBundle_task_delete:
    pattern: /{id}/delete
    defaults: { _controller: cursoTaskBundle:Default:delete, id: 0}
    options: 
        expose: true
    requirements:
        id: \d+

De no hacer esto, obtendríamos el error “Route 'cursoTaskBundle_task_delete does not exist” (lo veríamos en la consola de errores de javascript de nuestro navegador)

Y ya solo nos faltaría modificar show.html.twig de forma que al pulsar el enlace de borrar, se ejecute la función estasSeguro() que definimos anteriormente.

{% block message %}
	{% if tasks is defined %}
		<table style="border-spacing: 5px; margin-right: 5px; border-collapse: separate;">
			<tr style="font-weight: bold;">
				<td></td><!-- editar -->
				<td></td><!-- borrar -->
				<td>NOMBRE</td>
				<td>FECHA</td>
				<td>DESCRIPCI&Oacute;N</td>
				<td>URL</td>
 
			</tr>
		{% for tarea in tasks %}
			<tr style="font-size: 0.8em">
				<td><a href=" {{ path('cursoTaskBundle_task_edit', { 'id' : tarea.id } ) }}" >edit</a></td><!-- editar -->
				<td><a href="#" onclick="(estasSeguro({{ tarea.id }}))">delete</a></td><!-- borrar -->
				<td>{{ tarea.task }}</td>
				<td>{{ tarea.dueDate|date("m/d/Y")  }}</td>
				<td>{{ tarea.description }}</td>
				<td>{{ tarea.url }}</td>
 
			</tr>
		{% endfor %}
		</table>
	{% else %}
		<p>No hay tareas. Puedes <a href="{{ path('cursoTaskBundle_task_new2') }}">Insertar una nueva tarea</a></p>
	{% endif %}
 
{% endblock message %}

Y ya tendremos nuestro gestor de tareas con funcionalidad completa: insertar, editar y borrar.

Si deseamos exponer una lista de rutas más larga, podemos editar directamente config.yml:

# app/config/config.yml
fos_js_routing:
    routes_to_expose: [ route_1, route_2, ... ]

Symfony: cómo instalar un Bundle (FOSJsRoutingBundle)

Es algo que se pregunta mucha gente que empieza con Symfony: ¿cómo instalar un bundle nuevo?

En este ejemplo voy a indicar los pasos para instalar FOSJsRoutingBundle (que nos ayuda a crear rutas desde javascript). Mi instalación base de Symfony está en W:\WEB\wamp2.2\www\Symfony\

Si conoces GIT y lo usas, instalar un Bundle es tan simple como:

git submodule add git://github.com/FriendsOfSymfony/FOSJsRoutingBundle.git vendor/bundles/FOS/JsRoutingBundle

Si no conoces GIT (y no te apetece conocerlo), sigue estos pasos:

1. Descargar, descomprimir.
Lo primero es entrar a la web del bundle en cuestión y descargar el ZIP.

Después extraer la carpeta (que tendrá un nombre tal que FriendsOfSymfony-FOSJsRoutingBundle-526cf7d) en vendor/bundles/FOS y renombrarla a JsRoutingBundle.

En mi caso, queda en W:\WEB\wamp2.2\www\Symfony\vendor\bundles\FOS\JsRoutingBundle

2. Registrar el namespace en app/autoload.php (ojo, es en la parte de registerNamespaces, NO en la de registerPrefixes):

// app/autoload.php
$loader->registerNamespaces(array(
    // ...
    'FOS' => __DIR__.'/../vendor/bundles',
));

3. Registrar el bundle en app/AppKernel.php:

// app/AppKernel.php
public function registerBundles()
{
    $bundles = array(
        // ...
 
        new FOS\JsRoutingBundle\FOSJsRoutingBundle(),
    );
}

3. Registrar la ruta en app/config/routing.yml

# app/config/routing.yml
fos_js_routing:
    resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml"

4. Publicar los correspondientes assets

Si podéis crear symlinks:

php app/console assets:install --symlink web

Y si no podéis…

php app/console assets:install web

Y ya está… ¡instalado! 🙂

Todos los módulos se instalan de forma muy similar, asi que espero que este ejemplo os sirva.

EDIT:
Si la configuración no es correcta, os dará errores del tipo:

  The autoloader expected class "Symfony\Bundle\FOSJsRoutingBundle\DependencyInj
ection\FOSJsRoutingExtension" to be defined in file "W:\WEB\wamp2.2\www\Symfony\
app/../vendor/bundles\Symfony\Bundle\FOSJsRoutingBundle\DependencyInjection\FOSJ
sRoutingExtension.php". The file was found but the class was not in it, the clas
s name or namespace probably has a typo.

O del tipo:

W:\WEB\wamp2.2\www\Symfony>php app/console assets:install web
PHP Fatal error:  Class 'FOS\JsRoutingBundle\FOSJsRoutingBundle' not found in W:
\WEB\wamp2.2\www\Symfony\app\AppKernel.php on line 25
PHP Stack trace:
PHP   1. {main}() W:\WEB\wamp2.2\www\Symfony\app\console:0
PHP   2. Symfony\Component\Console\Application->run() W:\WEB\wamp2.2\www\Symfony
\app\console:22
PHP   3. Symfony\Bundle\FrameworkBundle\Console\Application->doRun() W:\WEB\wamp
2.2\www\Symfony\vendor\symfony\src\Symfony\Component\Console\Application.php:118
 
PHP   4. Symfony\Bundle\FrameworkBundle\Console\Application->registerCommands()
W:\WEB\wamp2.2\www\Symfony\vendor\symfony\src\Symfony\Bundle\FrameworkBundle\Con
sole\Application.php:66
PHP   5. Symfony\Component\HttpKernel\Kernel->boot() W:\WEB\wamp2.2\www\Symfony\
vendor\symfony\src\Symfony\Bundle\FrameworkBundle\Console\Application.php:80
PHP   6. Symfony\Component\HttpKernel\Kernel->initializeBundles() W:\WEB\wamp2.2
\www\Symfony\app\bootstrap.php.cache:511
PHP   7. AppKernel->registerBundles() W:\WEB\wamp2.2\www\Symfony\app\bootstrap.p
hp.cache:717
 
Fatal error: Class 'FOS\JsRoutingBundle\FOSJsRoutingBundle' not found in W:\WEB\
wamp2.2\www\Symfony\app\AppKernel.php on line 25
 
Call Stack:
    0.0003     635976   1. {main}() W:\WEB\wamp2.2\www\Symfony\app\console:0
    0.0228    2892904   2. Symfony\Component\Console\Application->run() W:\WEB\w
amp2.2\www\Symfony\app\console:22
    0.0274    3140600   3. Symfony\Bundle\FrameworkBundle\Console\Application->d
oRun() W:\WEB\wamp2.2\www\Symfony\vendor\symfony\src\Symfony\Component\Console\A
pplication.php:118
    0.0274    3140600   4. Symfony\Bundle\FrameworkBundle\Console\Application->r
egisterCommands() W:\WEB\wamp2.2\www\Symfony\vendor\symfony\src\Symfony\Bundle\F
rameworkBundle\Console\Application.php:66
    0.0274    3140600   5. Symfony\Component\HttpKernel\Kernel->boot() W:\WEB\wa
mp2.2\www\Symfony\vendor\symfony\src\Symfony\Bundle\FrameworkBundle\Console\Appl
ication.php:80
    0.0274    3140600   6. Symfony\Component\HttpKernel\Kernel->initializeBundle
s() W:\WEB\wamp2.2\www\Symfony\app\bootstrap.php.cache:511
    0.0274    3141224   7. AppKernel->registerBundles() W:\WEB\wamp2.2\www\Symfo
ny\app\bootstrap.php.cache:717

Si habéis seguido los pasos de arriba al pie de la letra, debería funcionar. No obstante, aseguraos de que el fichero Symfony\vendor\bundles\FOS\JsRoutingBundle existe y contiene

namespace FOS\JsRoutingBundle;
 
use Symfony\Component\HttpKernel\Bundle\Bundle;
 
/**
 * FOSJsRoutingBundle class.
 *
 * @author      William DURAND <william.durand1@gmail.com>
 */
class FOSJsRoutingBundle extends Bundle
{
}

Symfony: create and update an Entity using doctrine [FULL EXAMPLE]

The Symfony form documentation is better than it used to be, but forms are still one of the most tricky things to figure out.

I just wanted to save someone’s time by showing a full working example that deals with forms, doctrine, views, routing and controller issues.

I really encourage you to read Symfony official documentation about forms and Doctrine before going on with my example. It will be useful to open The Symfony2 API in a new tab.

The goal

We’ll be building this “task reminder” web app using Symfony (forms and Doctrine). Hope this helps someone.

The Model (aka Entity)

So, lets take a look at my entity\Task.php file. Nothing fancy here. A task consists of an id, a task (name), a due date, an url and a description. The necessary information is provided to make Symfony “understand” our Task objets and map the attributes to Database table.

Note the strategy=AUTO parameter in Id field. When using this, Doctrine will generate Id’s begining with 1. This is useful, as will be seen after.

To avoid writing all the (stupid) lines related to getters and setters you could just write the attributes, then run…

php app/console doctrine:generate:entities curso/TaskBundle/Entity/Task

… and all the necessary setters and getters would be created (don’t you love Symfony already?)

<?php
// src/curso/TaskBundle/Entity/Task.php
namespace curso\TaskBundle\Entity;
 
// para el mapping entre campos de la BD y Entity
use Doctrine\ORM\Mapping as ORM;
 
/**
 * @ORM\Entity
 * @ORM\Table(name="task")
 */
class Task2
{
 
	/**
	* @ORM\Id
	* @ORM\Column(type="integer", unique=true)
	* @ORM\GeneratedValue(strategy="AUTO")
	*/
	protected $id;
 
	/**
	 * El nombre de la tarea, como máximo de 20 caracteres
	 * @ORM\Column(type="string", length=20)
	*/
    protected $task;
 
	/**
	 * La fecha de finalización de la tarea
	 * @ORM\Column(type="date")
	 */
    protected $dueDate;
 
 
	/**
	 * La descripción de la tarea
	 * @ORM\Column(type="text", length=300)
	*/
	protected $description;
 
	/**
	 * La URL relacionada de algún modo con la tarea
	 * @ORM\Column(type="text", nullable=true)
	*/
	protected $url;
 
    public function getTask(){
        return $this->task;
    }
 
    public function setTask($task){
        $this->task = $task;
    }
 
    public function getDueDate(){
        return $this->dueDate;
    }
 
    public function setDueDate(\DateTime $dueDate = null){
        $this->dueDate = $dueDate;
    }
 
	public function getDescription(){
        return $this->description;
    }
 
    public function setDescription($description){
		$this->description = $description;
    }
 
    public function getUrl(){
		return $this->url;
    }
 
	public function setUrl($url){
		$this->url = $url;
	}
 
    public function getId()
    {
        return $this->id;
    }
 
    public function setId($id)
    {
        $this->id = $id;
    }
}

Lets add some validations to that class. This is Resources/config/validation.yml and contains the field descriptions (that will match the ones in the form definition). Along with the fields are the messages to display in case of form errors and some (stupid) URL check using a regex.

curso\TaskBundle\Entity\Task2:
    properties:
        task:
            - NotBlank:  {message: "Debes darle nombre a la tarea"}
            - MaxLength: {limit: 20, message: "El nombre de la tarea no puede tener más de {{ limit }} caracteres" }
            - MinLength: {limit: 1, message: "El nombre de la tarea no puede tener menos de {{ limit }} caracteres" }
        dueDate:
            - NotBlank: ~
            - Type: \DateTime
            - Date: {message: "Debes insertar una fecha válida"}
        description:
             - MaxLength: { limit: 300, message: "Descripción limitada a {{ limit }} caracteres" }
        url:
            - Url: 
            - Regex: 
                pattern: "/.*\.es$/"
                message: "La URL debe terminar en .es"

At this point you should create the table in the database. I am assuming you have a MySQL server and Doctrine already configured to use it.

Well, I got good news for you. No (boring) process of table creation. Symfony does the job for you! 🙂

php app/console doctrine:schema:update --force

The routing

Next, we should define the paths in Resources/config/routing.yml
Four paths will be defined: one for the new task, one for showing all tasks, another one to edit a task and another one to show messages (success)
Note the (security) requirement in the id parameter (must be a number!) and default id value (0 is not an index that Doctrine produces)

# src/curso/TaskBundle/Resources/config/routing.yml
cursoTaskBundle_task_new:
    pattern: /new
    defaults: { _controller: cursoTaskBundle:Default:new }
 
cursoTaskBundle_task_show:
    pattern: /show
    defaults: { _controller: cursoTaskBundle:Default:show }	
 
cursoTaskBundle_task_edit:
    pattern: /{id}/edit
    defaults: { _controller: cursoTaskBundle:Default:edit, id: 0 }
    requirements: 
          id: \d+	
 
cursoTaskBundle_task_success:
    pattern: /success/{nombreTarea}
    defaults: { _controller: cursoTaskBundle:Default:success, nombreTarea: - }

The controller

Now, the juice. The code is explained in the comments, but is easy to follow although your spanish skills are not that great 😉

Every action that was previously defined in the routing part is created.

I would also like to share this image because it helps people understand what is going on with Forms.

<?php
 
// src/curso/TaskBundle/Controller/DefaultController.php
 
namespace curso\TaskBundle\Controller;
use curso\TaskBundle\Entity\Task;
use curso\TaskBundle\Entity\Task2;
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
 
 
class DefaultController extends Controller
{
    /* 
     * Nueva tarea
     */
    public function newAction(Request $request)
    {
	// quiero saber el anyo actual:
	$thisyear = date('Y');
 
        // vamos a crear una tarea nueva!
        $task = new Task2();
        $form = $this->createFormBuilder($task)
            ->add('task', 'text',array('label' => 'Tarea:'))
            ->add('dueDate', 'date', array('label' => 'Fecha limite', 'years' => range($thisyear, $thisyear+5)))
			->add('description', 'textarea', array('label' => 'Descripcion', 'required' => 'false'))
			->add('url', 'url', array('required' => false))
            ->getForm();
 
        if ($request->getMethod() == 'POST') {
                        // segunda vez que se llama a newAction, ahora ya tenemos los datos 
                        // de la tarea en el $request. Asi que hacemos el "bind" al formulario...
			$form->bindRequest($request);
 
                        // y comprobamos si es válido (validation.yml!)
			if ($form->isValid()){
				// hacer cosas con los datos del form.
                                // Guardarlos en BD, p ej.
 
				$em = $this->getDoctrine()->getEntityManager();
				$em->persist($task); // = decirle a Symfony que "controle" este objeto!
				$em->flush();        // podría agrupar varias queries y despues flush las lanzaría a la BD a la vez!
 
				return $this->redirect($this->generateUrl('cursoTaskBundle_task_success', array('nombreTarea' => $task->getTask()) ));
			}
		}
 
        return $this->render('cursoTaskBundle:Default:new2desplegado.html.twig', array(
            'form' => $form->createView(),
        ));
    }
 
    /* 
     * Obtiene todas las tareas
     */
    public function showAction(){
 
		$tasks = $this->getDoctrine()
			->getRepository('cursoTaskBundle:Task2')
			->findAll();
			//->findByTask('Ejemplo tarea');  //con esta línea podría buscar una tarea con ese nombre exacto
 
		return $this->render('cursoTaskBundle:Default:show.html.twig', array(
			'tasks' => $tasks
		));
	}
 
 
	/*
	 * Editar una tarea
	 */ 
	public function editAction($id, Request $request)
        {
		if (!$id) {
			throw $this->createNotFoundException('No se encuentra la tarea con id = '.$id);
		}
 
		// valido que existe la tarea asociada a ese ID...
		$em = $this->getDoctrine()->getEntityManager();
		$task = $em->getRepository('cursoTaskBundle:Task2')->find($id);
		if (!$task){
			throw $this->createNotFoundException('No se encuentra la tarea con id = '.$id);
		}
 
		// quiero saber el anyo actual:
		$thisyear = date('Y');
 
                // Creo el formulario (con sus campos) de forma dinámica.
                // Como voy a repetir el formulario que ya creé en newAction, quizá sería interesante
                // plantearme crear una clase aparte y usar $this->createForm para crearlo "de forma estática"
		$form = $this->createFormBuilder($task)
			->add('task', 'text',array('label' => 'Tarea:'))
                        ->add('dueDate', 'date', array('label' => 'Fecha limite', 'years' => range($thisyear, $thisyear+5)))
			->add('description', 'textarea', array('label' => 'Descripcion', 'required' => 'false'))
			->add('url', 'url', array('required' => false))
                        ->getForm();
 
		if ($request->getMethod() == 'POST') {
			// ya he recibido el resultado del formulario...
 
			// mappeo los valores del request a los del form...
			$form->bindRequest($request);
 
			// y compruebo si es válido...
			if ($form->isValid()){
				// es válido, asi que actualizo la BD...
				$em->flush();
				return $this->redirect($this->generateUrl('cursoTaskBundle_task_success', array('nombreTarea' => $task->getTask()) ));
			}
		}
 
		// PRIMERA INVOCACION A EDIT: hay que dejarle al user modificar la info!
		return $this->render('cursoTaskBundle:Default:edit2desplegado.html.twig', array(
                        'form' => $form->createView(),
			'id'   => $task->getId(),
        ));
    }
 
    /*
     * Mostrar pantalla de "éxito al realizar las operaciones".
     */
    public function successAction($nombreTarea){
		if (!strcmp($nombreTarea,"-")){
			$message = "Qu&eacute; tramas, moreno? ¡No deberías invocar esta URL de forma manual!";
		}
		else{
			// Voy a generar un mensaje...
			$message = "La tarea '".$nombreTarea."' se ha guardado.";
		}
 
		return $this->render('cursoTaskBundle:Default:success.html.twig', array(
           'message' 	=> $message
		));
	}

The Templates (view) + Assets (Css)

And last but not least, the TWIG templates I used along with the CSS.
Some days ago I wrote about how to manage Assets with Assetic. Note how this is applied here (and YUI CSS Compressor filter is used).

The first template is a general-purpose three-column layout that will be the “base” for our minisite. Let’s call it template-base-curso.html.twig

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
{% block head %}
<head>
	<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
	<title>
		{% block title %}
			{{ title|default("Template Base para TWIG") }}
		{% endblock title %}
	</title>
 
 
	{% block description %}
	<meta name="description" content="{{ description|default('Plantilla TWIG base por Miguel Martin') }}"/>
	{% endblock description %}
 
	{% block keywords %}
	<meta name="keywords" content="twig, template, free, design, 960, grid" />
	{% endblock keywords %}
 
	{% block styles %}
		{% stylesheets
			'@cursoe5Bundle/Resources/public/css/layout/mainStyle.css'
			'@cursoe5Bundle/Resources/public/css/layout/reset.css'
			'@cursoe5Bundle/Resources/public/css/layout/colors.css'
			output = 'css/compiled/styles.css'
			filter = 'yui_css'
		%}
		<link href="{{ asset_url }}" type="text/css" rel="stylesheet" media="screen" />
		{% endstylesheets %}
	{% endblock styles %}
 
	{% block script %}
		{% javascripts
			'@cursoe5Bundle/Resources/public/js/*'
		%}
		<script src="{{ asset_url }}" type="text/javascript"></script>
		{% endjavascripts %}
 
	<script type="text/javascript">
	//<![CDATA[
		//$(document).ready(function(){ 	
 
		//}); 
	//]]> 
	</script>
	{% endblock script %}
</head>
{% endblock head %}
 
<body>
	<div id="wrapper">
 
<!-- ******************************************** HEADER ************************************ -->
 
{% block header %}
	<div id="header">
		<div id="titulo-centro">
			<h1> {{ titulo|default("TEMPLATE BASE") }}</h1>
		</div>
	</div>
{% endblock header %}
<!-- ******************************************** /HEADER ************************************ -->
 
 
	<div id="content">
 
	<div id="breadcrumbs">
	{% block breadcrumbs %}
		<ul>
			<li class"=first"><a href="{{ path('cursoe5Bundle_homepage') }}">HOME </a></li>
			<li class="actual"><a href="="{{ path('cursoTaskBundle_test_template') }}">Template</a></li>
		</ul>
	{% endblock breadcrumbs %}
	</div>
	<br />
 
<!-- ******************************************** LEFT-COLUMN ************************************ -->
	<div id="left-column">
{% block menusub %}	
		<div id="menu-sub">
			<ul>
				<li class"=first"><a href="{{ path('cursoe5Bundle_homepage') }}">HOME </a></li>
				<li class="actual"><a href="{{ path('cursoTaskBundle_test_template') }}">Template</a></li>
 
				<li><a href="{{ path('cursoTaskBundle_task_new2') }}">Insertar tarea</a></li>
				<li><a href="{{ path('cursoTaskBundle_task_show') }}">Mostrar tareas</a></li>
				<li class="last"><a href="#">link 4</a></li>
			</ul>
		</div>
{% endblock menusub %}
	</div>
<!-- ******************************************** /LEFT-COLUMN ************************************ -->
 
<!--====MAIN COLUMN START====-->
<div id="main-column">
{% block maincolumn %}
 
<h2>Template base en TWIG</h2><br />
<p class="text-justify">Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum</p>
<br />
<p class="text-justify">Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?</p>
 
 
{% endblock maincolumn %}
</div>
 
<!--====MAIN COLUMN END====-->
 
 
<!--====RIGHT COLUMN START====-->
 
<div id="right-column">
{% block rightcolumn %}
 
<p class="text-justify xxsmall">But I must explain to you how all this mistaken idea of denouncing pleasure and praising pain was born and I will give you a complete account of the system, and expound the actual teachings of the great explorer of the truth, the master-builder of human happiness. No one rejects, dislikes, or avoids pleasure itself, because it is pleasure, but because those who do not know how to pursue pleasure rationally encounter consequences that are extremely painful. Nor again is there anyone who loves or pursues or desires to obtain pain of itself, because it is pain, but because occasionally circumstances occur in which toil and pain can procure him some great pleasure. To take a trivial example, which of us ever undertakes laborious physical exercise, except to obtain some advantage from it?</p>
 
{% endblock rightcolumn %}
</div>
 
<!--====RIGHT COLUMN END====-->
 
</div>
 
<!--=================CONTENT END==============-->
 
<!--=================FOOTER START==============-->
 
<div id="footer">
{% block footer %}
&copy;2012 All Rights Reserved Miguel Martin | <a href="http://www.leccionespracticas.com" target="_blank" >Design by MiguelMartin</a> | <a href="http://validator.w3.org/check?uri=referer" target="_blank" rel="nofollow" >XHTML</a> | <a href="http://jigsaw.w3.org/css-validator/" target="_blank" rel="nofollow" >CSS</a></div>
{% endblock footer %}
 
<!--=================FOOTER END==============-->
</div>
</body>
</html>

The view associated with “INSERT NEW TASK” is new2desplegado.html.twig. Please note how the form is displayed here!

{# src/curso/TaskBundle/Resources/views/Default/new2desplegado.html.twig #}
{% extends "cursoTaskBundle:Default:template-base-curso.html.twig" %}
 
{% block title %}
			Insertar nueva tarea
{% endblock title %}
 
{% block header %}
	<div id="header">
		<div id="titulo-centro">
			<h1>INSERTAR NUEVA TAREA</h1>
		</div>
	</div>
{% endblock header %}
 
{% block breadcrumbs %}
		<ul>
			<li class"=first"><a href="/">HOME </a></li>
			<li class="actual"><a href="/curso/template">Template</a></li>
			<li class="last"><a href="/curso/new2">Nueva tarea</a></li>
 
		</ul>
{% endblock breadcrumbs %}
 
{% block maincolumn %}
 
{% block message %}
 
{% endblock message %}
 
<div id="form-div" style="width: 800px; margin:auto; padding:10px;">
	<form action="{{ path('cursoTaskBundle_task_new2') }}" method="post" {{ form_enctype(form) }}>
 
		<div id="errores_form" class="error">
			<!-- errores generales del formulario -->
			{{ form_errors(form) }}
		</div>
 
		<div id="formcontent">
			<table style="border-collapse:separate; border-spacing:1em;">
				<tr>
					<td>{{ form_label(form.task) }}</td>
					<td>{{ form_widget(form.task) }}</td>
					<td class="error">{{ form_errors(form.task) }}</td>
				</tr>
				<tr>
					<td>{{ form_label(form.dueDate) }}</td>
					<td>{{ form_widget(form.dueDate) }}</td>
					<td class="error">{{ form_errors(form.dueDate) }}</td>
				</tr>
				<tr>
					<td>{{ form_label(form.description) }}</td>
					<td>{{ form_widget(form.description) }}</td>
					<td class="error">{{ form_errors(form.description) }}</td>
				</tr>
				<tr>
					<td>{{ form_label(form.url) }}</td>
					<td>{{ form_widget(form.url) }}</td>
					<td class="error">{{ form_errors(form.url) }}</td>
				</tr>
				<tr></tr>
				<tr>{{ form_rest(form) }}</tr>
				<tr>
					<td></td><td><input type="submit" formnovalidate /></td>
				<tr>
 
			</table>			
		</div>
 
	    <br />
	</form>
</div>
{% endblock maincolumn %}
 
{% block rightcolumn %}
<p>Utiliza el formulario para introducir una tarea nueva</p>
{% endblock rightcolumn %}

Now the one that shows all the tasks in a table: show.html.twig:
Notice how the link to the edit is created!

{% extends "cursoTaskBundle:Default:new2desplegado.html.twig" %}
 
{% block title %}
			Listado de tareas
{% endblock title %}
 
 
{% block header %}
	<div id="header">
		<div id="titulo-centro">
			<h1>MOSTRAR TAREAS</h1>
		</div>
	</div>
{% endblock header %}
 
{% block breadcrumbs %}
		<ul>
			<li class"=first"><a href="/">HOME </a></li>
			<li class="actual"><a href="/curso/template">Curso</a></li>
			<li class="last">Mostrar tareas</li>
		</ul>
{% endblock breadcrumbs %}
 
{% block maincolumn %}
{% block message %}
	{% if tasks is defined %}
		<table style="border-spacing: 5px; margin-right: 5px; border-collapse: separate;">
			<tr style="font-weight: bold;">
				<td></td><!-- editar -->
				<!--<td>ID</td>-->
				<td>NOMBRE</td>
				<td>FECHA</td>
				<td>DESCRIPCI&Oacute;N</td>
				<td>URL</td>
 
			</tr>
		{% for tarea in tasks %}
			<tr style="font-size: 0.8em">
				<td><a href=" {{ path('cursoTaskBundle_task_edit', { 'id' : tarea.id } ) }}" style="color:#A90641; font-size:0.9em; ">edit</a></td><!-- editar -->
				<!--<td>{{ tarea.id }}</td>-->
				<td>{{ tarea.task }}</td>
				<td>{{ tarea.dueDate|date("m/d/Y")  }}</td>
				<td>{{ tarea.description }}</td>
				<td>{{ tarea.url }}</td>
 
			</tr>
		{% endfor %}
		</table>
	{% else %}
		<p>No hay tareas. Puedes <a href="{{ path('cursoTaskBundle_task_new2') }}">Insertar una nueva tarea</a></p>
	{% endif %}
 
{% endblock message %}
{% endblock maincolumn %}
 
{% block rightcolumn %}
 
{% endblock rightcolumn %}

The view for showing the “Success” message is called success.html.twig:

{% extends "cursoTaskBundle:Default:new2desplegado.html.twig" %}
 
{% block title %}
			Tarea guardada
{% endblock title %}
 
{% block header %}
	<div id="header">
		<div id="titulo-centro">
			<h1>Tarea guardada</h1>
		</div>
	</div>
{% endblock header %}
 
 
{% block breadcrumbs %}
		<ul>
			<li class"=first"><a href="/">HOME </a></li>
			<li class="actual"><a href="/curso/template">Template</a></li>
			<li class="last">Tarea completada</li>
		</ul>
{% endblock breadcrumbs %}
 
{% block maincolumn %}
{% block message %}
{{ message|raw }}
<br /><br />
<p>Utiliza los enlaces del menu para realizar una nueva tarea o visualizar las tareas existentes</p>
{% endblock message %}
{% endblock maincolumn %}

The view associated with the “EDIT TASK X” is edit2desplegado.html.twig:

{# src/curso/TaskBundle/Resources/views/Default/new2desplegado.html.twig #}
{% extends "cursoTaskBundle:Default:template-base-curso.html.twig" %}
 
{% block title %}
			Modificar tarea
{% endblock title %}
 
{% block header %}
	<div id="header">
		<div id="titulo-centro">
			<h1>MODIFICAR TAREA</h1>
		</div>
	</div>
{% endblock header %}
 
{% block breadcrumbs %}
		<ul>
			<li class"=first"><a href="/">HOME </a></li>
			<li class="actual"><a href="/curso/template">Template</a></li>
			<li class="last">Editar tarea</li>
 
		</ul>
{% endblock breadcrumbs %}
 
{% block maincolumn %}
 
{% block message %}
 
{% endblock message %}
 
<div id="form-div" style="width: 800px; margin:auto; padding:10px;">
	<form action="{{ path('cursoTaskBundle_task_edit', { 'id' : id } ) }}" method="post" {{ form_enctype(form) }}>
 
		<div id="errores_form" class="error">
			<!-- errores generales del formulario -->
			{{ form_errors(form) }}
		</div>
 
		<div id="formcontent">
			<table style="border-collapse:separate; border-spacing:1em;">
				<tr>
					<td>{{ form_label(form.task) }}</td>
					<td>{{ form_widget(form.task) }}</td>
					<td class="error">{{ form_errors(form.task) }}</td>
				</tr>
				<tr>
					<td>{{ form_label(form.dueDate) }}</td>
					<td>{{ form_widget(form.dueDate) }}</td>
					<td class="error">{{ form_errors(form.dueDate) }}</td>
				</tr>
				<tr>
					<td>{{ form_label(form.description) }}</td>
					<td>{{ form_widget(form.description) }}</td>
					<td class="error">{{ form_errors(form.description) }}</td>
				</tr>
				<tr>
					<td>{{ form_label(form.url) }}</td>
					<td>{{ form_widget(form.url) }}</td>
					<td class="error">{{ form_errors(form.url) }}</td>
				</tr>
				<tr></tr>
				<tr>{{ form_rest(form) }}</tr>
				<tr>
					<td></td><td><input type="submit" formnovalidate /></td>
				<tr>
 
			</table>			
		</div>
 
	    <br />
	</form>
</div>
{% endblock maincolumn %}
 
{% block rightcolumn %}
<p>Utiliza el formulario para editar la tarea nueva</p>
{% endblock rightcolumn %}

Now the stylesheets (remember to put them into Resources\public\css\layout\ !)

reset.css (compressed):

html,body,div,span,applet,object,iframe,h1,h2,h3,h4,h5,h6,p,blockquote,pre,a,abbr,acronym,address,big,cite,code,del,dfn,em,font,img,ins,kbd,q,s,samp,small,strike,strong,sub,sup,tt,var,b,u,i,center,dl,dt,dd,ol,ul,li,fieldset,form,label,legend,table,caption,tbody,tfoot,thead,tr,th,td{margin:0;padding:0;border:0;outline:0;font-size:100%;vertical-align:baseline;background:transparent}body{line-height:1}ol,ul{list-style:none}blockquote,q{quotes:none}blockquote:before,blockquote:after,q:before,q:after{content:'';content:none}:focus{outline:0}ins{text-decoration:none}del{text-decoration:line-through}table{border-collapse:collapse;border-spacing:0}

mainStyle.css

.tt1 {background:#FF0000;}
.tt2 {background:#FF00FF;}
.tt3 {background:#FFFF00;}
 
html { }
 
body {font: 13px/1.5 Verdana, Arial, sans-serif; background:url(../_images/bck2.jpg) 50% 0 repeat-x;}
 
a:focus {outline: 1px dotted invert;}
 
a:link {color:#000; text-decoration:none;}
a:visited {color:#000; text-decoration:none;}
a:hover {color:#000; text-decoration:none;}
a:active {color:#000; text-decoration:none;}
 
#footer a:link {color:#666; text-decoration:none;}
#footer a:visited {color:#666; text-decoration:none;}
#footer a:hover {color:#666; text-decoration:none;}
#footer a:active {color:#666; text-decoration:none;}
 
hr {border-color: #ccc; border-style: solid; border-width: 1px 0 0; clear: both; height: 0;}
 
p { }
sup {position: relative;top: -3px;vertical-align: top;font-size: 80%;}
sub {position: relative;bottom: -5px;vertical-align: top;font-size: 80%;}
 
h1 {font-size: 25px;}
h2 {font-size: 23px;}
h3 {font-size: 21px;}
h4 {font-size: 19px;}
h5 {font-size: 17px;}
h6 {font-size: 15px;}
 
.xxsmall {font-size: 10px;}
.xsmall {font-size: 12px;}
.small {font-size: 14px;}
.medium {font-size: 16px;}
.large {font-size: 22px;}
.xlarge {font-size: 26px;}
.xxlarge {font-size: 32px;}
 
ol {list-style: decimal;}
ul {list-style: square;}
li {margin-left: 30px;}
 
p, dl, hr, h1, h2, h3, h4, h5, h6, ol, ul, pre, table, address, fieldset {margin-bottom: 20px;}
 
#wrapper {overflow:hidden; width:960px; margin:20px auto;}
 
html body * span.clear,html body * div.clear,html body * li.clear,html body * dd.clear{background:none;border:0;clear:both;display:block;float:none;font-size:0;list-style:none;margin:0;padding:0;overflow:hidden;visibility:hidden;width:0;height:0}
 
.margin-left {margin-left:20px;}
.margin-right {margin-right:20px;}
.margin-top {margin-top:20px;}
.margin-bottom {margin-bottom:20px;}
.margin-left-half {margin-left:10px;}
.margin-right-half {margin-right:10px;}
.margin-top-half {margin-top:10px;}
.margin-bottom-half {margin-bottom:10px;}
.margin-bottom-none {margin-bottom:0;}
 
img.centered {display:block;margin-left:auto;margin-right:auto;}
img.alignright {display: inline;}
img.alignleft {display: inline;}
.alignright {float:right;}
.alignleft {float:left;}
 
.bold {font-weight:bold;}
.italic {font-style:italic;}
.text-left {text-align:left;}
.text-right {text-align:right;}
.uppercase {text-transform:uppercase;}
.text-justify{text-align:justify;}
 
/*=================MAIN END==============*/
 
/* BREADCRUMBS! */
#breadcrumbs {margin-left:160px;}
#breadcrumbs ul li{display: inline; }
#breadcrumbs ul li:before{ content:"\00BB \0020";}
 
/*=================HEADER START==============*/ 
 
#header {width:960px;float:left; min-height: 80px;}
 
/*=================HEADER END==============*/
 
/*=================CONTENT START==============*/
 
#content {width:960px;float:left; padding-top:10px; padding-bottom: 10px;}
 
/*======LEFT COLUMN START======*/
 
#left-column {width:160px;float:left;}
 
#menu-sub {float:left;}
#menu-sub ul {list-style:none;} 
#menu-sub ul li {font-size:16px;}
 
/*======LEFT COLUMN END======*/
 
/*======MAIN COLUMN START======*/
 
#main-column {width:630px;float:left;padding-right:10px;}
 
/*======MAIN COLUMN END======*/
 
 
/*======RIGHT COLUMN START======*/
 
#right-column {width:140px;float:left;padding-left:10px; padding-right:10px;}
 
/*======RIGHT COLUMN END======*/
 
/*=================CONTENT END==============*/
 
/*=================FOOTER START==============*/
 
#footer {width:960px;float:left;text-align:center;color:#666; min-height:30px; padding: 15px;}
 
/*=================FOOTER END==============*/

And last one, colors.css (compressed):

a{color:#A90641}a:hover{color:#A90641}a:visited{color:#A90641}a:active{color:#A90641} body{ background:#464646; color:#333;} #content{ color:#EEE; font-size: 0.8em;background-color: #343333;} #header{color:#FCFAD0;border-bottom:thick double #A90641;background:#111;}#footer{font-size:0.7em;border-top:thick double #A90641;background: #111;color:#5B605F;font-weight:bold;}#menu-sub a{font-size:0.8em; color:#A90641;}#menu-sub {padding-left:5px;}#menu-sub a:hover{font-size:0.8em; color:#CB2863;}#menu-sub a:visited{font-size:0.8em; color:#A90641;}#titulo-centro{margin:auto; width: 920px;padding:20px;text-align:center;}#breadcrumbs a{color:#A90641}.actual a{font-weight:bold;}

And… that’s all, folks.

Please comment if you notice any bugs 🙂

Update:
Spanish readers might find this links interesting:

A bit outdated, but quite interesting, basic form tutorial and another, more advanced one, about understanding symfony form binding. This one also ispired me

Symfony: Yui CSS compression with Assetic on Windows (WAMP) systems [SOLVED]

When I read about Symfony’s Assetic’s filters I felt quite excited and decided to try yui_css filter. When it comes to linux, everything worked just fine out of the box with the last Symfony release. But when I tried it in my Windows machine, it kept complaining ([exception] 500 | Internal Server Error | RuntimeException | blah-blah) and it has not an easy task to make yui work.

Just in case anyone can use this, here are the steps I followed to fix it:

0. Install JAVA JRE (in c:\Program Files (x86)\Java\jre6\), set the classpath and so on…

1. Prepare your templates for the use of filter='yui_css'. For instance (notice line 6):

1
2
3
4
5
6
7
8
9
{% stylesheets
	'@cursoe5Bundle/Resources/public/css/layout/mainStyle.css'
	'@cursoe5Bundle/Resources/public/css/layout/reset.css'
	'@cursoe5Bundle/Resources/public/css/layout/colors.css'
			output = 'css/compiled/styles.css'
			filter = 'yui_css'
%}
<link href="{{ asset_url }}" type="text/css" rel="stylesheet" media="screen" />
{% endstylesheets %}

2. Edit www-wamp-dir\Symmfony\config\config.yml and set the java path and the yui_css path. Here is my Assetic Configuration in config.yml (edit lines 5 and 11!)

1
2
3
4
5
6
7
8
9
10
11
# Assetic Configuration (www-wamp-dir\Symmfony\config\config.yml)
assetic:
    debug:          %kernel.debug%
    use_controller: false
    java: c:\\Program Files (x86)\\Java\\jre6\\bin\\java.exe
    filters:
        cssrewrite: ~
        yui_css: 
            jar: %kernel.root_dir%\Resources\java\yuicompressor-2.4.7.jar
        # closure:
        #     jar: %kernel.root_dir%/java/compiler.jar

3. Download the last yui compressor jar (yuicompressor-2.4.7) and extract its contents.
Move the jar (usually located in the ‘build’ folder) to the Symfony %kernel.root_dir%/Resources/java/yuicompressor-2.4.7.jar (%kernel.root_dir% is the one called ‘app’, which contains cache, config, logs, … folders. You should mkdir Resources and java folders inside).

3. If you get errors like “Runtime exception ...” (as this guy does) download the last Assetic stable release from assetic github and extract its contents.
Rename the extracted folder -originally called something like ‘kriswallsmith-assetic-b25d1fd‘)- to ‘assetic‘ and move it to wamp-www-dir\Symfony\vendor\ (will replace the old version! make sure you backup the old stuff before doing this!)

4. Assetic relies on Process Module. Previous version of Assetic had the code of Process module directly copied into Assetic (and had some bugs, like the PHP_WINDOWS_VERSION_MAJOR issue), but the last one just relies on the Symfony’s Process Module. [I recommend using Composer to make your life easier]

If you test your site and the CSS is broken, it is likely to be because of this. Check for something like “Fatal error: Class 'Symfony\Component\Process\ProcessBuilder' not found in src/Assetic/Filter“.
This means you have not installed ProcessBuilder, so download the last Process Module, which includes ProcessBuilder.
Extract the folder (originally called something like ‘symfony-Process-2e4da8c‘), rename it to ‘Process‘ and move it to wamp-www-dir\Synfony\vendor\symfony\src\Symfony\Component (will replace the old version! make sure you backup the old stuff before doing this!)

5. If you followed the steps, everything should be working by now. If you find some annoying messages like something was not found, or command not valid, or so, please make sure you have your cache cleared and your assets dumped:

php app/console --env=dev cache:clear
php app/console --env=dev cache:clear --no-debug
php app/console --env=dev assets:install web --no-debug
php app/console --env=dev assetic:dump --no-debug

Good luck! 🙂

[SOLVED] Symfony Assets: “An exception has been thrown during the rendering of a template (“Route “_assetic_XXX” does not exist.”)

These days I am learning some Symfony and been struggling with Assetic. In the production environment, I added some image assets to my templates like follows:

<img src="{{ asset('bundles/cursoe5/images/en.png') }} " alt = "bandera"/>

It worked like a charm after running:

php app/console assets:install web

But whenever I tried to add assets in this way:

{% image
      '@cursoe5Bundle/Resources/public/images/es.png'
%}
<img src="{{ asset_url }} " alt="bandera" />
{% endimage %}

All I got were exceptions arising: An exception has been thrown during the rendering of a template (“Route “_assetic_XXX” does not exist.”).

I did some Google research and found these lines. After running them, everything worked. Still wondering what the problem was, and whether these lines are all necessary or not. They do the job, anyways 🙂

php app/console --env=dev cache:clear --no-debug
php app/console --env=dev assetic:dump --no-debug
php app/console --env=dev assets:install web --no-debug

For the curious ones, here is my template. Note that it works both ways:

{# src/curso/e5Bundle/Resources/views/Default/sesiones.html.twig #}
<style>	*{background-color:#333;}</style>
<div id="message" style="width: 50%; margin:auto; border: 5px dashed #6F6; font-weight:bold;padding:5px; font-family:Verdana, Geneva, sans-serif;color:#6F6">
	{% if locale == 'en' %}
		<!-- <img src="{{ asset('bundles/cursoe5/images/en.png') }} " alt = "bandera"/>-->
		{% image
			'@cursoe5Bundle/Resources/public/images/es.png'
		%}
			<img src="{{ asset_url }} " alt="bandera" />
		{% endimage %}
	{% elseif locale == 'fr' %}
		<img src="{{ asset('bundles/cursoe5/images/fr.png') }} " alt = "bandera"/>
	{% elseif locale  == 'de' %}
		<img src="{{ asset('bundles/cursoe5/images/de.png') }} " alt = "bandera"/>
	{% else %}
		<img src="{{ asset('bundles/cursoe5/images/es.png') }} " alt = "bandera"/>
	{% endif %}
	<p style="margin-left:auto; margin-right:auto; text-align:center">{{ message|raw }}</p>
</div>

Also note that (quote from Symfony book)
In the dev environment, Assetic generates paths to CSS and JavaScript files that don’t physically exist on your computer. But they render nonetheless because an internal Symfony controller opens the files and serves back the content (after running any filters).
This kind of dynamic serving of processed assets is great because it means that you can immediately see the new state of any asset files you change. It’s also bad, because it can be quite slow. If you’re using a lot of filters, it might be downright frustrating.

More interesting stuff related with Assetic can be found here:
** Asset Management in Symfony
** Manejar Assets en Symfony (ES)
** Optimize images with Assetic
** Optimizar imágenes con Assetic
** Discussion in Google Groups

Symfony YAML caracteres especiales y tildes [resuelto]

Si usas Symfony para desarrollar tus proyectos web estarás familiarizado con los ficheros de validación YML (YAML). Estos ficheros (normalmente llamados validation.yml) contienen reglas del siguiente tipo:

curso\TaskBundle\Entity\Task2:
    properties:
        task:
            - NotBlank: ~
        dueDate:
            - NotBlank: ~
            - Type: \DateTime
        description:
             - MaxLength: { limit: 300, message: "Descripción limitada a {{ limit }} caracteres" }
        url:
            - Url: 
            - Regex: 
                pattern: "/.*\.es$/"
                message: "La URL debe terminar en .es"

Si editas estos ficheros y quieres escribir caracteres con tilde o especiales (por ejemplo, el carácter ñ eñe), asegúrate de guardarlos en UTF-8 SIN BOM. Si no lo haces, obtendrás errores del tipo:

* The YAML value does not appear to be valid UTF-8 (si lo guardas como unicode y contiene tildes)

* Unable to parse XXX at line 1 (near … (si lo guardas como utf-8 con BOM).

Para evitar estos problemas recomiendo utilizar el editor Notepad++ y en el menú de “codificación” marcar “Codificar en UTF-8 SIN BOM”. ¡Problema resuelto! 🙂

Instalación de Symfony2 en WAMP con PHP5.3.8, extensión intl y acelerador ACP [resuelto]

Con versiones anteriores a php5.3.8 encontramos el problema 'Upgrade your intl extension with a newer ICU version (4+)'. Para solucionarlo, basta con seguir algunos pasos:

1. Descargar e instalar >WAMP Server 2.2 en c:\wamp2.2\
La versión que instalamos contiene PHP5.3.8 y Apache2.2.21

2. Descargar Symfony 2.0 (Edición Standard) EN VERSIÓN ZIP (algunas pruebas con la versión en tar gz me han dado problemas) y descomprimir los archivos en c:\wamp2.2\Symfony
— 2.1. Arrancar wamp server ejecutando C:\wamp2.2\wampmanager.exe
— 2.2. Abrir el navegador e ir a http://localhost/Symfony/web/config.php Debemos ver algo asi:

Symfony config screen

Symfony config screen

— 2.3. Lo más probable es que nos recomiende instalar APC y intl. Seguiremos, para ello, los pasos 2 y 3.

3. Activar las extensiones php_intl:
— 3.1. Copiar los icu*.dll de C:\wamp2.2\bin\php\php5.3.8 a C:\wamp2.2\bin\apache\Apache2.2.21\bin (si no hacemos este paso, Symfony NO detectará el php_intl y seguirá diciendo que lo instalemos)
— 3.2. Activar la extensión php_intl: WampManager > PHP > Extensions > php_intl
— 3.3. Reiniciar WAMP: WampServer > Restart all services

4. Instalar acelerador PHP APC:
— 4.1. Descargar http://downloads.php.net/pierre/php_apc-3.1.5-5.3-vc9-x86.zip [2012-05-01 Esa versión de la librería ya no se encuentra en la página original, pero se puede descargar una que funciona haciendo click aqui]
— 4.2. Descomprimir php_apc.dll en C:\wamp2.2\bin\php\php5.3.8\ext
— 4.3. Activar php_apc en WampManager > PHP > Extensions > php_apc
— 4.4. Reiniciar WAMP: WampServer > Restart all services

En este punto, al ir a http://localhost/Symfony/web/config.php deberíamos ver la siguiente pantalla, que indica que podemos empezar a configurar Symfony porque el entorno ya está preparado.