Using Multiple Primary Keys in Admin Generator

Sometimes, the table we're using force us to use multiple primary keys as shown in the schema below (config/schema.yml):

connection:            propel
defaultIdMethod:       native
package:               lib.model

    tableName:         state
      id:              { type: varchar, size: 2, primaryKey: true }
      country_id:      { type: varchar, size: 2, primaryKey: true }
      name:            { type: varchar, size: 50, index: true }

By default, the symfony Admin Generator will only support single primary key. Since symfony 1.2, which adds sfRoute class, using multiple primary keys is possible by a configuration modifications and adds a few lines of code.

So now, generate the admin module by issuing:

symfony propel:generate-admin frontend State

Look at the generated route in apps/frontend/config/routing.yml:

  class: sfPropelRouteCollection
    model:                State
    module:               state
    prefix_path:          state
    column:               id
    with_wildcard_routes: true

Only a single primary key was generated, so the idea is concatenate the primary keys as a single column, separated by a dash (-) for example.

Modify the route:

  class: sfPropelRouteCollection
    model:                State
    module:               state
    prefix_path:          state
    column:               countryandstate
    with_wildcard_routes: true
      object:             doSelectForRoute
  requirements:           { countryandstate: '[a-zA-Z0-9\.\_\-\:]+' }

As seen in the modified route, we change the column name to countryandstate, add a model-methods for object configuration and change the countryandstate column requirements.

When an object converted as a route, the sfRoute class will check for toParams() method, so we can provide the value of countryandstate column by adding this code:

// lib/model/State.php

class State extends BaseState
  public function toParams()
    return array('countryandstate' => implode('-', array($this->getCountryId(), $this->getId())));

A model-methods for object configuration in the routing above, changes the method used by the sfRoute class to retrieve an object which match a route.

// lib/model/StatePeer.php

class StatePeer extends BaseStatePeer
   * Retrieve State object for routing.
   * @param array $parameters  The route parameters
   * @return State
  public static function doSelectForRoute($parameters)
    if (!isset($parameters['countryandstate']))
      return null;

    $c = new Criteria();
    list($country, $state) = explode('-', $parameters['countryandstate']);
    $c->add(StatePeer::COUNTRY_ID, $country)
      ->add(StatePeer::ID, $state);

    return StatePeer::doSelectOne($c);

The rest is, to customize the generated module and adjusting the form as you need.


  1. In the function doSelectForRoute, consider what happens if $parameters[‘countryandstate’] is not set. The criteria object, $c, is not modified which means the call to StatePeer::doSelectOne will return the first object found after a SELECT with empty criteria. If the table is not empty, this will simply be the first record in the table which is clearly not the intended result.

    I suggest reversing the logic of the if statement. That is, “if (!isset(…)) { return array();}”. Make this the first line of the function and the rest of the code can be linear (no “if”).

  2. You saved my day!!!


  3. Does it also work for Doctrine?

    • I’m not familiar with Doctrine, but the method may still the same.

      You can modify the route as the above example, add a toParams() method to your Doctrine Model and a doSelectForRoute() method (or you can name it whatsever) to your Doctrine Table.

  4. It doesn’t work for me. The toParams method is called when I want add a new row ( action “New” ).

    The issue is one of both keys is not known at this moment. The form is not filled yet.

    My schema.yml:
    feuille_heures_id: { type: integer, primary: true }
    offre_id: { type: integer, primary: true }
    FeuilleHeures: { local: feuille_heures_id, foreign: id, foreignAlias: LigneFeuilleHeures }
    Offre: { local: offre_id, foreign: id, foreignAlias:LigneFeuilleHeures }

    “offre_id” will be selected in form. But this key is required in toParams:

    public function toParams() {
    echo “Param:”.$this->getOffreId().”-“.$this->getFeuilleHeuresId();
    return array(‘offrefeuille’ => implode(‘-‘, array($this->getOffreId(), $this->getFeuilleHeuresId())));

    Why toParams() is called when executeNew() is called ?

    • Does it throw an Exception? Have you try clearing the cache? toParams() method is called when an object is converted to a route.

  5. hi,
    unfortunately this trick doesn’t work for symfony 1.4. The _list_td_batch_actions.php triggers a fatal error. It tries to echo the primaryKey and while the primaryKey is an array, a simple ‘echo’ doesn’t work.

    Anyone here with a solution for symfon 1.4 ?


  6. My response to this

    I’ve been dealing with this problem for two days now. I’ve found two solutions (working with symfony 1.4)
    1) create a getter for composite primary key and use this ( return $keypart1.’-‘.$keypart2.’-‘.$keypart3) and use this in the url…
    2) today I’ve found another solution, I’ think it’s better…

    For you try to change :Array (in the routing settings) to the url part compound from collumns of your composite primary key …

       class: sfDoctrineRouteCollection 
         model:                InternationalPresence 
         module:               int_presence 
         prefix_path:          int_presence 
         column:               competitor_id/:iso_country_code/:presence_type_id
         with_wildcard_routes: true

    The result is longer url, but it’s ok for me… and it works for me.

    What doesn’t work for my is deleting rows on the list page … using the checkboxes.

    And I’ve had another problem – one column from my composite key is integer autoincrement (second is string). This autoincrementation didn’t work, and this was cousing an arror during creation a new row (symfony wan’t to redirect to a edit page, but in the object there wasn’t generated id yet…) So I’ve created my method for generating the key and set it manually in my form object in the doUpdateObject() method :

    class PageForm extends BasePageForm
      protected function doUpdateObject($values)
        // Set id if it's not set
        if (empty($values['id']))
          $values['id'] = Doctrine::getTable('Page')->getNextId();

    getNextId is in the table class…

    class PageTable extends Doctrine_Table
    	public function getNextId()
    		return $this->getMaxId() + 1;
    	public function getMaxId()
    		$q = $this->createQuery('p')
    			->select('MAX(p.pgid) as maxid');
    		return $q->fetchOne()->maxid;

    Hope this will help somebody …

  7. I have several problems with this:
    first: I have a module generated from a relationship (url_category), whose primary key is (URL_ID-category_id). when I try to edit a list item, I get the following error: 404 | Not Found | sfError404Exception Action “suggestions/56” does not exist.

    Second: When I try to use the action to eliminate batch I get the following error: 500 | Internal Server Error | PropelException
    [Wrapped: SQLSTATE [22P02]: Invalid text representation: 7 ERROR: input syntax is not valid for integer: “Array”]

    If someone could help me urgently to resolve these problems, I would greatly appreciate them.

  8. Thank you Tomor, your solution saved my day!

    I really couldn’t figure out what the issue “Action “moduleName/rowName” does not exist” was all about, but adding an array of primary keys to routing.yml solved the problem.

    Like this:
    column: competitor_id/:iso_country_code/:presence_type_id

  9. Thks Tomor, I’ve been dealing with the same problem for a long time, but your solution work fine.
    Like this:
    column: competitor_id/:iso_country_code/:presence_type_id

Leave a Reply