Autogenerating forms from Doctrine models

Tags:

In a previous post I mentioned how Django’s model forms are awesome.

I really like the idea of being able to generate forms automatically from models – I mean the models already should contain most of the data you’d need: the fields, field types and how they will be stored.

Since I was already quite familiar with Doctrine’s internals, I knew it would be possible to find out all the data on the models quite easily, and set upon creating a class which generates Zend Framework’s Zend_Form based form classes from Doctrine models…

Edit 10.10.2008: Updated the post to reflect some minor changes in the CU_ModelForm class

Theory of Operation

The idea is quite simple. Simply get all the columns from the model and use them to generate fields for a form, and thanks to Doctrine, the implementation was simple too.

I borrowed some basic ideas from Django: a basic form should be easy to create, since this is essentially a convenience feature, but it also has to be possible to modify them to adapt to different tasks.

The very simplest form would look like this:

<?php
class ExampleForm extends CU_ModelForm
{
    //This defines what model the form uses
    protected $_model = 'Example';
}

This would give you a class with the same interface as Zend_Form, but it would automatically get fields from the model called Example. Having the same interface as Zend_Form means that you can essentially use the class just like any other Zend_Form instance; it just has some more magic. :)

Basic usage

Creating new records

//ExampleController.php
class ExampleController extends Zend_Controller_Action
{
    public function formAction()
    {
        $form = new ExampleForm(array(
            'action' => '.',
            'method' => 'post'
        ));
 
        //the form does not have a submit button by default
        $form->addElement('submit', 'Save');
 
        if($this->getRequest()->isPost() && $form->isValid($_POST))
        {
            //This saves the form's data to the DB.
            //$record will be the new model instance created when saving.
            $record = $form->save();
 
            //redirect elsewhere after completion
            $this->_redirect('/something');
        }
 
        //assign the form to the view
        $this->view->form = $form;
    }
}
 
//view script for formAction
<h2>Fill this form</h2>
<?= $this->form; ?>

The formAction method is a pretty typical form process action: it creates a form and validates it if the request was a form submission, or displays it if it wasn’t or something was not valid. In the action, we add a submit button to the form, as the form generator does not add a submit button by default as you may wish to use the forms as subforms or such.

Editing existing records

Editing existing records is similar to what seen above, but we must first load a record and assign it to the form before rendering, validating or saving:

//Get row with id=10
$record = Doctrine::getTable('Example')->find(10);
$form->setRecord($record);

After using setRecord, the default field values will be read from the record passed to the method. Also, when calling save(), any modifications will be saved to this record instead of a new one.

Modifying the form behavior

The class in its current implementation supports both creating new records and editing existing ones, and it can also display some relations as select boxes and subforms. You can also make it ignore chosen columns so it won’t autogenerate fields for them and such. Other options include giving labels for fields and switching their types.

Advanced settings example

<?php
class AdvancedForm extends CU_ModelForm
{
    protected $_model = 'Article';
 
    //By default, many-relations will be ignored but let's enable them
    protected $_generateManyFields = true;
 
    //Let's ignore these two cols so they don't get a field
    protected $_ignoreColumns = array('created_at', 'updated_at');
 
    //Make the content column's field type 'textarea' instead of the default 'text'
    protected $_fieldTypes = array(
        'content' => 'textarea'
    );
 
    //Give some human-friendly labels for the fields:
    protected $_fieldLabels = array(
        'name' => 'Article name',
        'content' => 'Article content',
        'category_id' => 'Category'
    );
 
    //Give a label to a many relation
    protected $_relationLabels = array(
        'ArticleComment' => 'Comment'
    );
 
    //this method is called before the form is generated
    protected function _preGenerate()
    {
        //We need to tell the form loader where our custom many-relation tables are located
        $this->getPluginLoader(self::FORM)->addPrefixPath('My_Form', 'My/Form');
    }
 
    //this method is called after the form is generated
    protected function _postGenerate()
    {
        //Add a submit button
        $this->addElement('submit', 'Save');
    }
}

In the above snippet you can see all the configuration options and the two event methods. Most of the variables should be quite self-explanatory. However, the plugin loader part may be a bit confusing at first, so let’s look at what it does.

The many relations for models are rendered as subforms. For this, the parent form needs to know where the subforms are located so it can use them. By default, it will look at App/Form/Model/ModelName.php, so in the above example, it would look at App/Form/Model/ArticleComment.php. If you put your forms in the default directory, you won’t need to call addPrefixPath, but in the case where they aren’t in that path, you’ll need to tell it where they are. The ModelName.php forms should be other forms that extend CU_ModelForm.php, so for example the ArticleComment.php would contain a class called My_Form_ArticleComment and it would extend CU_ModelForm just like this one does.

Also, as I mentioned earlier, the form will not generate a submit button by default as it does not know what you wish to do with the form in the end. However, here we add a _postGenerate method that automatically adds a submit button so we won’t have to worry about it.

Download

The code for CU_ModelForm can be found in my public SVN repo, more specifically the files you need are CU/ModelForm.php and CU/Validate/DbRowExists.php. ModelForm.php is the main class and DbRowExists.php is used by the relation support to validate that the selected item exists in the database and is valid for the relation.

Please let me know what you think, any bugs and ideas :)

Check the second model form post for some more usage examples and info on internals.