How to customize sfGuardUser in sfDocrineGuard

One of the questions I see quite frequently is “How do I customize the user management interface for sfDoctrineGuard?” There are many different ways to go about it, but let me tell you about mine.

If you know me, you know I’m about attention to detail. I want the absolute cleanest solution you can come up with, and then I want it to look polished. So what you’re about to see will be quite a bit more involved than you would normally expect for such a tutorial, but I believe you will agree, at the end, that it was well worth the trip.

The first step is to get ready to customize. It’s easy, I promise. Here we go!

mkdir apps/backend/modules/sfGuardUser
mkdir apps/backend/modules/sfGuardUser/config
cp plugins/sfDoctrineGuardPlugin/modules/sfGuardUser/config/* apps/backend/modules/sfGuardUser/config/

That’s all you need to do, to begin customization!

Ok, so we’re all set up to change our page… so what do we want to do?

To make it easier to follow along, we’ll set our goal as the following: Add a ‘Name’ field to the user form.

This will take a few steps:

  1. Create our sfGuardUserProfile table and object.
  2. Add a great piece of useful code to BaseFormDoctrine.class.php that lets us make this look awesome.
  3. Create a custom form that includes our new relationship in the mix.
  4. Modify the generator.yml we copied over to:
    1. Use that form
    2. Add the new field to the top of the form.
    3. One last little tweak.

That being said… let’s get started!

1. We add our new object and relation. Open your config/doctrine/schema.yml file and add the following:

sfGuardUserProfile:
  tableName: sf_guard_user_profiles
  columns:
    user_id: { type: integer(4), primary: true }
    name: { type: varchar(255) }
  relations:
    User:
      local: user_id
      class: sfGuardUser
      type: one
      foreignType: one
      foreignAlias: Profile
      onDelete: CASCADE

Note: pay special attention to integer(4). If you forget this, MySQL (and I assume other databases) won’t be able to build the relation properly as the field types will not match.

Now we need to rebuild things. If you don’t know how to do a migration, here’s the code you want:

./symfony doctrine:generate-migrations-diff
./symfony doctrine:migrate
./symfony doctrine:build --all-classes

Your sfDoctrineGuardUserProfile is ready to go, so let’s build our new admin form.

2. Add embedMergeForm to BaseFormDoctrine

The embedMergeForm function originates from (I believe) Roland Tapken of Cybso in this post. It’s a great piece of code. It gives us the function of embedded forms, with the look of merged forms. Here is the code I do (also adapted with one of the fixes from the comments section):

<?php
// File: lib/form/doctrine/BaseFormDoctrine
 
/**
 * Project form base class.
 *
 * @package    dcms
 * @subpackage form
 * @author     Jacob Mather
 * @version    SVN: $Id: sfDoctrineFormBaseTemplate.php 23810 2009-11-12 11:07:44Z Kris.Wallsmith $
 */
abstract class BaseFormDoctrine extends sfFormDoctrine
{
  /**
   * Embeds a form like "mergeForm" does, but will still
   * save the input data.
   */
  public function embedMergeForm($name, sfForm $form)
  {
    // This starts like sfForm::embedForm
    $name = (string) $name;
    if (true === $this->isBound() || true === $form->isBound())
    {
      throw new LogicException('A bound form cannot be merged');
    }
    $this->embeddedForms[$name] = $form;
 
    $form = clone $form;
    unset($form[self::$CSRFFieldName]);
 
    // But now, copy each widget instead of the while form into the current
    // form. Each widget ist named "formname|fieldname".
    foreach ($form->getWidgetSchema()->getFields() as $field => $widget)
    {
      $widgetName = "$name-$field";
      if (isset($this->widgetSchema[$widgetName]))
      {
        throw new LogicException("The forms cannot be merged. A field name '$widgetName' already exists.");
      }
 
      $this->widgetSchema[$widgetName] = $widget;                           // Copy widget
      $this->validatorSchema[$widgetName] = $form->validatorSchema[$field]; // Copy schema
      $this->setDefault($widgetName, $form->getDefault($field));            // Copy default value
 
      if (!$widget->getLabel())
      {
        // Re-create label if not set (otherwise it would be named 'ucfirst($widgetName)')
        $label = $form->getWidgetSchema()->getFormFormatter()->generateLabelName($field);
        $this->getWidgetSchema()->setLabel($widgetName, $label);
      }
    }
 
    // And this is like in sfForm::embedForm
    $this->resetFormFields();
  }
 
  /**
   * Override sfFormDoctrine to prepare the
   * values: FORMNAME|FIELDNAME has to be transformed
   * to FORMNAME[FIELDNAME]
   */
  public function updateObject($values = null)
  {
    if (is_null($values))
    {
      $values = $this->values;
      foreach ($this->embeddedForms AS $name => $form)
      {
        foreach ($form AS $field => $f)
        {
          if (isset($values["$name-$field"]))
          {
            // Re-rename the form field and remove
            // the original field
            $values[$name][$field] = $values["$name-$field"];
            unset($values["$name-$field"]);
          }
        }
      }
    }
 
    // Give the request to the original method
    parent::updateObject($values);
  }
}

Save your file, and let’s move on to step three.

3. Build our new interface form.

Let’s make a new file, lib/form/doctrine/myGuardUserAdminForm.class.php, and fill it with the following:

