Code snippets for symfony 1.x

Navigation

Refine Tags

Snippets tagged "form"

Validator for latitude / longitude

<?php
/**
 * This validator accepts either a four-member array such as
 * array( 'deg' => 12, 'min' => 34, 'sec' => 56, 'dir' => 'N' )
 * or a numeric value such as 12.3456
 */
class ynValidatorLatLong extends sfValidatorBase
{
  protected function configure($options = array(), $messages = array())
  {
    parent::configure( $options, $messages );
 
    $this->addRequiredOption('axis');  // 'latitude' || 'longitude'
 
    $this->addMessage('invalid_axis', 'The selected axis is invalid.');
  }
 
  protected function doClean( $value )
  {
    $max = 0;
 
    switch ( $this->getOption('axis') ) {
      case 'latitude':
        $max = 90;
        break;
 
      case 'longitude':
        $max = 180;
        break;
 
      default:
        throw new sfValidatorError($this, 'invalid_axis');
    }
 
    if ( is_array( $value ) ) {
      $value = $this->arrayToInt( $value );
    }
 
    if ( ! is_numeric( $value ) ) {
      throw new sfValidatorError($this, 'invalid');
    }
 
    if ( abs( $value ) > $max ) {
      throw new sfValidatorError($this, 'invalid');
    }
 
    return $value;
  }
 
  protected function arrayToInt( array $value )
  {
    if ( count( $value ) != 4 ) {
      throw new sfValidatorError($this, 'invalid');
    }
 
    if (
      ! isset( $value['deg'] )
      || ! isset( $value['min'] )
      || ! isset( $value['sec'] )
      || ! isset( $value['dir'] )
    ) {
      throw new sfValidatorError($this, 'invalid');
    }
 
    if ( preg_match( '/\D/', $value['deg'].$value['min'].$value['sec'] ) ) {
      // non-integer component
      throw new sfValidatorError($this, 'invalid');
    }
 
    $multiplier = 1;
 
    switch ( strtolower( $value['dir'] ) ) {
      case 'n':
        $axis = 'latitude';
        break;
 
      case 's':
        $axis = 'latitude';
        $multiplier = -1;
        break;
 
      case 'e':
        $axis = 'longitude';
        break;
 
      case 'w':
        $axis = 'longitude';
        $multiplier = -1;
        break;
 
      default:
        throw new sfValidatorError($this, 'invalid');
    }
 
    if ( $axis != $this->getOption('axis') ) {
      throw new sfValidatorError($this, 'invalid');
    }
 
    return ( $value['deg'] + $value['min']/60 + $value['sec']/60/60 ) * $multiplier;
  }
}
 
/*
 * UNIT TEST
 */
 
<?php
require_once dirname(__FILE__).'/../../bootstrap/unit.php';
 
$t = new lime_test();
 
$lat_validator = new ynValidatorLatLong( array( 'axis' => 'latitude' ) );
$lon_validator = new ynValidatorLatLong( array( 'axis' => 'longitude' ) );
 
$lat_good = array(
  array( 'deg' => 50, 'min' => 12, 'sec' => 14, 'dir' => 'N' ),
  array( 'deg' => 50, 'min' => 12, 'sec' => 14, 'dir' => 'S' ),
  50.456789,
  50,
  -50.456789,
  -50
);
 
$lat_bad = array(
  'three members' => array( 'deg' => 50, 'min' => 12, 'sec' => 14 ),
  'invalid n/s 1' => array( 'deg' => 50, 'min' => 12, 'sec' => 14, 'dir' => 'F' ),
  'invalid n/s 2' => array( 'deg' => 50, 'min' => 12, 'sec' => 14, 'dir' => 'E' ),
  'non-integer member' => array( 'deg' => 50, 'min' => 12, 'sec' => 14.2, 'dir' => 'N' ),
  'out of range' => array( 'deg' => 100, 'min' => 12, 'sec' => 14, 'dir' => 'N' ),
);
 
$lon_good = array(
  array( 'deg' => 150, 'min' => 12, 'sec' => 14, 'dir' => 'E' ),
  array( 'deg' => 150, 'min' => 12, 'sec' => 14, 'dir' => 'W' ),
  50.456789,
  50,
  -50.456789,
  -50
);
 
$lon_bad = array(
  'three members' => array( 'deg' => 150, 'min' => 12, 'sec' => 14 ),
  'invalid e/w 1' => array( 'deg' => 150, 'min' => 12, 'sec' => 14, 'dir' => 'F' ),
  'invalid e/w 2' => array( 'deg' => 150, 'min' => 12, 'sec' => 14, 'dir' => 'N' ),
  'non-integer member' => array( 'deg' => 150, 'min' => 12, 'sec' => 14.2, 'dir' => 'E' ),
  'out of range' => array( 'deg' => 200, 'min' => 12, 'sec' => 14, 'dir' => 'E' ),
);
 
foreach ( $lat_good as $value ) {
  try {
    $clean = $lat_validator->clean( $value );
    $t->ok( is_numeric( $clean ), 'is numeric' );
 
    if ( is_array( $value ) ) {
      switch( $value['dir'] ) {
        case 'E':
          $multiplier = 1;
          break;
        case 'W':
          $multiplier = -1;
          break;
      }
 
      $t->ok( abs($clean) >= $value['deg'] && abs($clean) <= $value['deg']+1, "value $clean makes sense" );
      $t->ok( $clean * $multipler >= 0, 'direction correct' );
    }
    else {
      $t->is( $value, $clean, 'value makes sense' );
    }
  }
  catch( sfValidatorError $e ) {
    $t->fail( $e->getMessage() );
  }
}
 
foreach ( $lat_bad as $message => $value ) {
  try {
    $lat_validator->clean( $value );
    $t->fail( $message );
  }
  catch( sfValidatorError $e ) {
    $t->pass( $message );
  }
}
 
