Archive for the ‘Astuces Symfony’ Category

Quick Doctrine NestedSet reference

I discovered Doctrine’s NestedSet behavior this week. I wasn’t able to find any resource on the Internet which would summarize how to create schema and fixture all together. So let me share with you this small reference article, maybe it will spare you some minutes.

First on your schema.yml

# let's say you got an Item table, with some subitems
Item:
  actAs:
    NestedSet:
      hasManyRoots: true # it means there can be several "main" items. If set to false, there is one main item, and all others subitems are bounded to this first one
      rootColumnName: item_id # Speaks for itself, that's where the relation between items will be stored
  columns:
    name: { type: string } # let's give our item a name
# You can leave it like this, but I added some relations, so i can fetch one item's subitems easily
    item_id: { type: integer, default: null }
  relations:
    Item:
      local: item_id
      foreign: id
      foreignAlias: Subitems

This schema will build your model with 3 extra fields : rgt, lft and level. Level is 0 if the record is root. Rgt and Lft are filled according to this behavior : Nested Set Model.

Now we got this, let’s create some fixtures :

Item:
  item_1:
    name: First item
    children: [] # It means for now, it has no subitems. You have to specify it, otherwise you're doctrine:build-all commands will fail
  item_2:
    name: Second item
    children:
      item_2a:
        name: Second item, First subitem
      item_2b:
        name: Second item, Second subitem
        children:
          ...

So now, instead of calling for example a foreach($item->getSubitems() as $subitem) which will occur in an extra query to the database if no Subitems exist (even if you have leftJoined your relations), you can test : if($item->getRgt() – $item->getLft() > 1). If the difference is greater than one, the current $item got subitems children.

Hope it will help.

Pre-fill a form in the backend with symfony

Pre-fill a form in Symfony

Hi all,

I hope I didn’t miss anything evident since my issue really sounds easy at first sight… What is my problem ? I just want to re-use in the backend objects that already exist to pre-fill some forms.

Let’s illustrate the situation with a concrete exemple before jumping into the code. A shop sells more than 100 natural and organic personal beauty products based on lets say code (butter, face-cream…). Each reference is a complicated and somehow granny-accross-centuries-receipe secret that is made of more than 30 ingredients… plus a pink-tainting product to reach girls. This shop is so successful that the owner has opened a new one just one block away. It sells the exact same range of products, he just replaced the pink-tainting color by a blue-tainting because he now wants to target a male audience. In his backend, he will have to « create » the new shop and then for each one of the 100 product to populate the ingredients list with the 30 identical ingredients, only replacing the pink with the blue tainting-product. How tedious !!! What he’d like to do is create his new shop duplicating the previous products and kind of « crediting » (mix of creating and editing) the object, replacing only one field (the tainting product).

It’s just weird that this basic functionnality took me so many time to build. Did I miss a point in my symfony learning ?? So here is how I solved the problem…

  1. Create a component to display a productToDuplicate field above the form
  2. Add a hint of AJAX to autocomplete your search field
  3. Add a PopulateProductForm action to create a pre-filled form
  4. Write a PopulateProductFormSuccess to display pre-filled form
  5. Add a CreatePopulated action to save pre-filled form as a new Object

Create a component to display a productToDuplicate field above the classic shop form

Why creating a component and not just a simple partial that you would call in the generator.yml file of your module ? Well, we need an action to create the search widget and then a template to display it above the form. Seems to match components functions…

Here is the action to write :

//apps/backend/modules/myModule/actions/components.class.php
public function executeProductToDuplicateFinder()
  {
  	$this->productForm = new sfForm();
  	$this->productForm->setWidgets(array(
	  	'duplicated_product' => new sfWidgetFormJQueryAutocompleter(
	      array(
	        'url'    => '/backend.php/product/findProduct',
	        'config' => '{ scrollHeight: 250 ,
	                       autoFill: true ,
	                       cacheLength: 1 ,
	                       delay: 50,
	                       matchSubset: false}',
	        'label' => 'Duplicate a product :'
	      ),
	      array(
	      'size'=> '70'
	      )
      )));
  }

If you want more information on the sfWidgetFormJQueryAutocompleter, just go and see this previous article about autocomplete widget

Here is the component’s template associated. Notice that you passed the productForm to the template so you can « echo » it with all the methods symfony offers you.

