Caution: You are browsing the legacy 1.x part of this website.
This version of symfony is not maintained anymore. If some of your projects still use this version, consider upgrading.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.

Master Symfony2 fundamentals

Be trained by SensioLabs experts (2 to 6 day sessions -- French or English).
trainings.sensiolabs.com

Discover the SensioLabs Support

Access to the SensioLabs Competency Center for an exclusive and tailor-made support on Symfony
sensiolabs.com

Day 09: local improvements

Previously on symfony

During day eight, we added AJAX interactions to askeet without pain. The application is now quite usable, but could use a lot of little improvements. Rich text should be allowed in the questions body, and primary keys should not appear in the URIs. All that is not difficult to put in place with symfony: today will be a good occasion to practice what you already learned, and to check that you know how to manipulate all the layers of the MVC architecture.

Allow rich text formatting on questions and answers

Markdown

The question and answer bodies only accept plain text for now. To allow basic formatting - bold, italic, hyperlinks, images, etc. - we will use an external library rather than reinvent the wheel.

If you have taken a look at the symfony documentation in text format, you probably know that we are big Markdown fans. Markdown is a text-to-HTML conversion tool, and a syntax for text formatting. The great advantage of Markdown over, for instance, Wiki or forum syntax, is that a plain text markdown file is still very readable:

Test Markdown text
------------------

This is a **very simple** example of [Markdown][1].
The best thing about markdown is its _auto-escape_ feature for code chunks:

    <a href="http://www.symfony-project.com">link to symfony</a>

>The `<` and `>` are properly escaped as `&lt;` and `&gt;`,
>and are not interpreted by any browser

[1]: http://daringfireball.net/projects/markdown/   "Markdown"

This Markdown renders as follow:

Test Markdown text

This is a very simple example of Markdown. The best thing about markdown is its auto-escape feature for code chunks:

 <a href="http://www.symfony-project.com">link to symfony</a>

The < and > are properly escaped as &lt; and &gt;, and are not interpreted by any browser

Markdown library

Although originally written in Perl, Markdown is available as a PHP library at PHP Markdown. That's the one we will use. Download the markdown.php file and put it in the lib folder of the askeet project (askeet/lib/). That's all: It is now available to all the classes of the askeet applications, provided that you require it first:

require_once('markdown.php');   

We could call the Markdown converter each time we display the body of a message, but that would require too high a load on our servers. We'd rather convert the text body to an HTML body when the question is created, and store the HTML version of the body in the Question table. You are probably getting used to this, so the model extension won't be a surprise.

Extend the model

First, add a colomn to the Question table in the schema.xml:

<column name="html_body" type="longvarchar" />

Then, regenerate the model and update the database:

$ symfony propel-build-model
$ symfony propel-build-sql
$ symfony propel-insert-sql

Override the setBody method

When the ->setBody() method of the Question class is called, the html_body column must also be updated with the Markdown conversion of the text body. Open the askeet/lib/model/Question.php model file, and create:

public function setBody($v)
{
  parent::setBody($v);
 
  require_once('markdown.php');
 
  // strip all HTML tags
  $v = htmlentities($v, ENT_QUOTES, 'UTF-8');
 
  $this->setHtmlBody(markdown($v));
}

Applying the htmlentities() function before setting the HTML body protects askeet from cross-site-scripting (XSS) attacks since all <script> tags are escaped.

Update the test data

We will add some Markdown formatting to some of the questions of the test data (in askeet/data/fixtures/test_data.yml), to be able to check that the conversion works properly:

