![]() |
|
Snippets |
|
Let's take an example. You want to have the user culture in front of each of your URL.
So, your routing configuration look like something like that:
page: url: /:culture/:page params: ... article: url: /:culture/:year/:month/:day/:slug params: ... ...
This is the easy part. But now, every time you call url_for() or link_to() you have to pass the culture parameter.
For such situations, there is a better way. The routing has a special configuration parameter named sf_routing_defaults that you can set with default values:
sfConfig::set('sf_routing_defaults', array('culture' => 'en'));
And now, the culture parameter will automatically added to the parameters that you pass to the url_for() or link_to() helpers.
You sometimes need to access the __() helper for text translation outside of a template. There are two ways to do it.
You can include the I18NHelper.php manually in your action, or use the symfony command to include a helper (sfLoaded::LoadHElpers()):
sfLoader::LoadHelpers(array('I18N'));
The problem is that this will add the helper functions to the global namespace. Alternately, use the following syntax:
sfContext::getInstance()->getI18n()->__($text, $args, 'messages');
Note: prior to 1.0, you had to do this (now deprecated)
sfConfig::get('sf_i18n_instance')->__($text, $args, 'messages');
(updated 29/11/06)
Symfony 1.1 comes with a complete new form system. It works completely according to the MVC draft:
Make sure you have a running Symfony 1.1 based project and application and modules. In this example I build the form inside the myModule module and myLogin action.
My form makes use of i18n, which is in my case autoloaded in settings.yml.
This tutorial uses Symfony 1.1 beta4 and RC1. There are a little important changes with respect to beta3, which I don't cover.
I also expect you to have practical knowledge and a little bit experience with Symfony as system. I will not explain how you i18n implements or modules shield with security.yml.
The form gets two import fields: username and password. Furthermore is there a hidden field in which the URI comes that the user requested, but got redirected to the loginform (through Symfony’s security.yml and settings.yml). This will be used to go to that URI again after a successful login.
Both imput fields are required I will build a custom validation for a correct username/password check. Also we want to make use of a little new protection feature: CSRF.
The form takes uses i18n for multilinguity, I will use English primarily, but the view has been prepared for other languages.
We will begin with the action which contains the primary control.
/** * Executes myLogin action * * Login functionality. * * @param void * @return void * @access public */ public function executeMyLogin() { // Erase auth data $this->getUser()->clearCredentials(); $this->getUser()->setAuthenticated(FALSE); // Build login form $oForm = new inloggenForm(); if ($this->getRequest()->isMethod('post')) { // When called through POST (form submit) $oForm->bind( array('username' => $this->getRequest()->getParameter('username'), 'password' => $this->getRequest()->getParameter('password'), 'referrer' => $this->getRequest()->getParameter('referrer'), ) ); // Save orginal requested location in referrer field $oForm->setDefault('referrer', $this->getRequest()->getParameter('referrer')); if ($oForm->isValid()) { // When validations OK $aValues = $oForm->getValues(); sfContext::getInstance()->getLogger()->debug($aValues['username']); // Authentification $this->getUser()->setAuthenticated(TRUE); // To requested page $this->redirect($this->getRequest()->getParameter('referrer')); } } else { // Save original requested uri in form $oForm->setDefault('referrer', sfContext::getInstance()->getRequest()->getUri()); } $this->oForm = $oForm; // form to view }
Logic of authentification is set after POST further. Through the sfForm::bind() method couples the input of the user coupled with the form controller.
I made this in myModule/lib/form/inloggenForm.class.php. In the form is defined and coupled with the validations and the formatting.
<?php class inloggenForm extends sfForm { /** * Prepare validators */ private function _setValidators() { $this->oValGebruikersnaam = new sfValidatorAnd( array( new sfValidatorString(array('min_length' => 3, 'max_length' => 50), array('min_length' => 'The username should be at least three characters long', 'max_length' => 'The username should be fifty characters long at most', ) ), new sfValidatorCallback(array('callback' => array('cmsLoginValidator', 'execute'), 'arguments' => array() ), array('invalid' => 'This username/password combination is unknown') ), ), array('required' => TRUE), array('required' => 'The username is mandatory') ); $this->oValWachtwoord = new sfValidatorString(array('required' => TRUE), array('required' => 'The password is mandatory') ); $this->oValReferrer = new sfValidatorString(array('required' => FALSE)); } /** * Prepare widgets */ private function _setWidgets() { $this->oWidGebruikersnaam = new sfWidgetFormInput(array(), array('id' => 'username', 'size' => 25)); $this->oWidWachtwoord = new sfWidgetFormInputPassword(array(), array('id' => 'password', 'size' => 10)); $this->oWidReferrer = new sfWidgetFormInputHidden(array(), array('id' => 'referrer')); } /** * Configure form */ public function configure() { $this->_setValidators(); $this->_setWidgets(); /* * Set validators */ $this->setValidators(array('username' => $this->oValGebruikersnaam, 'password' => $this->oValWachtwoord, 'referrer' => $this->oValReferrer, ) ); /* * Set widgets */ $this->setWidgets(array('username' => $this->oWidGebruikersnaam, 'password' => $this->oWidWachtwoord, 'referrer' => $this->oWidReferrer, ) ); /* * Set decorator */ $oDecorator = new sfWidgetFormSchemaFormatterDiv($this->getWidgetSchema()); $this->getWidgetSchema()->addFormFormatter('div', $oDecorator); $this->getWidgetSchema()->setFormFormatterName('div'); $this->getWidgetSchema()->setHelps(array('username' => 'Please enter your username', 'password' => 'Please enter your password' ) ); } /** * Bind override */ public function bind(array $taintedValues = null, array $taintedFiles = array()) { $request = sfContext::getInstance()->getRequest(); if ($request->hasParameter(self::$CSRFFieldName)) { $taintedValues[self::$CSRFFieldName] = $request->getParameter(self::$CSRFFieldName); } parent::bind($taintedValues, $taintedFiles); } }
There are two private methods:
I overloaded the earlier mentioned bind() method to process the CSRF token internally. This fiels is regulated by sfForm internally, you to do need to worry about it much, only with the actual functionality.
The configure() method contains the functionality. The validators are created and linked to the corresponding fields and also are the widgets. Also, the decorator is defined with I will go into later on.
In _setValidators() sfValidatorBase objects are made for each field.
The password field check is the simplest, only the required input is checked. The object is of the type sfValidatorString (extend sfValidatorBase) without extra controls, only the required attribute with errortext is specified. In principle, you can use each validator as -empty- container.
The username is a combination; it is required, there are restrictions to the the stringlength and a correct login is checked. The stringcontrole/requirement is checked through sfValidatorString, which is quite simple.
The check for a correct login is done through a special validator, that does a callback to an custom function: sfValidatorCallback. This is explained later.
Since these two validators check the same field , the sfValidatorAnd validator is used that combines several validators. All validators must be satisfied. Of course Symfony also offers sfValidatorOr that checks that at least one underlying validator satisfies.
As you can see the callback validator calls to a custom class/method: myLoginValidator::execute().
/** * Fiel with myLoginValidator klasse * * @package - */ /** * Validation correct username/password * * @author Jordi Backx (Snowkrash) * @copyright Copyright © 2008, Jordi Backx (Snowkrash) * @package - */ class myLoginValidator { /** * execute validator * * @param sfValidatorBase Validator instance that calls this method * @param string Value of field that sfValidatorCallback checks * @param array Arguments for correct working * * @return value field when OK. Nothing if error (sfValidatorError exception) */ public static function execute ($oValidator, $sValue, $aArguments) { if ( /* check OK */ ) { // Return waarde veld indien controle OK return $sValue; } // Throw exception when not OK throw new sfValidatorError($oValidator, 'invalid', array('value' => $sValue, 'invalid' => $oValidator->getOption('invalid'))); } }
I set this here with no further logic, that is application specific, thus that you 'll have to do yourself. The base structure can be used. The three parameters must be defined, otherwise the whole application crashes.
In _setWidget() sfWidget objects are made for each field.
The widgets are the form elements: finally <input>, <select> etc tags in combination with labels and errortexts.
Each widget can have HTML attributes, which will be printed inside the form elements.
Finally the form must be printed to the screen through a view template.
<p><?php echo __('You need to log in to be able to use the Content Management System.') ?></p> <div id="formContainer"> <?php if ($oForm->getErrorSchema()->getErrors()) { ?> <div id="formulierFouten"> <ul> <?php foreach ($oForm->getErrorSchema() as $sError) { ?> <li><?php echo __($sError) ?></li> <?php } ?> </ul> </div> <?php } ?> <form action="<?php echo url_for('myModule/myLogin') ?>" method="post"> <?php echo $oForm['username']->renderLabel(__($oForm['username']->renderLabelName())); echo $oForm['username']->renderRow(__($oForm->getWidgetSchema()->getHelp('username'))); ?> <?php echo $oForm['password']->renderLabel(__($oForm['password']->renderLabelName())); echo $oForm['password']->renderRow(__($oForm->getWidgetSchema()->getHelp('password'))); ?> <?php echo $oForm['referrer']->render(array('value' => $oForm->getDefault('referrer'))) ?> <?php echo $oForm['_csrf_token'] ?> <label for="inloggen"> </label><input type="submit" value="Inloggen" id="inloggen" class="aanmeldenSubmit" /> </form> </div>
You can see all the i18n code (__() helper) and some non-Symfony 1.0 form building. Errorlists are built through the errorSchema which is available within the form object, the texts themself can be translated as you can see.
Also the labels and help texts are squeezed through i18n. The field names are in English, because the labels are based on these and must go through i18n. This way everything can be translated.
You can print the whole form with an echo of $oForm (goes through __toString()), but you have more control over the layout when you use specific widgetrender functions, like I do with renderRow(). This method takes the helptext as an argument, with is also translated.
The submit button is no widget, so we place it ourselves the old-fashioned way ... no helper, that is so Symfony 1.0.
That one is new. It is there, but we never defined it. It is created within sfForm and only since beta4 when indicated in settings.yml:
#Form security secret (CSRF protection)
csrf_secret: hierjeeigenc0d3 # Unique secret to enable CSRF protection or false to disable
You can choose your own code, on which the hash inside the CSRF value is based.
The form functionally is ready, but we want more control over the layout. I am a supporter of the tableless HTML design and the standard formatter of sfForm uses ... tables. Well, we can do better.
The form controller showed the coupling with my own formatter:
/* * Set decorator */ $oDecorator = new sfWidgetFormSchemaFormatterDiv($this->getWidgetSchema()); $this->getWidgetSchema()->addFormFormatter('div', $oDecorator); $this->getWidgetSchema()->setFormFormatterName('div');
I will now go into this part.
I have a class sfWidgetFormSchemaFormatterDiv in sfWidgetFormSchemaFormatterDiv.class.php made in the application-level lib/ directory so that all modules of can use it.
This takes care of the HTML layout of the form elements.
class sfWidgetFormSchemaFormatterDiv extends sfWidgetFormSchemaFormatter { protected $rowFormat = '%error%%field%<br />%help%<br />', $helpFormat = '<span class="help">%help%</span>', $errorRowFormat = '<div>%errors%</div>', $errorListFormatInARow = '%errors%', $errorRowFormatInARow = '<div class="formError">↓ %error% ↓</div>', $namedErrorRowFormatInARow = '%name%: %error%<br />', $decoratorFormat = '<div id="formContainer">%content%</div>'; }
A good article is available that describes this system.
For people that wonder why the label (%label% placeholder) is not used: $rowFormat sets the layout of the renderRow() method and since I want to render the label separately (i18n), it must not be rendered a second time by renderRow().
Hopefully the above can be a good help for your own form in Symfony 1.1. The documentation is quite scarce at the moment, so each bit of help will be welcome.
If the English is somewhat bad, I did a automatic translation of my original Dutch version of the article and tuned that a bit. The reason? I am lazy. ;-)
If you find errors in the above, it is because of copying my code probably. Please mention it in the comments.
Good luck!
Returns localized image <img> tag bassed on the the user culture and the image file location in the filesystem.
Based on the technique recomended in the symfony advent calendar day twenty-three
Inside images directory, create one directory for each culture in use plus one (named 'no-loc' by default) for non-localized images or images that doesn't change for different cultures.
web/images/en_US
web/images/es_MX
web/images/fr_FR
................
web/images/non-loc
<?php /** * Returns localized image <img> tag bassed on the the user culture * and the image file location in the filesystem * * If the image file is found under the directory corresponding * the user culture, it returns its img tag, * else returns img tag under 'non-loc'directory. * * Structure example: * * web/images/en_US * web/images/es_MX * web/images/fr_FR * ................ * web/images/non-loc * * $source - relative path within culture directory * $options - is a normal options array passed to image_tag function * */ function local_image_tag($source, $options = array()) { $images_path = sfConfig::get('sf_root_dir') . '/web/images'; $non_localized_dir = 'non-loc'; $culture = sfContext::getInstance()->getUser()->getCulture(); $default_ext = 'png'; if (strpos(basename($source), '.') === false) { $source .= '.'.$default_ext; } if (!$culture || !(is_file($images_path.'/'.$culture.'/'.$source))) { return image_tag($non_localized_dir.'/'.$source, $options); } return image_tag($culture.'/'.$source, $options); } ?>
Call example:
<?php echo local_image_tag('some/image', 'alt=image'); /* Will search in /web/images/<culture>/some/image.png, and if file is not found, will return image tag in /web/images/non-loc/some/image.png */ ?>
If you are dealing with i18n and client side effects, you might need an equivalent for the format_number_choice() helper in Javascript. Here is a simplified version:
function format_number_choice(text_string, replace_hash, number) { pattern = new RegExp("\\["+number+"\\]\s?([^\|]*)"); function replace_in_string(text_string, replace_hash) { for(var i in replace_hash) text_string = text_string.replace(new RegExp(i), replace_hash[i]); return text_string; } matches=text_string.match(pattern); if(matches != null) return replace_in_string(matches[1], replace_hash); else { pattern = /\[else\]\s?([^\|]*)/; matches=text_string.match(pattern); if(matches != null) return replace_in_string(matches[1], replace_hash); else return "not found"; } }
Use it in your HTML code as follows:
<script language="JavaScript" type="text/javascript"> document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", {'Apple' : 'fruit'}, 0)) document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", [], 1)) document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", [], 2)) document.write(format_number_choice("[0] No apple |[1] One Apple |[2] Two Apples |[else] Many Apples", [], 3)) </script>
This will output:
No fruit One Apple Two Apples Many Apples
Now, just use enclose the first argument with the __() helper, and you have an internationalized format_number_choice() on the client side.
In an i18n application, if your template uses the input_date_tag() helper, the format of the date sent to the submit action will depend on the user culture. But then, how can you handle this date to, say, store it in a database in a culture independent format?
The answer lies in the sfI18N class:
$date= sfContext::getInstance()->getRequest()->getParameter('birth_date'); $user_culture = sfContext::getInstance()->getUser()->getCulture(); list($d, $m, $y) = sfI18N::getDateForCulture( $date, $user_culture );
Now you have the day, month and year of the date in the $d, $m and $y variables, and you can do whatever you want with them.
If you use a Propel date setter, you can even call it directly with:
$person->setBirthDate("$y-$m-$d");
The same applies for the sfI18N::getTimestampForCulture() method.
By default, I18n content in database does not support fallback in default culture. This snippet allow you to enable I18n content fallback in order to always have a default value for your texts.
This snippet is sponsored by Dorigo consultants.
To enable the fallback, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Fetch the i18n object for this object culture. * * @return mixed A i18n object * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. */ public function getCurrentTableClassNameI18n() { if (!isset($this->current_i18n[$this->culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $this->culture); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $this->culture); } else // Create a translation for this culture { $new_i18n = new TableClassNameI18n(); $default_culture = sfConfig::get('sf_i18n_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if ($obj) { $obj->copyInto($new_i18n); } $new_i18n->setId($this->getId()); $new_i18n->setCulture($this->culture); $this->setTableClassNameI18nForCulture($new_i18n, $this->culture); } } return $this->current_i18n[$this->culture]; }
You now need to add default translation when you create a new object. We do that with this doSave function.
To use this function, edit your table object class in lib/model/TableClassName.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Stores the object in the database while setting default culture if necessary. * * If the object is new, it inserts it; otherwise an update is performed. * All related objects are also updated in this method. * * @param Connection $con The database connection * @return int The number of rows affected by this insert/update and any referring fk objects' save() operations. * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. * @see save() */ protected function doSave($con) { $default_culture = sfConfig::get('sf_i18n_default_culture'); // We try to fetch the default culture translation to initialise the new culture. if (!isset($this->current_i18n[$default_culture])) { $obj = TableClassNameI18nPeer::retrieveByPK($this->getId(), $default_culture, $con); if ($obj) // Test if there is a translation for current culture { $this->setTableClassNameI18nForCulture($obj, $default_culture); } } else { $obj = $this->current_i18n[$default_culture]; } if(!$obj && isset($this->current_i18n[$this->culture])) { $new_i18n = new TableClassNameI18n(); $this->current_i18n[$this->culture]->copyInto($new_i18n); $new_i18n->setId($this->getId()); $new_i18n->setCulture($default_culture); $this->setTableClassNameI18nForCulture($new_i18n, $default_culture); } return parent::doSave($con); }
To complete this snippet, here is a fallback version of doSelectWithI18n.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Selects a collection of TableClassName objects pre-filled with their i18n objects. * * @param Criteria $criteria * @param string $culture The selected culture. * @param Connection $con An optional database connection * @return array Array of TableClassName objects. * @throws PropelException Any exceptions caught during processing will be * rethrown wrapped into a PropelException. */ public static function doSelectWithI18n(Criteria $c, $culture = null, $con = null) { if ($culture === null) { $culture = sfContext::getInstance()->getUser()->getCulture(); } $default_culture = sfConfig::get('sf_i18n_default_culture'); // Set the correct dbName if it has not been overridden if ($c->getDbName() == Propel::getDefaultDB()) { $c->setDbName(self::DATABASE_NAME); } TableClassNamePeer::addSelectColumns($c); $startcol = (TableClassNamePeer::NUM_COLUMNS - TableClassNamePeer::NUM_LAZY_LOAD_COLUMNS) + 1; TableClassNameI18nPeer::addSelectColumns($c); $c->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $c->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($c->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $c->add($criterion); $rs = BasePeer::doSelect($c, $con); $results = array(); $uncultured_results = array(); while($rs->next()) { $omClass = TableClassNamePeer::getOMClass(); $cls = Propel::import($omClass); $obj1 = new $cls(); $obj1->hydrate($rs); $obj1->setCulture($culture); if(isset($results[$obj1->getId()])) { $obj1 = $results[$obj1->getId()]; } $omClass = TableClassNameI18nPeer::getOMClass($rs, $startcol); $cls = Propel::import($omClass); $obj2 = new $cls(); $obj2->hydrate($rs, $startcol); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); $obj2->setTableClassName($obj1); if(!isset($uncultured_results[$obj1->getId()])) { $uncultured_results[$obj1->getId()] = $obj1; } if($obj2->getCulture() == $culture) { $uncultured_results[$obj1->getId()] = false; } if(!isset($results[$obj1->getId()])) { $results[$obj1->getId()] = $obj1; } elseif($obj2->getCulture() == $culture) { // Move result to the end of results array to fit eventual sort // criteria (ugly fix). unset($results[$obj1->getId()]); $results[$obj1->getId()] = $obj1; } } foreach($uncultured_results as $obj1) { if($obj1) { $obj1->setCulture($default_culture); $default_culture_object = $obj1->getCurrentTableClassNameI18n(); if($default_culture_object) { $obj2 = new TableClassNameI18n(); $default_culture_object->copyInto($obj2); $obj2->setCulture($culture); $obj2->setTableClassName($obj1); $obj1->setTableClassNameI18nForCulture($obj2, $obj2->getCulture()); } $obj1->setCulture($culture); } } return array_values($results); }
If you want to use a pager with a filter on translations, you will need this count method. Once this method present, the magic is done by :
$criteria->setDistinct(); $pager->setCriteria($criteria); $pager->setPeerMethod('doSelectWithI18n'); $pager->setPeerCountMethod('doCountWithI18n');
Note : The setDistinct is very important for this snippet to work. It should not falsify your results, and without it, the doCountWithI18n method could return bad results.
To enable the fallback, edit your table object peer class in lib/model/TableClassNamePeer.php and add the following code. Then search and replace TableClassName by your table object class name.
/** * Returns the number of rows matching criteria with I18N criteria. * * @param Criteria $criteria * @param boolean $distinct Whether to select only distinct columns (You can also set DISTINCT modifier in Criteria). * @param Connection $con An optional database connection * @param string $culture The selected culture. * @return int Number of matching rows. */ public static function doCountWithI18n(Criteria $criteria, $distinct = false, $con = null, $culture = null) { // we're going to modify criteria, so copy it first $criteria = clone $criteria; $default_culture = sfConfig::get('sf_i18n_default_culture'); if ($culture === null) { // We use current user culture. $culture = sfContext::getInstance()->getUser()->getCulture(); } // clear out anything that might confuse the ORDER BY clause $criteria->clearSelectColumns()->clearOrderByColumns(); $criteria->addSelectColumn(TableClassNamePeer::COUNT_DISTINCT); // just in case we're grouping: add those columns to the select statement foreach($criteria->getGroupByColumns() as $column) { $criteria->addSelectColumn($column); } $criteria->addJoin(TableClassNamePeer::ID, TableClassNameI18nPeer::ID); $criterion = $criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $culture); $criterion->addOr($criteria->getNewCriterion(TableClassNameI18nPeer::CULTURE, $default_culture)); $criteria->add($criterion); $rs = TableClassNamePeer::doSelectRS($criteria, $con); if ($rs->next()) { return $rs->getInt(1); } else { // no rows returned; we infer that means 0 matches. return 0; } }
This filter use the accepted-languages browser setting to setup the user culture. The autodetection is run once by session.
This filter is sponsored by Dorigo consultants.
It check app.yml for accepted_languages setting in order to fit the available application locales.
It allow the use of sf_culture request parameter overide (this overide is avaible by default in symfony afaik). So you can use language changing links in your application.
It may be enhanced by a check of the really available locales but i don't know how it can be done.
To use this filter, create a file named SwitchLanguageFilter.class.php in lib/ or app/frontend/lib and fill it with this code and follow its documentation :
<?php /** * Setup user culture from request * * from http://www.symfony-project.com/snippets/snippet/80 * Thanks to François Zaninotto * Thanks to Garfield-fr on #symfony-fr * * Add the following lines in your app.yml to configure your application available languages. * all: * accepted: * languages: [en, fr] * * Then add this to your filters.yml * * # Filter that setup user culture * mySwitchLanguageFilter: * class: SwitchLanguageFilter * * @package SwitchLanguageFilter * @subpackage filter * @author Pierre-Yves Landuré <py.landure@dorigo.fr> */ class SwitchLanguageFilter extends sfFilter { /** * Check that the language is a valid application culture. * @param string $language The tested language code * @param string $default_language The default language code * * @return string $language if no accepted languages is set, * else $language if it is in accepted languages * else $default_language */ private function getAvailableCulture($language, $default_language = null) { $all_languages = sfConfig::get('app_accepted_languages', array()); if(count($all_languages)) { if(in_array($language, $all_languages)) // Test if language is available { return $language; } else // Else test if first part of language is available { $language_parts = explode('_', $language); if(count($language_parts)) { if(in_array($language_parts[0], $all_languages)) { return $language_parts[0]; } } } return $default_language; } return $language; } /** * The filter call. */ public function execute ($filterChain) { $context = $this->getContext(); $user = $context->getUser(); $default_culture = sfConfig::get('sf_i18n_default_culture'); $selected_culture = $user->getCulture(); if(!$user->getAttribute('sf_culture_autodetected', false)) { $browser_languages = $context->getRequest()->getLanguages(); foreach($browser_languages as $language) { $allowed_culture = $this->getAvailableCulture($language); if($allowed_culture) { $selected_culture = $allowed_culture; break; } } $user->setAttribute('sf_culture_autodetected', true); } $selected_culture = $context->getRequest()->getParameter('sf_culture', $selected_culture); $selected_culture = $this->getAvailableCulture($selected_culture, $default_culture); if($selected_culture != $user->getCulture()) { // The user wants to see the page in another language $user->setCulture($selected_culture); } $filterChain->execute(); } }
Symfony 1.1 comes with a complete new form system. It works completely according to the MVC draft:
Make sure you have a running Symfony 1.1 based project and application and modules. In this example I build the form inside the myModule module and myLogin action.
My form makes use of i18n, which is in my case autoloaded in settings.yml.
This tutorial uses Symfony 1.1 beta4 and RC1. There are a little important changes with respect to beta3, which I don't cover.
I also expect you to have practical knowledge and a little bit experience with Symfony as system. I will not explain how you i18n implements or modules shield with security.yml.
The form gets two import fields: username and password. Furthermore is there a hidden field in which the URI comes that the user requested, but got redirected to the loginform (through Symfony’s security.yml and settings.yml). This will be used to go to that URI again after a successful login.
Both imput fields are required I will build a custom validation for a correct username/password check. Also we want to make use of a little new protection feature: CSRF.
The form takes uses i18n for multilinguity, I will use English primarily, but the view has been prepared for other languages.
We will begin with the action which contains the primary control.
/** * Executes myLogin action * * Login functionality. * * @param void * @return void * @access public */ public function executeMyLogin() { // Erase auth data $this->getUser()->clearCredentials(); $this->getUser()->setAuthenticated(FALSE); // Build login form $oForm = new inloggenForm(); if ($this->getRequest()->isMethod('post')) { // When called through POST (form submit) $oForm->bind( array('username' => $this->getRequest()->getParameter('username'), 'password' => $this->getRequest()->getParameter('password'), 'referrer' => $this->getRequest()->getParameter('referrer'), ) ); // Save orginal requested location in referrer field $oForm->setDefault('referrer', $this->getRequest()->getParameter('referrer')); if ($oForm->isValid()) { // When validations OK $aValues = $oForm->getValues(); sfContext::getInstance()->getLogger()->debug($aValues['username']); // Authentification $this->getUser()->setAuthenticated(TRUE); // To requested page $this->redirect($this->getRequest()->getParameter('referrer')); } } else { // Save original requested uri in form $oForm->setDefault('referrer', sfContext::getInstance()->getRequest()->getUri()); } $this->oForm = $oForm; // form to view }
Logic of authentification is set after POST further. Through the sfForm::bind() method couples the input of the user coupled with the form controller.
I made this in myModule/lib/form/inloggenForm.class.php. In the form is defined and coupled with the validations and the formatting.
<?php class inloggenForm extends sfForm { /** * Prepare validators */ private function _setValidators() { $this->oValGebruikersnaam = new sfValidatorAnd( array( new sfValidatorString(array('min_length' => 3, 'max_length' => 50), array('min_length' => 'The username should be at least three characters long', 'max_length' => 'The username should be fifty characters long at most', ) ), new sfValidatorCallback(array('callback' => array('cmsLoginValidator', 'execute'), 'arguments' => array() ), array('invalid' => 'This username/password combination is unknown') ), ), array('required' => TRUE), array('required' => 'The username is mandatory') ); $this->oValWachtwoord = new sfValidatorString(array('required' => TRUE), array('required' => 'The password is mandatory') ); $this->oValReferrer = new sfValidatorString(array('required' => FALSE)); } /** * Prepare widgets */ private function _setWidgets() { $this->oWidGebruikersnaam = new sfWidgetFormInput(array(), array('id' => 'username', 'size' => 25)); $this->oWidWachtwoord = new sfWidgetFormInputPassword(array(), array('id' => 'password', 'size' => 10)); $this->oWidReferrer = new sfWidgetFormInputHidden(array(), array('id' => 'referrer')); } /** * Configure form */ public function configure() { $this->_setValidators(); $this->_setWidgets(); /* * Set validators */ $this->setValidators(array('username' => $this->oValGebruikersnaam, 'password' => $this->oValWachtwoord, 'referrer' => $this->oValReferrer, ) ); /* * Set widgets */ $this->setWidgets(array('username' => $this->oWidGebruikersnaam, 'password' => $this->oWidWachtwoord, 'referrer' => $this->oWidReferrer, ) ); /* * Set decorator */ $oDecorator = new sfWidgetFormSchemaFormatterDiv($this->getWidgetSchema()); $this->getWidgetSchema()->addFormFormatter('div', $oDecorator); $this->getWidgetSchema()->setFormFormatterName('div'); $this->getWidgetSchema()->setHelps(array('username' => 'Please enter your username', 'password' => 'Please enter your password' ) ); } /** * Bind override */ public function bind(array $taintedValues = null, array $taintedFiles = array()) { $request = sfContext::getInstance()->getRequest(); if ($request->hasParameter(self::$CSRFFieldName)) { $taintedValues[self::$CSRFFieldName] = $request->getParameter(self::$CSRFFieldName); } parent::bind($taintedValues, $taintedFiles); } }
There are two private methods:
I overloaded the earlier mentioned bind() method to process the CSRF token internally. This field is regulated by sfForm internally, you to do need to worry about it much, only with the actual functionality.
The configure() method contains the functionality. The validators are created and linked to the corresponding fields and also are the widgets. Also, the decorator is defined with I will go into later on.
In _setValidators() sfValidatorBase objects are made for each field.
The password field check is the simplest, only the required input is checked. The object is of the type sfValidatorString (extend sfValidatorBase) without extra controls, only the required attribute with errortext is specified. In principle, you can use each validator as -empty- container.
The username is a combination; it is required, there are restrictions to the the stringlength and a correct login is checked. The stringcontrole/requirement is checked through sfValidatorString, which is quite simple.
The check for a correct login is done through a special validator, that does a callback to an custom function: sfValidatorCallback. This is explained later.
Since these two validators check the same field , the sfValidatorAnd validator is used that combines several validators. All validators must be satisfied. Of course Symfony also offers sfValidatorOr that checks that at least one underlying validator satisfies.
As you can see the callback validator calls to a custom class/method: myLoginValidator::execute().
/** * Fiel with myLoginValidator klasse * * @package - */ /** * Validation correct username/password * * @author Jordi Backx (Snowkrash) * @copyright Copyright © 2008, Jordi Backx (Snowkrash) * @package - */ class myLoginValidator { /** * execute validator * * @param sfValidatorBase Validator instance that calls this method * @param string Value of field that sfValidatorCallback checks * @param array Arguments for correct working * * @return value field when OK. Nothing if error (sfValidatorError exception) */ public static function execute ($oValidator, $sValue, $aArguments) { if ( /* check OK */ ) { // Return waarde veld indien controle OK return $sValue; } // Throw exception when not OK throw new sfValidatorError<