‘Pick or create’ embedded form the propel way

In my recent application, i want a user able to select the input from the existing options, or create new if the option is not available. After googling on the internet, I found an interesting article how to overcome these problem.
Inspired by Propel 1.5 features ( embedRelation and mergeRelation), I rewrite the method in that article, so we just simply write ’embedPickOrCreateForm ()’ to implement the problem.

Here are the full source code.

<?php
class dePickOrCreateForm extends sfForm
{
    const EMBEDDED_STRING = 'pickorcreate';

    protected $embeddedModelClass;
    protected $embeddedId;
    protected $defaultEmbeddedId;
    protected $options;

    public function __construct($relationName = null, $relationId = null, $defaultRelationId = null, $options = array(), $CSRFSecret = null)
   {
      $this->embeddedModelClass = $relationName;
      $this->embeddedId = $relationId;
      $this->defaultEmbeddedId = $defaultRelationId;
      $this->options = $options;

      parent::__construct(array(), $options, $CSRFSecret);
   }

   protected function renderWidgetForEmbeddedId()
   {
     $this->widgetSchema[$this->embeddedId] = new sfWidgetFormPropelChoice(array('model' => $this->embeddedModelClass, 'add_empty' => true));
     $this->widgetSchema[$this->embeddedId]->setOptions(array_merge($this->widgetSchema[$this->embeddedId]->getOptions(), $this->options));
     $this->setDefault($this->embeddedId, $this->defaultEmbeddedId);
     $this->widgetSchema->setLabel($this->embeddedId, 'Choose one…');
     $this->validatorSchema[$this->embeddedId] = new sfValidatorPass();
   }

   protected function createEmbeddedForm()
   {
     $embeddedFormClass = $this->embeddedModelClass.'Form';
     $form = new $embeddedFormClass;
     $this->embedForm(self::EMBEDDED_STRING, $form);
     $this->widgetSchema->setLabel(self::EMBEDDED_STRING, 'Or Create New');
     $this->validatorSchema[self::EMBEDDED_STRING] = new sfValidatorPass();
   }

   public function configure()
   {
     $this->renderWidgetForEmbeddedId();
     $this->createEmbeddedForm();
     $embeddedValidator = clone $this->validatorSchema[self::EMBEDDED_STRING];
     $this->validatorSchema->setPostValidator(
       new sfValidatorAnd(array(
         new sfValidatorOr(array(
    	   new sfValidatorSchemaFilter($this->embeddedId, new sfValidatorPropelChoice(array('model' => $this->embeddedModelClass, 'column' => 'id', 'required'=>true))),
    	   new sfValidatorSchemaFilter(self::EMBEDDED_STRING, $embeddedValidator)
         )),
    	 new sfValidatorCallback(array('callback' => array($this, 'checkEmbedded')))
       ))
     );
   }

   public function checkEmbedded($validator, $values, $argument)
   {
      if(!empty($values[$this->embeddedId]))
      {
         unset($this[self::EMBEDDED_STRING], $values[self::EMBEDDED_STRING]);
      }
      else
      {
         unset($this[$this->embeddedId], $values[$this->embeddedId]);
      }
      return $values;
    }

    public function getEmbeddedModelClass()
    {
       return $this->embeddedModelClass;
    }

    public function getEmbeddedId()
    {
       return $this->embeddedId;
    }
}
<?php
abstract class deFormPropel extends sfFormPropel
{
   public function embedPickOrCreateForm($relationName, $options = array())
   {
      $options = array_merge(array('title' => $relationName, ), $options);
      $pickOrCreateForm = $this->getPickOrCreateForm($relationName, $options);
      $this->embedForm($options['title'], $pickOrCreateForm);
   }
	
   public function getPickOrCreateForm($relationName, $options)
   {
      $relationMap = $this->getRelationMap($relationName);
      $columnMappings = array_keys($relationMap->getColumnMappings());
      $relationId = call_user_func(array(constant(get_class($this->getObject()).'::PEER'), 'translateFieldName'), $columnMappings[0], BasePeer::TYPE_COLNAME, BasePeer::TYPE_FIELDNAME);
				
      $defaultRelationId = null;
      if (!$this->getObject()->isNew())
      {
         $getter = 'get'.call_user_func(array(constant(get_class($this->getObject()).'::PEER'), 'translateFieldName'), $relationId, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_PHPNAME);
	 $defaultRelationId = $this->getObject()->$getter();
      }
		
      // create pickorcreate form
      $pickOrCreateForm = new dePickOrCreateForm($relationName, $relationId, $defaultRelationId, $options);
    
      return $pickOrCreateForm;
   }
	
   /*for backward compatible with propel 1.4*/
   protected function getRelationMap($relationName)
   {
      $tableMap = call_user_func(array(constant(get_class($this->getObject()).'::PEER'), 'getTableMap'));
      return $tableMap->getRelation($relationName);
  }
  
