: Creation of the new admin generator theme :


When creating an admin generator them, in fact you will create a new plugin. Let's create the following folder:

/plugins/sfTutorialAdminThemePlugin/data/generator/sfPropelAdmin

Then copy into it, the following folder: (replace your_symfony_lib by the correct path)

/your_symfony_data_dir/symfony/generator/sfPropelAdmin/default

Then rename default to TutorialAdminTheme At this point your plugin folder should look like this:

tuto1-2


OK. Now we have our plugin structure, let's initialize an admin generated module. For example on the Question table. Run the following command: (don't forget to check the generator documentation while reading this tutorial :))

symfony propel-init-admin backend questions Question

Now we should have our basic admin generated page with the default theme.


Now to check if we are really using our new theme, we will just modify it a little:
Open the
sfTutorialAdminThemePlugin/data/generator/sfPropelAdmin/TutorialAdminTheme/template/templates/_list_header.php
and add some text:

<h1>TutorialAdminTheme</h1>


Refresh the page, you should see the title we just added in the _list_header.php file.
OK, so at this point we have our plugin witch contains a new admin generator theme called TutorialAdminTheme we can go trough the next step.

: Extending the new theme :


For now we've just added some text in one of the template used. Now we will see how the modify the generator to add the feature we want. There are several steps for this, we will:

  • Extend the default sfActions class
  • Extend the default sfPropelAdminGenerator class
  • Add new properties to the generator.yml file
  • Modify the generated action class
  • Modify the generated templates


:: Extending the action class ::


So let's go. 1st step we will create a new action class in witch we will put several new functions that will be used by our new theme.
In the lib folder of the plugin (create this one) create a new file, let's call it tuActions.class.php, thus this class will just extend the standart symfony sfActions.class.php. I used to take 2 letters of the project name in order to prefix extended class of the project, in this case tu for tutorial (or whatever you want...) Your file should look like this:

[php]
<?php
/**
 * Modified actions class for out new admin theme.
 *
 * @package    tutorial1
 * @author     COil
 * @since      3 apr 08
 */

class tuActions extends sfActions
{
}
?>


OK, now we have our class, we have to tell the admin generator to use this one instead of the standard sfActions class. Open the:
plugins\sfTutorialAdminThemePlugin\data\generator\sfPropelAdmin\TutorialAdminTheme\template\actions\actions.class.php

