This tutorial is a quick technical introduction for symfony 1.2. It is for developers who have already worked with symfony 1.0 and 1.1 and who want to quickly learn new features of symfony 1.2.
First, please note that symfony 1.2 is compatible with PHP 5.2.4 or later, whereas symfony 1.1 works with PHP 5.1 and symfony 1.0 with PHP 5.0.
If you want to upgrade from 1.1 or 1.0, please read the UPGRADE file found in the symfony distribution. You will have there all the information needed to safely upgrade your projects to symfony 1.2.
Propel
Propel has been upgraded to version 1.3, which replaces support for Creole with PDO, and includes many new features including: object instance pooling, master-slave connections, native nested-set support, and better date handling.
The databases.yml
configuration file now uses the PDO syntax:
dev: propel: param: classname: DebugPDO all: propel: class: sfPropelDatabase param: dsn: mysql:dbname=example;host=localhost username: username password: password encoding: utf8 persistent: true pooling: true classname: PropelPDO
The transaction api has change slightly: ->begin
has been renamed
->beginTransaction()
and ->rollback()
has been renamed ->rollBack()
.
The ::doSelectRS
method has been renamed to ::doSelectStmt
.
For more information on Propel 1.3, please read the complete documentation on their website.
Doctrine
Doctrine is a first-class citizen in symfony 1.2. It means that symfony 1.2 comes bundled with both the Propel plugin and the Doctrine one. All the built-in features are available for both Propel and Doctrine, so feel free to choose the ORM that suits you better.
Request
You can now simulate PUT
and DELETE
requests from a browser by using the
POST
method and adding a special sf_method
parameter:
<form action="#" method="POST"> <input type="hidden" name="sf_method" value="PUT" /> <!-- // ... --> </form>
The form_tag()
helper has been updated to automatically generate the hidden
tag for methods different from GET
or POST
. So, the opening form
tag
from the code above can be generated by using the form_tag()
helper like this:
<?php echo form_tag('#', array('method' => 'PUT')) ?>
As the link_to()
helper now embeds a CSRF token if CSRF protection is enabled,
you can check the token when the link is clicked by using the checkCSRFProtection()
method:
public function executeDelete($request) { $request->checkCSRFProtection(); }
The checkCSRFProtection()
throws an exception if the token is not present or
not valid.
Forms
Nested forms
One of the main limitation of Propel forms in symfony 1.1 was the inability to auto-save objects from embedded forms. This has been implemented in symfony 1.2, which means that when you save a Propel form, symfony will automatically save the main object and all the objects related to the embedded forms.
New sfForm
methods
The sfForm::renderFormTag()
method generates the opening
form
tag for the current form. It also adds the enctype
attribute if the form needs to be multipart and adds a
hidden tag if the form method is not GET
or POST
:
<?php echo $form->renderFormTag(url_for('@article_update'), array('method' => 'PUT')) ?>
The sfFormPropel
class overrides renderFormTag()
to automatically change
the HTTP method based on the related object: if the object is new, the method
will be POST
, and if the object already exists, the method will be PUT
.
The sfForm::hasErrors()
method returns true if the
form has some errors and false otherwise. This methods returns false
if the
form is not bound. This method is quite useful in a template:
<?php if ($form->hasErrors()): ?> The form has some errors you need to fix. <?php endif; ?>
The sfForm::renderUsing()
method allows to
render the form using a specific widget schema formatter. This allows to
use a formatter directly within a template:
// in a template, the form will be rendered using the "list" form formatter echo $form->renderUsing('list');
The sfForm::renderHiddenFields()
method returns HTML for hidden widgets.
This can be useful when a form's fields are rendered individually in a template:
<form action="<?php echo url_for('@some_route') ?>"> <?php echo $form->renderHiddenFields() ?> <ul> <?php echo $form['name']->renderRow() ?> </ul> <input type="submit" /> </form>
The sfForm::getStylesheets()
and sfForm::getJavaScripts()
methods return
the stylesheets and the JavaScripts files needed to render the form. These files
can be defined in the form itself by overriding these two methods,
or more commonly by each widget (see the Widget paragraph to learn how).
In a template, you can use the include_javascripts_for_form()
and
include_stylesheets_for_form()
helpers to render them as HTML. They both
take a form object as an argument:
<?php include_javascripts_for_form($form) ?> <?php include_stylesheets_for_form($form) ?>
sfForm
implements the Iterator
and the Countable
interfaces
to allow easier iterating on form fields in a template:
<ul> <?php foreach ($form as $field): ?> <li><?php echo $field ?></li> <?php endforeach; ?> </ul>
Cleaned-up values pre-processing
A way to pre-process the cleaned up values before they are used by Propel to update the object has been added.
If you want to pre-process a value, you need to add an updateXXXColumn()
method
to your form where XXX is the PHP name of the Propel column.
The method must return the processed value or false
to remove the value from
the array of cleaned up values:
class UserForm extends sfFormPropel { // ... protected function updateProfilePhotoColumn($value) { // if the user has not submitted a profile_photo, // remove the value from the values as we want // to keep the old one if (!$value) { return false; } // remove the old photo // save the photo on the disk // change the value to the new file name return $filename; } }
Validators
sfValidatorSchemaCompare
The sfValidatorSchemaCompare
constant values have been changed. No change to
your code need to be done, but now, you can use nice shortcuts.
The following two examples are equivalent:
// symfony 1.1 and 1.2 $v = new sfValidatorSchemaCompare('left', sfValidatorSchemaCompare::EQUAL, 'right'); // symfony 1.2 only $v = new sfValidatorSchemaCompare('left', '==', 'right');
sfValidatorChoice
sfValidatorChoice
has now a multiple
option to make it behave like a
sfValidatorChoiceMany
.
sfValidatorPropelChoice
has now a multiple
option to make it behave like a
sfValidatorPropelChoiceMany
.
sfValidatorDateRange
symfony 1.2 comes with a new sfValidatorDateRange
validators to validate a range
of dates.
sfValidatorFile
The sfValidatedFile
class has been made a bit more flexible when it saves
the file.
Its constructor takes a new argument, the path to use when saving the file.
So, when saving a file, you can now:
pass an absolute filename as before:
$file->save(sfConfig::get('sf_upload_dir').'/filename.pdf');
pass a relative path (symfony will make it absolute by prepending the path):
$file->save('filename.pdf');
pass
null
. The filename will be auto-generated by thegenerateFilename()
method:$file->save();
Moreover, the save()
method returns the filename of the saved
file (relative to the path argument).
As the sfValidatedFile
object is created by the sfValidatorFile
validator,
a new path
option has been added to the latter and is simply passed to the
sfValidatedFile
constructor:
$this->validatorSchema['file'] = new sfValidatorFile(array('path' => sfConfig::get('sf_upload_dir')));
The sfFormPropel
takes advantages of these new enhancements to automate
the saving of the files related to a Propel object, thanks to the new
saveUploadedFile()
method.
So, if you have a Propel object with a file
column, and if you pass a path
when defining the file
validator in your form, symfony will take care of
everything for you:
if no file is uploaded in the form
- do nothing and do not change anything in the database
if a file is uploaded in the form
- remove the old file
- save the new one
- set the
file
column to the filename (relative to the given path)
So, in the related action, you can now just use $this->form->save()
to save the object and save the file automatically.
By default, symfony will generate a unique file name by computing
a hash and adding the guessed extension. If you want to change the filename,
you can simply create a generateXXXFilename()
method in your object
where XXX is the PHP name of the column. The method is given the
sfValidatedFile
object:
public function generateFileFilename(sfValidatedFile $file) { return $this->getId().$file->getExtension($file->getOriginalExtension()); }
You can of course also create a updateXXXColumn()
as we have seen before to
override this behavior and manage the file uploading process by yourself.
sfValidatorI18nChoiceCountry
and sfValidatorI18nChoiceLanguage
The sfValidatorI18nChoiceCountry
and sfValidatorI18nChoiceLanguage
validators
had a required culture
option in symfony 1.1. As the culture is not used in those
validators, the culture
option is now deprecated. It is still there to maintain
backward compatibility but you don't need to provide it anymore.
Setting default values for required
and invalid
error codes related messages
Two new methods have been added to the sfValidatorBase
class allowing to define
default standard messages for required
and invalid
error codes:
sfValidatorBase::setRequiredMessage('This field is required'); sfValidatorBase::setInvalidMessage('The value provided for this field is invalid');
Widgets
Widget options
All widgets have a new default
option. This option sets the default value
for the widget:
$widget = new sfWidgetFormInput(array('default' => 'default value')); $widget->setDefault('default value'); echo $widget->getDefault();
You can also set default values for widget schemas:
$widget = new sfWidgetFormSchema(...); $widget->setDefaults(array('name' => 'default value')); $widget->setDefault('name', 'default value'); var_export($widget->getDefaults());
These defaults are taken into account in the forms when you don't override them with form defaults.
All widgets have a new label
option. This option sets the label for the widget
when used in a widget schema context:
$widget = new sfWidgetFormInput(array('label' => 'Enter your name here')); $widget->setLabel('Enter your name here'); echo $widget->getLabel();
Widgets JavaScripts and stylesheets
Widgets can declare the JavaScripts and stylesheets they need to be rendered with
the getJavaScripts()
and getStylesheets()
methods:
class MyWidget extends sfWidgetForm { public function getJavaScripts() { return array('/path/to/a/file.js'); } public function getStylesheets() { return array('/path/to/a/file.css' => 'all', '/path/to/a/file.css' => 'screen,print'); } }
The getJavaScripts()
method must return an array of JavaScript files.
The getStylesheets()
method must return an array with files as keys and media as values.
New widgets
symfony 1.2 comes with some new widgets:
sfWidgetFormDateRange
sfWidgetFormFilterInput
sfWidgetFormFilterDate
sfWidgetFormI18nSelectCurrency
sfWidgetFormSelectCheckbox
sfWidgetFormChoice
family
There is also a new choice
widget family:
sfWidgetFormChoice
sfWidgetFormChoiceMany
sfWidgetFormPropelChoice
sfWidgetFormPropelChoiceMany
By default, these widgets behave like their Select
counterpart. But they are
a wrapper on top of the sfWidgetFormSelect
, sfWidgetFormSelectRadio
, and
sfWidgetFormSelectCheckbox
widgets. They use one of these three widgets for the
rendering. To change the default widget, you have some options:
expanded
:- if false, then the widget will be a
select
tag - if true and
multiple
is false, then the widget will be a list ofradio
tags - if true and
multiple
is true, then the widget will be a list ofcheckbox
tags
- if false, then the widget will be a
renderer_options
: When creating the widget (select
,radio
,checkbox
), this is the options you want to pass to the renderer widgetrenderer_class
: The class to use instead of the default onerenderer
: You can also pass a widget object directly (this overrides the previous options).
Here are some example:
$widget = new sfWidgetFormPropelSelect(array('model' => 'Article')); // is equivalent to $widget = new sfWidgetFormPropelChoice(array('model' => 'Article')); // change the rendering to use a radio list $widget->setOption('expanded', true); // create a multiple select $widget = new sfWidgetFormPropelChoiceMany(array('model' => 'Article')); // change the rendering to use a checkbox list $widget->setOption('expanded', true);
These new widgets are now used by default for the Propel generated forms.
Response
sfWebResponse::getCharset()
The sfWebResponse::getCharset()
returns the current
response charset. This charset is automatically updated if
you change it by setting the content type.
sfWebResponse::getStylesheets()
and sfWebResponse::getJavascripts()
The getStylesheets()
and getJavascripts()
methods can now return all the
stylesheets and JavaScripts ordered by position if you pass sfWebResponse::ALL
as their first argument:
$response = new sfWebResponse(new sfEventDispatcher()); $response->addStylesheet('foo.css'); $response->addStylesheet('bar.css', 'first'); var_export($response->getStylesheets()); // outputs array( 'bar.css' => array(), 'foo.css' => array(), )
The sfWebResponse::ALL
is also now the default value for the position argument.
Prototype and Scriptaculous
In symfony, 1.2 the bundled Prototype
and Scriptaculous libraries and their associated helpers (JavascriptHelper
) have been moved to a
core plugin.
Core plugins behave like any plugin but are shipped with symfony.
This will make the JavaScript and CSS files of the new sfProtoculousPlugin
(the more or less unofficial name of the often featured duo) behave like real plugin
assets. They are now in web/sfProtoculousPlugin
rather than in web/sf
(as it has been in 1.0 and 1.1). The prototype_web_dir
setting will also now point
to the new directory.
In addition some very basic javascript helpers that are reusable by any JS framework, have
been extracted to a JavascriptBaseHelper
which stays in core.
As a new addition, javascript_tag()
now can behave as slot()
. This allows such usage
<?php javascript_tag() ?> alert('All is good') <?php end_javascript_tag() ?>
Tests
sfBrowser::info()
When you write a lot of functional tests for a given module, it is sometimes
useful to have some visual information about what it is being done.
As of symfony 1.2, the info()
method outputs some text to help categorize
your tests:
$browser-> info('First scenario')-> // ... some tests info('Second scenario')-> // ... some more tests ;
Debugging tests
When a problem occurs in a functional test, the HTML transfered to the browser help to diagnose the cause. As of symfony 1.2, this is quite easy to display the generated HTML without having to interrupt the fluent interface style:
$browser-> get('/a_uri_with_an_error')-> with('response')->debug()-> // some tests that won't be executed ;
The debug()
method will output the response headers and content and will
interrupt the flow of the browser.
Testers
The sfTestFunctionalBase
class now delegates the actual tests to sfTester
classes. symfony 1.2 has several built-in tester classes:
request
:sfTesterRequest
response
:sfTesterResponse
user
:sfTesterUser
view_cache
:sfTesterViewCache
All the old methods from the test browser have been moved to one of the tester class:
method name | tester class | new method name |
---|---|---|
isRequestParameter |
sfTesterRequest |
isParameter |
isRequestFormat |
sfTesterRequest |
isFormat |
isStatusCode |
sfTesterResponse |
isStatusCode |
responseContains |
sfTesterResponse |
contains |
isResponseHeader |
sfTesterResponse |
isHeader |
checkResponseElement |
sfTesterResponse |
checkElement |
isUserCulture |
sfTesterUser |
isCulture |
isCached |
sfTesterViewCache |
isCached |
isUriCached |
sfTesterViewCache |
isUriCached |
The tester classes also comes with new test methods:
tester class | new method name |
---|---|
sfTesterRequest |
hasCookie |
sfTesterRequest |
isCookie |
sfTesterRequest |
isMethod |
sfTesterUser |
isAuthenticated |
sfTesterUser |
hasCredential |
sfTesterUser |
isAttribute |
sfTesterUser |
isFlash |
Here is how to use the new testers:
$browser-> get('/')-> with('request')->isParameter('module', 'foo')-> with('request')->isParameter('module', 'bar') ;
The call to the with()
method returns the tester object on which you can call
any of its methods.
If you want to call several methods on the same tester object, you can put them in a 'block':
$browser-> get('/')-> with('request')->begin()-> isParameter('module', 'foo')-> isParameter('module', 'bar')-> isMethod('get')-> isFormat('html')-> end()-> with('response')->begin()-> isStatusCode(200)-> checkElement('body', 'foo')-> isHeader('Content-Type', 'text/html; charset: UTF-8')-> isRedirected(false)-> end() ;
You can add your own tester by creating a class that inherits from sfTester
and register it:
$browser->setTester('my_tester', 'myTesterClassName');
You can also extends the existing tester classes by overriding the default class names:
$browser->setTester('request', 'myTesterRequest');
To improve testing pages with forms, we added a select()
and deselect()
method to the test browser. This currently supports selecting and deselecting
checkboxes and selecting radiobuttons, which will cause the other radiobuttons
of the same name
to be deselected.
$browser-> select('aRadioId')-> select('aCheckboxId')-> deselect('anotherCheckbox');
Cookies
Cookies are now extensively supported in the sfBrowser
and sfTestBrowser
classes.
You can manage cookies between requests by using the setCookie()
,
removeCookie()
, and clearCookies()
methods of the sfBrowser
class:
$b-> setCookie('foo', 'bar')-> removeCookie('bar')-> get('/')-> // ... clearCookies()-> get('/')-> // ...
You can also test cookies with the hasCookie()
and isCookie()
methods from
the sfTesterRequest
class:
$b-> get('/')-> with('request')->begin()-> hasCookie('foo')-> hasCookie('foobar', false)-> isCookie('foo', 'bar')-> isCookie('foo', '/a/')-> isCookie('foo', '/!z/')-> end() ;
The hasCookie()
method takes a Boolean as its second argument to be able to
test the fact that a cookie is not set.
The second argument of the isCookie()
method behaves as the second argument
of the checkElementResponse
method. It can be a string, a regular expression,
or a negative regular expression.
The browser class also automatically expires cookies as per the expire
value
of each cookie.
Request
You can now test the HTTP method used by the request in your functional tests
using the isMethod()
method from the request tester:
$b-> setField('login', 'johndoe')-> click('Submit')-> with('request')->isMethod('post');
Links
When you simulate a click on a button or on a link, you give the name to the click()
method. But you don't have the possibility to differentiate two different links or buttons
with the same name.
As of symfony 1.2, the click()
method takes a third argument to pass some options.
You can pass a position
option to change the link you want to click on:
$b-> click('/', array(), array('position' => 1))-> // ... ;
By default, symfony clicks on the first link it finds in the page.
You can also pass a method
option to change the method of the link or the form
you are clicking on:
$b-> click('/delete', array(), array('method' => 'delete'))-> // ... ;
This is very useful when a link is converted to a dynamic form generated
with JavaScript. You can also simulate the CSRF token generated by the
link_to()
helper by passing a _with_csrf
option:
$b-> click('/delete', array(), array('method' => 'delete', '_with_csrf' => true))-> // ... call('/delete', 'delete', array('_with_csrf' => true))-> // ... ;
Forms
If you use the new form framework, you can now test the errors generated by the submitted form:
$browser-> click('save', array(...))-> with('form')->begin()-> hasErrors()-> isError('name', 'Required.')-> isError('name', '/Required/')-> isError('name', '!/Invalid/')-> isError('name')-> isError('name', 1)-> end() ;
You can also debug a form with the debug()
method:
$browser-> click('save', array(...))-> with('form')->debug()-> // some tests that won't be executed ;
It will output the submitted values and all the errors if any.
Propel
The Propel plugin comes with a propel tester. This tester is not registered by default:
$browser->setTester('propel', 'sfTesterPropel');
This tester provides a check()
method to test a Propel model:
$browser-> post('...', array(...))-> with('propel')->begin()-> check('Article', array(), 3)-> check('Article', array('title' => 'new title'))-> check('Article', array('title' => 'new title'))-> check('Article', array('title' => 'new%'))-> check('Article', array('title' => '!new%'), 2)-> check('Article', array('title' => 'title'), false)-> check('Article', array('title' => '!title'))-> end() ;
It takes a model class name as its first argument, a Criteria
object
or an array of conditions as the second one. The third one can be one of:
true
: To check that theCriteria
returns at least one objectfalse
: To check that theCriteria
returns no object- an integer to check the number of returned objects
Coverage
There is a new test:coverage
task that output the code coverage for some given
tests:
./symfony test:coverage test/unit/model/ArticleTest.php lib/model/Article.php
The first argument is a test file or a test directory. The second one is the file or directory you want to cover.
If you want to know which lines are not covered, simply add the --detailed
option:
./symfony test:coverage --detailed test/unit/model/ArticleTest.php lib/model/Article.php
Loggers
symfony 1.2 comes with a new built-in logger: sfVarLogger
. This logger class
logs all the messages as an array for later use. It's up to you to get the logs
and do something with them:
$log = new sfVarLogger(); $log->log('foo'); var_export($log->getLogs()); // outputs array( 0 => array( 'priority' => 6, 'priority_name' => 'info', 'time' => 1219385295, 'message' => 'foo', 'type' => 'sfOther', 'debug_stack' => array() ) )
Each log is an associative array with the following keys: priority
, priority_name
,
time
, message
, type
, and debug_stack
.
The debug_stack
attribute is only set if you turns the xdebug_logging
option to true
.
It then contains the stack trace returned by xdebug as an array.
The sfVarLogger
is also the base class for the sfWebDebugLogger
class. So, the
xdebugLogging
option of sfWebDebugLogger
has been renamed to xdebug_logging
.
Web Debug Toolbar
The web debug toolbar is now customizable. The toolbar is composed of panels. A panel is
an object that extends sfWebDebugPanel
and provides all the needed information to display
if on the toolbar.
By default, the following panels are automatically registered:
name | class name |
---|---|
symfony_version | sfWebDebugPanelSymfonyVersion |
cache | sfWebDebugPanelCache |
config | sfWebDebugPanelConfig |
logs | sfWebDebugPanelLogs |
time | sfWebDebugPanelTimer |
memory | sfWebDebugPanelMemory |
db | sfWebDebugPanelPropel |
You can customize the web debug toolbar by listening to the debug.web.load_panels
event.
The listener can then add new panels, remove existing ones, or even replace some.
The sfWebDebugPanelLogs
panel filters the logs to be displayed by notifying the
debug.web.filter_logs
event.
For example, the sfWebDebugPanelPropel
and sfWebDebugPanelTimer
connect to this event
to remove all Propel related logs and timer logs from the logs panel.
The total time displayed on the web debug toolbar is now computed with $_SERVER['REQUEST_TIME']
(we used to compute it with sfConfig::get('sf_time_start')
, which does not exist anymore).
This means that the time displayed will be much larger than in symfony 1.0 and 1.1.
Tasks
The Propel tasks relying on Phing now output a clear error message if the embed Phing task fails.
Some CLI tasks takes an application name as an argument because they need to connect to the database. We need an application because the configuration can be customized on an application basis and all the symfony configuration system is based on the application level.
For these tasks, this argument has been removed in favor of an optional "application"
option. If you don't provide the "application" option, symfony will take the database
configuration from the project databases.yml
file.
The following task signatures have been changed accordingly:
propel:build-all-load
propel:data-dump
propel:data-load
note
This is possible because sfDatabaseManager
now takes a project configuration or an
application configuration. For the curious one, this works because
sfDatabaseConfigHandler
now returns a static or a dynamic configuration based
on an array of configuration files (see the execute()
and evaluate()
methods).
To ease the debugging, the propel:build-model
, propel:build-all
, and
propel:build-all-load
tasks do not remove the generated XML schemas anymore
if you pass the --trace
option.
propel:insert-sql
The propel:insert-sql
task removes all the data from the database.
As it destroys information, it now asks the user to confirm the execution of
the task. The same goes for the propel:build-all
and propel:build-all-load
tasks,
as they call the propel:insert-sql
task.
If you want to use these tasks in a batch and want to avoid the confirmation question,
pass the no-confirmation
option:
$ php symfony propel:insert-sql --no-confirmation
Before symfony 1.2, the propel:insert-sql
task was the only task to read its
database configuration information from propel.ini
. As of symfony 1.2, it
reads its information from databases.yml
. So, if you use several different
connections in your model, the task will take those into account.
Thanks to this new feature, you can now use the --connection
option
if you want to only load SQL statements for a given connection:
$ php symfony propel:insert-sql --connection=propel
You can also use the --env
and the --application
options to select a
specific configuration to use:
$ php symfony propel:insert-sql --env=prod --application=frontend
New methods available for your tasks
The sfTask
base class now provides three new methods to use in your tasks:
logBlock()
: Logs a message in a styled block (default styles are: INFO, COMMENT, QUESTION, and ERROR)ask()
: Asks a question to the user and returns the given answeraskConfirmation()
: Asks a confirmation to the user and return true if the user confirmed, false otherwise
propel:generate-module
The propel:generate-crud
has been renamed to propel:generate-module
. The old
task name is still available as an alias.
The non-atomic-actions
option of propel:generate-module
has been removed
and some new options have been added:
- singular: The singular name for the actions and templates
- plural: The plural name for the actions and templates
- route-prefix: The route prefix to use
- with-propel-route: Whether the related routes are Propel aware
propel:generate-module-for-route
The new propel:generate-module-for-route
generates a module based on a route
definition:
php symfony propel:generate-module-for-route frontend articles
app:routes
As symfony now can auto-generate routes (see below), the app:routes
task
displays the list of current routes for an application:
php symfony app:routes frontend
If you want to get some details about a route, just add the route name:
php symfony app:routes frontend articles_update
plugin:publish-assets
As plugins are normally installed via a task and this invokes the creation of the symlink in the web directory this is required for core plugins as well.
To do this manually you need to invoke:
symfony plugin:publish-assets --only-core
The target is that the project creation and upgrade task do this for you.
Routing
Routing options
The routing takes two new options:
generate_shortest_url
: Whether to generate the shortest URL possibleextra_parameters_as_query_string
: Whether to generate extra parameters as a query string
By default, they are set to false
in the default factories.yml
configuration
file to keep backward compatibility with symfony 1.0 and 1.1.
You can also enable or disable these options on a route basis:
articles: url: /articles/:page param: { module: article, action: list, page: 1 } options: { generate_shortest_url: true }
This route will generate the shortest URL possible. So, if you pass a page
of 1
, the generated URL will be /articles
:
echo url_for('@articles?page=1'); // /articles // would have been /articles/1 in symfony 1.1 echo url_for('@articles?page=2'); // /articles/2
Here is an example for extra_parameters_as_query_string
:
articles: url: /articles options: { extra_parameters_as_query_string: true }
This route will accept extra parameters and add them as a query string:
echo url_for('@articles?page=1'); // /articles?page=1 // would not have matched the route in symfony 1.1 echo url_for('@articles?page=2'); // /articles?page=2
sfRoute
The sfPatternRouting
class now stores its routes as an array of sfRoute
objects instead of a flat associative array.
By default, symfony uses the built-in sfRoute
object, but you can change
it by specifying a class
entry in your routing.yml
configuration file:
foo_bar: url: /foo/:bar class: myRoute param: { module: foo, action: bar }
All the routing logic is contained in the sfRoute
class, which means you
can override the parsing and the generation logic with your own.
tip
The sfRoute
class is much more modular than the old sfPatternRouting
class
to allow easier customization of the default behavior. The "compilation" phase
has been refactored into smaller methods, the code has been simplified,
and it is based on a "real" tokenizer.
If you connect your routes with PHP code, you must now pass a sfRoute
instance
as the second argument for the connect()
, preprendRoute()
, appendRoute()
,
and insertRouteBefore()
methods:
$route = new myRoute('/foo/:bar', array('module' => 'foo', 'action' => 'bar')); $routing->connect('foo_bar', $route);
When a request comes in, the matching route object is stored as an attribute of the
request and you can get it from an action via the getRoute()
method:
public function executeIndex() { $route = $this->getRoute(); }
The sfRoute
constructor takes an array of options as its last argument to
allow the customization of each route. In the routing.yml
configuration file,
use to options
key:
article: url: /article/:id-:slug options: { segment_separators: [/, ., -] }
sfRequestRoute
Symfony has another built-in route, sfRequestRoute
, which can enforce the
HTTP method for your routes:
article: url: /article/:id requirements: { sf_method: get } class: sfRequestRoute
If you don't pass a sf_method
requirement, symfony will enforces a get
, or
a head
request.
This is made possible because the parse()
method of sfRouting
now takes a
context as a second argument. When the request calls the routing, it passes
the following context:
method
: The HTTP method nameformat
: The request formathost
: The hostnameis_secure
: Whether the request was called with HTTPS or notrequest_uri
: The full request URIprefix
: The prefix to add to each generated route
With the previous routing configuration, the article
route will only match
requests with a get
HTTP method. If you define several routes with the same
url
but different method requirements, you can pass sf_method
as a parameter
when you generate a route:
<?php echo link_to('Great article', '@article?id=1&sf_method=get')) ?>
sfObjectRoute
symfony 1.2 comes with another route class that extends sfRequestRoute
,
sfObjectRoute
.
sfObjectRoute
binds a route to a PHP object. A sfObjectRoute
will
call some methods on your PHP class to get the object, or a collection of
objects, related to the route.
article: url: /article/:id class: sfObjectRoute options: { model: Article, type: object, method: getById }
When an incoming URL matches the route, the sfObjectRoute::getObject()
method will get the related object by calling the Article::getById()
method.
The same goes for a collection of objects:
articles: url: /articles/newest class: sfObjectRoute options: { model: Article, type: list, method: getNewest }
sfPropelRoute
sfPropelRoute
extends sfObjectRoute
to bind a route to a Propel model.
In your actions, you can retrieve the related object or collection of objects
by using the sfObjectRoute::getObject()
and sfObjectRoute::getObjects()
methods:
public function executeList($request) { $this->articles = $this->getRoute()->getObjects(); } public function executeShow($request) { $this->article = $this->getRoute()->getObject(); }
Here is an example for an Article
Propel object:
article: url: /article/:id param: { module: article, action: show } class: sfPropelRoute options: { model: Article, type: object } articles: url: /articles param: { module: article, action: list } class: sfPropelRoute options: { model: Article, type: list, method: getPublishedArticleCriteria }
If you don't define a method, sfPropelRoute
will retrieve the object
by building a Criteria
object based on the available route variables.
The sfPropelRoute
has two main advantages over sfRoute
:
When a request comes in and the route matches the URL, the matching
sfPropelRoute
object will be available in your actions, and the relatedArticle
object will be available by calling thegetObject()
method. Moreover, if the object does not exist in the database, it will automatically redirect the user to a 404 error page. This means less boiler-plate code in your actions.When you generate a link for this route, you can use the new
url_for()
signature and pass the article object directly for the parameters argument:<?php echo url_for('article', $article) ?>
If you have to pass extra parameters (to enforce a HTTP method for example), you can use an array like this:
<?php echo url_for('article', array('sf_subject' => $article, 'sf_method' => 'get')) ?>
And of course, you can still use the full parameters:
<?php echo url_for('article', array('id' => $article->getId(), 'slug' => $article->getSlug())) ?>
Or use the internal URI:
<?php echo url_for('@article?id='.$article->getId().'&slug='.$article->getSlug()) ?>
The
sfPropelRoute
converts the article object to an array of parameters automatically.
sfPropelRoute
does not only work with the primary key. It can also work with
any column. In the following example, let's add the slug
column to the route
pattern:
article: url: /article/:id/:slug param: { module: article, action: show } class: sfPropelRoute options: { model: Article, type: object }
But sometimes, you want to put in the pattern some information that does not
exist in the database. In this case, you can pass a method
option:
post: url: /post/:id/:year/:month/:day/:slug param: { module: article, action: show } class: sfPropelRoute options: { model: Article, type: object, method: getObjectForRoute }
The getObjectForRoute()
receives an array of parameters as its first
argument and must return an Article
object:
class ArticlePeer extends BaseArticlePeer { static public function getObjectForRoute($parameters) { $criteria = new Criteria(); $criteria->add(self::ID, $parameters['id']); return self::doSelectOne($criteria); } }
sfPropelRoute
also provide a setListCriteria()
method to override the
route parsed parameters. This is especially useful when you want to refine the
objects returned by the route, based on some other parameters, like
a filter for example:
public function executeList($request) { $this->filters = new ArticleFormFilter(); if ($request->isMethod('post')) { $this->filters->bind($request->getParameter('article_filters')); if ($this->filters->isValid()) { $this->getRoute()->setListCriteria($this->filters->getCriteria()); } } $this->articles = $this->getRoute()->getObjects(); }
sfRouteCollection
, sfObjectRouteCollection
, and sfPropelRouteCollection
symfony also comes with "collection" route classes.
These classes (sfRouteCollection
, sfObjectRouteCollection
, sfPropelRouteCollection
)
generate standard routes based on a definition:
articles: class: sfPropelRouteCollection options: { model: Article, module: article }
By default, this route will generate seven routes:
- list:
articles GET /articles.:sf_format
- new:
articles_new GET /articles/new.:sf_format
- create:
articles_create POST /articles.:sf_format
- edit:
articles_edit GET /articles/:id/edit.:sf_format
- update:
articles_update PUT /articles/:id.:sf_format
- delete:
articles_delete DELETE /articles/:id.:sf_format
You can customize the generation with several options:
- model: The model class name
- actions: The list of actions to generate (from the 7 listed above)
- module: The module name
- prefix_path: The prefix path to add for every route
- column: The column name for the primary key (
id
by default) - with_show: Whether to add the show method or not
- segment_names: The segment names for
new
andedit
- model_methods: The methods to use to get a collection or an object
- requirements: The default requirements (by default, the
id
requirement is\d+
) - route_class: The route class to use
(by default
sfObjectRoute
forsfObjectRouteCollection
andsfPropelRoute
forsfPropelRouteCollection
) - collection_actions: A list of actions to add as collection actions
- object_actions: A list of actions to add as object actions
Here is another example with some options:
articles: class: sfPropelRouteCollection options: model: Article module: article prefix_path: /foo methods: [ list, edit, update ] segment_names: { list: nouveau, edit: edition } collection_actions: { filter: post } object_actions: { publish: put }
URL helpers
link_to()
and url_for()
If you want to create a link to a resource that must be submitted with the
POST
, PUT
, or DELETE
HTTP method, the link_to()
helper can convert
a link to a form if you pass the method
option:
<?php echo link_to('@article_delete?id=1', array('method' => 'delete')) ?>
For POST
, DELETE
, and PUT
methods, the link_to()
helper also embeds a CSRF
token if CSRF protection is enabled in settings.yml
.
The old post
option is still valid but deprecated:
// is deprecated <?php echo link_to('@some_route', array('post' => true)) ?> // and equivalent to <?php echo link_to('@some_route', array('method' => 'post')) ?>
The url_for()
and link_to()
helpers support new signatures.
Instead of an internal URI, they can now also take the route name and
an array of parameters:
echo url_for('@article', array('id' => 1)); echo link_to('Link to article', '@article', array('id' => 1));
The old behavior still works without changing anything to your code.
Configuration
Before symfony 1.2, all the plugins installed under the plugins
directory,
and all built-in plugins were automatically loaded.
As of symfony 1.2, you need to enable the plugins you want to use in your projects.
You can do this in your ProjectConfiguration
class. Here is how to enable the
Doctrine plugin and disable the Propel one:
public function setup() { $this->enablePlugins('sfDoctrinePlugin'); $this->disablePlugins('sfPropelPlugin'); }
You can add several plugins in one call by passing an array of plugin names:
public function setup() { $this->enablePlugins(array('sfDoctrinePlugin', 'sfGuardPlugin')); }
You can also change the order in which plugins are loaded by using the setPlugins
method:
[php] public function setup() { $this->setPlugins(array('sfDoctrinePlugin', 'sfCompat10Plugin')); }
To enable the 1.0 compatibility plugin, you need to enable it in your configuration:
public function setup() { $this->enablePlugins('sfCompat10Plugin'); }
By default, symfony only enables the Propel plugin.
You can also enable all installed plugins:
public function setup() { $this->enableAllPluginsExcept('sfDoctrinePlugin'); }
The previous example allows you to enable all plugins except the Doctrine one. If you upgrade from 1.0 or 1.1, this line will make symfony behave like in symfony 1.0 and 1.1.
Plugin configuration
Plugins now have the option of providing a plugin configuration class. These
plugin configuration classes setup autoloading for the plugin, and are
instantiated in sfProjectConfiguration
. This means symfony 1.2 tasks no
longer require an application argument for plugin classes to be used. This
allows plugins to connect to command.*
events, something that was not
previously possible.
Filters
symfony is now able to generate filters based on your model thanks to the
propel:build-filters
task:
$ php symfony propel:build-filters
This task generates filter form classes in lib/filter/
by default.
To filter an Article
model, here is the code you need in your actions:
$this->filters = new ArticleFormFilter(); if ($request->isMethod('post')) { $this->filters->bind($request->getParameter('article_filters')); if ($this->filters->isValid()) { $this->articles = ArticlePeer::doSelect($this->filters->getCriteria()); } }
And here is the related template code:
<form action="<?php echo url_for('@articles_filter') ?>" method="post"> <table> <?php echo $filters ?> <tr> <td colspan="2"> <a href="<?php echo url_for('@articles') ?>">Reset</a> <input type="submit" value="Filter" /> </td> </tr> </table> </form>
If you want to persist the filters between requests, here is a small working example:
public function executeList($request) { $this->filters = new ArticleFormFilter($this->getUser()->getAttribute('article_filters')); if ($request->isMethod('post')) { $this->filters->bind($request->getParameter('article_filters')); if ($this->filters->isValid()) { $this->getUser()->setAttribute('article_filters', $this->filters->getValues()); } } $criteria = $this->filters->buildCriteria($this->getUser()->getAttribute('article_filters')); $this->articles = ArticlePeer::doSelect($criteria); }
Exception Templates
symfony now respects the current request format when rendering any uncaught
exceptions. You can customize each format's output by adding a template to
your project or application config/error
directory.
For example, an uncaught exception during an XML request could render
config/error/exception.xml.php
when your application is in debug mode, or
config/error/error.xml.php
when your application is not in debug mode.
If you had customized the 500 error template in your project, you will need to manually move it to the new directory:
- For symfony 1.1: from
config/error500.php
toconfig/error/error.html.php
- For symfony 1.0: from
web/errors/error500.php
toconfig/error/error.html.php
Smaller improvements
symfony 1.2 also comes with a lot of improvements here and there. Here are some of them.
Action
In an action, you can now generate a URL by using the routing object directly
by calling generateUrl()
:
public function executeIndex() { $this->redirect($this->generateUrl('homepage')); }
The generateUrl()
method takes a route name, an array of parameters, and
whether to generate an absolute URL as its constructor arguments.
By default, when you use the redirectIf()
or redirectUnless()
methods
in your actions, symfony automatically changes the response HTTP status
code to 302.
These two methods now have an additional optional argument to change this default status code:
$this->redirectIf($condition, '@homepage', 301); $this->redirectUnless($condition, '@homepage', 301);
The redirect()
method already have this feature.
YAML
The YAML parser now supports full merge key (see http://yaml.org/type/merge.html for more examples).
$yaml = new sfYamlParser(); var_export($yaml->parse(<<<EOF default_param: &default_param datasource: propel phptype: mysql hostspec: localhost database: db username: user password: pass param: <<: *default_param username: myuser password: mypass EOF )); // outputs array( 'default_param' => array( 'datasource' => 'propel', 'phptype' => 'mysql', 'hostspec' => 'localhost', 'database' => 'db', 'username' => 'user', 'password' => 'pass', ) 'param' => array( 'datasource' => 'propel', 'phptype' => 'mysql', 'hostspec' => 'localhost', 'database' => 'db', 'username' => 'myuser', 'password' => 'mypass', ) )
sfParameterHolder
The has()
method of sfParameterHolder
has been changed to be more
semantically correct.
It now returns true
even if the value is null
:
$ph = new sfParameterHolder(); $ph->set('foo', 'bar'); $ph->set('bar', null); $ph->has('foo') === true; $ph->has('bar') === true; // returns false under symfony 1.0 or 1.1
The sfParameterHolder::has()
method is used by the hasParameter()
and
hasAttribute()
methods available for a large number of core classes.
image_tag()
helper
In symfony 1.0 and 1.1 the image_tag()
helper would generate the alt
attribute of the img-tag from the filename. This now only happens if
sf_compat_10
is on. The new behaviour eases finding of unset alt attributes
using a (x)html validator. As a bonus, there is now a alt_title
option that
will set alt and title attribute to the same value, which is useful for cross
browser tooltips.
View Configuration
As of symfony 1.2, it is now possible to change the View class used by
symfony to render the partials by setting the partial_view_class
in
module.yml
. The class must extend sfPartialView
. Similar to the
view_class
setting, symfony will automatically appends PartialView
to the partial_view_class
value:
# module.yml all: partial_view_class: my
Factories
The sfViewCacheManager
class is now configurable in factories.yml
:
view_cache_manager: class: sfViewCacheManager
View
You can override sfViewCacheManager::generateCacheKey()
by defining a
sf_cache_namespace_callable
setting. As of symfony 1.2, the callable
is now called with an additional argument, the view cache manager instance.
Events
symfony 1.2 comes with new events:
user.change_authentication
: notified when a user authentication status changes. The event takes theauthenticated
flag as an argument after the change occurred.debug.web.load_panels
(cf. web debug toolbar paragraph)debug.web.filter_logs
(cf. web debug toolbar paragraph)
I18N
Internally, the sfCultureInfo
class is now only used as a singleton.
Even if it is still possible to bypass the getInstance()
method and instantiate
a new object directly, it is deprecated. By using the singleton, the performance
are better as we only instantiate one culture info object per request and it
also means that you can now override some culture information globally in your
configuration classes.
The sfCultureInfo::getCountries()
, sfCultureInfo::getCurrencies()
, and
sfCultureInfo::getLanguages()
methods now take an optional argument which allows
to restrict the return value:
// will only return the FR and ES countries in english $countries = sfCultureInfo::getInstance('en')->getCountries(array('FR', 'ES'));
The sfCultureInfo::getCurrencies()
method now returns an array of currency names.
In previous symfony versions, it returned an array with the symbol and the name.
To get the old behavior, just pass true
as the second argument:
$currencies = sfCultureInfo::getInstance('en')->getCurrencies(null, true);
To get the translation of a single country, language, or currency, you can now
use the getCountry()
, getCurrency()
, and getLanguage()
methods from the
sfCultureInfo
class.
The XLIFF
support has been enhanced to allow the generated files to be
edited by standard GUI tools (see https://open-language-tools.dev.java.net/).
Include Path Management
symfony 1.2 adds the static method sfToolkit::addIncludePath()
for easily
managing the PHP include_path
setting. This method accepts two parameters:
a single path or array of paths, and a position string (either "front" or
"back"). The second parameter defaults to "front".
// an example from sfPropelPlugin sfToolkit::addIncludePath(array( sfConfig::get('sf_root_dir'), sfConfig::get('sf_symfony_lib_dir'), realpath(dirname(__FILE__).'/../lib/vendor'), ));
Admin Generator
The admin generator has been rewritten for symfony 1.2. It is based on the form
framework. To generate an admin module, use the propel:generate-admin
task:
$ php symfony propel:generate-admin backend Article
By default, the task adds a REST route for your module. You can also create a route by yourself and pass it as an argument instead of the model class name:
$ php symfony propel:generate-admin backend article
The configuration is done via the generator.yml
. All the configuration is now
done under the config
key:
generator: class: sfPropelGenerator param: model_class: DemoCategory theme: admin non_verbose_templates: true with_show: false singular: ~ plural: ~ route_prefix: categories with_propel_route: 1 config: actions: ~ fields: ~ list: ~ filter: ~ form: ~ edit: ~ new: ~
You can now have different configuration for the edit
and new
form, and they
both inherit from the form
configuration.
The name
entry has been renamed label
.
The old admin generator has been kept for backward compatibility, so you can still
use it if you want by running the propel:init-admin
task.
This work is licensed under the Creative Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported License license.