foreach ( $lon_good as $value ) {
  try {
    $clean = $lon_validator->clean( $value );
    $t->ok( is_numeric( $clean ), 'is numeric' );
 
    if ( is_array( $value ) ) {
      switch( $value['dir'] ) {
        case 'E':
          $multiplier = 1;
          break;
        case 'W':
          $multiplier = -1;
          break;
      }
 
      $t->ok( abs($clean) >= $value['deg'] && abs($clean) <= $value['deg']+1, "value $clean makes sense" );
      $t->ok( $clean * $multipler >= 0, 'direction correct' );
    }
    else {
      $t->is( $value, $clean, 'value makes sense' );
    }
  }
  catch( sfValidatorError $e ) {
    $t->fail( $e->getMessage() );
  }
}
 
foreach ( $lon_bad as $message => $value ) {
  try {
    $lon_validator->clean( $value );
    $t->fail( $message );
  }
  catch( sfValidatorError $e ) {
    $t->pass( $message );
  }
}
 
by yitznewton on 2011-02-27, tagged coordinates  form  latitude  longitude  validator 

Tha django-multilingual style for I18n forms (ability to remove selected translations)

This snippet was created under the inspiration of using the django-multilingual package. Also an additional code is used to solve the problem of saving the many-to-many relations in embedded forms (http://trac.symfony-project.org/ticket/5867#comment:21).

The idea is that in all embedded I18n forms appears checkbox "delete" for each translation. When creating a new object this checkbox is checked for all translation form. To use the translation it's necessary to clear the checkbox in this translation form. When you save a form such I18n embedded form is also being validated and saved, and the forms with checkboxes checked are not validated and not saved. Also the objects of I18n embedded forms with checked checkbox are removed.

Probably it's not ideal solution but it works good for me. If you have any idea how to improve it you're always welcome.

The changes in code are only in one file /lib/forms/doctrine/BaseFormDoctrine.class.php:

<?php
 
/**
 * Project form base class.
 *
 * @package    symfony
 * @subpackage form
 * @author     Svyatoslav Mankovski <slawka@slawka.net>
 * @version    SVN: $Id: sfDoctrineFormBaseTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
 */
abstract class BaseFormDoctrine extends sfFormDoctrine {
 
  protected static $I18nDeleteFieldName = '_i18n_delete';
 
  public function __construct($object = null, $options = array(), $CSRFSecret = null) {
 
    parent::__construct($object, $options, $CSRFSecret);
 
    $this->addI18nDeleteField();
 
    $this->resetFormFields();
  }
 
  public function addI18nDeleteField() {
 
    if ($this->getObject()->getTable()->getParentGenerator() instanceof Doctrine_I18n) {
      $this->widgetSchema[self::$I18nDeleteFieldName] = new sfWidgetFormInputCheckbox(array(
                'label' => sfContext::getInstance()->getI18N()->__('Remove'),
              ));
      $this->setDefault(self::$I18nDeleteFieldName, $this->isNew());
      $this->widgetSchema->moveField(self::$I18nDeleteFieldName, sfWidgetFormSchema::FIRST);
      $this->validatorSchema[self::$I18nDeleteFieldName] = new sfValidatorPass();
    }
 
    return $this;
  }
 
  public function bind(array $taintedValues = null, array $taintedFiles = null) {
 
    if ($this->getObject()->getTable()->hasGenerator('Doctrine_I18n')) {
      $forms = 0;
      $deletes = 0;
      $first_form = '';
      foreach ($taintedValues as $name => $field) {
        if (is_array($field)) {
          $first_form = $first_form == '' ? $name : $first_form;
          $forms++;
          if (isset($field[self::$I18nDeleteFieldName])) {
            $deletes++;
          }
        }
      }
      foreach ($taintedValues as $name => $field) {
        if (is_array($field) && isset($field[self::$I18nDeleteFieldName])) {
          if ($forms > $deletes || $forms == $deletes && $first_form != $name) {
            unset($this->validatorSchema[$name]);
            $this->validatorSchema[$name] = new sfValidatorPass();
          } else {
            unset($taintedValues[$name][self::$I18nDeleteFieldName]);
          }
        }
      }
    }
 
    parent::bind($taintedValues, $taintedFiles);
  }
 
  public function bindAndSave($taintedValues, $taintedFiles = null, $con = null) {
 
    if ($this->getObject()->getTable()->getParentGenerator() instanceof Doctrine_I18n) {
      if (isset($taintedValues[self::$I18nDeleteFieldName])) {
        unset($taintedValues[self::$I18nDeleteFieldName]);
 
        $object = clone $this->getObject();
 
        $object->delete($con);
 
        return true;
      }
    }
 
    return parent::bindAndSave($taintedValues, $taintedFiles, $con);
  }
 
  public function saveEmbeddedForms($con = null, $forms = null, $taintedValues = null) {
    if (null === $con) {
      $con = $this->getConnection();
    }
 
    if (null === $forms) {
      $forms = $this->embeddedForms;
    }
 
    if (null === $taintedValues) {
      $taintedValues = $this->taintedValues;
    }
 
    foreach ($forms as $name => $form) {
      $values = isset($taintedValues[$name]) ? $taintedValues[$name] : array();
      if ($form instanceof sfFormObject) {
        unset($form[self::$CSRFFieldName]);
 
        $form->bindAndSave($values, $this->taintedFiles, $con);
        $form->saveEmbeddedForms($con, null, $values);
      } else {
        $this->saveEmbeddedForms($con, $form->getEmbeddedForms(), $values);
      }
    }
  }
 
}
 
by Svyatoslav Mankovski on 2010-12-10, tagged form  i18n 

symfony 1.x form framework: how to add extra fields into forms?

Assumption: there is a UserMessage class and the related form UserMessageForm. Both were generated by Doctrine based on a schema.yml declaration. Problem: one wishes to you add input fields into this form.

Approach: one can add any pairs of widgets/validators which have no direct relationship to the presented model. Doing so, one need to care about two main aspects:

  1. configuring the form with the extra field
  2. processing of the submitted value (if any) at the proper moment without disturbing the big rest of form functionality.

For step 1, configuring the form: add the widget and the validator to the form.

In .../UserMessageForm.class.php:
 
  public function configure()
  {
    $this->widgetSchema['comment_for_log'] = new sfWidgetFormInputText();
    $this->validatorSchema['comment_for_log'] = new sfValidatorString();
  }
 

For step 2 processing user input after submission: process the value and act accordingly. Typically this happens only after validation did succeed (i.e. not validation errors did occur).

For this purpose, a dedicated method for each field gets called (in case that the method exists). The following example checks whether a non-empty text was entered by the user. If so, it does store a log entry. The function in this example returns a boolean 'false' value to tell symfony not to process this value further.

In .../UserMessageForm.class.php:
 
  public function updateCommentForLogColumn($value)  // constructed method name with camelized field name
  {
    if (! empty($value)) {     // e.g. only if the user entered a comment (btw this is the cleaned value)
      // do whatever you need to do based on the contents of this field.
      // e.g. we create and save a log entry as follows:
      $logEntry = new AppLogEntry();    // assume we have this sort of logging feature
      $logEntry->setComment($value);    // the comment entered by the user
      $logEntry->save();
    }
    return false;  // tell symfony that we have cared about this value, so no further processing will occur
  }
}
 

