![]() |
|
Snippets |
|
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!
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($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 gedefinieerd 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 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.
Goog luck!
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(); } }
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.