file (what a path !) As you can see, this file is not a standard php file, because it's php that generate php... But once what have understood the way it works it's not so hard to modify it. So the <php tags are standard php tags that will be used by the admin generator and the [php tags will be rendered as <?php tags in the generated code.
Line 13 we will then replace sfActions class by our new one. So you just have to modify the 2 first letter:

[php]
class <?php echo $this->getGeneratedModuleName() ?>Actions extends tuActions


Ok, let's check out admin page if is still OK. Clear you sf cache and then refresh.
There shouldn't be any change as our new actions class is still empty, but let's verify that our modification was taken in account. To do this, just go to the cache directory:

tutorial/cache/backend/dev/modules/autoQuestions/actions/actions.class.php

If all is OK the file should start like this:

[php]
<?php
// auto-generated by sfPropelAdmin
// date: 2008/04/03 14:12:38
?>
<?php

/**
 * autoQuestions actions.
 *
 * @package    ##PROJECT_NAME##
 * @subpackage autoQuestions
 * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
 * @version    SVN: $Id: actions.class.php 7997 2008-03-20 12:29:34Z noel $
 */
class autoQuestionsActions extends tuActions
{
  public function executeIndex()
  {
    return $this->forward('questions', 'list');
  }
?>


Indeed the autoQuestionsActions class extends out new tuActions class. Perfect. We will come back to our new actions class later.

:: Modification of the generator.yml file ::


First of all, we will build a standart generator file for our Question table, let's use this basic one:

generator:
  class:              sfPropelAdminGenerator
  param:
    model_class:      Question
    theme:            TutorialAdminTheme

    fields:
      id:                 { name: ID }
      user_id:            { name: User }
      title:              { name: Title }
      interested_users:   { name: Interested user }
      reports:            { name: Reports }
      created_at:         { name: Created at , params: date_format='dd/MM/yyyy' }
      updated_at:         { name: Updated at, params: date_format='dd/MM/yyyy' }

    list:
      layout:             tabular
      title:              Questions
      filters:            [id, user_id, title, interested_users, reports, created_at, updated_at]
      display:            [id, user_id, title, interested_users, reports, created_at, updated_at]
      max_per_page:       10
      object_actions:
        _edit:            ~
        _delete:          ~
      actions:
        _create:          ~
      sort:            [id, asc]


Refresh the page, it should work.
Now we will add settings for our new actions on objects, let's add just after the sort key (and at the same level), the following line:

      object_global_actions:
        delete:           { name: Deleted the selection }


Here we define a new section object_global_actions that will contain the actions we will be able to do on a list of selected object, like the delete function.
Ok, now we have our new properties in our generator.yml file, let's add our check boxes now, that's the funny part of the tutorial. :)

:: Modification of the admin generator templates ::


First, we need to know witch file we have to modify, we are on the list screen, thus we will have to modify 2 files of our template plugin folder: _list.php and _list_td_actions.php
Open _list.php, first we need to retrieve the new properties that we have defined, add at the top of the file:

<?php $object_global_actions = $this->getParameterValue('list.object_global_actions'); ?>

<?php if ($object_global_actions): ?>
  [?php echo form_tag('<?php echo $this->getModuleName(); ?>/global_action', array('name' => 'global_action_form')); ?]
  [?php echo input_hidden_tag('object_name', '<?php echo $this->getSingularName() ?>'); ?]
<?php endif; ?>


As you can see, our new parameter section is called list.object_global_actions and we can retrieve it with the getParameterValue function. (this is a function of the sfPropelAdminGenerator class) Then we declare a new form that will include our check boxes. It will post data to the global_action function, it's a new function that we will have to add in our tuActions class. We also need the name of the object, that's why we need this hidden tag after the form_tag. In this file, it is tested if there are actions defined for the rows, but now we have 2 types of actions, the standard ones, delete, edit... and our new global ones global_delete... So we will have now to check both. Modify the following line (17):

[php]
<?php if ($this->getParameterValue('list.object_actions')): ?>


by

[php]
<?php if ($this->getParameterValue('list.object_actions') || $object_global_actions): ?>


So as you can see we are checking if we have "object" actions OR "global" actions. We must do the same for the others lines where we have the following test:

if ($this->getParameterValue('list.object_actions')):

It should be another one in this _list file so modify this other test in the same way.
Now we must add our submit buttons and close the form tag, let's add the following code just before the </tr></foot> tags.

<th>
  <?php if ($object_global_actions): ?>
    [?php $confirm_lib = __('Are you sure ?') ; ?]
    <?php foreach ($object_global_actions as $object_global_action => $params): ?>
      [?php echo submit_tag('<?php echo $params['name']; ?>', array('name' => '<?php echo 'global_action['. $object_global_action. ']'; ?>', 'confirm' => $confirm_lib)); ?]<br/>
    <?php endforeach; ?>
    </form>
  <?php endif; ?>&nbsp;
</th>


So here we are iterating through our global actions (we just have one for now), and we build the corresponding submit button with an alert message and we close our form. (no really at the good place but that's not important for now). OK, so we have made some modifications, let's see if our page has changed. Let's refresh it. (Don't forget to clear the cache each time you will modify the admin generator templates or action)
Oops, we can see the delete button Delete the selection , but it seems that the main table is broken. Indeed, it's because we have modified the numbers of columns displayed if the table.
In the following line (28), add "one column":

[php]
<?php
<th colspan="<?php echo $this->getParameterValue('list.object_actions') || $object_global_actions ? count($this->getColumns('list.display'))  + 1: count($this->getColumns('list.display')); ?>">


Thus replace it by:

[php]
<th colspan="<?php echo $this->getParameterValue('list.object_actions') || $object_global_actions ? count($this->getColumns('list.display')) : count($this->getColumns('list.display')) -1; ?>">


Clear the cache and refresh, OK, it looks better now. But our submit button is useless as we don't have any check box. :)

: Adding the check boxes :


Now we have to modify our 2nd templates, the _list_td_actions.php, open it, at the top of the file add the same line we have in the _list.php template, also add the same test on the following if statement as we already done. Then just after the foreach statement add the following code:

<?php if ($object_global_actions): ?>
  <li>[?php echo checkbox_tag('<?php echo $this->getSingularName() ?>[id]['. <?php echo $this->getPrimaryKeyValue(); ?>.']', 1, false); ?]</li>
<?php endif; ?>


As you can see, we will use the singular name of the object to build the name of the check box tag (in this case question) and we will use its primary key to send the data as an array to the action function, so we can easily retrieve the objects we have selected.
Clear the cache and refresh.
OOps, it seems that i have forgotten something, indeed here i am using a new function of the sfGenerator class getPrimaryKeyValue. Once again let's see how to do this.
So put the following code in the lib folder of our plugin and let's call it tuPropelAdminGenerator.class.php

<?php

/**
 * Class that extends the default sfPropelAdminGenerator class.
 *
 * @subpackage tutorial1
 * @author     COil
 * @since      03 apr 08
 */

class tuPropelAdminGenerator extends sfPropelAdminGenerator
{

  /**
   * Returns the value of the primary key for a row, if there are many 
   * keys they are returned separated by an underscore.
   * 
   * @author COil
   * @since  03 apr 08
   */
  function getPrimaryKeyValue($prefix = '')
  {
    $params = array();
    foreach ($this->getPrimaryKey() as $pk)
    {
      $phpName   = $pk->getPhpName();
      $fieldName = sfInflector::underscore($phpName);
      $params[]  = $this->getColumnGetter($pk, true, $prefix);
    }
    return (count($params) > 1) ? implode('_', $params) : $params[0];
  }
}
?>


So this class extends the default sfPropelAdminGenerator class in order to provide a new function. This function will allow us to retrieve the value of a primary key and will also able to manage cases where there are more than one key for this object. OK, we now have our class, but once again we must tell our admin module to use it. Just check the 2nd line of our generator.yml file, and change sfPropelAdminGenerator to tuPropelAdminGenerator ! Easy ! ;)
Clear the cache and refresh. yeah ! This time is seems to work ! Good job. :)
At this point, you list should look like this:

