Day 03: dive into the MVC architecture
Previously on symfony
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.
The MVC model
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.
Change the layout
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>
note
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
and layout.css
). Copy them into your askeet/web/css/
directory and edit your frontend/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/
A few words about environments
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.
note
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.
Redefine the default homepage
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.
note
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 the default/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 a default
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.
Define test data
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.
note
There is no need to define values for the created_at
and updated_at
columns, since symfony knows how to fill them by default.
Create a batch to populate the database
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.
Batch skeleton
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.
note
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.
Data import
Now that the frame of the batch is ready, it is time to make it do something. The batch has to:
- read the YAML file
- Create instances of Propel objects
- Create the related records in the tables of the linked database
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.
note
The DIRECTORY_SEPARATOR
constant is used here to be compatible with Windows and *nix platforms.
Launch the batch
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.
note
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');
Accessing the data in the model
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:
- The action requires the records of the
Question
table that satisfy an empty criteria - i.e. all the questions - This list of records is put in an array (
$questions
) that is passed to the template - The template iterates over all the questions passed by the action
- The templates shows the value of the columns of each record
The ->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.
Modify the question/list template
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.
Cleanup
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
See you Tomorrow
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
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.