Getting Started with Symfony 1.4

Author’s note: This post was originally published Sun, 07/31/2011 – 18:14.  Since then, I have abandoned Symfony development (both versions 1.4 and 2.x) in favor of developing my own, AJAX-oriented framework which uses YAML files to define database schema and CRUD pages.  I call it JAX Framework.  You can read about it here: http://www.jaxframework.org and here: http://sourceforge.net/projects/jaxframework

The original article follows.

 

I’ve recently started using the Symfony framework for web application development, and I like it quite a bit. In this article, I share some notes I took for myself while I was learning to use Symfony.

Without any further ado, let’s get started.

Download the prerequisites checker here: http://sf-to.org/1.4/check.php
Run it like this: php php check_configuration.php
Fix any problems it finds.

Create a project folder, named according to your project. In my examples, I’ll typically be using something like “sfproject”, which is what the Symfony intro uses.

A copy of symfony would typically exist under the lib/vendor/symfony folder under your project folder. So if your project folder is located at /home/sfproject, symfony would be under /home/sfproject/lib/vendor/symfony. If the symfony version is 1.4.1, when you extract the tarball under lib/vendor, you’ll get a symfony-1.4.1 directory. Rename the directory to make it right. Put all together, you’d do something like this (replace with the path to the folder where the symfony tarball is):
cd lib/vendor
tar zxpf /symfony-1.4.1.tgz
mv symfony-1.4.1 symfony
cd ../..

Generate a new project:
php lib/vendor/symfony/data/bin/symfony generate:project

Generate the frontend app (or backend, if there is a backend app):
./symfony generate:app frontend

Fix directory permissions, if needed:
chmod 777 cache log

Enable the rewrite module for apache2. For ubuntu:
a2enmod rewrite
/etc/init.d/apache2 restart (or: service apache2 restart)

Create a virtual host for apache, named according to your project. This example is for a project named “sfproject” located at /home/sfproject. The filename is /etc/httpd/conf.d/sfproject.conf (change filename to match your project name):

ServerName sfproject
ServerAlias *.sfproject
DocumentRoot “/home/sfproject/web”
DirectoryIndex index.php

AllowOverride All
Allow from All

Alias /sf /home/sfproject/lib/vendor/symfony/data/web/sf

AllowOverride All
Allow from All

Restart apache when done.

Edit /etc/hosts. Find the line that looks something like this:
127.0.0.1 localhost.localdomain localhost

Add your project name onto the end, like this:
127.0.0.1 localhost.localdomain localhost sfproject

Point a browser at your project (replace sfproject with your project name):
http://sfproject
You should see the symfony startup page, which says something like “Symfony Project Created”.

Create the database for your project on MySQL or whatever, and make sure you have a good user/password configured on the database server. An example in MySQL:
mysql -u root
drop database if exists sfproject;
create database sfproject;
grant all privileges on *.* to ‘sfproject’@’%’ identified by ‘enter-your-password-here’;
grant all privileges on *.* to sfproject@localhost identified by ‘enter-your-password-here’;

Configure the database. Replace mysql with the database type, if it’s not mysql. Replace with the hostname or IP address of the database server. Replace with the name of the database. Replace and with the database username and password you configured.
./symfony configure:database “mysql:host=;dbname=”
Example:
./symfony configure:database “mysql:host=localhost;dbname=sfproject” sfproject enter-your-password-here

If you have a multiple-module project, do like this for each module (if it’s a backend module, replace frontend with backend):
./symfony generate:module frontend
NOTE: Typically, one module equals one CRUD page. So for each database table for which you’ll be creating a CRUD page, there will be one module to handle that table’s CRUD. Think of a symfony module as the “CRUD application” for a database table.

Edit config/doctrine/schema.yml. Define your database schema.

Edit data/fixtures/fixtures.yml, or any other yml file(s) in that directory (read the notes in fixtures.yml). Add your fixtures (default rows which get added to the database when it is initialized).

Build everything related to the model:
./symfony doctrine:build –all-classes
./symfony doctrine:generate-migrations-models
./symfony doctrine:migrate
./symfony doctrine:data-load

