Symfony World blog is not maintained anymore. Check new sys.exit() programming blog.

extending doctrine admin module: filtered sum

Scene from "Breakfast at Tiffany's" by Blake Edwards (1961)

Another post for symfony beginners. I'll show how to display a sum (or any other function) of all elements filtered in an admin module. All elements - meaning the ones displayed on the current page (list pagination) and all the rest which is not currently visible. This is going to be really easy.

action

Let's start with calculations. We need to get the sum of all filtered elements and pass it to the View. As this article is not about MVC design pattern, I'll just override executeIndex action and put the calculations code inside (to make it as easy as possible, though calculations should be done in model, not controller).

class xxxActions extends autoXxxActions
{
  public function executeIndex(sfWebRequest $request)
  {
    parent::executeIndex($request);
    $query = $this->buildQuery()->copy();
    $root_alias = $query->getRootAlias();
    $total_data = $query
      ->limit(0)
      ->select("SUM({$root_alias}.cash_total) AS sum")
      ->fetchArray();
    $this->total_count = $total_data[0]['sum'];
  }
}
As you can see, we've got a Xxx model which holds the cash_total: decimal column, representing a sum of money. The $this->buildQuery()->clone() part does all the magic - we have the query with all filters set by the user in the interface. We will only tell doctrine to calculate the sum of all filtered elements for us ($total_count variable will be available in the indexSuccess.php template). The ->limit(0) clears the SQL limit clause, of course.

templates

This part is boring, actually. We need to override two more files: fetch them from cache and put them in the module/template directory. These files are: indexSuccess.php in which the line

<?php include_partial('event/list', array('pager' => $pager, 'sort' => $sort, 'helper' => $helper)) ?>
should be replaced with:
<?php include_partial('event/list', array('pager' => $pager, 'sort' => $sort, 'helper' => $helper, 'total_count' => $total_count)) ?>
and the _list.php partial which should have few lines added:
<tfoot>
  <tr><!-- added code starts here -->
    <th colspan="6">
      w sumie: <?php echo Tools::priceFormat($total_count, true) ?>
    </th>
  </tr><!-- added code ends here -->
  <tr>
    <th colspan="6">
       <?php if ($pager->haveToPaginate()): ?>
         <?php include_partial('event/pagination', array('pager' => $pager)) ?>
       <?php endif; ?>
As you can see, it's trivial, yet useful.

The sum of all filtered elements is visible in the footer of the doctrine admin module list table, as you can see below, but you can put it anywhere you want (as long as it's in the index action templates):

other functions

Of course, you can use other aggregate functions, such as average, minumum or maximum element - just take a look at the MySQL documentation. You may also create your own functions.

symfony basics: form default values

This is some stuff for symfony beginners, who still want to learn symfony 1.4. You may set default form values for all kind of forms (including doctrine forms). Set one default value at a time:

class XxxForm extends BaseXxxForm
{
  public function configure()
  {
    $this->setDefault ('field', 'value');
  }
}
or set a whole array of them:
public function configure()
{
  $this->setDefaults(array(
    'field_1' => 'value_1',
    'field_2' => 'value_2',
    // ...
    'field_x' => 'value_x'
  ));
}

default values for new objects

Sometimes you want to set the default form values just before the object is created, because it'd be easier for the aplication user to fill in some data. For example, the owner/author of a blog post may be set default to the current logged in user - or the date of an event may be set to now - and so on. This can be achieved with the isNew method of the doctrine form class (lib/form/doctrine/XxxForm.class.php):

if ($this->isNew())
{
  $this->setDefault ('created_at'1, date('Y-m-d 00:00:00'));
  $this->setDefault ('created_by'2, sfContext::getInstance()->getUser()3->getId());
}

moreover

You may implement whatever complex conditions you want your doctrine form to follow. Look at some of examples below:

  • current time - php time function
  • language/localization (default country when registering a new user) - use Accept-Language HTTP
  • default settings set for a registered user - fetch individual user settings from database (doctrine query)
  • last used item (category/product/etc.) - a user inserts or updates a large amount of data, when he choses a specific item (category/product) it can be saved in its session ($user->set/getAttribute()) - when another record is processed, last used item is used as default (which, again, lowers time needed for the user to work)
A well designed interface includes lots of form default values, so that users don't have to waste their time on picking up the same values over and over again.

symfony dynamic max_per_page

max_per_page

In this post I'll show a very easy and a really useful thing. It is dynamic max_per_page value of the list pager. Such feature gives you the possibility to change the number of elements displayed in a list just by one click. It can be used both in the frontend and backend (the pager is the same) - I'll use the doctrine generated admin module (and the interface will be placed inside the filters box): interface allowing backend user to change the max_per_page value in the 'filters' box

templates

First, let's make it visible. Add the following entry to application config/app.yml file:

all:
  const:
    max_per_page: [ 10, 25, 50 ]
Now we may refer to app_const_max_per_page config value which holds few standard max_per_page values to be used (this can be used in many different admin modules). Let's say we've got an admin module for our custom MyModel model. Now, override the cached _filters.php template: fetch it from cache/admin/dev/modules/autoMyModel/templates and put it in apps/APP/modules/my_model/templates. Take a look at the following part of the code:
          </td>
        </tr>
      </tfoot>
      <tbody>
        <!-- insert here -->
        <tr>
          <td colspan="2">
and insert few lines of code (replace the comment) to get the following:
          </td>
        </tr>
      </tfoot>
      <tbody>
        <tr>
          <td colspan="2">
            set maximum elements per page:
            <?php foreach (sfConfig::get('app_const_max_per_page') as $value): ?>
              <span class="max_per_page_selector"><?php echo link_to($value, 'my_model/setMaxPerPage?max='.$value) ?></span>
            <?php endforeach; ?>
          </td>
        </tr>
        <tr>
          <td colspan="2">

controller/action

The interface to change max_per_page is ready, so we have to improve the controller now. Let's add an action which stores the number of elements to display per page in user session (symofny has nice set(get)Attributes methods). So here it comes:

  /**
   * Sets my_model list's max per page config value, using user session
   * attribute.
   *
   * @param sfWebRequest $request
   */
  public function executeSetMaxPerPage(sfWebRequest $request)
  {
    $this->getUser()->setAttribute('my_model.max_per_page', $max = $request->getParameter('max'));
    $this->getUser()->setFlash('notice', 'max_per_page has been set to: '.$max);
    $this->redirect('@my_model');
  }

configuration generator

And finally, tell the pager to look for the custom value each time the list is going to be rendered. We need to override the method in configuration generator of the admin module:

class my_modelGeneratorConfiguration extends BaseMy_modelGeneratorConfiguration
{
  /**
   * Returns max_per_page config value for my_model module. If it's not
   * defined manually by the user, default value is returned.
   *
   * @return Integer
   */
  public function getPagerMaxPerPage()
  {
    if ($max = sfContext::getInstance()->getUser()->getAttribute('my_model.max_per_page'))
      return $max;
    else
      return parent::getPagerMaxPerPage();
  }
It's all as easy as it could be. The controller searches the servers for the current user session data and returns either the custom data (if found) or the default value (which is taken from the generator.yml file).

Unfortunately, the sfContext::getInstance() is used here (this causes a lot of problems when the default context problem occurs). After a quick look I didn't find the better way to access the user from the configuration generator (but if you know how to - let me know ;).

(I'm wondering why it's not built in into symfony).