Day 03: dive into the MVC architecture ====================================== Previously on symfony --------------------- During [day two](2.txt) 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](http://www.symfony-project.com/book/1_0/06-Inside-the-Controller-Layer) of the symfony book. The MVC model ------------- Today will be the first dive in the world of the [MVC architecture][1]. 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](http://www.symfony-project.com/book/1_0/02-Exploring-Symfony-s-Code) 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][2], 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: [php]
getRaw('sf_content') ?>
>**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](http://svn.askeet.com/tags/release_day_3/web/css/). > >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 `` 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/ ![updated layout](/images/askeet/congratulations_new.gif) 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](http://www.symfony-project.com/book/1_0/05-Configuring-Symfony) 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](http://www.symfony-project.com/book/1_0/06-Inside-the-Controller-Layer) 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](http://www.symfony-project.com/book/1_0/09-Links-and-the-Routing-System) 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][3] 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](http://www.symfony-project.com/book/1_0/19-Mastering-Symfony-s-Configuration-Files) of the symfony book. As of now, if you are not familiar with the YAML syntax, you should [get started][3] 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](http://www.symfony-project.com/book/1_0/08-Inside-the-Model-Layer) 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] 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: 1. read the YAML file 2. Create instances of Propel objects 2. 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: [php] $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 ![loaded data](/images/askeet/fixtures.gif) 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: > > [php] > $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](http://www.symfony-project.com/book/1_0/06-Inside-the-Controller-Layer) of the symfony book. Let's have a look at the code that is executed: actions.class.php: [php] public function executeList () { $this->questions = QuestionPeer::doSelect(new Criteria()); } listSuccess.php: [php] ... getId(), 'question/show?id='.$question->getId()) ?> getTitle() ?> getBody() ?> getCreatedAt() ?> getUpdatedAt() ?> Step-by-step, here is what it does: 1. The action requires the records of the `Question` table that satisfy an empty criteria - i.e. all the questions 2. This list of records is put in an array (`$questions`) that is passed to the template 3. The template iterates over all the questions passed by the action 4. 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](2.txt) ?) 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][4] 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]

popular questions

getInterests()) ?>

getTitle(), 'question/show?id='.$question->getId()) ?>

getBody(), 200) ?>
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/ ![better question list](/images/askeet/question_list_day3.gif) The number of interested users appears correctly close to each question. To get the presentation of the above capture, download the [`main.css`](http://svn.askeet.com/tags/release_day_3/web/css/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](http://www.symfony-project.com/book/1_0/04-The-Basics-of-Page-Creation) 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 [1]: http://en.wikipedia.org/wiki/Model-view-controller "Model-View-Controller definition at Wikipedia" [2]: http://en.wikipedia.org/wiki/Decorator_pattern "Decorator pattern definition at Wikipedia" [3]: http://www.yaml.org/ "YAML" [4]: http://propel.phpdb.org/docs/user_guide/ "Propel documentation"