//apps/backend/modules/myModule/template/_productToDuplicateFinder
<?php use_helper('Form');?>
<!-- Display an autocomplete input text to find a product and duplicate it -->
<div class="sf_admin_form">
  <!-- helper indicating to call populateProductForm action, dont forget to close it... -->
  <?php echo $productForm->renderFormTag('populateProductForm')?>
	  <table>
	    <tr>
	      <th>
	      <?php echo $productForm['duplicated_product']->renderLabel();?>
	      </th>
	      <td>
	        <?php echo $productForm['duplicated_product']->renderError();?>
	        <?php echo $productForm['duplicated_product']->render();?>
	        <?php echo submit_image_tag('myFolder/tick.png'); ?>
	      </td>
	    </tr>
	  </table>
  </form>
</div>

Make sure you closed the tag that you implicitely opened with the renderFormTag method…

Add a hint of AJAX to autocomplete your search field

I will go quickly through this part since it is not the heart of the article. In a few words, you have to write an action that will first

  • Write a method that will match the objects of ProductTable with the ‘q’ string parameter
  • Put all the results you got into a result table (use a foreach loop)
  • Return your Json encoded result
    return $this->renderText(json_encode($results));

Add a PopulateProductForm action to create a pre-filled form

This is the action we called when clicking on our ‘submit’ button in the product finder. So what do we want to do ? We want to display a product form, I should even say a new product form, but pre-filled with the properties of the selected existing product. That’s easy :

//apps/backend/myModule/actions/actions.class.php
public function executePopulateProductForm(sfWebRequest $request)
  {
  	//throw exception if not POST request
  	$this->forward404Unless($request->isMethod('post'), 'not a "POST" request');

  	//collect id of duplicated_product
  	$duplicated_product_id = $request->getParameter('duplicated_product');
  	//Check if duplicated_product ID is valid
  	$this->forward404Unless($duplicated_product = Doctrine::getTable('Product')->find($duplicated_product_id) , sprintf('unknow id for a product (%s)', $duplicated_product_id));

    //set copied_product name and shop to null
  	$duplicated_product->setName(null);
  	$duplicated_product->setShopId(null);

    //create a form based on the copied_product
    $this->form = new BackendProductForm($duplicated_product);
    //in the backend the object must be passed to the template
    $this->product = $this->form->getObject();

   }

The only thing that remains is to display this form built on the existing duplicated_product. The problem will be that our form won’t be considered as a new one by symfony (and you can’t set his isNew() property to true since this method is protected…). But for now, let’s display it.

Write a PopulateProductFormSuccess to display pre-filled form

What I did was to go in the cache and then to copy the code from an editSuccess method just adapting it…

//apps/backend/modules/myModule/template/populateProductFormSuccess
//apps/backend/modules/myModule/template/populateProductFormSuccess
<?php use_helper('I18N', 'Date') ?>
<?php include_partial('product/assets') ?>

<div id="sf_admin_container">
  <h1><?php echo __('New Product', array(), 'messages') ?></h1>

  <?php include_partial('product/flashes') ?>

  <div id="sf_admin_header">
    <?php include_partial('product/form_header', array('product' => $product, 'form' => $form, 'configuration' => $configuration)) ?>
  </div>

  <div id="sf_admin_content">
    <?php //include_partial('product/form', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?>

    <!-- BEGINNING OF THE NEW PART -->
	  <div class="sf_admin_form">

	    <?php //echo form_tag_for($form, '@form') ?>
	    <!-- Replaced by the hand written form_tag to force a redirect to a create action -->
	    <form action="<?php echo url_for('product/createPopulated') ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>

	    <?php echo $form->renderHiddenFields() ?>

	    <?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('product/form_fieldset', array('product' => $product, 'form' => $form, 'fields' => $fields, 'fieldset' => $fieldset)) ?>
	    <?php endforeach; ?>

	    <?php include_partial('product/form_actions', array('product' => $product, 'form' => $form, 'configuration' => $configuration, 'helper' => $helper)) ?>
	  </form>
	  </div>
    <!-- END OF THE NEW PART -->

  </div>

  <div id="sf_admin_footer">
    <?php include_partial('product/form_footer', array('product' => $product, 'form' => $form, 'configuration' => $configuration)) ?>
  </div>
</div>

Now we have our pre-filled product with all the ingredients already filled. You just have to change the tainting color from pink to blue and here it is… The last step is to save our new product in the data base.… That’s the aim of the following line of code that I changed from the cache :

