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.

Selecting a media player for your project

There are a number of options to consider when selecting a media player for a project. Recently, I had a large media panel to build and which player to use was likely the most important piece of the puzzle.

Here are the factors I used in determining which media player to use:

  • Speed – How fast did it load?
  • Interface – How easy was it to use?
  • API – How easy will it be for me to work with?
  • Documentation – How easy will it be for me to figure out how to work with it?

There are three players I considered primarily. There are many others I looked at, but these three were a clear cut above the rest.

Here’s the break-down of them, and we’ll start with the least compatible for the tasks I was trying to accomplish.

MediaElement.js

In a world moving rapidly towards trying to accomplish 100% cross-platform content compatibility, especially with the recent proliferation of HTML5, new tools are a constant need. MediaElement.js is a giant step forward in this area. It takes the hodgepodge of various presentations throughout the browsers, and consolidates them into one consistent presentation.

The good:

  • Very slick presentation. Allowing the control panel to be completely within the HTML/CSS means you can make any sort of change you need. As I’m planning to add at least one button later, that was a definite bonus.
  • Consistent presentation across platforms.
  • Allows us to use the HTML5 <video> and <audio> tags, to let the latest browsers handle the video natively and then allow us to fall back to other players for less capable browsers.
  • It supports both a fallback Flash and Silverlight player, for maximum ability to ensure video is displayed.

The bad:

  • It was quite slow in my testing. The lag time between when an embedded <video> tag was detected and the video was ready to play, was extremely noticeable. It’s likely also due to loading up and then realize it needs to load a Flash or Silverlight player.
  • It didn’t handle dynamic changes very well. I think this could be solved by integrating it as a jQuery UI Widget as opposed to essentially just a basic wrapper routine.

Flowplayer

What initially impressed me with Flowplayer was the promise it showed for extending the base display with html overlays to help add contextual information to the video. Then I started looking over the site, and noticed that the documentation is absolutely top notch. These guys aren’t playing around. Very impressive.

The good:

  • The documentation. Oh my … word.
  • The API. Not only is it very accessible and well thought out, it’s throughly documented.
  • Playlists. It’s something I missed when playing with MediaElement.js.
  • Have I mentioned that Flowplayer is well documented.

The bad:

  • It’s huge. At roughly two times the size of JW Player, Flowplayer is the heavyweight of the bunch, which slows down that all important first page-load impression.
  • There’s some weirdness in it’s playlist system. When you push in a new playlist, the “cover image” isn’t loaded until you run play, which adds more delay between when the user initiates an action and when the player is where I will call “ready”.

JW Player

JW Player is now on version 5.2. I first used it somewhere back in version 3, and as a disclosure, own an unlimited license for both versions 3 and 4. I have been consistently impressed by the flexibility of the player, and it’s ability to handle whatever I throw at it… once you figure out how to tell it what it wants to know.

The good:

  • Flexibility is the name of the game.
  • It did everything I wanted it to, in a way that I could live with.
  • It was lightweight and the quickest to go from page load to presentation.

The bad:

  • The documentation could use some work. Instead of having answers at my finger tips, I had to search for them. Even for pretty basic things.
  • Some more descriptive errors might be nice, as opposed to things simply not working.

In the end, JW Player worked best for my purposes on this project, but every project is different, and I hope this helps some of you save some time.

Using JavaScript Responsibly

Since we’ve started the conversation, let’s take a step back and talk about something a little less fun, but just as important: when is it okay to require JavaScript use?

My hard and fast rule is that anywhere a login is required to reach, you can require JavaScript. If it’s publicly accessible, however, you should do everything possible to avoid requiring JavaScript. It takes more work to have a JavaScript and non-JavaScript version, but it makes sure your site remains accessible.

How to add jQuery to your Symfony project, part 2.

In part 1 we explored why exactly we may want to build our own jQuery plugin, so now in part two, we will cover exactly how to do so.

Note: these examples are through a bash command line, as I am fairly certain that is the most common way one interacts with Symfony. If you’re using Windows, you’ll get the gist of what to do, however.

Let’s start with initializing the plugin:

./symfony generate:plugin majaxJqueryPlugin

Now we will switch to our plugin directory:

cd plugins/majaxJqueryPlugin