Basically that's it.

An alternative to the dedicated updateYourSpecialFieldColumn() method could be to override the doUpdate($values) method in the form, allowing to do more general things because there is access to all submitted values (that's basically the only difference between the two approaches, though). I personally prefer the dedicated method whenever possible, as it doesn't disturb any other logic that may (at the same time, or sometimes later) implemented in the same form.

Another good thing is that since all this code is all packed in a Doctrine transaction, so either both the form object (UserMessage) will be saved as well as the log (LogEntry) be generated, or none of the two objects be altered/created at all.

(15.9.2010/SCH)

by Raphael Schumacher on 2010-09-15, tagged doctrine  fields  form  symfony14  validator  widget 

Set "class" or any attribute of ALL fields in Symfony Form

class ClientForm extends BaseClientForm
{
  public function configure() {
    $fields = $this->getWidgetSchema()->getFields();
    foreach ($fields as $f) {
        $f->setAttribute("class", "client_form_field");
    }
  }
}
 
by gerry eng on 2010-05-09, tagged attribute  form 

yet another read-only field method

(Sorry for not formatting the code, but the code tag didn't work...)

by 4Him on 2010-03-24, tagged field  form  readonly 
(1 comment)

Validator to make a field read-only

Sometimes, when you display the form for a Doctrine object, you want to make some fields read-only..

You can unset the field, and just display the value in the template. But it's better to still display an input form. You can also mess with the template, etc.

Here's a validator to achive the same goal, with no need to touch the template.

class sfValidatorReadOnlyField extends sfValidatorBase
{
  /**
   * Configures the current validator
   * 
   * Available options (required):
   *
   *  * object: The current form object
   *  * field:  The current form field
   *
   * Available error codes:
   *
   * * changed
   *
   * @param array $options   An array of options
   * @param array $messages  An array of error messages
   *
   * @see sfValidatorBase
   **/
  protected function configure($options = array(), $messages = array())
  {
    $this->addRequiredOption('object');
    $this->addRequiredOption('field');
 
    $this->addMessage('changed', 'You don\'t want to mess with me, kid!');
  }
 
  /**
   * @see sfValidatorBase
   */
  protected function doClean($value)
  {
    // Ok, we could just reset the value, but it's so more fun this way
    $object = $this->getOption('object');
    if ($value != $object->get($this->getOption('field')))
    {
      throw new sfValidatorError($this, 'changed');
    }
 
    return (string) $value;
  }
}
 

In your form class, use this code :

public function configure()
{// make some fields readonly
    $this->widgetSchema['first_name']->setAttribute('readonly', 'readonly');
    $this->widgetSchema['last_name']->setAttribute('readonly', 'readonly');
    $this->widgetSchema['email_address']->setAttribute('readonly', 'readonly');
 
    $this->validatorSchema['first_name'] = new sfValidatorReadOnlyField(array('field' => 'first_name', 'object' => $this->getObject()));
    $this->validatorSchema['last_name'] = new sfValidatorReadOnlyField(array('field' => 'last_name', 'object' => $this->getObject()));
    $this->validatorSchema['email_address'] = new sfValidatorReadOnlyField(array('field' => 'email_address', 'object' => $this->getObject()));
 
…
}
 
by Thibault Jouannic on 2010-02-15, tagged form  validator 
(1 comment)

Globally format forms

If you are like me, you see the nice Forms chapter, but when you follow the instructions, your forms don't look nice like the examples.

I gleaned the following techniques from various forum posts. I take NO credit for this, it's heavily snatched from other sources.

First, create your format class, which lays out all forms. I use tables in my design, but you can find examples of div based versions elsewhere, )such as here).

/lib/sfWidgetFormSchemaFormatterCustom.class.php:

<?php
class sfWidgetFormSchemaFormatterCustom extends sfWidgetFormSchemaFormatterTable
{
  protected
    $rowFormat       = "<tr class='formrow'>\n  <th class='formheader'>%label%:</th>\n  <td class='forminput'>%error%%field%%help%%hidden_fields%</td>\n</tr>\n",
    $errorRowFormat  = "<tr><td colspan='formerror' colspan=\"2\">\n%errors%</td></tr>\n",
    $helpFormat      = '<br />%help%',
    $decoratorFormat = "<table class='form'>\n  %content%</table>";
}
 

The key here is to insert into each HTML element the global classes to format your code. Above, you can see I added the 'form' class to the table, 'formheader' to the header rows, and 'forminput' for each field.

Next, you need to globally enable this formatter for all forms:

/config/ProjectConfiguration.class.php:

<?php
 
require_once dirname(dirname(__FILE__)).'/lib/vendor/symfony/lib/autoload/sfCoreAutoload.class.php';
sfCoreAutoload::register();
 