*** TODO: This part still doesn’t seem to work. The “./symfony doctrine:generate-migrations-db” says “Could not generate migration classes from database”. If you use “./symfony doctrine:generate-migrations-models” instead, it says it generates the migrations, but they remain untouched in lib/migration/doctrine/, and it fails to do make any changes to the database. This page covers migrations, but there doesn’t seem to be any way to easily bring an arbitrary database into sync with the current schema: http://www.denderello.com/publications/guide-to-doctrine-migrations. It may be necessary to “rm -f lib/migration/doctrine/*” before generating any migrations. Also see: http://www.slideshare.net/weaverryan/the-art-of-doctrine-migrations
Following any subsequent changes to the schemas, do this to update the model classes and database:
rm -f lib/migration/doctrine/*
./symfony doctrine:generate-migrations-db
./symfony doctrine:migrate
./symfony doctrine:build –all-classes
Conclusion: Doctrine migrations in Symfony appear to be broken. This is sad, because it really should work as advertised. You should be able to do whatever you need to do to your schema, and then run a single command and have it compare the current schema with that of the database and automatically apply the required DDL changes to the database to bring it into line with the current schema. There should be no need to generate migrations manually. In its current state, the Doctrine migration system should more aptly be called the Doctrine migraine system.

NOTE: If you ever need to drop a table, delete it from schema.yml and then do this:
./symfony doctrine:clean-model-files (or ./symfony doc:clean)
If the migration worked as advertised, you would just do a migration at this point. Alas, you’ll need to drop the table manually in mysql or whatever database you’re using.

NOTE: To show all available doctrine tasks, run:
./symfony list doctrine

Edit apps/frontend/templates/layout.php to define the site-wide layout. This is the top-to-bottom layout (header, template, footer). Do something like this:

<?php include_http_metas() ?>
<?php include_metas() ?>
<?php include_title() ?>

<?php include_stylesheets() ?>
<?php include_javascripts() ?>

>
<?php include_component_slot(‘topNav’); ?>

<?php include_component_slot(‘leftNav’); ?>

<?php echo $sf_content ?>

Put javascript files, css files and image files under web/js, web/css and web/images, respectively.

Edit apps/frontend/config/view.yml and add any stylesheets and/or javascript files (comma-separated) you want to include automatically in every page. Note that main.css is already there by default.

You can add custom per-module stylesheets and javascript files at the module level, or at the page level, by editing apps/frontend/modules//config/view.yml. You can even put custom HTML blocks into the view for that module. You can either add them under the “stylesheets:” and/or “javascripts:” entries, or define each action to have additional stylesheets and/or javascript files by creating new entries in the form “:”. Or, you can just call the use_stylesheet(‘.css’) and/or use_javascript(‘.css’) function inside each individual template in order to minimize the amount of configuration junk you throw into view.yml.

The action controllers are located in apps/frontend/modules//actions/actions.class.php. Each action is a function like this:
public function execute(sfWebRequest $request)
Each action handler function basically does all of the database work, and populates various properties into $this. Example (look for $this->jobeet_jobs):
public function executeIndex(sfWebRequest $request)
{
$this->jobeet_jobs = Doctrine_Core::getTable(‘JobeetJob’)
->createQuery(‘a’)
->execute();
}

Each view template is located in apps/frontend/modules//templates/Success.php. Each property which was set in $this in the action, is available as a PHP variable for use in the view template, like this (look for $jobeet_jobs):
<?php foreach ($jobeet_jobs as $i => $job): ?>
“>
<?php echo $job->getLocation() ?>

getId()) ?>”>
<?php echo $job->getPosition() ?>

<?php echo $job->getCompany() ?>

<?php endforeach ?>

NOTE: It is possible use one class per action, as below. Howerver, since each module generally corresponds to a CRUD page for a single table and its relations, it usually won’t be necessary.
apps/frontend/modules/pfmgr2/actions/indexAction.class.php
<?php
class indexAction extends sfActions {
public function executeIndex(sfWebRequest $request) {
// put action handler code here.
}
}

NOTE: Be careful to keep all model-related code in the model classes themselves, so that the controller is simply calling model functions and setting properties for the view to use.

NOTE: If you have an action URI like “posting-types”, that will translate to an action named “postingtypes”, because the non-class-compatible characters will be stripped out. Be careful to name your class file, class, action handler function and action success template file correspondingly. You can also customize the action URIs in the routing.yml file (see below).

In apps/frontend/templates/layout.php, you can define slots (placeholders) which will be filled in with HTML by the templates.
In apps/frontend/templates/layout.php:
<?php include_slot(‘title’) ?>
To provide a default value for the slot when the template does not provide a value for it:
<?php include_slot(‘title’, ‘Jobeet – Your best job board’) ?>
In apps/frontend/modules/job/templates/showSuccess.php:
<?php slot(
‘title’,
sprintf(‘%s is looking for a %s’, $job->getCompany(), $job->getPosition()))
?>

URL routing is configured in apps/frontend/config/routing.yml. You can add module-specific routing configuration in apps/frontend/modules//config/routing.yml. It’s possible that in some cases, custom routing won’t be needed because the default routes look pretty good.

If you want a default module to handle the homepage for the site, do something like this in apps/frontend/config/routing.yml:
homepage:
url: /
param: { module: job, action: index }

In each routing rule, when you see something like :module, that’s assigning a value to a variable. The module and action variables are used by symfony to route to a specific module, and a specific action within that module. The special “*” character means use the rest of the path as name-value pairs in the form /name/value, so for example /id/37 would set the id variable to 37.

The url_for() helper will take a URL like ‘job/show?id=37’ and convert it to /job/show/id/37. The preferred way is to use url_for(‘@default?module=job&action=show&id=37’) which would also return /job/show/id/37.

See here for more info on routing: http://www.symfony-project.org/jobeet/1_4/Doctrine/en/05

Two ways to do a query.
Simple:
$this->jobeet_jobs = Doctrine::getTable(‘JobeetJob’)
->createQuery(‘a’)
->execute();
More control:
$q = Doctrine_Query::create()
->from(‘JobeetJob j’)
->where(‘j.created_at > ?’, date(‘Y-m-d H:i:s’, time() – 86400 * 30));

$this->jobeet_jobs = $q->execute();
These kinds of custom queries should be created in custom functions inside the model, and called from the actions. Avoid writing ad hoc queries inside the actions.

To customize the model, you just edit the entity’s class file. These are located in lib/model/doctrine. The generated base classes should NEVER be edited by hand because they are re-generated by Doctrine when you make a schema change. They are located in lib/model/doctrine/base. Only edit php classes which exist directly in lib/model/doctrine. For each entity you created in the model, there will be two files here:
.class.php
Table.class.php
Normally you would only edit the first one (.class.php). You can add special finder methods which find by specific criteria, or you can override the save(Doctrine_Connection $conn = null) method to do some calculations and assign the results of those calculations to columns before calling:
return parent::save($conn);

You can put application-wide configuration parameters into apps/frontend/config/app.yml:
all:
active_days: 30
You can retrieve these like this (note the “app_” prefix):
$activeDays = sfConfig::get(‘app_active_days’);

Component slots are useful for headings, navigation, etc. To do one, edit apps/frontend/templates/layout.php and call include_component_slot(), like this (this shows two separate component slots):
<?php include_component_slot(‘topNav’); ?>

<?php include_component_slot(‘leftNav’); ?>
Then edit apps/frontend/config/view.yml and add this under the “default” section (pfmgr2 is module name; topNav and leftNav are component label names):
default:
components:
topNav: [pfmgr2, topNav]
leftNav: [pfmgr2, leftNav]
Create a components.class.php file to handle the component events (required). This one is in apps/frontend/modules/pfmgr2/actions/components.class.php:
<?php
class pfmgr2Components extends sfComponents {
public function executeTopNav(sfWebRequest $request) {
(put component controller code here)
}

public function executeLeftNav(sfWebRequest $request) {
(put component controller code here)
}
}
NOTE: A better way, is to put each component’s controller code into a separate file, like this:
apps/frontend/modules/pfmgr2/actions/topNavComponent.class.php:
<?php
class topNavComponent extends sfComponents {
public function executeTopNav(sfWebRequest $request) {
if ($this->getUser()->isAuthenticated()) {
$this->isAuthenticated = true;
$this->fullName = $this->getUser()->getName();
} else {
$this->isAuthenticated = false;
}
}
}

This one provides a complete navigation menu, removing menu items which the user can’t access.
apps/frontend/modules/pfmgr2/actions/leftNavComponent.class.php:
<?php
class leftNavComponent extends sfComponents {
public static $LEFT_NAV_MENU_GROUPS;
protected $security;
protected $securityByModuleName;

public function executeLeftNav(sfWebRequest $request) {
$this->menuGroups = array();
$user = $this->getUser();

$isAuthenticated = $user->isAuthenticated();
$isSuperAdmin = $isAuthenticated && $user->isSuperAdmin();

$context = $this->getContext();
$routing = $context->getRouting();

$this->security = array();
$this->securityByModuleName = array();

$group = array();
foreach (self::$LEFT_NAV_MENU_GROUPS as $menuGroup) {
foreach ($menuGroup[‘items’] as $menuGroupItem) {

$params = $routing->parse($menuGroupItem[‘uri’]);

if (!isset($this->securityByModuleName[$params[‘module’]])) {
$this->security = array();
if ($fn = $context->getConfigCache()->checkConfig
(‘modules/’.$params[‘module’].’/config/security.yml’, true)) {
require($fn);
}
$this->securityByModuleName[$params[‘module’]] = $this->security;
$this->security = array();
}

if ($this->isActionSecure($params[‘module’], $params[‘action’])) {
if (!$isAuthenticated) {
continue;
}
if ((!$isSuperAdmin) &&
(!$user->hasCredential
($this->getActionCredentials($params[‘module’], $params[‘action’])))) {
continue;
}
}

if (!isset($group[‘name’])) $group[‘name’] = $menuGroup[‘name’];
if (!isset($group[‘items’])) $group[‘items’] = array();
$group[‘items’][] = $menuGroupItem;
}
if (!empty($group)) {
$this->menuGroups[] = $group;
$group = array();
}
}
}

protected function getModuleSecurityValue($moduleName, $actionName, $name, $default = null) {
$actionName = strtolower($actionName);
if (isset($this->securityByModuleName[$moduleName][$actionName][$name])) {
return $this->securityByModuleName[$moduleName][$actionName][$name];
}
if (isset($this->securityByModuleName[$moduleName][‘all’][$name])) {
return $this->securityByModuleName[$moduleName][‘all’][$name];
}
return $default;
}

protected function isActionSecure($moduleName, $actionName) {
return $this->getModuleSecurityValue($moduleName, $actionName, ‘is_secure’, false);
}

protected function getActionCredentials($moduleName, $actionName) {
return $this->getModuleSecurityValue($moduleName, $actionName, ‘credentials’);
}
}
leftNavComponent::$LEFT_NAV_MENU_GROUPS = array(
array(
‘name’=>’Setup’,
‘items’=>array(
array(‘name’=>’Users’, ‘uri’=>’guard/users’),
array(‘name’=>’Groups’, ‘uri’=>’guard/groups’),
array(‘name’=>’Posting Types’, ‘uri’=>’postingType/index’),
array(‘name’=>’Account Types’, ‘uri’=>’accountType/index’),
array(‘name’=>’Accounts’, ‘uri’=>’account/index’),
array(‘name’=>’Incomes/Expenses’, ‘uri’=>’incomeExpense/index’),
),
),
array(
‘name’=>’Data Entry’,
‘items’=>array(
array(‘name’=>’Enter/Reconcile Postings’, ‘uri’=>’posting/index’),
),
),
array(
‘name’=>’Reports’,
‘items’=>array(
array(‘name’=>’Posting Details’, ‘uri’=>’postingDetailReport/index’),
array(‘name’=>’Income/Expenses’, ‘uri’=>’incomeExpenseReport/index’),
),
),
);

Create php files to generate the components’ HTML content. You can put HTML and PHP code in these templates; they are part of the view. These are located in apps/frontend/modules/pfmgr2/templates and are named _topNav.php and _leftNav.php respectively.
_topNav.php:

Personal Finance Manager

<?php if ($isAuthenticated) { ?>
Logged in as: <?php echo esc_specialchars($fullName); ?>
<?php echo link_to(‘Log out’, ‘sf_guard_signout’); ?>
<?php } else { ?>
You are not logged in.
<?php } ?>

_leftNav.php:
<?php
echo link_to(‘Home’, ‘homepage’);
foreach ($menuGroups as $menuGroup) {
echo “”.esc_specialchars($menuGroup[‘name’]).”\n

      \n”;
      foreach ($menuGroup[‘items’] as $menuGroupItem) {
      echo ”

    • “.link_to($menuGroupItem[‘name’], $menuGroupItem[‘uri’]).”

\n”;
}
echo ”

 

\n”;
}

————————————
Securing an application with a login
————————————

Install sfDoctrineGuardPlugin:
./symfony plugin:install sfDoctrineGuardPlugin

Create an administrative user and some permissions (will need to reload or migrate the database).
data/fixtures/010_pfmgr2.yml (filename and data may change, depending on your application):
sfGuardUser:
sgu_admin:
username: admin
password: admin
is_super_admin: true
first_name: Administrator
email_address: admin@some.email.address

sfGuardPermission:
sgp_edit_users: { name: Edit Users, description: Edit Users }
sgp_edit_groups: { name: Edit Groups, description: Edit Groups }
sgp_edit_posting_types: { name: Edit Posting Types, description: Edit Posting Types }
sgp_edit_acct_types: { name: Edit Account Types, description: Edit Account Types }
sgp_edit_income_expenses: { name: Edit Income/Expenses, description: Edit Incomes/Expenses }
sgp_edit_accts: { name: Edit Accounts, description: Edit Accounts }
sgp_edit_postings: { name: Edit Postings, description: Edit Postings }
sgp_reconcile: { name: Reconcile, description: Reconcile }
sgp_view_posting_detail_report: { name: View Posting Detail Report, description: View Posting Detail Report }
sgp_view_income_expense_report: { name: View Income Expense Report, description: View Income/Expense Report }

Configure symfony to use sfDoctrineGuardPlugin.
apps/frontend/config/settings.yml:
all:
.settings:
enabled_modules: [default, sfGuardAuth, sfGuardUser, sfGuardGroup, sfGuardForgotPassword]
.actions:
login_module: sfGuardAuth # To be called when a non-authenticated user
login_action: signin # Tries to access a secure page
secure_module: sfGuardAuth
secure_action: secure

apps/frontend/config/security.yml:
default:
is_secure: true

Configure the required credentials for each module (modify this for your application).
apps/frontend/modules/sfGuardUser/config/security.yml:
all:
is_secure: true
credentials: [ Edit Users ]

apps/frontend/modules/sfGuardGroup/config/security.yml:
all:
is_secure: true
credentials: [ Edit Groups ]

apps/frontend/modules/postingType/config/security.yml:
all:
is_secure: true
credentials: [ Edit Posting Types ]

Now any secure page should require login, and any action which requires credentials should prevent the user from executing that action if the user doesn’t have the required credentials.

——————————-
Develop a custom form generator
——————————-

apps/frontent/lib/myFormGenerator.class.php:
<?php
class myFormGenerator extends sfDoctrineFormGenerator {
public function getWidgetClassForColumn($column) {
switch ($column->getDoctrineType()) {
case ‘date’:
case ‘timestamp’:
return ‘sfWidgetFormTextDateInputJQueryDatePicker’;
break;
default:
return parent::getWidgetClassForColumn($column);
}
}

public function getWidgetOptionsForColumn($column) {
switch ($column->getDoctrineType()) {
case ‘timestamp’:
return “array(‘include_time’ => true)”;
break;
default:
return parent::getWidgetOptionsForColumn($column);
}
}
}

————————————————-
Rebuilding Model, Forms, etc. After Schema Change
————————————————-

./symfony doctrine:build-model
./symfony doctrine:build-filters
./symfony doctrine:build-forms –generator-class=myFormGenerator
./symfony cc

————————————–
Wiping out and Rebuilding the Database
————————————–

./symfony doctrine:build –db –and-load

——————————————————-
Making Plugin Assets Available to Web Pages and Actions
——————————————————-

./symfony plugin:publish-assets

———————————————————–
Generating database administration pages based on the model
———————————————————–

To generate an admin page whose URI will be /postingType, for the PfmPostingType entity, do the following:
./symfony doctrine:generate-admin frontend PfmPostingType –module=postingType

NOTE: Symfony likes for you to use one module per major entity.

NOTE: You can edit generator.yml for the module, and update various things about the administration.

—————————-
Creating, packaging a plugin
—————————-

Be sure to install sfTaskExtraPlugin, which gives you the extra tasks needed to create and package plugins.
./symfony plugin:install sfTaskExtraPlugin

To create a new plugin:
./symfony generate:plugin myFirstPlugin

To create a module within a plugin:
./symfony generate:plugin-module myFirstPlugin myFirstModule

Package the plugin as a PEAR package for release on symfony-project.org, so other users can install it using “./symfony plugin:install myFirstPlugin”. This will create a PEAR-compatible .tar.gz file in the plugin’s directory, which can be uploaded to symfony-project.org under the plugin admin page (you must have admin permissions for the plugin).
./symfony plugin:package myFirstPlugin
Enter the release number (ie 1.0.0) and stability (ie stable).

Add the plugin to SVN on the symfony-project.org website.

Add each plugin release on symfony-project.org as a PEAR package so others can easily install it.

—————————
Making Admin Forms Use AJAX
—————————

./symfony plugin:install sfAjaxAdminDoctrineThemePlugin

Follow the instructions in plugin/sfAjaxAdminDoctrineThemePlugin/README.

—————————–
Making Admin Forms Look Nicer
—————————–

./symfony plugin:install sfFormExtraPlugin

Edit lib/form/doctrine/.class.php. Add something like the following in the configure() function, as needed:
$this->widgetSchema[‘posting_date’] = new sfWidgetFormJQueryDate();

Clear the cache:
./symfony cc

That’s all for now. I hope this article has given you some good information and pointers on using symfony. It’s a really nice, stable framework for developing web applications.

Go forth and write software!

Leave a Reply

Your email address will not be published. Required fields are marked *