tuto1-3


Now we have finished to modify our form, we must implement the function that will catch the submitted data (global_action).

Modifying the tuActions.class.php


So first we need to catch witch button was clicked, add the following method to our tuActions.class.php :

<?php
  /**
   * Catch the button that was clicked for a global action.
   *
   * @author COil
   * @since  3 apr 08
   */
  public function executeGlobal_action()
  {
    $action_key  = $this->getRequestParameter('global_action');
    $this->forward404Unless($action_key);
    $this->forward($this->getModuleName(), 'global_'. key($action_key));
  }
?>


So we get the key of the global_key array that was submitted. Then we forward to the action defined by the submit button, so here we take the convention that all global actions are prefixed by global_, so in this case the method we will have to implement is global_delete.
Now we just have to implement this function, here is the code (try to di it by yourself !! :) ), once again just add it to the tuActions class:

<?php
  /**
   * Generic delete function for the global action delete.
   *
   * @author COil
   * @since  03 apr 08
   */
  public function executeGlobal_delete()
  {
    $object_name = $this->getRequestParameter('object_name');    
    $human_object_name = sfInflector::humanize($object_name);
    $peer_class = $human_object_name. 'Peer';
    $ids = $this->getRequestParameter($object_name. '[id]');

    $this->forward404Unless($object_name && $human_object_name && $peer_class && 
      class_exists($peer_class)
    );
    
    try 
    {
      if ($ids)
      {
        $c = new Criteria();
        $c->add(constant($peer_class. '::ID'), $keys = array_keys($ids), Criteria::IN);
        call_user_func_array(array($peer_class, 'doDelete'), array($c));
        $this->setFlash('notice', 'The following '. $human_object_name. ' were deleted : '. implode(', ', $keys));
      }
      else
      {
        $this->getRequest()->setError('delete', 'Please choose at least one '. $human_object_name. '.');
      }
    }
    catch (Exception $e)
    {
      $this->getRequest()->setError('delete', 'Could not delete the selected '. $human_object_name. '. Make sure they do not have any associated items.');
    }

    $this->forward($this->getModuleName(), 'list');
  }
?>


We can see that we need here the name of the object in order to know the peer class that we will have to use. We must construct the Criteria dynamically and then we can call the doDelete function. OK, that should be good now ! Clear the cache and test ! It seems OK. :) We don't see the notice so you have to modify the templates in order to see them.
Edit the _list_header.php template and then include in it the _edit_messages.php in it. That's it ! :)
(be careful there is a little trap in this last exercise...)

tuto1-4


Now, it's your turn to work !! ;) So here are some exercises if you want to continue...

Exercice 1: Modify the templates/action to avoid the error when no message is selected
Exercice 2: Implement a duplicate object function in the same way as the delete function
Exercice 3: Add select all / unselect all / invert selection links (ok that's javacript this time)
Exercice 4: Add a test to force the user to select a row berore clicking on a button (js again)


I hope you enjoyed this tutorial, feel free to add comments, remarks or to report errors. See you. COil :)