<form action="<?php echo url_for('product/createPopulated') ?>" method="post" <?php $form->isMultipart() and print 'enctype="multipart/form-data" ' ?>>

I force a createPopulated action when submitting the form…

Add a CreatePopulated action to save pre-filled form as a new Object

Indeed, when symfony saves an object, it first check whereas the form isNew or not. If isNew is true, a new object will be created in the database. In the edit case, it will just do an update.

So what you have to do is create a new Form in your action, but in the same time use the request object to get the tainted values. If you only do this, you will be very disappointed : symfony will update, not create, an object. A way to avoid this is to set the tainted values ‘id’ to null !! See what it looks like :

//apps/backend/myModule/actions/actions.class.php
public function executeCreatePopulated(sfWebRequest $request)
  {
  	//create a new Form so that isNew() will be true and save will
  	//trigger an insert in the database
  	$this->form = $this->configuration->getForm();
    $this->product = $this->form->getObject();

    //We have to set the id to null otherwise it will try to update the object, not insert
    $tainted_values = $request->getParameter($this->form->getName()) ;
    $tainted_values['id'] = '';

    //we modify the $request so that we can call the processForm method without overriding it
    $request->setParameter($this->form->getName(),$tainted_values);

    $this->processForm($request, $this->form);

    $this->setTemplate('edit');
  }

And here it is ! Ouf… If anybody has a simpler solution to offer, he will be welcomed. Hope this was useful.

Symfony forms saving process

I always had troubles understanding what really happens in the symfony form process.

Even though most of the time everything works great following tutorials, forums, etc.. I needed to understand the magic behind. I wasn’t just quite curious. I found myself one day lost in my own code, I had overriden severals methods but I didn’t know why this one was calling that one, why this one was never called, why this one didn’t work… It worked, that way, but not this way … well, shame on me, I didn’t truly understand what I was doing.

My sticking points mainly came from embedded forms, which are a truly helpful feature but not so easy to implements sometimes. For instance, I’ll try to explain why many to many relationships are not saved in embedded forms (see this ticket) and I’ll share with you what I found to solve this issue. I’ll also try to highlight why files uploads in embedded forms is bit tricky to implements and how to deeply bind forms.

But back to the Forms in general, let’s take an admin generated module and see what’s going on when you add or edit an object. It took me two days of reading and overriding symfony’s methods, but I finally came up with this little diagram. This diagram may not follow regular standards or naming conventions, simply because I don’t know what they are, I just put it my way and I hope it’s clear enough for a basic understanding. Keep in mind that I may be wrong somewhere, if so, please tell me. Doing this helped me a lot, and having a look at this diagram, when overriding forms methods still helps me.

By the way, I’m currently using symfony 1.2.6 with Doctrine 1.2.

symfony forms saving process

Now let’s talk about many to many relationships in embedded forms.

If you ever tried to embed a form that must manage many to many relationship(s), you propably noticed that the relationships are NOT saved.
Here’s why.
An object is saved to the database simply by calling $myObject->save() Doctrine method. This saves the object properties, one to one, or one to many relationships, but not many to many because the relationships are stored in a different table.
That’s why when you do « php symfony generate-admin« , Symfony overrides the doSave() method and add save***List() methods to it, to actually save the many to many relationships.
But if you look at the saving process, the doSave() methods of embedded forms are never called.
And that’s a bit more complex than what it looks like. We could just override the saveEmbeddedForms method to call doSave() on embedded forms like on the ticket but it didn’t work for me.

I’m not 100% sure, maybe I missed something, but here’s what I understood.
Calling doSave() on an embedded form implies two things:

– that the form must be bound before. This is because auto-generated save***List() methods are testing ‘if($form->isValid())’ which is nearly the same as ‘if($form->isBound)’. « So let’s Just bind the embeddedForms before calling doSave » I thought. (This is exactly what is done in the ticket by the way)

-that all the process after the doSave() method will be executed, which seems to be useless to me because updateObjectEmbeddedForms() already call updateObject() on the child objects and saveEmbeddedForms() already calls itself  on child objects too.

I tried to embed an sfGuardForm with this system. Saving a new object went fine, but for some reasons, editing didn’t work. After some a lot of debug, I found that it triggered a hidden validation error « An object with the same ‘username’ already exists ». Honestly I don’t really remember on which level it happened, but I think this is linked to the fact that some methods are executed twice and shouldn’t be. Even calling the bind() method on embedded forms seems to me to be a redundancy. When you submit a form, doClean() deeply clean the values, meaning even embedded forms values are passed to the validators, and returned cleaned. So if you call bind() on an embedded form it will call the clean method again.