class ProjectConfiguration extends sfProjectConfiguration
{
  public function setup()
  {
    // for compatibility / remove and enable only the plugins you want
    $this->enableAllPluginsExcept(array('sfDoctrinePlugin', 'sfCompat10Plugin'));
    sfWidgetFormSchema::setDefaultFormFormatterName('Custom');
  }
}
 

Naturally, make sure you manage your plugins appropriately. They key line is the:

sfWidgetFormSchema::setDefaultFormFormatterName('Custom');
 

Now, customize your CSS files:

.form {
  padding-top:15px;
}
.formrow {
 
}
.formheader {
  text-align: right;
  font-weight: bold;
  padding-right: 10px;
  vertical-align: middle;
}
td.forminput input {
  width: 300px;
}
.formerror {
 
}
 

Hope this helps others.

by Mike Crowe on 2009-10-24, tagged form  formatter  sfform  widget 

unset all not needed form widgets

Common Probem.

You have a large and small form. For example a contact form.

Form orginal includes 50 fields and your small form is extend form the orginal form and includes 15 fields. normaly you have to unset very 35 field in the small one.

But you could also write this lines:

class ContactSimpleForm extends ContactForm
{
  public function configure()
  {
 
    parent::configure();
 
    $this->wantedFields = array(
        'firstname',
        'lastname',
    );
 
    foreach ($this as $fieldName => $widget){
        if (!in_array($fieldName, $this->wantedFields)){
            unset($this->widgetSchema[$fieldName]);
            unset($this->validatorSchema[$fieldName]);
        }
    }
}
 
by excessive demon on 2009-09-18, tagged form  unset  validator  widget 
(3 comments)

How to validate that at least one checkbox has been checked

I spent some time trying to figure out how to make sure that one checkbox was checked before saving a form in symfony 1.2.

I read about Global Validators (http://www.symfony-project.org/forms/1_2/en/02-Form-Validation#chapter_02_global_validators) but I couldn't find anything to fit my needs. Finally, after looking at this snipped: http://www.symfony-project.org/cookbook/1_2/en/conditional-validator , I created the validation I needed, here is the snipped to help someone in the same situation:

class SampleForm extends BaseSampleForm
{
  public function configure()
  {
 
  // add a post validator
    $this->validatorSchema->setPostValidator(
      new sfValidatorCallback(array('callback' => array($this, 'checkAtLeastOne')))
    );
 
 
    unset(
      $this['created_at'], $this['updated_at']
 
    );
  }
 
  public function checkAtLeastOne($validator, $values)
  {
    if (!$values['sunday'] && !$values['monday'] && !$values['tuesday'] && !$values['wednesday'] && !$values['thursday'] && !$values['friday'] && !$values['saturday'])
    {
      // no checkbox was checked, throw an error
      throw new sfValidatorError($validator, 'Check at least one day of the week');
    }
 
    // at least one checkbox is checked, return the clean values
    return $values;
  }
 
 
}
 
by carlos m on 2009-06-26, tagged box  check  checkbox  checked  form  global  validator 
(2 comments)

A task to extract i18n labels from a form

A piece of code that will extract the existing labels from a form into a xliff file. It will only pick out the labels that have been set by the widgetForm->setLabel() command.

example for UserForm.class.php you can extract it using:

   php symfony i18n:extract-form user
 

.

<?php
 
class i18nExtractFormTask extends sfBaseTask
{
  protected function configure()
  {
    // // add your own arguments here
    // $this->addArguments(array(
    //   new sfCommandArgument('my_arg', sfCommandArgument::REQUIRED, 'My argument'),
    // ));
 
    $this->addOptions(array(
      new sfCommandOption('application', null, sfCommandOption::PARAMETER_REQUIRED, 'The application name', 'frontend'),
      new sfCommandOption('env', null, sfCommandOption::PARAMETER_REQUIRED, 'The environment', 'dev'),
      new sfCommandOption('connection', null, sfCommandOption::PARAMETER_REQUIRED, 'The connection name', 'default'),
      new sfCommandOption('source', null, sfCommandOption::PARAMETER_REQUIRED, 'Source language', 'en'),
      // add your own options here
    ));
 
    $this->addArgument('form', sfCommandOption::PARAMETER_REQUIRED, 'Name for the form to translate');
 
 
    $this->namespace        = 'i18n';
    $this->name             = 'extract-form';
    $this->briefDescription = 'Extracts the labels from a form';
    $this->detailedDescription = <<<EOF
The [i18n-extract-form|INFO] task extracts the labels from a form class into a XLIFF file.
Call it with:
   for CoolForm.class.php you can extract it using:
 
  [php symfony i18n-extract-form cool|INFO]
EOF;
  }
 
  protected function execute($arguments = array(), $options = array())
  {
    // initialize the database connection
    $databaseManager = new sfDatabaseManager($this->configuration);
    $connection = $databaseManager->getDatabase($options['connection'] ? $options['connection'] : null)->getConnection();
 
    $sFormname = ucfirst($arguments['form']).'Form';
    $oForm = new $sFormname(false);
    $labels = $oForm->getWidgetSchema()->getLabels();
    $sfMessageSource = new sfMessageSource_XLIFF(sfConfig::get('sf_app_i18n_dir'));
    $sfMessageSource->setCulture($options['source']);
    $sfMessageSource->load($sFormname);
    $existingMessages = $sfMessageSource->read();
    $aKeys = array_keys($existingMessages);
    $aMessages = array_keys($existingMessages[$aKeys[0]]);
    $unLabeled = array();
    foreach ($labels as $name => $value)
    {
        if (!is_null($value))
        {
 
            if (!in_array($value,$aMessages))
            {
                $sfMessageSource->append($value);
                $this->logSection('i18n', "Adding: " . $value);
            }
        } else {
            $unLabeled[] = $name;
        }
    }
 
 
    // an extra service: generate code for the not yet labeled parts: still a bit buggy
    $unLabeled = array_diff($unLabeled, array("_csrf_token"));
    $unLabeled = array_values($unLabeled);
 
       $return = "Code for unlabeled fields.:\n";
       foreach ($unLabeled as $label)
       {
           $return .= "\t\"" . '$this->widgetSchema->setLabel("' . $label .'","")'."\n";
       }
  }
}
 
by Adje on 2009-06-15, tagged form  i18n  xliff 
(1 comment)

Login form in Symfony 1.1, with the new form system

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 design

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.

The start: the action

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.

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.

Validators

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.

The custom logincheck

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.

Widgets

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.

The view

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">&nbsp;</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.

CSRF token

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">&darr;&nbsp;%error%&nbsp;&darr;</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().

Conclusion

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!

by Jordi Backx on 2008-05-10, tagged 11  csrf  decorator  form  formatter  i18n  login  sfform  symfony  validator  widget 

Using a link to submit a form or button to link

For ergonomic and graphism reasons, I sometimes prefer to use link rather than buttons and/or button rather than links...

To do so, I created this small function :

<?php
# File : JsHelper.php
 
// use of "normal" Javascript Helper
use_helper('Javascript');
 
 
/*!
 * Create a link that submit the closest parent form
 *
 * @param $string    string : text to display
 * @param $options mixed : options to be pass to the tag
 * @return string                : HTML code for the link
 */
function link_to_submit( $string, $options = null )
{
  $func = ";var a=this.ancestors();for(var i in a){if(a[i].tagName=='FORM'){try{a[i].onsubmit();} catch(err){a[i].submit();}break;}}";
  return link_to_function( $string, $func, $options );
}
 
 
 
/*!
 * Create a button that act as a link
 *
 * @param $string    string : text to display in the button
 * @param $url         string : URL of the link
 * @param $options mixed : options to be pass to the tag
 * @return string                : HTML code for the link
 */
function button_to_link( $string, $url, $options = null )
{
  $url = url_for($url);
 
  $func = ";document.location = '$url';return false;";
  if ( is_string($options) ) $options.= " onclick=$func";
  else $options['onclick'] = $func;
 
  return tag('button', $options ).htmlspecialchars($string).'</button>';
}
 

Then, you simply use it as a normal link_to function :

<? use_helper('Js') ?>
 
<?=form_tag('module/action')?>
  <?=input_tag('text')?>
  <?=link_to_submit('Validate')?>
  <?=link_to('Cancel','module/index')?>
</form>
 

Or :

<? use_helper('Js') ?>
 
<?=form_tag('module/action')?>
  <?=input_tag('text')?>
  <?=submit_tag('Validate')?>
  <?=button_to_link('Cancel','module/index')?>
</form>
 

In these two examples, links/buttons of the form are graphicly consistents. For a better user experience ;)

PS : works as well with AJAX forms form_remote_tag()

PPS : Don't forget that thanks to CSS, you can make a button looking like a link (et vice-versa).

PPPS : Don't forget that buttons and links doesn't have the same meaning for search engine bots.

PPPPS : Don't forget that this rely on the fact that user is using javascript!!!

by jugjug on 2008-03-26, tagged form  javascript  link  submit 
(2 comments)

Integration of the TinyFCK widget

There are two big open source rich text editors:

Some people prefer the first, some other the second, but there’s a problem for who likes to use TinyMCE: it lacks of a free file manager/uploader.

What we actually need is to have a TinyMCE installation with a free file manager/uploader, thus we take it from FCKEditor.

This is TinyFCK: a TinyMCE with the FCKEditor’s file manager/uploader, nothing less, nothing more.

Installation of TinyFCK rich text editing is described here. You need to download the editor from the project website (http://p4a.crealabsfoundation.org/tinyfck) and unpack it in a temporary folder. Copy the tiny_fck/ directory into your project web/js/ directory, and define the path to the library and new rich editor class in settings.yml.

all:
  .settings:
    rich_text_editor_class: TinyFCK
    rich_text_js_dir:  js/tiny_fck
 

After that create new text editor class file sfRichTextEditorTinyFCK.class.php in your project or application lib/ folder.

<?php
 
/*
 * (c) 2004-2006 Fabien Potencier <fabien.potencier@symfony-project.com>
 * (c) 2007 Eugene Krasichkov <megazlo@megazlo.com>
 *  
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
 
/**
 * sfRichTextEditorTinyFCK implements the TinyFCK rich text editor.
 *
 * <b>Options:</b>
 *  - css - Path to the TinyFCK editor stylesheet
 *
 *    <b>Css example:</b>
 *    <code>
 *    / * user: foo * / => without spaces. 'foo' is the name in the select box
 *    .foobar
 *    {
 *      color: #f00;
 *    }
 *    </code>
 *
 */
class sfRichTextEditorTinyFCK extends sfRichTextEditor
{
  /**
   * Returns the rich text editor as HTML.
   *
   * @return string Rich text editor HTML representation
   */
  public function toHTML()
  {
    $options = $this->options;
 
    // we need to know the id for things the rich text editor
    // in advance of building the tag
    $id = _get_option($options, 'id', $this->name);
 
    // use tinymce's gzipped js?
    $tinymce_file = _get_option($options, 'tinymce_gzip') ? '/tiny_mce_gzip.php' : '/tiny_mce.js';
 
    // tinymce installed?
    $js_path = sfConfig::get('sf_rich_text_js_dir') ? '/'.sfConfig::get('sf_rich_text_js_dir').$tinymce_file : '/sf/tinymce/js'.$tinymce_file;
    if (!is_readable(sfConfig::get('sf_web_dir').$js_path))
    {
      throw new sfConfigurationException('You must install TinyFCK to use this helper (see rich_text_js_dir settings).');
    }
 
    sfContext::getInstance()->getResponse()->addJavascript($js_path);
 
    use_helper('Javascript');
 
    $tinymce_options = '';
    $style_selector  = '';
 
    // custom CSS file?
    if ($css_file = _get_option($options, 'css'))
    {
      $css_path = stylesheet_path($css_file);
 
      sfContext::getInstance()->getResponse()->addStylesheet($css_path);
 
      $css    = file_get_contents(sfConfig::get('sf_web_dir').DIRECTORY_SEPARATOR.$css_path);
      $styles = array();
      preg_match_all('#^/\*\s*user:\s*(.+?)\s*\*/\s*\015?\012\s*\.([^\s]+)#Smi', $css, $matches, PREG_SET_ORDER);
      foreach ($matches as $match)
      {
        $styles[] = $match[1].'='.$match[2];
      }
 
      $tinymce_options .= '  content_css: "'.$css_path.'",'."\n";
      $tinymce_options .= '  theme_advanced_styles: "'.implode(';', $styles).'"'."\n";
      $style_selector   = 'styleselect,separator,';
    }
 
    $culture = sfContext::getInstance()->getUser()->getCulture();
 
    $tinymce_js = '
tinyMCE.init({
  mode: "exact",
  language: "'.strtolower(substr($culture, 0, 2)).'",
  elements: "'.$id.'",
  plugins : "table,save,advhr,advimage,advlink,emotions,iespell,insertdatetime,preview,zoom,flash,searchreplace,print,paste,directionality,fullscreen,noneditable,contextmenu",
  theme: "advanced",
  theme_advanced_buttons1_add_before : "save,newdocument,separator",
    theme_advanced_buttons1_add : "fontselect,fontsizeselect",
    theme_advanced_buttons2_add : "separator,insertdate,inserttime,preview,zoom,separator,forecolor,backcolor,liststyle",
    theme_advanced_buttons2_add_before: "cut,copy,paste,pastetext,pasteword,separator,search,replace,separator",
    theme_advanced_buttons3_add_before : "tablecontrols,separator",
    theme_advanced_buttons3_add : "emotions,iespell,flash,advhr,separator,print,separator,ltr,rtl,separator,fullscreen",
    theme_advanced_toolbar_location : "top",
    theme_advanced_toolbar_align : "left",
    theme_advanced_statusbar_location : "bottom",
  theme_advanced_resizing : true,
  theme_advanced_resize_horizontal : false,
  plugin_insertdate_dateFormat : "%Y-%m-%d",
  plugin_insertdate_timeFormat : "%H:%M:%S",
  file_browser_callback : "tinyFCKFileBrowserCallBack",
  paste_use_dialog : false,
  extended_valid_elements: "img[class|src|border=0|alt|title|hspace|vspace|width|height|align|onmouseover|onmouseout|name]",
  apply_source_formatting : true,
  relative_urls: false,
  debug: false
  '.($tinymce_options ? ','.$tinymce_options : '').'
  '.(isset($options['tinymce_options']) ? ','.$options['tinymce_options'] : '').'
});
 
  function tinyFCKFileBrowserCallBack(field_name, url, type, win) {
    var connector = "../../filemanager/browser.html?Connector=connectors/php/connector.php";
    var enableAutoTypeSelection = true;
 
    var cType;
    tinyfck_field = field_name;
    tinyfck = win;
 
    switch (type) {
      case "image":
        cType = "Image";
        break;
      case "flash":
        cType = "Flash";
        break;
      case "file":
        cType = "File";
        break;
    }
 
    if (enableAutoTypeSelection && cType) {
      connector += "&Type=" + cType;
    }
 
    window.open(connector, "tinyfck", "modal,width=600,height=400");
  }';
 
    if (isset($options['tinymce_options']))
    {
      unset($options['tinymce_options']);
    }
 
    return
      content_tag('script', javascript_cdata_section($tinymce_js), array('type' => 'text/javascript')).
      content_tag('textarea', $this->content, array_merge(array('name' => $this->name, 'id' => get_id_from_name($id, null)), _convert_options($options)));
  }
}
 

Then clear cache:

symfony cc
 

Once this is done, toggle the use of rich text editing in text areas by adding the rich=true option. You can also specify custom options for the TinyFCK editor using the tinymce_options option.

<?php echo textarea_tag('name', 'default content', 'rich=true size=10x20')) ?>
 => a rich text edit zone powered by TinyFCK
<?php echo textarea_tag('name', 'default content', 'rich=true size=10x20tinymce_options=language:"fr",theme_advanced_buttons2:"separator"')) ?>
=> a rich text edit zone powered by TinyFCK with custom parameters
 
by Eugene Krasichkov on 2007-12-27, tagged editor  fckeditor  form  forms  helper  rich  tinyfck  tinymce 
(1 comment)

Defining a default value for fields on the create form using admin generator

Say you want the "create" form to have default values in some of the fields.

Unfortunately, if you hardcode values like this in the edit section of generator.yml, it will break the edit form for existing objects.

  edit:
    fields:
      thing_field: {params:value=foo}  # WRONG
 

The solution is to override the getThingOrCreate method the admin generator generates in its actions.class.php file.

If you're dealing with a class/module named 'post', you can copy and paste the generated function out of cache/frontend/dev/modules/autoPost/templates/_edit_form.php into apps/frontend/modules/post/actions/actions.class.php and add the defaults like this:

protected function getPostOrCreate($id = 'id')
  {
    if (!$this->getRequestParameter($id))
    {
      $post = new Post();
      // add some default values
      $post->setThing('foo');
           // default publish 24 hours from now
      $post->setPublishTime(date('Y-n-j G:i', time() + 60*60*24 ));
    }
    else
    {
      $post = PostPeer::retrieveByPk($this->getRequestParameter($id));
 
      $this->forward404Unless($post);
    }
 
    return $post;
  }
 
by Nathan Vonnahme on 2007-12-06, tagged admingenerator  create  default  form  value 

Disable fillin filter after validation success

When using fillin filter, after validation success (all form fields are ok) if you want display page without form, fillin filter is still enabled and throws exception "Exception - No form found in this page". Use this snippet to prevent it.

in sfFillInFormFilter.class.php file

  public function execute($filterChain)
  {
    // execute next filter
    $filterChain->execute();
 
    $context  = $this->getContext();
    $response = $context->getResponse();
    $request  = $context->getRequest();
 
    // add these two lines
    if(! $request->hasErrors())
        return;
 
by jarecky on 2007-11-20, tagged fillin  filter  form  validation 

Display Primary Key in Admin Generator Edit Form

By default, we can't display primary keys in the admin generator edit view.

But I wanted to do just that.

Here is an example for the module/table "article", and the primary key column "id" :

Beware of the tricky spelling of the partial name in the generator.yml file if, like me, you like underscores...

by Vincent Texier on 2007-08-01, tagged admin  form  generator 
(1 comment)

Double List for ManyToMany relationships (deprecated)

I needed the object_admin_double_list helper of the admin generator for my regular projects (without using admin generator). So I ported the admin_double_list helper a bit and now I want to publish this for you.

The admin_double_list helper is for selecting multiple items from a pool of items using ManyToMany relationships. For example to associate a user to multiple groups.

The helper itself has two parts: - two helper functions - two javascript functions

After that I have an example how the three controller, view and model parts handle this helper.

The snippet itself

Helper file

/**
 * two multiline select tags with associated and unassociated items
 *
 * @return string
 * @param object object
 * @param string method of object
 * @param array options
 * @param array html options of select tags
 **/
function double_list($object, $method, $options = array(), $html_options = array())
{
  $options = _parse_attributes($options);
 
  // get the lists of objects
  list($all_objects, $objects_associated, $associated_ids) = _get_object_list($object, $method, _get_option($options, 'through_class'));
 
  // options
  $html_options['multiple'] = _get_option($html_options, 'multiple', true);
  $html_options['size'] = _get_option($html_options, 'size', 10);
  $html_options['class'] = 'double_list';
 
  $label_assoc = _get_option($options, 'associated_label', 'Zugehörige Gruppen');
  $label_all   = _get_option($options, 'unassociated_label', 'Gruppenliste');
  $name1 = _get_option($options, 'associated', 'associated');
  $name2 = _get_option($options, 'unassociated', 'unassociated');
  $form = _get_option($options, 'form_id', 'editForm');
 
  // unassociated objects
  $objects_unassociated = array();
  foreach ($all_objects as $object)
  {
    if (!in_array($object->getPrimaryKey(), $associated_ids))
      $objects_unassociated[] = $object;
  }
 
  // select tags
  $select1 = select_tag($name1, options_for_select(_get_options_from_objects($objects_associated), '', $options), $html_options);
  unset($html_options['class']);
  $select2 = select_tag($name2, options_for_select(_get_options_from_objects($objects_unassociated), '', $options), $html_options);
 
  // output skeloton
  $html =
'<div style="float:left; padding-right: 20px;">
  <label for="%s">%s</label>
  %s
</div>
<div class="float:left; padding-right: 20px; padding-top: 20px">%s<br />%s</div>
<div class="float:left;">
  <label for="%s">%s</label>
  %s
</div>
<div style="clear:both"></div>';
 
  // include js library
  $response = sfContext::getInstance()->getResponse();
  $response->addJavascript('/js/double_list.js', 'last');
 
  return sprintf($html,
      $name1, $label_assoc, $select1,
      link_to_function(image_tag('resultset_previous'), "double_list_move(\$('{$name2}'), \$('{$name1}'))"),
      link_to_function(image_tag('resultset_next'), "double_list_move(\$('{$name1}'), \$('{$name2}'))", 'style=display:block'),
      $name2, $label_all, $select2,
      $form
    );
}
 
/**
 * retrieve object list via propel
 *
 * @return array
 * @param object root object
 * @param string retrieving method
 * @param string name of satellite class
 **/
function _get_object_list($object, $method, $middleClass)
{
  // get object
  $object = $object instanceof sfOutputEscaper ? $object->getRawValue() : $object;
 
  // get all objects
  $objects = sfPropelManyToMany::getAllObjects($object, $middleClass);
  // get related objects
  $objects_associated = sfPropelManyToMany::getRelatedObjects($object, $middleClass);
  // get ids
  $ids = array_map(create_function('$o', 'return $o->getPrimaryKey();'), $objects_associated);
 
  return array($objects, $objects_associated, $ids);
}
 

You probably want to modify the look of the list using css (like I do). So you can change the $js variable like you want f.e. adding class names. And you perhaps also want to change the image paths (I used two icons of the famfamfam icon library).

The javascript

Put the code below in a file called double_list.js in your js directory. (For individual path and file name, modify the double_list helper, search for $response)

function double_list_move(src, dest)
{
  for (var i = 0; i < src.options.length; i++)
  {
    if (src.options[i].selected)
    {
      dest.options[dest.length] = new Option(src.options[i].text, src.options[i].value);
      src.options[i] = null;
      --i;
    }
  }
}
 
function double_list_submit()
{
  // get all selects with double list class
    selects = $$('select.double_list');
 
    selects.each(function(element){
        for (var i = 0; i < element.options.length; i++)
            element.options[i].selected = true;
    });
 
    return true;
}
 

Example

I would like to show an example how to handle this helper in the three patterns.

The example is easy. Assigning an user to many groups.

Model layer

We have to build a table which handles the ManyToMany relationship.

user_group:
  _attributes:
    phpName:    UserGroup
  group_id:
    type:       integer
    primaryKey: true
    foreignTable:groups
    foreignReference:id
    onDelete:   cascade
  user_id:
    type:       integer
    primaryKey: true
    foreignTable:users
    foreignReference:id
    onDelete:   cascade
 

Don't forget to rebuild all db stuff and to clear the cache.

Presentation layer

Now we display the double_list:

<?php echo double_list($user, 'getUserGroups', 'through_class=UserGroup associated=groups unassociated=not_groups associated_label=Associated Groups unassociated_label=Group list') ?>
 

The first parameter is the user object, than the method of the object retrieving the UserGroup records. I think the options are clear.

Controller layer

At last we have to save the selection of the user. Before doing this we have to delete all UserGroup objects of the user, because we would assign it twice, if the item was selected before.

// clear group data to save it again
$c = new Criteria();
$c->add(UserGroupPeer::USER_ID, $user->getId());
UserGroupPeer::doDelete($c);
 
// save groups
$groups = $this->getRequestParameter('groups');
if ($groups)
{
  foreach ($groups as $id)
  {
    $group = new UserGroup();
    $group->setGroupId($id);
    $group->setUserId($user->getId());
    $group->save();
  }
}
 

It was a bit too long. As I said, this is a port of the original object_admin_double_list helper, which can be practically only used with the Admin Generator.

Please check the snippet, because I use this in a little bit more customized version.

by Halil Köklü on 2007-07-04, tagged admin  form  propel 
(2 comments)

Generic action to save edit-in-place fields

One stumbler for me was that I didn't realize that the form created by the input_in_place_editor_tag() helper only submits one key/value pair (value=xxx), so to tell the action which PK to use or which object field to update, you have to pass that information on the form URL (field and id).

One way to solve it would be to write a different action for each field you want to update, but I wanted something I could reuse without writing additional actions, so I added two GET parameters:

So, in my showSuccess.php template:

<h1 id="eipTitle"><?php echo $comment->getTitle() ? $comment->getTitle() : "click to edit" ?></h1>
<?php echo input_in_place_editor_tag(
    'eipTitle',
    'comment/ajaxUpdate?field=title&commentid='.$comment->getCommentid(),
    array(
    'cols'   => 40,
    'rows'   => 1,
    ))
 
   ?>

BTW, you can read about the valid options you can pass in the third argument to input_in_place_editor_tag() at the script.aculo.us Wiki

And for the action, I made it as generic as possible (actually it would be nice if Propel generated an action like this next to the regular executeUpdate action):

private function raiseEipError ($message) 
  {
    $this->logMessage($message, 'err');
    return $this->renderText( "ERROR:  $message" );
  }
 
  public function executeAjaxUpdate()
  {
    $id = $this->getRequestParameter('commentid');
    $field = $this->getRequestParameter('field');
    $this->logMessage("commentid:$id and field='$field'", 'debug');
 
    // Check for required params.  
    // Return a nice message to the user and in the log if there's a problem.
    if (! $field) 
      return $this->raiseEipError( "No field parameter passed in form action" );
    $valid_fields = CommentPeer::getFieldNames(BasePeer::TYPE_FIELDNAME);
    if (! in_array($field, $valid_fields)) 
      return $this->raiseEipError( "Invalid field parameter '$field' passed in form action.\n".
                                   " Valid fields: (" . implode(", ", $valid_fields) . ")" );
    if (! $id) 
      return $this->raiseEipError( "No ID parameter passed in form action" );
 
    $comment = CommentPeer::retrieveByPk($id);
    if (! $comment) 
      return $this->raiseEipError( "Object with ID $id not found" );
 
    $comment->setByName($field, $this->getRequestParameter('value'), BasePeer::TYPE_FIELDNAME);
    $comment->save();
    return $this->renderText( $comment->getByName($field, BasePeer::TYPE_FIELDNAME) );
  }
by Nathan Vonnahme on 2007-05-04, tagged action  ajax  editinplace  form  inputinplaceeditortag  update 

Generic action to save edit-in-place fields

One stumbler for me was that I didn't realize that the form created by the input_in_place_editor_tag() helper only submits one key/value pair (value=xxx), so to tell the action which PK to use or which object field to update, you have to pass that information on the form URL (field and id).

One way to solve it would be to write a different action for each field you want to update, but I wanted something I could reuse without writing additional actions, so I added two GET parameters:

So, in my showSuccess.php template:

<h1 id="eipTitle"><?php echo $comment->getTitle() ? $comment->getTitle() : "click to edit" ?></h1>
<?php echo input_in_place_editor_tag(
    'eipTitle',
    'comment/ajaxUpdate?field=title&commentid='.$comment->getCommentid(),
    array(
    'cols'   => 40,
    'rows'   => 1,
    ))
 
   ?>

BTW, you can read about the valid options you can pass in the third argument to input_in_place_editor_tag() at the script.aculo.us Wiki

And for the action, I made it as generic as possible (actually it would be nice if Propel generated an action like this next to the regular executeUpdate action):

private function raiseEipError ($message) 
  {
    $this->logMessage($message, 'err');
    return $this->renderText( "ERROR:  $message" );
  }
 
  public function executeAjaxUpdate()
  {
    $id = $this->getRequestParameter('commentid');
    $field = $this->getRequestParameter('field');
    $this->logMessage("commentid:$id and field='$field'", 'debug');
 
    // Check for required params.  
    // Return a nice message to the user and in the log if there's a problem.
    if (! $field) 
      return $this->raiseEipError( "No field parameter passed in form action" );
    $valid_fields = CommentPeer::getFieldNames(BasePeer::TYPE_FIELDNAME);
    if (! in_array($field, $valid_fields)) 
      return $this->raiseEipError( "Invalid field parameter '$field' passed in form action.\n".
                                   " Valid fields: (" . implode(", ", $valid_fields) . ")" );
    if (! $id) 
      return $this->raiseEipError( "No ID parameter passed in form action" );
 
    $comment = CommentPeer::retrieveByPk($id);
    if (! $comment) 
      return $this->raiseEipError( "Object with ID $id not found" );
 
    $comment->setByName($field, $this->getRequestParameter('value'), BasePeer::TYPE_FIELDNAME);
    $comment->save();
    return $this->renderText( $comment->getByName($field, BasePeer::TYPE_FIELDNAME) );
  }
by whoknows on 2007-05-04, tagged action  ajax  editinplace  form  inputinplaceeditortag  update