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

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

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, ... ]