And finally for those who don’t care why and just want to know how, here’s my solution.

First of all, let’s deeply bind embedded forms, i.e binding embedded forms, embedded forms in embedded forms etc … Symfony does NOT bind embedded forms by default and I wonder why, is it a mistake? By the way, this is why files uploads in embedded forms won’t work. The processUploadFile() method is using getValues() which works only on a bound form .Anyway, we can’t just call the bind() method on embedded forms as we saw before, so let’s manually bind these forms.

I created a method which I put in BaseFormDoctrine.

//lib/form/doctrine/BaseFormDoctrine.class.php
 public function bindEmbeddedForms($embedded_forms, $values)
 {
    if($this->isValid())
    {
      foreach ($embedded_forms as $name => $form)
      {
        $form->isBound = true;
	$form->values = $values[$name];

	if ($form->embeddedForms)
	{
	  $this->bindEmbeddedForms($form->embeddedForms, $values[$name]);
	}
      }
    }
  }

And I just call it at the end of the bind method.

  public function bind(array $taintedValues = null, array $taintedFiles = null) {

      parent::bind($taintedValues, $taintedFiles);
      $this->bindEmbeddedForms($this->embeddedForms, $this->getValues());

  }

Then override the saveEmbeddedForms() method to take care of the many to many relationships.

 //lib/form/doctrine/BaseFormDoctrine.class.php
 public function saveEmbeddedForms($con = null, $forms = null)
  {
    if (is_null($con))
    {
      $con = $this->getConnection();
    }

    if (is_null($forms))
    {
      $forms = $this->embeddedForms;
    }

    foreach ($forms as $key => $form)
    {
      if ($form instanceof sfFormDoctrine)
      {
      	/*
      	 * --------------------
      	 * only modification
      	 */
      	if(method_exists(new $form(), 'doSaveManyToMany'))
      	{
          $form->doSaveManyToMany($con);
      	}
      	else
      	{
          $form->getObject()->save($con);
      	}
      	/*
      	 * --------------------
      	 */
        $form->saveEmbeddedForms($con);
      }
      else
      {
        $this->saveEmbeddedForms($con, $form->getEmbeddedForms());
      }
    }
  }

This method is just a doSave() method with the calls to updateObject() and saveEmbeddedForms() removed.

//lib/form/doctrine/BaseFormDoctrine.class.php
  public function doSaveManyToMany($con = null)
  {
    if (is_null($con))
    {
      $con = $this->getConnection();
    }

    $this->object->save($con);

   /*
    * Save the many-2-many relationship
    */
    $this->save***List($con);
  }

I’m sure there’s a better way to do it, but you got the idea. Hope it can help.

Improving Autocompletion

Updated on 2nd Oct. 2009 : some code was erased in my_javascript.js snippet because of < and >

Ajax autocompletion is really easy with sfWidgetFormJQueryAutocompleter. But the input hidden (which is the value you really matter about when processing the form) that is rendered previous to the input text box (which is only there for humans 🙂 ) is not filled-in if user fill it in very quickly. It can be tedious, because when submitting the form, the user will notice (or maybe even not) that the data he thought he filled-in has not been saved. For ergonomics purpose, i’ve wrote down a little and easy piece of code that will check data in the input text and fill in the input hidden. Plus, there is a tick or delete image that will show up, warning the user as a pre-validation of its data.

We are going to create an Ajax action that retrieve a string value from an input text box, check in our model if this string match any record, and if so, fetch and send back the ID to fill it in the input hidden of the autocompleter. I’m using Symfony 1.2.6 with Doctrine 1.2, and sfFormExtraPlugin is installed. We will assume the sfWidgetFormJQueryAutocompleter setup is already done.

  • 1. The layout
  • 2. The javascript (using jQuery)
  • 3. The routing.yml
  • 4. The module’s action.class.php
  • 5. The model’s table class

This code is simplified from one of our module : originally, we got an input text, and when you fill it in, a second one appears after the first one. And then a third one when you fill in the second on, and so on. (see this previous article (in french))
First, let’s see the layout in the middle of our form :

_myLayout.php

<div>
<label for="my_detail[input_auto]">Item</label>
render('my_module[my_detail][input_auto]', $data->getId(), array('value' => $data->getName()))?>
<img src="edit.png" alt="No data in the input" title="No data in the input" />
</div>

