Base form classes and child model classes in auto-generated Symfony code

Took me a moment to figure this out, so I will put this here, in case I need to remember this later.

I was debugging some form code tonight, because my forms are not getting populated with data when the user wants to edit old entries. These are the forms that were auto-generated by Symfony, so they should work perfectly.

The module name is “Alumni” and its code is for working with the database table called “alumni”. In the actions.class.php file for the “alumni” module I have this:

public function executeEdit($request)  {
$this->form = new AlumniForm(AlumniPeer::retrieveByPk($request->getParameter('id')));
}

This is auto-generated code.

The job of AlumniPeer is obvious enough – given an id (which it expects to find in the URL) it finds a row in the database, gets the data, and turns that into an “Alumni” model object. Note that the class called “Alumni” is a class for dealing with the database – this tripped me up for a moment. The class called “Alumni” has nothing to do with the forms. But the forms classes will eventually need this info.

I looked to see what AlumniForm looked like.

class AlumniForm extends BaseAlumniForm
{
public function configure()
{
}
}

It is an empty form class that inherits from its base class.

BaseAlumniForm has just two methods:

setup()

getModelName()

The setup() method just initiates an array of validators, for handling the forum input, and an array of form widgets, so it knows how to create the forms.

The getModelName() method really just returns the name of the model:

public function getModelName() {
return ‘Alumni’;
}

Since there is no constructor, I became curious where setup() was called from. BaseAlumniForm inherits from BaseFormPropel which inherits from sfFormPropel. I took a look at its constructor:

public function __construct(BaseObject $object = null, $options = array(), $CSRFSecret = null)
{
$class = $this->getModelName();
if (is_null($object))
{
$this->object = new $class();
}
else
{
if (!$object instanceof $class)
{
throw new sfException(sprintf('The "%s" form only accepts a "%s" object.', get_class($this), $class));
}

$this->object = $object;
$this->isNew = false;
}

parent::__construct(array(), $options, $CSRFSecret);
$this->updateDefaultsFromObject();
}

There is something quite elegant about the way this auto-generated Symfony code fits together. The first thing that happens is that the constructor calls the getModelName() method in the BaseAlumniForm child class (actually the grand-child class). This, as you can see above, returns “Alumni”, so sfFormPropel now knows which class is suppose to be handling the database data (or rather, which class is suppose to return a model to call a row of data in the database). If for some reason the constructor has not been handed an instance of Alumni, then it creates a new instance. But above we saw that in actions.class.php, the form class is given an instance of the correct model:

$this->form = new AlumniForm(AlumniPeer::retrieveByPk($request->getParameter('id')));

So, at least in the auto-generated code, the constructor to sfFormPropel is always being given an instance of the correct model. If it was handed an object of the wrong class, then it would throw an appropriate exception.

The constructor ends by calling the constructor in its parent, which is sfForm:

public function __construct($defaults = array(), $options = array(), $CSRFSecret = null)
{
$this->setDefaults($defaults);
$this->options = $options;

$this->validatorSchema = new sfValidatorSchema();
$this->widgetSchema = new sfWidgetFormSchema();
$this->errorSchema = new sfValidatorErrorSchema($this->validatorSchema);

$this->setup();
$this->configure();

$this->addCSRFProtection($CSRFSecret);
$this->resetFormFields();
}

This is where setup() gets called.

This is also where configure() gets called. Mind you, configure() starts off as an empty method in the class AlumniForm:

public function configure()
{
}

This is where you do all your customization. They offer this example in the Symfony Cookbook, in the article How To Implement A Choice In Forms:

// lib/form/DemoArticleForm.class.php
class DemoArticleForm extends BaseDemoArticleForm
{
public function configure()
{
$this->widgetSchema['status'] = new sfWidgetFormChoice(array(
'choices' => DemoArticlePeer::getStatusChoices()
));

$this->validatorSchema['status'] = new sfValidatorChoice(array(
'choices' => array_keys(DemoArticlePeer::getStatusChoices())
));
}
}

In this example, they are making a change to the widgetSchema and validatorSchema arrays, which would have been set in the setup() method of the BaseDemoArticleForm class.

I am planning on building a new scaffolding system for Syfmony. One of the many great things about the vibrant community that surrounds Ruby On Rails is that for every default in RoR, there are competing alternatives – for instance, you can replace the default scaffolding with ActiveScaffold. Similarly, I’d like to build an alternative scaffolding system for Symfony. Understanding how the forms are currently built is the first step.

Leave a Reply