Question:
  q1:
    title: What shall I do tonight with my girlfriend?
    user_id: fabien
    body:  |
      We shall meet in front of the __Dunkin'Donuts__ before dinner, 
      and I haven't the slightest idea of what I can do with her. 
      She's not interested in _programming_, _space opera movies_ nor _insects_.
      She's kinda cute, so I __really__ need to find something 
      that will keep her to my side for another evening.

  q2:
    title: What can I offer to my step mother?
    user_id: anonymous
    body:  |
      My stepmother has everything a stepmother is usually offered
      (watch, vacuum cleaner, earrings, [del.icio.us](http://del.icio.us) account). 
      Her birthday comes next week, I am broke, and I know that 
      if I don't offer her something *sweet*, my girlfriend 
      won't look at me in the eyes for another month.

You can now repopulate the database:

$ php batch/load_data.php

Modify the templates

The showSuccess.php template of the question module can be sightly modified:

...
<div class="question_body">
  <?php echo $question->getHtmlBody() ?>
</div>
...

The list template fragment (_list.php) also shows the body, but in a truncated version:

<div class="question_body">
  <?php echo truncate_text(strip_tags($question->getHtmlBody()), 200) ?>
</div>

Everything is now ready for the final test: display the three pages that were modified, and observe the formatted text coming from the test data:

http://askeet/question/list
http://askeet/recent
http://askeet/question/show/stripped_title/what-shall-i-do-tonight-with-my-girlfriend    

markdown text

The same goes for the Answer body: An alternate html_body column has to be created in the model, the ->setBody() method needs to be overridden, and the answers displayed in question/show have to use the ->getHtmlBody() method instead of the ->getBody(). As the code is exactly the same as above, we won't decribe it here, but you will find it in today's SVN code.

Hide all ids

Another good practice in symfony actions is to avoid as much as possible to pass primary keys as request parameters. This is because our primary keys are mainly auto-incremental, and this gives hackers too much information about the records of the database. Plus, the displayed URI doesn't mean anything, and that's bad for the search engines.

Take the user profile page, for instance. For now, it uses the user id as a parameter. But if we make sure that the nickname is unique, it could as well be the parameter for the request. Let's do it.

Change the action

Edit the user/show action:

public function executeShow()
{
  $this->subscriber = UserPeer::retrieveByNickname($this->getRequestParameter('nickname'));
  $this->forward404Unless($this->subscriber);
 
  $this->interests = $this->subscriber->getInterestsJoinQuestion();
  $this->answers   = $this->subscriber->getAnswersJoinQuestion();
  $this->questions = $this->subscriber->getQuestions();
}

Change the model

Add the following method to the UserPeer class in the askeet/lib/model/ directory.

public static function retrieveByNickname($nickname)
{
  $c = new Criteria();
  $c->add(self::NICKNAME, $nickname);
 
  return self::doSelectOne($c);
}

Change the template

The pages that display a link to the user profile must now mention the user's nickname instead of his/her id.

In the question/showSuccess.php, question/_list.php templates, replace:

<?php echo link_to($question->getUser(), 'user/show?id='.$question->getUserId()) ?>

by:

<?php echo link_to($question->getUser(), 'user/show?nickname='.$question->getUser()->getNickname()) ?>

The same kind of modification goes for the answer/_answer.php template.

Add the routing rule

Add a new rule in the routing configuration for this action so that the url pattern shows a nickname request parameter:

user_profile:
  url:   /user/:nickname
  param: { module: user, action: show }    

After a symfony clear-cache, the last thing to do is to test your modifications.

Routing

Apart from today's additions, many of the actions written until now use the default routing, so the module name and the action name are often displayed in the address bar of the browser. You already learned how to fix it, so let's define URL patterns for all the actions. Edit the askeet/apps/frontend/config/routing.yml:

# question
question:
  url:   /question/:stripped_title
  param: { module: question, action: show }

popular_questions:
  url:   /index/:page
  param: { module: question, action: list, page: 1 }

recent_questions:
  url:   /recent/:page
  param: { module: question, action: recent, page: 1 }

add_question:
  url:   /add_question
  param: { module: question, action: add }

# answer
recent_answers:
  url:   /recent/answers/:page
  param: { module: answer, action: recent, page: 1 }

# user
login:
  url:   /login
  param: { module: user, action: login }

logout:
  url:   /logout
  param: { module: user, action: logout }

user_profile:
  url:   /user/:nickname
  param: { module: user, action: show }

# default rules
homepage:
  url:   /
  param: { module: question, action: list }

default_symfony:
  url:   /symfony/:action/*
  param: { module: default }

default_index:
  url:   /:module
  param: { action: index }

default:
  url:   /:module/:action/*

If you navigate in the production environment, you are strongly advised to clear the cache before testing this configuration modification.

One good practice of symfony routing is to use the rule names in a link_to() helper instead of the module/action. Not only is it faster (the routing engine doesn't need to parse the routing configuration to find the rule to apply), but it also allows you to modify the action behind a rule name later. The routing chapter of the symfony book explains that more in detail.

<?php link_to('@user_profile?id='.$user->getId()) ?>
// is better than
<?php link_to('user/show?id='.$user->getId()) ?>

Askeet follows the symfony good practices, so the code that you will download at the end of this day's tutorial contains only rule names in the link helpers. Replacing action/module by @rule in all the templates and custom helper is not very fun to do, so the last advice concerning routing is: Write the routing rules as you create actions, and use rule names in the link helpers from the beginning.

See you Tomorrow

Today's changes were longer to read than to understand. In addition, the modifications described in the tutorial were repeated for similar cases in the overall code. Although no real new feature was added today, the code changed a lot.

If you feel that you didn't learn much about symfony today, it means that you are getting ready to start your own project. The process of creating an action, modifying the model to have it serve the action as needed, write a simple template to output the action and edit the configuration to integrate the new action into the logic of the application are the basics of symfony development.

All the good practices exposed here (using external libraries instead of rewriting it in symfony, not showing primary keys in the application, using routing rule names instead of module/action) will keep your application clean, safe, fast and maintainable.

But the askeet application is far from finished! The functionnality that lacks the most is the ability to add a new question and to add a new answer. That's what we will develop tomorrow.

Do you have a suggestion about the additional feature of the 21st day? Make sure you send it to the askeet mailing-list. Stay tuned!