<?php
 
class myGuardUserAdminForm extends BasesfGuardUserAdminForm
{
  public function configure()
  {
    $uprof = new sfGuardUserProfileForm($this->object->Profile);
    unset($uprof['user_id']);
    $this->embedMergeForm('Profile', $uprof);
  }
}

Ok! Hard stuff done! Last little bits now!

4. Modifying the generator.yml to use our new form, and expose our new field.

All you have to do now is update the form section of the generator.yml to look like the following:

      form:
        class: myGuardUserAdminForm
        display:
          "NONE":                   [Profile-name, username, password, password_again]

Now all you have to do is clear your cache, and go to your sfGuardUser form page, and you will see your new field!

5. Cleaning up messes.

Remember back in the schema.yml where we specified the onDelete: CASCADE? Bad news, that probably didn’t take care of it. There’s a bug somewhere between Symfony and Doctrine where sometimes foreign key constraints aren’t always handled appropriately. To counteract that, we will just run an SQL command to correctly establish the relationship so that when users are removed, we also remove their auxiliary profile data.

Here’s your magic:

ALTER TABLE sf_doctrine_guard_user_profiles ADD CONSTRAINT sf_doctrine_guard_user_profiles_user_id_sf_guard_user_id  FOREIGN KEY (user_id) REFERENCES sf_guard_user(id) ON DELETE CASCADE;

And we’re done! See? It was quite a long trip, but I hope you agree with me, the result is worth it.

02/16/2011 – Note: A thanks to hectorh30 from #symfony for pointing out an error in step three regarding the name of the profile form. It has been corrected.

02/12/2011 – Note: And in step one about paths. Thanks hectorh30!

03/03/2011 – Note: Thanks to dmclark for pointing out myDoctrineGuardAdminForm should be myGuardUserAdminForm

03/28/2011 – Note: Thanks to Richard Linkster for pointing out a copy command that was all busted up.

10 Comments

  1. Kai

    This symfony configuration reference could be usefull: http://www.symfonyreference.com

    Reply

  2. David Clark

    I think there is a typo in step 4.

    myDoctrineGuardAdminForm does not work – should it be myGuardUserAdminForm

    Reply

  3. Richard Linkster

    Should “cp plugins/sfDoctrineGuard/modules/sfGuardUser/config/* apps/backend/modules/sfGuardUser/config/” be instead “cp plugins/sfDoctrineGuardPlugin/modules/sfGuardUser/config/* apps/backend/modules/sfGuardUser/config/”?

    Reply

    • Jacob Mather

      Thanks Richard, I appreciate you pointing that out!

      Reply

  4. Richard Linkster

    I have successfully followed your guide and added a user profile object. Thanks!

    However, now I want to have one of the attributes of the profile have a many-to-many relationship with the user profile. For example, I want to relate one or more phone numbers to the user profile where these phone numbers may be associated with other users (imagine users who live in the same house and hence have the same phone number).

    I was able to get the multi-select to appear in the edit form, but whenever I press “save” nothing is saved. Can you give some advice on how I might need to alter your steps to do this?

    Reply

    • Jacob Mather

      Richard,

      Being an embedded form, sfGuardUserProfileForm doesn’t get the method called that it needs to have called to save the many-to-many relationship by default.

      https://github.com/jmather/majaxDoctrineMediaPlugin/blob/master/lib/form/doctrine/majaxMediaRegistryEntryEmbeddedForm.class.php < -- this is what my embedded form that has a many-to-many relationship that has to be saved looks like. I'm not sure if all pieces are needed at this point, but I know it works. You'll replace $this->saveGalleriesList($con) with the m2m function doctrine created for you (look in your BasesfGuardUserProfileForm class and it should be obvious).

      If you need more help, look for me on #symfony on FreeNode IRC. If that’s gibberish, go to http://webchat.freenode.net and use #symfony for channels.

      Reply

      • Richard Linkster

        I found the method $this->saveSitesList($con) in the BasesfGuardUserProfileForm class. But now I do not know where or in which file to call this method.

        Reply

        • Jacob Mather

          Add it to the saveEmbeddedForms function in myGuardUserAdminForm. If it still doesn’t work, add the isValid and processValues functions from the file I referenced.

          Reply

  5. David Clark

    I have resorted to creating a new model, following this code exactly.

    I get The “sfGuardUserProfileForm” form only accepts a “sfGuardUserProfile” object.

    Reply

  6. Timo

    Hi!
    Thank you for this, this is exactly what i needed. I like your approach for the cleanest solution possible, i use to work the same way, even when it takes 3 times as long.
    Unfortunately, with my implementation comes a strange bug i don’t really understand right now: The mask to create a new user is rendered twice: Once with the standard values coming from the original table and once including the fields from the profile table.
    I also changed the label of the “password again” field in the generator.yml, which is set on both, the one only containing the original fields and the new one containing also the fields from the profile table…
    I guess it has something to do with the

    unset($form[self::$CSRFFieldName]);

    inside the embedMergeForm function, but i don’t know how to solve this.
    By the way, i use Symfony 1.4 and of course cleared the cache 😉
    Any idea would be greatly appreciated… Thanks in advance!
    Greets, Timo

    Reply

Leave a Reply

*

twitter