We also got a Javascript code, we’re using jQuery, but i’m sure you’ll be able to find out how to do the trick with some other library. The objective is to run the code when the input text is blured:

my_javascript.js

jQuery(document).ready(function() {
  jQuery('[id^=autocomplete_my_detail]').blur(function(){

    [...]

    if(this.value.length > 0)
    {
      // ... we call an ajax method to retrieve an ID matching the current input value
      findIdForString(jQuery(object).val());
    }
  }

  var findIdForString = function(string) {
    jQuery.ajax({
      type: "GET",
      url: 'findItemIdForString',
      data: { string: string },
      dataType: 'html',
      success: function(result){
        // we check if result is a number (otherwise it's null: no ID has been found for the given string)
	var pattern = new RegExp('[0-9]+');

	if(result.match(pattern))
	{
	  // an item ID has been identified for the given string
	  // modify the hidden value with the retrieved ID
	  var hidden_html = '<input id="transit_line_stations_list_input_auto_'+counter+'" class="auto_station" type="hidden" name="transit_line[stations_list][input_auto_'+counter+']" value="'+result+'" />'
          jQuery('#my_module_my_detail_input_auto).replaceWith(hidden_html);

          // modify the icon image
          setImage('tick');
	}
	else
	{
	  // no item ID has been identified for the given string
          // Here, the input hidden is modified, because,
          // as I have wrote above, we are using this code
          // in a multi-inputs page. So if the user fill in
          // correctly the input, blur, comes back to the input
          // and fill in badly, we have to unset hidden value
          var hidden_html = '<input id="transit_line_stations_list_input_auto_'+counter+'" class="auto_station" type="hidden" name="transit_line[stations_list][input_auto_'+counter+']" />';
          jQuery('#my_module_my_detail_input_auto).replaceWith(hidden_html);

          // modify the icon image
          setImage('delete');
        }

	function setImage(icon) {
	  if(icon == 'delete')
	    var title = 'ID unknown';
	  else if(icon == 'tick')
	    var title = 'ID identified';

          var img_html = '<img src="'+icon+'.png" alt="'+title+'" title="'+title+'" />';
	  jQuery('#image_my_module_my_detail).replaceWith(img_html);
	};
      }
    });
  };
}

Then of course comes the action, named findItemIdForString.

action.class.php

public function executeFindItemIdForString(sfWebRequest $request)
{
  $string = $request->getParameter('string');

  $item_id = Doctrine::getTable('MyModel')->findOneItemIdForString($string);

  return $this->renderText($item_id);
}

Don’t forget to update your routing.yml file with something like :

routing.yml
ajax_findItemIdForString:
  url:    /*/findItemIdForString
  param:  { module: my_module, action: findItemIdForString }

And the findOneItemIdForString method in MyModelTable.class.php :

MyModelTable.class.php

public function findOneItemIdForString($string)
{
  $q = Doctrine_Query::create()
    ->select('m.id')
    ->from('MyModel m')
    ->where('m.name LIKE ?', $string)
    ->fetchOne();

  if($q instanceOf Doctrine_Collection)
  {
    return $q->getId();
  }
  else
  {
    return $q['id'];
  }
}

Let’s finish with a screenshot of the result. This example shows how to add subway stations, with little green tick icons when findOneItemIdForString($string) has returned something, red cross when nothing has been found, and the edit icon when the ajax action has not yet been run.
Screenshot

sfValidatorI18nFloat a homemade validator for internationalized input numbers

Getting a number from your model and output it in a i18n/l10n way is really easy thanks to Symfony helpers. But there is no native way to easily input a number and validate its pattern before saving in the model. As one might know, float numbers don’t always have the same format from a culture to an other. One thousand five hundred dot fifty one is written 1,500.51 in USA, but it’s 1 500,51 in France. And a PHP clean float use nor comma nor space as a thousand separator, and a dot as a decimal separator.
So, inspired by this post on the Symfony forum, I have written a sfValidatorI18nFloat validator to input such things. Hope it can help some of you. Save it in your lib directory.

<?php
/**
 * Validate a number format input depending on its culture, ie:
 * fr_FR: 19,50 ==> 19.50
 * @author Simon Hostelet
 * @version 2009-09-16
 */
class sfValidatorI18nFloat extends sfValidatorBase
{
  protected function configure($options = array(), $messages = array())
  {
    $this->addMessage('max', '"%value%" must be less than %max%.');
    $this->addMessage('min', '"%value%" must be greater than %min%.');

    $this->setMessage('invalid', '"%value%" is not a valid float.');

    $this->addOption('min');
    $this->addOption('max');
  }

  protected function doClean($value)
  {
    $clean = self::I18nNumberToPhpNumber($value, sfContext::getInstance()->getUser()->getCulture());

    if(!is_numeric($clean))
    {
      throw new sfValidatorError($this, 'invalid', array('value' => $value));
    }

    if ($this->hasOption('max') && $clean > $this->getOption('max'))
    {
      throw new sfValidatorError($this, 'max', array('value' => $value, 'max' => $this->getOption('max')));
    }

    if ($this->hasOption('min') && $clean getOption('min'))
    {
      throw new sfValidatorError($this, 'min', array('value' => $value, 'min' => $this->getOption('min')));
    }

    return $clean;
  }

  /**
   * Check what are the decimal and thousand separators for the current culture.
   * Replace these separators with PHP compatible ones.
   * @param $number float
   * @param $culture  string
   * @return float PHP-cleaned
   */
  static public function I18nNumberToPhpNumber($number, $culture = 'en')
  {
    $numberFormatInfo = sfNumberFormatInfo::getInstance($culture);
    $number = str_replace($numberFormatInfo->getDecimalSeparator(), '.', $number);
    $number = str_replace($numberFormatInfo->getGroupSeparator(), '', $number);

    return $number;
  }
}

?>

Access the form values in prevalidator

I’m using a preValidator to validate a price in a form. Say the user enters « 22,35 », I want to format this string to « 22.35 ». And I need to do this before the actual validator (sfValidatorNumber) of the field, so that the string « 22,35 » will be formatted and pass the sfValidatorNumber.

How to do this? Set a preValidator:

public function configure()
{
        $this->validatorSchema->setPreValidator(
               new sfValidatorCallback(array('callback' => array($this, 'validatePrice')))
         );
}

public function validatePrice($validator, $values)
{
        ...

    	$values['price'] = str_replace(',', '.', $values['price']);
    	return $values;
}

Easy.

But this does not work ! 😀 Contrary to postValidators, $values are not modified in preValidators.

I found a little patch to fix this (after one hour trying to fix it on my own by overriding proccessForm() method etc…), on this ticket of symfony trac.

There’s no need to complicate, our time is short (Jason Mraz – I’m Yours ♫):

//lib/vendor/symfony/lib/validator.sfValidatorSchema.class.php
-LINES-
                    // pre validator
            	    try
           	    {
                         // Replace this line
123	 	          $this->preClean($values);
                         // with this one
 		          $values = $this->preClean($values);
             	    }
        	    catch (sfValidatorErrorSchema $e)
        	    {
…	…
              	  {
        	   if (is_null($validator = $this->getPreValidator()))
	           {
                         //Replace this line
221  	                  return;
                         // with this one
 		          return $values;
	            }
	             //Replace this line
224	 	     $validator->clean($values);
                    // with this one
 		     return $validator->clean($values);
	  }

Now it works !

I think that this is an important fix to have.

I use symfony 1.2.6, i didn’t try 1.2.8 but I didn’t see the patch in the changelog.

Pense bête d’accès aux différents paramètres

Manque d’habitude ? Manque de doc ? Je ne sais pas vous, mais parfois j’ai du mal à savoir comment appeler tel ou tel paramètre depuis tel ou tel « endroit ». Un petit pense-bête pour éviter d’avoir à rechercher encore et toujours :

Depuis une classe du modèle, exemple : sfContext::getInstance()->getMethodName() edit: non, il ne faut pas faire ca en fait, ça empêche de builder et load les fixtures… Donc j’avais dans Model.class.php un override de save(). Plusieurs modules sont basés sur ce model, donc je voulais vérifier selon le contexte quelle opération entreprendre. Il semble que ce ne soit pas la bonne solution. Je vais vérifier ça.

A compléter…

Ajouter un champ vide via un sfWidget pour un Filter

J’ai deux tables en relation many-to-many : myClassA et myClassB. Du côté du backend, je voudrais que le filtre qui apparait à côté de la liste de myClassA affiche deux champs : un input text pour recherche dans myClassA.name et un select pour discriminer selon 1 valeur de myClassB. Par défaut, le FormFilter de Symfony présente un input select multiple, mais ce n’est pas forcément pertinent puisque si on selectionne 4 valeurs, le filtre s’effectuera comme « je veux que tu m’affiches les valeurs de myClassA pour lesquels la liaison many-to-many a myClassB corresponde a valeur1 OU valeur2 OU valeur3 OU valeur4 ».

Dans l’absolu, j’aurais préféré du ET. Je n’ai d’ailleurs pas essayé ca, si quelque a déjà tenté l’expérience… En attendant, j’ai voulu faire un select plus classique où l’ont choisi une seule valeur. Pour éviter de recopier pas mal de code, je vous conseille si ce n’est déjà fait de lire mon EXCELLENT article précédent sur les sfWidgetFormChoice et la valeur des options !

Si je mets bêtement un sfWidgetFormChoice, il y aura obligatoirement une valeur de myClassB séléctionnée. Or je veux pouvoir ajouter un champ « vide ». J’avais d’abord tenté d’ajouter un ‘with_empty’ => true dans les paramètres du widget, mais ca ne semblait pas exister pour ce type d’objet. Finalement, la méthode suivante fonctionne :

lib/filter/doctrine/BackendMyClassAFormFilter.class.php

class BackendMyClassAFormFilter extends MyClassAFormFilter
{
  ...
  // on récupère les données de la class myClassB comme vu
  // dans l'article dont j'ai donné l'adresse ci-dessus :
  $myClassBData = Doctrine::getTable('myClassB')->getFormChoiceMyClassB();

  // on ajoute a un array une valeur vide
  $data_list[0] = '';

  // POUR INFO
  // Si il s'agit d'une recherche non pas dans une relation many-to-many, mais one-to-many,
  // avec donc une foreign key dans la table myClassA, égale a myClassB.id, alors il faut remplacer
  // la ligne précédente par :
  // $data_list[null] = '';

  // afin de garder les "identifiants => valeur" de myClassB,
  // on parse le tableau de résultat Doctrine, et on l'ajoute
  // a l'array créé juste avant
  foreach($myClassBData as $key => $value)
  {
    $data_list[$key] = $value;
  }
  $this->widgetSchema['myClassB_list'] = new sfWidgetFormChoice(array(
    // les choices sont issus de l'array data_list
    'choices' => $data_list,
    'label' => 'Class B',
  ));
  ...
}

C’est tout bête, mais il m’a fallu 3 ou 4 essais différents avant de trouver, donc si ça peut faire gagner du temps à certains…

L’autocompletion sur un input text du backend

L’autocompletion c’est cool et pratique !

Selon le fameux principe « Do not reinvent the wheel » souvent cité dans les tutos symfony, je vais utiliser le plugin sfFormExtraPlugin qui possède déjà un widget qui correspond à notre besoin.

Donc si vous ne l’avez toujours pas installé, un petit :

$ php symfony plugin:install sfFormExtraPlugin

$ php symfony cc

C’est le widget sfWidgetFormJQueryAutocompleter que je vais utiliser pour intégrer l’autocompletion au formulaire New/Edit d’un module du backend. Comme son nom l’indique ce widget utilise jQuery, pensez à l’inclure.

Je crée une classe BackendModuleForm.class.php.

//lib/form/doctrine/BackendModuleForm.class.php
class BackendModuleForm extends ModuleForm
{
    public function configure()
    {
       $this->addWidgets();
    }

    ...
    protected function addWidgets()
    {
       ...

       $this->widgetSchema['input_auto'] = new sfWidgetFormJQueryAutocompleter(array(
       'url'    => "myAction",
       'config' => '{ extraParams: { second_parametre: function() { return jQuery("#id_html").val(); } },
                      scrollHeight: 250 ,
                      autoFill: true }'
       ));
    }

}

Le paramètre ‘url’ correspond à la page ou le widget va aller chercher ses infos. Le widget récupère automatiquement la valeur du champ input et l’envoi dans une variable nommée ‘q’.

On configure directement le plugin en javascript avec le paramètre ‘config’. Pour passer d’autres valeurs à la requête, on utilise ‘ extraParams: ‘. Beaucoup d’options supplémentaires sont disponible, consulter la doc du plugin autocomplete de jQuery pour plus d’options.

Je prend en compte la classe dans generator.yml:

//apps/backend/modules/module/config/generator.yml
...

form:
 class: BackendModuleForm
 display:
   Informations module: [info1, info2, info3, info4, input_auto]

...

Je crée l’action correspondante:

//apps/backend/modules/module/actions/actions.class.php

<?php

...

class moduleActions extends autoModuleActions
{
     public function executeMyAction(sfWebRequest $request)
     {
         $this->getResponse()->setContentType('application/json');

         $string = $request->getParameter('q');
         $second_parametre = $request->getParameter('second_parametre');

         $req = Doctrine::getTable('mytable')->getDataWhere($string, $second_parametre);

         $results = array();
         foreach ( $req as $result )
         $results[$result->getId()] = $result->getName();

         return $this->renderText(json_encode($results));
     }
...

}

Le widget utilise le un parseur jSON pour la valeur de retour, il faut donc penser à encoder le résultat avant de le renvoyer.

Du coté du modèle, je fais une requête sur deux tables en relation many-to-many.

//lib/model/doctrine/mytableTable.class.php

class mytableTable extends Doctrine_Table
{
         ...

	  public function getDataWhere($string, $second_parametre)
	  {
	    $q = Doctrine_Query::create()
	      ->from('Mytable m')
	      ->leftJoin('m.Othertable o')
	      ->where('m.name LIKE ?', "%$string%")
	      ->andWhere('o.other_id = ?', $second_parametre)
	      ->orderBy('m.name ASC')
	      ->execute()
	      ->getData();

	    return $q;
	  }
}

Je définie ensuite une route pour l’action:

//apps/backend/config/routing.yml
ajax_myaction:
  url:   /myAction
  param:  { module: module, action: myAction }

...

Je vais overrider le template _form auto-généré par le générator.yml dans le cache en le copiant dans le répertoire /templates/ du module.

J’ai choisi ce template de manière arbitraire, il servira juste à inclure les fichiers javascript/css nécessaire au widget. Le helper use_javascript() évite les inclusions multiples du même fichier et inclue proprement le javascript dans le <head> de la page.

//apps/backend/modules/module/templates/_form.php
...

<?php use_javascript('jquery-1.3.2.min.js') ?>
<?php use_javascript('/sfFormExtraPlugin/js/jquery.autocompleter.js') ?>
<?php use_stylesheet('/sfFormExtraPlugin/css/jquery.autocompleter.css') ?>

...
Autre méthode:

Pour l’autocomplétion, et l’ajax en général on aurait pu utilisé les helper javascript directement dans le template. Le principe consiste à définir un ‘observe_field()’ qui va surveiller un champ et déclencher une action à chaque changement.

Exemple:

<?php use_helper('Javascript'); ?>
<?php echo observe_field('mon_input_text', array(
                                  'update'   => 'divAupdate',
                                  'url'      => 'module/monaction',
                                  'method'   => 'get',
                                  'loading'  => "'Loading...'",
                                  'with'     => "'mon_parametre=' + $('mon_input_text').value",
                                  'frequency'=> '1',
                                  'script'   => true)) ?>
<?php echo use_helper('Javascript') ?>

Mais dans un souci d’efficacité et de continuité du modèle MVC, j’ai choisi la méthode du widget ^_^ .

Gérer plusieurs bases de données avec Symfony et Doctrine

Pour certains projets, on est obligé d’utiliser plusieurs bases de données différentes. Voilà comme faire avec Doctrine :

Dans config/databases.yml, ajoutez les différentes BDD de cette manière :

all:
  dbintranet:
    class: sfDoctrineDatabase
    param:
      dsn: 'pgsql:host=234.11.18.70;port=6543;dbname=mydatabase'
      username: postgres
      password: jDklEjjD
      encoding: utf-8

  dbextranet:
    class: sfDoctrineDatabase
    param:
      dsn: 'pgsql:host=23.63.15.16;port=5432;dbname=mydatabase'
      username: postgres
      password: SkEmDpZ
      encoding: utf-8

Ensuite, vous pouvez exécuter une commande SQL de cette manière :

$q =  Doctrine_Query::getConnection(‘dbextranet‘)
->getDbh()
->query(’SELECT * FROM projet_users ′)
->fetchAll();

Ou alors, si comme moi vous voulez tout faire en manuel, mais juste utiliser Doctrine pour la couche connexion SQL, vous pouvez faire comme ça :

$q = Doctrine_Manager::getInstance()
->getConnection(‘dbextranet’)
->getDbh()
->query(‘SELECT 1’)
->fetchAll();

foreach ( $q as $u ) echo « test ::  » . $u;

(Concrètement, Doctrine_Connection::getDbh() retourne l’instance PDO de PHP.)