![]() |
|
The Askeet Tutorialsymfony advent calendar day two: setting up a data model |
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 one of this long but interesting tutorial, we saw how to install the symfony framework, setup a new application and development environment, and bring safety to the code with source version control. By the way, the code of the application generated during the first day is available in the askeet SVN repository at:
http://svn.askeet.com/
The objectives for the second day are to define what the final result should be in terms of functionalities, sketch the data model, and begin coding. This will include generating an object-relational mapping and using it interactively to create, retrieve and update records in a database with an application scaffolding.
That's quite a lot. Let's get started.
What do you want to know? That's an interesting question. There are many interesting questions, like:
All these questions don't have only one answer, and the best answer is a matter of opinion. As a matter of fact, the questions that only have one answer are often the least interesting (like, how much is 1 + 1?) but the only ones to be solved on the web. That's not fair.
Meet askeet. A website dedicated to help people find answers to their questions. Who will answer those ticklish questions? Everybody. And everybody will be able to rate other people's answers, so that the most popular answers get more visibility. As the number of questions increases, it becomes impossible to organize them in categories and sub-categories, so the creator of a question will be able to tag it with any word he/she wants, "à la" del.icio.us. Of course, the popularity of tags will have to be represented through a tag bubble. If one wants to follow the answers to a particular question, he/she can subscribe to this question's RSS feed. All these functionalities have to be elegant and lightweight, so all the interactions that don't actually need a new page have to be of AJAX type. Eventually, a back-end is necessary to moderate questions and answers reported as spam, or to push artificially a question that the administrator finds encouraging.
Then you should ask: Haven't I already seen such a website on the web? Well, if you actually did, we're busted, but if you refer to faqts, eHow, Ask Jeeves or something similar, with no collaborative answers, no AJAX, no RSS and no tags, this is not the same website. We are talking about a web 2.0 application here.
The big deal about askeet is that it is not only a website, it is an application that anyone can download, install at home or in a company Intranet, tweak and add features to. The source code will be released with an open-source license. Your HR head is looking for a knowledge management system? You want to keep track of all the tricks you learned about fixing your car? You don't want to develop a Frequently Asked Questions section for your website? Search no more, for askeet exists. Well, it will exist, that's our Christmas present.
So how are you supposed to start a symfony application? It all depends on you. You could write stories, do a planning game and find a partner to do pair programming if you were an XP adept, or write a detailed specification of the website, together with a sketch of all the objects, states, interactions and so on if you were a UML fan.
But this tutorial isn't about application development in general, so we'll start with a basic relational data model, and add working features one by one. What we need is an application that can be used at the end of every day, not a gigantic ongoing bunch of code that never outputs anything. In an ideal world, we should write unit tests for any feature we add, but we honestly won't have time for that. One day will be dedicated to unit tests though, so keep on reading.
For this project, we will use a MySQL database with the InnoDB table
type to take advantage of the integrity constraints and transaction
support. We could have used a SQLite database for the first steps, to
avoid setting up a real database. This would have required only a few
changes in the databases.yml
file, that we'll leave for you to investigate as an exercise.
Obviously, there will be a 'question' and an 'answer' tables. We'll need a 'user' table, and we'll store the interest of users for a question in a 'interest' table, and the relevancy given by a person to an answer in a 'relevancy' table.
Users will have to be identified to add a question, to rate the relevancy an answer, or to declare interest to a question. Users won't need to be identified to add an answer, but an answer will always be linked to a user so that users with popular answers can be distinguished. The answers entered without any identification will be shown as contributions of a generic user, called 'Anonymous Coward'. It's easier to understand with an entity relationship diagram:
Notice that we've declared a created_at
field for each
table. Symfony recognizes such fields and sets the value to the current
system time when a record is created. That's the same for updated_at
fields: Their value is set to the system time whenever the record is updated.
schema.xml
The relational model has to be translated to a configuration file for symfony to understand it. That's the purpose of the schema.xml
or schema.yml
file, located in the askeet/config/
directory. Symfony supports schemas in the XML or YAML format.
There are two ways to write this file: by hand, and that's the way we like it, or from an existing database. Let's see the first solution.
First, we need to remove the sample YAML file installed by default:
$ svn delete config/schema.yml
The syntax of the schema.xml
, explained in detail on the Propel website, is relatively simple: It's an XML file, in which <table>
tags contain <column>
, <foreign-key>
and <index>
tags. Once you write one, you can write all of them. Here is the schema.xml
corresponding to the relational model described previously:
<?xml version="1.0" encoding="UTF-8"?> <database name="propel" defaultIdMethod="native" noxsd="true"> <table name="ask_question" phpName="Question"> <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" /> <column name="user_id" type="integer" /> <foreign-key foreignTable="ask_user"> <reference local="user_id" foreign="id"/> </foreign-key> <column name="title" type="longvarchar" /> <column name="body" type="longvarchar" /> <column name="created_at" type="timestamp" /> <column name="updated_at" type="timestamp" /> </table> <table name="ask_answer" phpName="Answer"> <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" /> <column name="question_id" type="integer" /> <foreign-key foreignTable="ask_question"> <reference local="question_id" foreign="id"/> </foreign-key> <column name="user_id" type="integer" /> <foreign-key foreignTable="ask_user"> <reference local="user_id" foreign="id"/> </foreign-key> <column name="body" type="longvarchar" /> <column name="created_at" type="timestamp" /> </table> <table name="ask_user" phpName="User"> <column name="id" type="integer" required="true" primaryKey="true" autoIncrement="true" /> <column name="nickname" type="varchar" size="50" /> <column name="first_name" type="varchar" size="100" /> <column name="last_name" type="varchar" size="100" /> <column name="created_at" type="timestamp" /> </table> <table name="ask_interest" phpName="Interest"> <column name="question_id" type="integer" primaryKey="true" /> <foreign-key foreignTable="ask_question"> <reference local="question_id" foreign="id"/> </foreign-key> <column name="user_id" type="integer" primaryKey="true" /> <foreign-key foreignTable="ask_user"> <reference local="user_id" foreign="id"/> </foreign-key> <column name="created_at" type="timestamp" /> </table> <table name="ask_relevancy" phpName="Relevancy"> <column name="answer_id" type="integer" primaryKey="true" /> <foreign-key foreignTable="ask_answer"> <reference local="answer_id" foreign="id"/> </foreign-key> <column name="user_id" type="integer" primaryKey="true" /> <foreign-key foreignTable="ask_user"> <reference local="user_id" foreign="id"/> </foreign-key> <column name="score" type="integer" /> <column name="created_at" type="timestamp" /> </table> </database>
Notice that the database name is set to propel
in this
file, whatever the actual database name. This is a parameter used to
connect the Propel layer to the symfony framework. The actual name of
the database will be defined in the databases.yml
configuration file (see below).
There is another way to create a schema.xml
if you have
an existing database. That is, if you are familiar with a graphical
database design tool, you will prefer to build the schema from the
generated MySQL database. Before you do that, you just need to edit the
propel.ini
file located in the askeet/config/
directory and enter the connection settings to your database:
propel.database.url = mysql://username:password@localhost/databasename
...where username
, password
, localhost
and databasename
are the actual connection settings of your database. You can now call the propel-build-schema
command (from the askeet/
directory) to generate the schema.xml
from the database:
$ symfony propel-build-schema
some tools allow you to build a database graphically (for instance Fabforce's Dbdesigner) and generate directly a
schema.xml
(with DB Designer 4 TO Propel Schema Converter).
Instead of creating a schema.xml
file, you can also create a schema.yml
file using the YAML schema format:
propel: _attributes: { noXsd: false, defaultIdMethod: none, package: lib.model } ask_question: _attributes: { phpName: Question, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } title: { type: longvarchar } body: { type: longvarchar } created_at: ~ updated_at: ~ ask_answer: _attributes: { phpName: Answer, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } question_id: { type: integer, foreignTable: ask_question, foreignReference: id } user_id: { type: integer, foreignTable: ask_user, foreignReference: id } body: { type: longvarchar } created_at: ~ ask_user: _attributes: { phpName: User, idMethod: native } id: { type: integer, required: true, primaryKey: true, autoIncrement: true } nickname: { type: varchar(50), required: true, index: true } first_name: varchar(100) last_name: varchar(100) created_at: ~ ask_interest: _attributes: { phpName: Interest, idMethod: native } question_id: { type: integer, foreignTable: ask_question, foreignReference: id, primaryKey: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true } created_at: ~ ask_relevancy: _attributes: { phpName: Relevancy, idMethod: native } answer_id: { type: integer, foreignTable: ask_answer, foreignReference: id, primaryKey: true } user_id: { type: integer, foreignTable: ask_user, foreignReference: id, primaryKey: true } score: { type: integer } created_at: ~
To use the InnoDB engine, one line has to be added to the propel.ini
file of the askeet/config/
directory:
propel.mysql.tableType = InnoDB
Once the schema.xml
is built, you can generate an
object model based on the relational model. In symfony, the object
relational mapping is handled by Propel, but encapsulated into the
symfony command:
$ symfony propel-build-model
This command (you need to call it from the root directory of the
askeet project) will generate the classes corresponding to the tables
defined in the schema, together with standard accessors (->get()
and ->set()
methods). You can look at the generated code in the askeet/lib/model/om/
directory. If you wonder why there are two classes per table, go and check the model chapter of the symfony book. These classes will be overridden each time that you do a build-model
,
and this will happen a lot in this project. So if you need to add
methods to the model objects, you have to modify the ones located in
the askeet/lib/model/
directory - these classes inherit from the /om
ones.
Now that symfony has an object model of the database, it is time to connect your project to the MySQL database. First, you have to create a database in MySQL:
$ mysqladmin -u youruser -p create askeet
Now open the askeet/config/databases.yml
configuration
file. If this is your first time with symfony, you will discover that
the symfony configuration files are written in YAML.
The syntax is very simple, but there is one major obligation in YAML
files: never use tabulations, always use spaces. Once you know that,
you are ready to edit the file and enter the actual connection settings
to your database under the all:
category:
all:
propel:
class: sfPropelDatabase
param:
phptype: mysql
host: localhost
database: askeet
username: youruser
password: yourpasswd
If you want to know more about symfony configuration and YAML files, read the configuration in practice chapter of the symfony book.
If you didn't write the schema.xml
file by hand, you probably already have the corresponding tables in your database. You can then skip this part.
For you keyboard fans, here is a surprise: You don't need to create
the tables and the columns in the MySQL database. You did it once in
the schema.xml
, so symfony will build the SQL statement creating all that for you:
$ symfony propel-build-sql
This command creates a lib.model.schema.sql
in the askeet/data/sql/
directory. Use it as a SQL command in MySQL:
$ mysql -u youruser -p askeet < data/sql/lib.model.schema.sql
Alternatively, you can also use the propel-insert-sql
task:
$ symfony propel-insert-sql
It is always good to see that the work done is useful. Until now, your browser wasn't of any use, and yet we are supposed to build a web application... So let's create a basic set of symfony templates and actions to manipulate the data of the 'question' table. This will allow you to create a few questions and display them.
In the askeet/
directory, type:
$ symfony propel-generate-crud frontend question Question
This generates a scaffolding for a question
module in the frontend
application, based on the Question
Propel object model, with basic Create Retrieve Update Delete actions
(which explains the CRUD acronym). Don't get confused: A scaffolding is
not a finished application, but the basic structure on top of which you
can develop new features, add business rules and customize the look and
feel.
The list of all the actions created by a CRUD generator is:
Action name | Description |
---|---|
list | shows all the records of a table |
index | forwards to list |
show | shows all the fields of a given record |
edit | displays a form to create a new record or edit an existing one |
update | modifies a record according to the parameters given in the request, then forwards to show |
delete | deletes a given record from the table |
You can find more about generated actions in the scaffolding chapter of the symfony book.
In the askeet/apps/frontend/modules/
directory, notice the new question
module and browse its source.
Whenever you add a new class that need to be autoloaded, don't forget to clear the config cache (to reload the autoloading cache):
$ symfony cc frontend config
You can now test it online by requesting:
http://askeet/question
Go ahead, play with it. Add a few questions, edit them, list them, delete them. If it works, this means that the object model is correct, that the connection to the database is correct, and that the mapping between the relational model of the database and the object model of symfony is correct. That's a good functional test.
You didn't write one line of PHP, and yet you have a basic application to use. That's not bad for the second day. Tomorrow, we'll start writing some code in order to have a welcoming home page that displays the list of questions. We will also add test data to our database using a batch process, and learn how to extend the model.
Now that you know what the application will do, you may be able to imagine an additional feature to it. Feel free to suggest anything using the askeet mailing-list, the most popular idea will become the 21st day addition of this symfony advent calendar.
Feel free to browse the source of today's tutorial (tag release_day_2
) at:
http://svn.askeet.com/tags/release_day_2
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.