   public function updateObjectEmbeddedForms($values, $forms = null)
   {
      if (null === $forms)
      {
         $forms = $this->embeddedForms;
      }
      foreach ($forms as $form)
      {
    	 if ($form instanceof dePickOrCreateForm)
    	 {
    	    $relationName = $form->getEmbeddedModelClass();
    	    $relationId = $form->getEmbeddedId();
    	    if(!empty($values[$relationName][$relationId]))
    	    {
    	       $setter = 'set'.call_user_func(array(constant(get_class($this->getObject()).'::PEER'), 'translateFieldName'), $relationId, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_PHPNAME);
	       $this->getObject()->$setter($values[$relationName][$relationId]);
	    }
    	    else if(!empty($values[$relationName][dePickOrCreateForm::EMBEDDED_STRING]))
    	    {
 	       $setter = 'set'.$relationName;
      	       $obj = new $relationName();
      	       foreach ($values[$relationName][dePickOrCreateForm::EMBEDDED_STRING] as $key => $value)
      	       {	
      		   if (null != $value)
      		   {
      		      $obj->setByName($key, $value, BasePeer::TYPE_FIELDNAME);
      		   }	 	
      	       }
      		$this->getObject()->$setter($obj);   		
    	    }
    	    unset($values[$relationName],$this[$relationName]);
    	 }
      }
       parent::updateObjectEmbeddedForms($values);
    }
}

how to use them in our applications?
Let’s just get started. Below is the schema that will be used :

<database name="propel" defaultIdMethod="native" noxsd="true" package="lib.model.Kontrak">
   <table name="perusahaan" phpName="Perusahaan"	description="Perusahaan">
     <column name="id" type="INTEGER" required="true" primaryKey="true" autoincrement="true" description="id peruahaan" />
     <column name="npwp" required="true" type="VARCHAR" size="255" description="NPWP" />
     <column name="nama" required="true" type="VARCHAR" size="255" description="Nama" />
     <column name="alamat" required="true" type="LONGVARCHAR"	description="Alamat" />
     <column name="telepon" type="VARCHAR" size="255" description="Telepon" />
     <column name="fax" type="VARCHAR" size="255"	description="Fax" />
     <column name="direktur" type="VARCHAR" size="255" required="true" />
     <column name="ktp_direktur" type="VARCHAR" size="100" required="true" />
     <column name="alamat_direktur" type="LONGVARCHAR" required="true" />
     <column name="akta_notaris" type="VARCHAR" size="255" required="true" />
   </table>
   <table name="kontrak">
     <column name="id" type="INTEGER" required="true" primaryKey="true" autoincrement="true" description="id kontrak" />
     <column name="tanggal" type="DATE" required="true" />
     <column name="nomor" type="VARCHAR" size="100" required="true" />
     <column name="nilai_penawaran" type="NUMERIC" required="true" />
     <column name="nilai_negosiasi" type="NUMERIC" required="true" default="0" />
     <column name="perusahaan_id" type="INTEGER" required="true" description="Perusahaan pemenang" />
     <foreign-key foreignTable="perusahaan" onDelete="cascade">
       <reference local="perusahaan_id" foreign="id" />
     </foreign-key>
  </table>
</database>  
  1. Generate all classes and admin module:
    ./symfony propel:build-all
    ./symfony propel:generate-admin frontend kontrak

  2. Open BaseFormPropel file, then change parent class with deFormPropel

    abstract class BaseFormPropel extends deFormPropel
    {
      public function setup()
      {
      }
    }
    
  3. Open KontrakForm.class.php file, then edit configure() function

    public function configure()
    {
        unset($this['perusahaan_id']);
        $this->embedPickOrCreateForm('Perusahaan'); 
    }
    

    Below is the screenshot when viewed in your favorite browser

Using Autocompleter for large data

