![]() |
|
The Askeet Tutorialsymfony advent calendar day three: dive into the MVC architecture |
WARNING: The SVN source code found in the release_day tags is outdated. Please refer to the current version until each day code is updated.
You are currently reading "The Askeet Tutorial" which is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.
![]() |
This work is licensed under a
Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License.
Translation of this work into another language is explicitly allowed. |
During day two you learned how to build an object model based on a relational data model, and generate scaffolding for one of these objects. By the way, the code of the application generated during the previous days is available in the askeet SVN repository at:
http://svn.askeet.com/
The objectives for the third day are to define a nicer layout for the site, define the list of questions as the default homepage, show the number of users interested by one question, and populate the database from sample text files in order to have test data. That's not much to do, but quite a lot to read and understand.
To read this tutorial, you should be familiar with the concepts of project, application, module and action in symfony as explained in the controller chapter of the symfony book.
Today will be the first dive in the world of the MVC architecture. What does this mean? Simply that the code used to generate one page is located in various files according to its nature.
If the code concerns data manipulation independent from a page, it should be located in the Model (most of the time in askeet/lib/model/
). If it concerns the final presentation, it should be located in the View; in symfony, the view layer relies on templates (for instance in askeet/apps/frontend/modules/question/templates/
)
and configuration files. Eventually, the code dedicated to tie all this
together and to translate the site logic into good old PHP is located
in the Controller, and in symfony the controller for a specific page is called an action (look for actions in askeet/apps/frontend/modules/question/actions/
). You can read more about this model in the MVC implementation in symfony chapter of the symfony book.
While our applications view will only change slightly today, we will manipulate a lot of different files. Don't panic though, since the organization of files and the separation of the code in various layers will soon become evident and very useful.
In application of the decorator design pattern,
the content of the template called by an action is integrated into a
global template, or layout. In other words, the layout contains all the
invariable parts of the interface, it "decorates" the result of
actions. Open the default layout (located in askeet/apps/frontend/templates/layout.php
) and change it to the following:
<!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"> <head> <?php echo include_http_metas() ?> <?php echo include_metas() ?> <?php echo include_title() ?> <link rel="shortcut icon" href="/favicon.ico" /> </head> <body> <div id="header"> <ul> <li><?php echo link_to('about', '@homepage') ?></li> </ul> <h1><?php echo link_to(image_tag('askeet_logo.gif', 'alt=askeet'), '@homepage') ?></h1> </div> <div id="content"> <div id="content_main"> <?php echo $sf_data->getRaw('sf_content') ?> <div class="verticalalign"></div> </div> <div id="content_bar"> <!-- Nothing for the moment --> <div class="verticalalign"></div> </div> </div> </body> </html>
We tried to keep the markup as semantic as possible, and to move all the styling into the CSS stylesheets. These stylesheets won't be described here, since CSS syntax is not the purpose of this tutorial. They are available for download though, in the SVN repository.
We created two stylesheets (
main.css
andlayout.css
). Copy them into youraskeet/web/css/
directory and edit yourfrontend/config/view.yml
to change the autoloaded stylesheets:stylesheets: [main, layout]
This layout is still lightweight for the moment, it will be rebuilt
later (in about a week). The important things in this template are the <head>
part, which is mostly generated, and the sf_content
variable, which contains the result of the actions.
Check that the modifications display correctly by requesting the home page - this time in the development environment:
http://askeet/frontend_dev.php/
If you wonder what the difference between http://askeet/frontend_dev.php/
and http://askeet/
is, you should probably have a look at the configuration chapter
of the symfony book. For now, you just need to know that they point to
the same application, but in different environments. An environment is
a unique configuration, where some features of the framework can be
activated or deactivated as required.
In this case, the /frontend_dev.php/
URL points to the development environment,
where the whole configuration is parsed at each request, the HTML cache
is deactivated, and the debug tools are all available (including a
semi-transparent toolbar located at the top right corner of the
window). The /
URL - equivalent to /index.php/
- points to the production environment, where the configuration is "compiled" and the debug tools deactivated to speed up the delivery of pages.
These two PHP scripts - frontend_dev.php
and index.php
- are called front controllers, and all the requests to the application are handled by them. You can find them in the askeet/web/
directory. As a matter of fact, the index.php
file should be named frontend_prod.php
, but as frontend
is the first application that you created, symfony deduced that you
probably wanted it to be the default application and renamed it to index.php
, so that you can see your application in the production environment by just requesting /
. If you want to learn more about the front controllers and the Controller layer of the MVC model in general, refer to the controller chapter in the symfony book.
A good rule of thumb is to navigate in the development environment until you are satisfied with the feature you are working on, then switch to the production environment to check its speed and "nice" URLs.
Remember to always clear the cache when you add some classes or when you change some configuration files to see the result in the production environment.
For now, if you request the home page of the new website, it shows a
'Congratulations' page. A better idea would be to show the list of
questions (referenced in these documents as question/list
and translated as: the list
action of the question
module). To do this, open the routing configuration file of the frontend application, found in askeet/apps/frontend/config/routing.yml
and locate the homepage:
section. Change it to:
homepage:
url: /
param: { module: question, action: list }
Refresh the home page in the development environment (http://askeet/frontend_dev.php/
); it now displays the list of questions.
if you are a curious person, you might have looked for this page containing the 'Congratulations' message. And you might be surprised not to find it in your
askeet
directory. As a matter of fact, the template for thedefault/index
action is defined in the symfony data directory and is independent from the project. If you want to override it, you can still create adefault
module in your own project.
The possibilities offered by the routing system will be detailed in the near future, but if you are interested, you can read the routing chapter of the symfony book.
The list displayed by the home page will remain quite empty, unless you add your own questions. When you develop an application, it is a good idea to have some test data at your disposal. Entering test data by hand (either via the CRUD interface of directly in the database) can be a real pain, that's why symfony can use text files to populate databases.
We'll create a test data file in the askeet/data/fixtures/
directory (this directory has to be created). Create a file called test_data.yml
with the following content:
User:
anonymous:
nickname: anonymous
first_name: Anonymous
last_name: Coward
fabien:
nickname: fabpot
first_name: Fabien
last_name: Potencier
francois:
nickname: francoisz
first_name: François
last_name: Zaninotto
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 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.
q3:
title: How can I generate traffic to my blog?
user_id: francois
body: |
I have a very swell blog that talks
about my class and mates and pets and favorite movies.
Interest:
i1: { user_id: fabien, question_id: q1 }
i2: { user_id: francois, question_id: q1 }
i3: { user_id: francois, question_id: q2 }
i4: { user_id: fabien, question_id: q2 }
First of all, you may recognize YAML here. If you are not familiar with symfony, you might not know that the YAML format is the favorite format for configuration files in the framework. It is not exclusive - if you are attached to XML or .ini files, it is very easy to add a configuration handler to allow symfony to read them. If you have time and patience, read more about YAML and the symfony configuration files in the configuration in practice chapter of the symfony book. As of now, if you are not familiar with the YAML syntax, you should get started right away, since this tutorial will use it extensively.
Ok, back to the test data file. It defines instances of objects,
labeled with an internal name. This label is of great use to link
related objects without having to define id
s (which are often auto-incremented and can not be set). For instance, the first object created is of class User
, and is labeled fabien
. The first Question
is labeled q1
. This makes it easy to create an object of class Interest
by mentioning the related object labels:
Interest:
i1:
user_id: fabien
question_id: q1
The data file given previously uses the short YAML syntax to say the same thing. You can find more about data population files in the data files chapter of the symfony book.
There is no need to define values for the
created_at
andupdated_at
columns, since symfony knows how to fill them by default.
The next step is to actually populate the database, and we wish to do that with a PHP script that can be called with a command line - a batch.
Create a file called load_data.php
in the askeet/batch/
directory with the following content:
<?php define('SF_ROOT_DIR', realpath(dirname(__FILE__).'/..')); define('SF_APP', 'frontend'); define('SF_ENVIRONMENT', 'dev'); define('SF_DEBUG', true); require_once(SF_ROOT_DIR.DIRECTORY_SEPARATOR.'apps'.DIRECTORY_SEPARATOR.SF_APP.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'config.php'); // initialize database manager $databaseManager = new sfDatabaseManager(); $databaseManager->initialize(); ?>
This script does nothing, or close to nothing: it defines a path, an application and an environment to get to a configuration, loads that configuration, and initializes the database manager. But that's already a lot: that means that all the code written below will take advantage of the auto-loading of classes, automatic connection to Propel objects, and the symfony root classes.
If you have examined symfony's front controllers (like
askeet/web/index.php
), you might find this code extremely familiar. That's because every web request requires access to the same objects and configuration, as a batch request does.
Now that the frame of the batch is ready, it is time to make it do something. The batch has to:
This might sound complicated, but in symfony, you can do that with two lines of code, thanks to the sfPropelData
object. Just add the following code before the final ?>
in the askeet/batch/load_data.php
script:
$data = new sfPropelData(); $data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');
That's all. A sfPropelData
object is created, and told to load all the data of a specific directory - our fixtures
directory - into the database defined in the databases.yml
configuration file.
The
DIRECTORY_SEPARATOR
constant is used here to be compatible with Windows and *nix platforms.
At last, you can check if these few lines of code were worth the hassle. type in the command line:
$ cd /home/sfprojects/askeet/batch
$ php load_data.php
Check the modifications in the database by refreshing the development home page again:
http://askeet/frontend_dev.php
Hooray, the data is there.
By default, the
sfPropelData
object deletes all your data before loading the new ones. You can also append to the current data:$data = new sfPropelData(); $data->setDeleteCurrentData(false); $data->loadData(sfConfig::get('sf_data_dir').DIRECTORY_SEPARATOR.'fixtures');
The page displayed when requesting the list
action of the question
module is the result of the executeList()
method (found in the askeet/apps/frontend/modules/question/actions/action.class.php
action file) passed to the askeet/apps/frontend/modules/question/templates/listSuccess.php
template. This is based on a naming convention that is explained in the controller chapter of the symfony book. Let's have a look at the code that is executed:
actions.class.php:
public function executeList () { $this->questions = QuestionPeer::doSelect(new Criteria()); }
listSuccess.php:
... <?php foreach ($questions as $question): ?> <tr> <td><?php echo link_to($question->getId(), 'question/show?id='.$question->getId()) ?></td> <td><?php echo $question->getTitle() ?></td> <td><?php echo $question->getBody() ?></td> <td><?php echo $question->getCreatedAt() ?></td> <td><?php echo $question->getUpdatedAt() ?></td> </tr> <?php endforeach; ?>
Step-by-step, here is what it does:
Question
table that satisfy an empty criteria - i.e. all the questions$questions
) that is passed to the templateThe ->getId()
, ->getTitle()
, ->getBody()
, etc. methods were created during the symfony propel-build-model
command call (do you remember yesterday ?) to retrieve the value of the id
, title
, body
, etc. fields. These are standard getters, formed by adding the prefix get
to the camelCased field name - and Propel also provides standard setters, prefixed with set
. The Propel documentation describes the accessors created for each class.
As for the mysterious QuestionPeer::doSelect(new Criteria())
call, it is also a standard Propel request. The Propel documentation will explain it thoroughly.
Don't worry if you don't understand all the code written above, it will become clearer in a few days.
Now that the database contains interests for questions, it should be
easy to get the number of interested users for one question. If you
have a look at the BaseQuestion.php
class generated by Propel in the askeet/lib/model/om/
directory, you will notice a ->getInterests()
method. Propel saw the question_id
foreign key in the Interest
table definition, and deduced that a question has several interests.
This makes it very easy to display what we want by modifying the listSuccess.php
template, located in askeet/apps/frontend/modules/question/templates/
. In the process, we'll remove the ugly tables and replace them with nice divs:
<?php use_helper('Text') ?> <h1>popular questions</h1> <?php foreach($questions as $question): ?> <div class="question"> <div class="interested_block"> <div class="interested_mark" id="mark_<?php echo $question->getId() ?>"> <?php echo count($question->getInterests()) ?> </div> </div> <h2><?php echo link_to($question->getTitle(), 'question/show?id='.$question->getId()) ?></h2> <div class="question_body"> <?php echo truncate_text($question->getBody(), 200) ?> </div> </div> <?php endforeach; ?>
You recognize here the same foreach
loop as in the original listSuccess.php
. The link_to()
and the truncate_text()
functions are template helpers
provided by symfony. The first one creates a hyperlink to another
action of the same module, and the second one truncates the body of the
question to 200 characters. The link_to()
helper is auto-loaded, but you have to declare the use of the Text
group of helpers to use truncate_text()
.
Come on, try on your new template by refreshing the development homepage again.
http://askeet/frontend_dev.php/
The number of interested users appears correctly close to each
question. To get the presentation of the above capture, download the main.css
stylesheet and put it in your askeet/web/css/
directory.
The propel-generate-crud
command created some actions and templates that will not be needed. It's time to remove them.
Actions to remove in askeet/apps/frontend/modules/question/actions/actions.class.php
:
executeIndex
executeEdit
executeUpdate
executeCreate
executeDelete
Template to remove in askeet/apps/frontend/modules/question/templates/
:
editSuccess.php
Today was a great first step in the world of the Model-View-Controller paradigm: By manipulating layouts, templates, actions and object of the Propel object model, you accessed all the layers of a MVC structured application. Don't worry if you don't understand all the bridges between these layers: It will become clearer little by little.
Many files were opened today, and if you want to know how files are organized in a project, refer to the file structure chapter of the symfony book.
Tomorrow will be another great day: We will modify views, setup a more complex routing policy, modify the model, and dig deeper into data manipulation and links between tables.
Until then, sleep tight, and feel free to browse the source of today's tutorial (tag release_day_3
) at:
http://svn.askeet.com/tags/release_day_3
If you find a typo or an error, please register and open a ticket.
If you need support or have a technical question, please post to the user mailing-list or to the forum.