We’re going to be providing web resources, so we will want to make web, js, and css directories. We will also make a temp directory to download files to:

mkdir web
mkdir web/js
mkdir web/css
mkdir temp

Prepare a place to download what we need:

cd temp

Here is our download and copy code. I have decided to host it from here, as jQuery UI doesn’t seem to provide direct download links.

wget "http://jmather.com/wp-content/plugins/download-monitor/download.php?id=3"
unzip jquery-ui-1.8.2.custom.zip
cp js/jquery-* ../web/js/
cp -a css/smoothness/ ../web/css/

Let’s clean up after ourselves…

cd ..
rm -rf temp

Now for the fun part, to make it work. Since I like things to ‘just work’, here’s how I will do it. I will take a page from jQuery Reloaded and use a helper to actually load the files into the response, but instead of making people manually add it where they want it, we will just automatically shove it in if the plugin is enabled!

It’s time to make our helper directory:

mkdir lib/helper

Here is the contents of lib/helper/MajaxjQueryHelper.php:

1
2
3
4
5
6
7
<?php
$jq = '/majaxJqueryPlugin/js/jquery-1.4.2.min.js';
$jqui = '/majaxJqueryPlugin/js/jquery-ui-1.8.2.custom.min.js';
$jquicss = '/majaxJqueryPlugin/css/smoothness/jquery-ui-1.8.2.custom.css';
sfContext::getInstance()->getResponse()->addJavascript($jq, 'first');
sfContext::getInstance()->getResponse()->addJavascript($jqui, 'first');
sfContext::getInstance()->getResponse()->addStylesheet($jquicss, 'first');

For the last little trick before we enable the plugin, open up config/majaxJqueryPluginConfiguration.class.php and add the following code to the initialize() function:

   	$helpers = sfConfig::get('sf_standard_helpers', array());
        $helpers[] = 'MajaxjQuery';
        sfConfig::set('sf_standard_helpers', $helpers);

Your completed majaxJqueryPluginConfiguration.class.php file should look like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?php
/**
 * majaxJqueryPlugin configuration.
 *
 * @package     majaxJqueryPlugin
 * @subpackage  config
 * @author	Jacob Mather
 * @version     SVN: $Id: PluginConfiguration.class.php 17207 2009-04-10 15:36:26Z Kris.Wallsmith $
 */
class majaxJqueryPluginConfiguration extends sfPluginConfiguration
{
  const VERSION = '1.0.0-DEV';
  /**
   * @see sfPluginConfiguration
   */
  public function initialize()
  {
   	$helpers = sfConfig::get('sf_standard_helpers', array());
        $helpers[] = 'MajaxjQuery';
        sfConfig::set('sf_standard_helpers', $helpers);
  }
}

You’re done! Now you just have to go back to your project root, and edit your project configuration to add the plugin, and publish it’s assets.

Here is the line to add to your ProjectConfiguration’s setup() function, just in case you need it:

    $this->enablePlugins('majaxJqueryPlugin');

To publish it’s assets:

./symfony plugin:publish

And now you’re ready to use jQuery throughout your Symfony project!

To download a copy of the majaxJqueryPlugin I have made (and save yourself some work!), use the link below, and you just have to follow the last step to enable the plugin in your project:

Download majaxJqueryPlugin v1.0.0 (109.06 kB zip)

How to add jQuery to your Symfony project, part 1.

The simplest way, hands down, is to simply include the sfJqueryReloadedPlugin in your project. This also enables you to include the sfAdminDashPlugin which makes for easy navigation and a nice login screen for your back-end systems.

However, the problem with jQuery Reloaded, is that it is both old, and for our purposes, incomplete.

Since the last update to jQuery Reloaded, jQuery has progressed to version 1.4.2 (from 1.3.2), and more importantly jQuery UI has upgraded to 1.8.2 (from 1.7.3) and added some very nice and easy pieces we can use in our quest to simplify some of the widgets users are commonly presented.

The other problem we will run into with jQuery Reloaded, is that it doesn’t provide the CSS half of the jQuery UI library, meaning we would not be able to see any widgets we used properly.

So, with all this in mind, you now understand why in Part 2, I will be showing you how to roll your own jQuery plugin for Symfony.

twitter