Those of you itching to open your text editor and lay down some PHP will be happy to know today's tutorial will get us into some development. We will define the Jobeet data model, use an ORM to interact with the database, and build the first module of the application. But as symfony does a lot of the work for us, we will have a fully functional web module without writing too much PHP code.
Enabling sfDoctrinePlugin
If you're reading this then you've decided to complete the Jobeet tutorial for
the Doctrine ORM instead of Propel. This is simple as all you need to do first
is enable sfDoctrinePlugin
and disable sfPropelPlugin
. This can be done with
the following code in your config/ProjectConfiguration.class.php
.
public function setup() { $this->enablePlugins(array('sfDoctrinePlugin')); $this->disablePlugins(array('sfPropelPlugin')); }
If you prefer to have all plugins enabled by default, you can do the following:
public function setup() { $this->enableAllPluginsExcept(array('sfPropelPlugin', 'sfCompat10Plugin')); }
note
After this change you will get an error until we configure the
config/databases.yml
file at a later step to use sfDoctrineDatabase
.
After making these changes be sure to clear your cache.
$ php symfony cc
As we will see later in the tutorial, each plugin can embed assets (images,
stylesheets, and JavaScripts). When installing or enabling a new plugin, we
should install them via the plugin:publish-assets
task:
$ php symfony plugin:publish-assets
We also need to remove the web/sfPropelPlugin
directory:
$ rm web/sfPropelPlugin
tip
Another recommendation when using Doctrine instead of Propel is to remove the
config/propel.ini
and config/schema.yml
so you have a clean installation
with no references to Propel.
$ rm config/propel.ini $ rm config/schema.yml
The Relational Model
The user stories we have written yesterday describe the main objects of our project: jobs, affiliates, and categories. Here is the corresponding entity relationship diagram:
In addition to the columns described in the stories, we have also added a
created_at
field to some tables. 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.
The Schema
To store the jobs, affiliates, and categories, we obviously need a relational database.
But as symfony is an Object-Oriented framework, we like to manipulate objects whenever we can. For example, instead of writing SQL statements to retrieve records from the database, we'd rather prefer to use objects.
The relational database information must be mapped to an object model. This can be done with an ORM tool and thankfully, symfony comes bundled with two of them: Propel and Doctrine. In this tutorial, we will use Doctrine.
The ORM needs a description of the tables and their relationships to create the related classes. There are two ways to create this description schema: by introspecting an existing database or by creating it by hand.
As the database does not exist yet and as we want to keep Jobeet database
agnostic, let's create the schema file by hand by editing the empty
config/doctrine/schema.yml
file:
tip
You will need to manually create the config/doctrine/
directory in your
project as it does not already exist:
$ mkdir config/doctrine
# config/doctrine/schema.yml JobeetCategory: actAs: { Timestampable: ~ } columns: name: { type: string(255), notnull: true, unique: true } JobeetJob: actAs: { Timestampable: ~ } columns: category_id: { type: integer, notnull: true } type: { type: string(255) } company: { type: string(255), notnull: true } logo: { type: string(255) } url: { type: string(255) } position: { type: string(255), notnull: true } location: { type: string(255), notnull: true } description: { type: string(4000), notnull: true } how_to_apply: { type: string(4000), notnull: true } token: { type: string(255), notnull: true, unique: true } is_public: { type: boolean, notnull: true, default: 1 } is_activated: { type: boolean, notnull: true, default: 0 } email: { type: string(255), notnull: true } expires_at: { type: timestamp, notnull: true } relations: JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id, foreignAlias: JobeetJobs } JobeetAffiliate: actAs: { Timestampable: ~ } columns: url: { type: string(255), notnull: true } email: { type: string(255), notnull: true, unique: true } token: { type: string(255), notnull: true } is_active: { type: boolean, notnull: true, default: 0 } relations: JobeetCategories: class: JobeetCategory refClass: JobeetCategoryAffiliate local: affiliate_id foreign: category_id foreignAlias: JobeetAffiliates JobeetCategoryAffiliate: columns: category_id: { type: integer, primary: true } affiliate_id: { type: integer, primary: true } relations: JobeetCategory: { onDelete: CASCADE, local: category_id, foreign: id } JobeetAffiliate: { onDelete: CASCADE, local: affiliate_id, foreign: id }
tip
If you have decided to create the tables by writing SQL statements, you can
generate the corresponding schema.yml
configuration file by running the
doctrine:build-schema
task:
$ php symfony doctrine:build-schema
The above task requires that you have a configured database in databases.yml
.
We show you how to configure the database in a later step. If you try and run this
task now it won't work as it doesn't know what database to build the schema for.
The schema is the direct translation of the entity relationship diagram in the YAML format.
The schema.yml
file contains the description of all tables and their columns.
Each column is described with the following information:
type
: The column type (boolean
,integer
,float
,decimal
,string
,array
,object
,blob
,clob
,timestamp
,time
,date
,enum
,gzip
)notnull
: Set it totrue
if you want the column to be requiredunique
: Set it totrue
if you want to create a unique index for the column.
note
The onDelete
attribute defines the ON DELETE
behavior of foreign keys,
and Doctrine supports CASCADE
, SET NULL
, and RESTRICT
. For instance, when
a job
record is deleted, all the jobeet_category_affiliate
related
records will be automatically deleted by the database.
The Database
The symfony framework supports all PDO-supported databases (MySQL, PostgreSQL, SQLite, Oracle, MSSQL, ...). PDO is the database abstraction layer bundled with PHP.
Let's use MySQL for this tutorial:
$ mysqladmin -uroot -p create jobeet Enter password: mYsEcret ## The password will echo as ********
note
Feel free to choose another database engine if you want. It won't be difficult to adapt the code we will write as we will use the ORM will write the SQL for us.
We need to tell symfony to use this database for the Jobeet project:
The default config/
databases.yml
contains a connection that references propel.
Because we're using Doctrine, we need to remove the config/databases.yml
so we
can re-generate it for Doctrine.
$ rm config/databases.yml
Now simply run the following command to generate a new database configuration file for Doctrine:
$ php symfony configure:database --name=doctrine --class=sfDoctrineDatabase "mysql:host=localhost;dbname=jobeet" root mYsEcret
The configure:database
task takes three arguments: the
PDO DSN, the username, and
the password to access the database. If you don't need a password to access
your database on the development server, just omit the third argument.
note
The configure:database
task stores the
database configuration into the
config/databases.yml
configuration file. Instead of using the task, you can
edit this file by hand.
caution
Passing the database password on the command line is convenient but
insecure.
Depending on who has access to your environment, it might be better to
edit the config/databases.yml
to change the password. Of course, to
keep the password safe, the configuration file access mode should also
be restricted.
The ORM
Thanks to the database description from the schema.yml
file, we can use some
Doctrine built-in tasks to generate the SQL statements needed to create the
database tables:
First in order to generate the SQL you must build your models from your schema files.
$ php symfony doctrine:build-model
Now that your models are present you can generate and insert the SQL.
$ php symfony doctrine:build-sql
The doctrine:build-sql
task generates SQL statements in the data/sql/
directory, optimized for the database engine we have configured:
# snippet from data/sql/schema.sql CREATE TABLE jobeet_category (id BIGINT AUTO_INCREMENT, name VARCHAR(255) NOT NULL COMMENT 'test', created_at DATETIME, updated_at DATETIME, slug VARCHAR(255), UNIQUE INDEX sluggable_idx (slug), PRIMARY KEY(id)) ENGINE = INNODB;
To actually create the tables in the database, you need to run the
doctrine:insert-sql
task:
$ php symfony doctrine:insert-sql
tip
As for any command line tool, symfony tasks can take arguments
and options.
Each task comes with a built-in help message that can be displayed by running
the help
task:
$ php symfony help doctrine:insert-sql
The help message lists all the possible arguments and options, gives the default values for each of them, and provides some useful usage examples.
The ORM also generates PHP classes that map table records to objects:
$ php symfony doctrine:build-model
The doctrine:build-model
task generates PHP files in the lib/model/
directory that can be used to interact with the database.
By browsing the generated files, you have probably noticed that Doctrine
generates three classes per table. For the jobeet_job
table:
JobeetJob
: An object of this class represents a single record of thejobeet_job
table. The class is empty by default.BaseJobeetJob
: The parent class ofJobeetJob
. Each time you rundoctrine:build-model
, this class is overwritten, so all customizations must be done in theJobeetJob
class.JobeetJobTable
: The class defines methods that mostly return collections ofJobeetJob
objects. The class is empty by default.
The column values of a record can be manipulated with a model object by using
some accessors (get*()
methods) and mutators (set*()
methods):
$job = new JobeetJob(); $job->setPosition('Web developer'); $job->save(); echo $job->getPosition(); $job->delete();
You can also define foreign keys directly by linking objects together:
$category = new JobeetCategory(); $category->setName('Programming'); $job = new JobeetJob(); $job->setCategory($category);
The doctrine:build-all
task is a shortcut for the tasks we have run in this
section and some more. So, run this task now to generate forms and validators
for the Jobeet model classes:
$ php symfony doctrine:build-all --no-confirmation
You will see validators in action at the end of the day and forms will be explained in great details on day 10.
As you will see later on, symfony autoloads PHP classes for you, which means
that you never need to use a require
in your code. It is one of the numerous
things that symfony automates for the developer but there is one downside:
whenever you add a new class, you need to clear the symfony cache. As the
doctrine:build-model
has created a lot of new classes, let's clear the cache:
$ php symfony cache:clear
tip
A symfony task is made of a namespace and a task name. Each one can be
shortened as much as there is no ambiguity with other tasks. So, the following
commands are equivalent to cache:clear
:
$ php symfony cache:cl $ php symfony ca:c
As the cache:clear
task is so commonly used, it has an even shorter
hardwired abbreviation:
$ php symfony cc
The Initial Data
The tables have been created in the database but there is no data in them. For any web application, there are three types of data:
Initial data: Initial data are needed for the application to work. For example, Jobeet needs some initial categories. If not, nobody will be able to submit a job. We also need an admin user to be able to login to the backend.
Test data: Test Data are needed for the application to be tested. As a developer, you will write tests to ensure that Jobeet behaves as described in the user stories, and the best way is to write automated tests. So, each time you run your tests, you need a clean database with some fresh data to test on.
User data: User data are created by the users during the normal life of the application.
Each time symfony creates the tables in the database, all the data are lost.
To populate the database with some initial data, we could create a PHP script,
or execute some SQL statements with the mysql
program. But as the need is
quite common, there is a better way with symfony: create YAML files in the
data/fixtures/
directory and use the doctrine:data-load
task to load them
into the database.
First, create the following fixture files:
# data/fixtures/categories.yml JobeetCategory: design: name: Design programming: name: Programming manager: name: Manager administrator: name: Administrator # data/fixtures/jobs.yml JobeetJob: job_sensio_labs: JobeetCategory: programming type: full-time company: Sensio Labs logo: sensio-labs.gif url: http://www.sensiolabs.com/ position: Web Developer location: Paris, France description: | You've already developed websites with symfony and you want to work with Open-Source technologies. You have a minimum of 3 years experience in web development with PHP or Java and you wish to participate to development of Web 2.0 sites using the best frameworks available. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_sensio_labs email: job@example.com expires_at: '2010-10-10' job_extreme_sensio: JobeetCategory: design type: part-time company: Extreme Sensio logo: extreme-sensio.gif url: http://www.extreme-sensio.com/ position: Web Designer location: Paris, France description: | Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in. Voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum. how_to_apply: | Send your resume to fabien.potencier [at] sensio.com is_public: true is_activated: true token: job_extreme_sensio email: job@example.com expires_at: '2010-10-10'
note
The job fixture file references two images. You can download them
(/get/jobeet/sensio-labs.gif
,
/get/jobeet/extreme-sensio.gif
) and put them
under the web/uploads/jobs/
directory.
A fixtures file is written in YAML, and defines model objects, labelled with a
unique name (for instance, we have defined two jobs labelled job_sensio_labs
and job_extreme_sensio
). This label is of great use to link related objects
without having to define primary keys (which are often
auto-incremented and cannot be set). For instance, the job_sensio_labs
job
category is programming
, which is the label given to the 'Programming'
category.
tip
In a YAML file, when a string contains line breaks (like the description
column in the job fixture file), you can use the pipe (|
) to indicate that
the string will span several lines.
Although a fixture file can contain objects from one or several models, we have decided to create one file per model for the Jobeet fixtures.
note
Propel requires that the fixtures files be prefixed with numbers to determine the order in which the files will be loaded. With Doctrine this is not required as all fixtures will be loaded and saved in the correct order to make sure foreign keys are set properly.
In a fixture file, you don't need to define all columns values. If not,
symfony will use the default value defined in the database schema. And as
symfony uses Doctrine to load the data into the database, all the built-in
behaviors (like automatically setting the created_at
or updated_at
columns) and the custom behaviors you might have added to
the model classes are activated.
Loading the initial data into the database is as simple as running the
doctrine:data-load
task:
$ php symfony doctrine:data-load
tip
The doctrine:build-all-reload
task is a shortcut for the doctrine:build-all
task
followed by the doctrine:data-load
task.
Run the doctrine:build-all-reload
task to make sure everything is generated from your schema. This will generate your forms, filters, models, drop your database and re-create it with all the tables.
$ php symfony doctrine:build-all-reload
See it in Action in the Browser
We have used the command line interface a lot but that's not really exciting, especially for a web project. We now have everything we need to create Web pages that interact with the database.
Let's see how to display the list of jobs, how to edit an existing job, and how to delete a job. As explained during day 1, a symfony project is made of applications. Each application is further divided into modules. A module is a self-contained set of PHP code that represents a feature of the application (the API module for example), or a set of manipulations the user can do on a model object (a job module for example).
Symfony is able to automatically generate a module for a given model that provides basic manipulation features:
$ php symfony doctrine:generate-module --with-show --non-verbose-templates frontend job JobeetJob
The doctrine:generate-module
generates a job
module in the frontend
application for the JobeetJob
model. As with most symfony tasks,
some files and directories have been created for you under the
apps/frontend/modules/job/
directory:
Directory | Description |
---|---|
actions/ |
The module actions |
templates/ |
The module templates |
The actions/actions.class.php
file defines all the available action for
the job
module:
Action name | Description |
---|---|
index |
Displays the records of the table |
show |
Displays the fields and their values for a given record |
new |
Displays a form to create a new record |
create |
Creates a new record |
edit |
Displays a form to edit an existing record |
update |
Updates a record according to the user submitted values |
delete |
Deletes a given record from the table |
You can now test the job module in a browser:
http://jobeet.localhost/frontend_dev.php/job
If you try to edit a job, you will notice the Category id drop down has a list
of all the category names. The value of each option is gotten from the __toString()
method.
Doctrine will try and provide a base __toString()
method by guessing a
descriptive column name like, title
, name
, subject
, etc. If you want
something custom then you will need to add your own __toString()
methods like below.
The JobeetCategory
model is able to guess the __toString()
method by using the name
column of the jobeet_category
table.
// lib/model/doctrine/JobeetJob.class.php class JobeetJob extends BaseJobeetJob { public function __toString() { return sprintf('%s at %s (%s)', $this->getPosition(), $this->getCompany(), $this->getLocation()); } } // lib/model/doctrine/JobeetAffiliate.class.php class JobeetAffiliate extends BaseJobeetAffiliate { public function __toString() { return $this->getUrl(); } }
You can now create and edit jobs. Try to leave a required field blank, or try to enter an invalid date. That's right, symfony has created basic validation rules by introspecting the database schema.
See you Tomorrow
That's all for today. I have warned you in the introduction. Today, we have barely written PHP code but we have a working web module for the job model, ready to be tweaked and customized. Remember, no PHP code also means no bugs!
If you still have some energy left, feel free to read the generated code for the module and the model and try to understand how it works. If not, don't worry and sleep well, as tomorrow, we will talk about one of the most used paradigm in web frameworks, the MVC design pattern.
As for any other day, today's code is available on the Jobeet SVN repository.
Checkout the release_day_03
tag:
$ svn co http://svn.jobeet.org/doctrine/tags/release_day_03/ jobeet/
This work is licensed under the Creative Commons Attribution-Share Alike 3.0 Unported License license.