Creating new relations by using jQuery Autocompleter widget in Symfony

Posted on Categories:PHP, MySQL, Symfony

This post describes how to create new relation by usin jQuery Autocompleter widget in Symfony project.
Recently, I’ve had a situation where I had to create a form for entering a product details, with a dropdown or autocomplete input for selecting product’s brand. If a user entered a non-existing brand, it had to be automatically created and assigned to product that the user was editing.

Creating an autocomplete input is done by inserting this in a form class:

// lib/form/doctrine/your_form.class.php
$this->widgetSchema['brand_id'] = new sfWidgetFormDoctrineJQueryAutocompleter(array(
    'url' => '/autocomplete_brand',
    'model' => 'Brand',
    'value_callback' => 'findOneById',
    'config' => "{max: 20}"
    ));
 
$this->setValidator('brand_id', new sfValidatorPass());

It is important to add a sfValidatorPass validator for brand_id field, so that Symfony won’t return an error when entering a name of non-existant brand in database.

For URL /autocomplete_brand create an action that handles jquery queries:

// apps/frontend/modules/autocomplete/actions.class.php
function executeAutocompletebrand(sfWebRequest $request)
{
      $limit = $request->getParameter('limit', 20);
      $result = BrandTable::findBrandByName($request['q'], $limit);
      $output = array();
      foreach($result as $r)
          $output[$r['id']] = $r['name'];
      return $this->renderText(json_encode($output));
}

After searching for items that match the entered input, check if there are results to display. If there are none, insert a result that will tell your application to create a new Brand relation:

// lib/model/doctrine/BrandTable.class.php
public static function findBrandByName($name, $limit = 20)
{
  $results = self::getInstance()->createQuery('b')
          ->where('b.name LIKE \'%'.$name.'%\'')
          ->limit($limit)
          ->execute(array(),Doctrine_Core::HYDRATE_ARRAY);
 
  if(count($results) == 0)
    return array(array('id'=>'new--'.$name, 'name'=>''.$name)); // If no brands are found, return this as a new Brand.
  else
    return $results;
}

Finally, create a post validator in which you’ll create the new Brand relation if field brand_id has been filled with a prefix new–. Your form class should now look like something like this:

// lib/form/doctrine/your_form.class.php
public function configure()
{
...
$this->widgetSchema['brand_id'] = new sfWidgetFormDoctrineJQueryAutocompleter(array(
    'url' => '/autocomplete_brand',
    'model' => 'Brand',
    'value_callback' => 'findOneById',
    'config' => "{max: 20}"
    ));
 
$this->setValidator('brand_id', new sfValidatorPass());
$this->validatorSchema->setPostValidator(
  new sfValidatorCallback(array('callback' => array($this, 'productPostValidate')))
);
 
...
}
 
/** Helper function for determening if a new brand has been entered **/
private function has_new_brand($brand_id)
{
  return strpos($brand_id, 'ew--') == 1;
}
 
/** Helper function for extracting the new brand name from brand_id field value **/
private function get_new_brand_name($brand_id)
{
  $tmp = explode('new--', $brand_id);
  return(end($tmp));
}
function productPostValidate($validator, $values)
{
  $tainted_vals = $this->getTaintedValues();
  $brand_id = $tainted_vals['brand_id'];
 
  if($this->has_new_brand($tainted_vals['brand_id']))
  {
    // Create new brand
    $brand = new Brand();
    $brand->setName($this->get_new_brand_name($tainted_vals['brand_id']));
    $brand->save();
    // Set brand id
    $values['brand_id'] = $brand->getId();
  }
...
}

This should enable you to automatically create a new brand entry if an entered name does not exist in your database.