For large data, html dropdown is not the right choice for displaying data. As an alternative, we can use autocompleter. In this article, I will use jquery autocompleter widget from sfFormExtraPlugin
Here are the step by step:

  1. Open KontrakForm.class.php file, then edit the function configure () so it looks like the following:

    public function configure()
    {
       unset($this['perusahaan_id']);
       $this->embedPickOrCreateForm(
         'Perusahaan', 
         array(
           'renderer_class'=>'sfWidgetFormPropelJQueryAutocompleter', 
           'renderer_options' => array('model' => 'Perusahaan', 'url'=> $this->getOption('url'),
          )    		
       ));
    }
    
  2. Open the file actions.class.php and add the following functions

    //kontrakActions
    public function executeNew(sfWebRequest $request)
    {
        $this->form = $this->configuration->getForm(null, array('url' => $this->getController()->genUrl('kontrak/ajax')));
        $this->kontrak = $this->form->getObject();
    }
      
    public function executeEdit(sfWebRequest $request)
    {
        $this->kontrak = $this->getRoute()->getObject();
        $this->form = $this->configuration->getForm($this->kontrak, array('url' => $this->getController()->genUrl('kontrak/ajax')));
    }
      
    public function executeAjax($request)
    {
       $this->getResponse()->setContentType('application/json');
       $perusahaans = PerusahaanPeer::retrieveForSelect($request->getParameter('q'), $request->getParameter('limit'));
       return $this->renderText(json_encode($perusahaans));
    }
    
  3. Copy files _form.php from the cache (cache/frontend/[env]/modules/autoKontrak/templates) into the kontrak template module, and load jquery (if you already load jquery, you can skip this step)

    <?php use_helper('jQuery') ?>
    <?php use_stylesheets_for_form($form) ?>
    <?php use_javascripts_for_form($form) ?>
    
    <div class="sf_admin_form">
      <?php echo form_tag_for($form, '@kontrak') ?>
        <?php echo $form->renderHiddenFields(false) ?>
    
        <?php if ($form->hasGlobalErrors()): ?>
          <?php echo $form->renderGlobalErrors() ?>
        <?php endif; ?>
    
        <?php foreach ($configuration->getFormFields($form, $form->isNew() ? 'new' : 'edit') as $fieldset => $fields): ?>
          <?php include_partial('kontrak/form_fieldset', array('kontrak' => $kontrak, 'form' => $form, 'fields' => $fields, 'fieldset' => $fieldset)) ?>
        <?php endforeach; ?>
    
        <?php include_partial('kontrak/form_actions', array('kontrak' => $kontrak, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?>
      </form>
    </div>
    
  4. Open the file PerusahaanPeer.php, then add the following function:

    //PerusahaanPeer.php
    static public function retrieveForSelect($q, $limit)
      {
        $criteria = new Criteria();
        $criteria->setIgnoreCase(true);
        $criteria->add(self::NAMA, '%'.$q.'%', Criteria::LIKE);
        $criteria->addAscendingOrderByColumn(self::NAMA);
        $criteria->setLimit($limit);
     
        $perusahaans = array();
        foreach (self::doSelect($criteria) as $perusahaan)
        {
          $perusahaans[$perusahaan->getId()] = (string) $perusahaan;
        }
     
        return $perusahaans;
      }
    
  5. And below is the screenshot:

feel free to modify and improve this code..
Hope it’s useful.. Thanks you

Update
Adding capabalities to embed form inside ‘create form’

   public function updateObjectEmbeddedForms($values, $forms = null)
   {
      if (null === $forms)
      {
         $forms = $this->embeddedForms;
      }
      foreach ($forms as $form)
      {
    	 if ($form instanceof dePickOrCreateForm)
    	 {
    	    $relationName = $form->getEmbeddedModelClass();
    	    $relationId = $form->getEmbeddedId();
    	    if(!empty($values[$relationName][$relationId]))
    	    {
    	       $setter = 'set'.call_user_func(array(constant(get_class($this->getObject()).'::PEER'), 'translateFieldName'), $relationId, BasePeer::TYPE_FIELDNAME, BasePeer::TYPE_PHPNAME);
	       $this->getObject()->$setter($values[$relationName][$relationId]);
            }
    	    else if(!empty($values[$relationName][dePickOrCreateForm::EMBEDDED_STRING]))
    	    {
               $setter = 'set'.$relationName;
    	       $createForm = $form->getEmbeddedForm(dePickOrCreateForm::EMBEDDED_STRING);
    	       $newValues = $this->removeNullValue($values[$relationName][dePickOrCreateForm::EMBEDDED_STRING]);
      	       $obj = $createForm->updateObject($newValues);
      	       foreach ($createForm->getEmbeddedForms() as $f)
      	       {
      	          $f->getObject()->$setter($obj);
      	       }
      	       $this->getObject()->$setter($obj);  		
    	    }
    	    unset($values[$relationName],$this[$relationName]);
    	 }
      }
       parent::updateObjectEmbeddedForms($values);
    }

    protected function removeNullValue($values)
    {
       $retval = array();
       foreach ($values as $key => $value)
       {
          if (null != $value)
          {
              if (is_array($value))
	      {
	          $retval[$key] = $this->removeNullValue($value);
	      }
	      else
	     {
	         $retval[$key] = $value;
	     }	
	  }
       }
       return $retval;
     }

Advertisements

One response to “‘Pick or create’ embedded form the propel way

  1. Charge November 26, 2011 at 7:50 pm

    I know this an old post; but still:
    Thank you for publishing this!
    It was a real time-saver for me; much appreciated.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: