Autogenerating forms from Doctrine models

June 2, 2008 – 7:37 am 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.

Share this:
  1. 33 Responses to “Autogenerating forms from Doctrine models”

  2. I hope I didn’t forget to mention anything important. I have a bad habit of doing that with blogposts and then editing them in a hurry… anyway, let me know if there’s anything you’d like more specific details on.

    By Jani Hartikainen on Jun 2, 2008

  3. Great idea. I’ve been missing something like this for awhile now. Haven’t tried your code yet but this would really be nice to have. Keep it up.

    By Daniel on Jun 2, 2008

  4. I’ll put this to the test tonight or tomorrow! It looks good, but I haven’t peaked at your code yet. :)

    By Josh Team on Jun 5, 2008

  5. I am playing with it now, really quick this line on the AdvancedForm is missing a ;

    protected $_generateManyFields = true

    should be

    protected $_generateManyFields = true;

    So far very impressive!

    By Josh Team on Jun 13, 2008

  6. great job
    i’m using it and works fine
    i also added a private function to CU_ModelForm to autoload form labels from a textfile
    with ZendTranslate

    something like:

    protected function _loadLabels(){
    $translate = new Zend_Translate(‘csv’, Zend_Registry::get(‘rootDir’).’/language/pt-br/lang.csv’, ‘pt_BR’);

    foreach($this->_getColumns() as $name => $definition)
    {
    $this->_fieldLabels[$name] = $translate->_($name);
    }
    }

    By Luiz Felipe on Jun 30, 2008

  7. Thanks for sharing this, works like a charm!

    I had to fix the following to make it work with the newest version of doctrine:

    Replaced
    Doctrine_Relation::ONE_AGGREGATE by Doctrine_Relation::ONE
    and
    Doctrine_Relation::MANY_AGGREGATE by Doctrine_Relation::MANY.

    see: http://tinyurl.com/4s4a4n

    Btw, which license does this code use?

    By muriel on Sep 25, 2008

  8. There seems to be an error in the example for editing an existing record. It should be:

    $instance = Doctrine::getTable(‘Example’)->find(10);
    $form->setRecord($instance);

    By LeisureLarry on Oct 9, 2008

  9. Yep, I’ve updated the class slightly and this is now a tad outdated. Will be fixing it in the near future though.

    By Jani Hartikainen on Oct 9, 2008

  10. Really nice work, thanks for sharing this. But I´ve another question, should the line telling ‘action’ => ‘.’ work or do I have to insert something like ‘module/controller/action’?

    Greats and thanks from Germany
    LeisureLarry (interiete.net)

    By LeisureLarry on Oct 9, 2008

  11. Right, I’ve updated the posts.

    LeisureLarry: . should work as long as it is the correct address

    By Jani Hartikainen on Oct 10, 2008

  12. Really thank you for your great work. After fiddling around something, I realized that the dot is the root of my project. Is this correct?

    By LeisureLarry on Oct 10, 2008

  13. Since the url you provide to the form would be used as the form’s action attribute, the dot would mean the current url. So if your url was foo.com/bar/baz and . as the action, the form would get submitted to foo.com/bar/baz

    By Jani Hartikainen on Oct 10, 2008

  14. That works fine, good work!

    I had a problem with many-to-one relations, so I fixed it for myself:

    Replaced:
    $label = (string)Doctrine_Manager::connection()->getTable($alias);

    by:
    $label = (string)Doctrine_Manager::connection()->getTable($relation->getClass());

    Hope not to crash anything else…

    By gorgo on Feb 4, 2009

  15. When using CU_ModelForm for adding new instance of the model, calling following code in _postSave() didn’t get the actual id:
    $user = $this->getRecord();
    echo $user->id; exit;

    To fix this, just add following row just before calling $this->_postSave($persist) in save() method:
    $this->setRecord($inst);

    By gorgo on Mar 25, 2009

  16. Great post! thanks.

    I downloaded your code and find it difficult to get to work with my project (CRUD and MANY relations). I am building an event registration site were many users can register to many events.

    Can you please post a complete working example for a simple form with CRUD controller/model and some relations (one & many)?

    On your google code there are few examples but none is simple CRUD controller/model operations for me to use as template/tutorial.

    Many thanks.

    By gadi on Apr 21, 2009

  17. gadi, I’m not quite sure what you’re refering to since I don’t have any code in Google Code

    By Jani Hartikainen on Apr 21, 2009

  18. I am having trouble to get my related subforms.

    My forms are in APPLICATION_PATH/forms

    e.g: APPLICATION_PATH/forms/User.php

    In the controller I just have to:
    $form = new My_Form_User();

    In the directory I have the related form Contact.php

    What should be instead:

    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’);
    }

    By dantan on Nov 10, 2009

  19. in Model.php around Line 297:

    $formClass = $this->_relationForms[$relation->getClass()];
    is giving the error:
    Fatal error: Call to a member function getClass() on a non-object

    no error (but no effect):
    $formClass = $this->_relationForms[$relation['model']];

    By dantan on Nov 10, 2009

  20. It is working now,

    Generating Many2Many Forms has some errors because of duplicate Id´s

    By dantan on Nov 10, 2009

  21. Another extension:

    If you have a ONE relation to another table, you can specify what string to display in the select box if you add the following to the ModelForm.php:

    New member:
    /**
    * Text value of relation in select box
    * key = related class name, value = field name which contains string
    */
    protected $_relationStrings = array();

    In relationsToFields():
    case Doctrine_Relation::ONE:
    $table = $relation->getTable();
    $idColumn = $table->getIdentifier();
    $stringValue=””;
    if (isset($this->_relationStrings["".$table->getTableName().""]))
    $stringValue = $this->_relationStrings["".$table->getTableName().""];

    $options = array(‘——‘);
    foreach($table->findAll() as $row)
    {
    $options[$row->$idColumn] = ($stringValue != “”) ? (string)$row->$stringValue : (string)$row;
    }

    By Andy on Dec 28, 2009

  22. Thanks for this post Jani. I got it running beautifully. After using Django I was thinking something like this would work like butter in Zend Framework.

    But I’m still brand new to Doctrine so I have plenty to learn…

    I’m wondering, have you written something up about generating the list of existing records to pass the id needed to edit a record? Or is that part just straight Doctrine?

    By Joe Devon on Mar 17, 2010

  23. It should be relatively straightforward to get a list of records with Doctrine to show a list like that.

    By Jani Hartikainen on Mar 17, 2010

  24. Hello I’ve just done a check out and read the code …
    Just a remark: don’t you think it could also be useful to pass the model instance as a reference via the option array in constructor ?

    By François Boukhalfa on Apr 14, 2010

  25. Can you provide a simple example with subforms and m:m relations? I have a modular setup where the forms are located in modules/admin/forms

    How can I configure this to work wit mvc setup?

    ‘$this->getPluginLoader(self::FORM)->addPrefixPath(‘My_Form’, ‘My/Form’);’ works

    By Cochuyt Joeri on Apr 27, 2010

  26. Thanks for this, it’s working beautifully! What do you think of adding the following two features?

    protected $_required = array(); // array of field elements to setRequired()

    protected $_errorMessages=array(); // array of custom error messages as $field=>$message

    By @talentedmrjones on May 11, 2010

  27. Great Article.
    But how can we fill combo box from database using this form. Help will be appreciated. Thanks.

    By Ajmal on Jul 27, 2011

  28. Ajmal, the many-to-one relations will be displayed as a select box. If you need something else displayed as such, you’ll need to change how the elements are created.

    By Jani Hartikainen on Jul 27, 2011

  29. Greeat job!!

    but what about performance?
    Before I finded your job, I was firstly thinking about make some kind of similar script, but that actually writes down the forms files, and not creating the form on the fly, because you probably don’t change your database so often.
    So what about making a script that writes down the needed forms!?
    I was thinking to extend this clases to do that, what do you think?

    By Ricardo Buqut on Aug 19, 2011

  30. Ricard, the performance has been quite good at least when I’ve used it.

    If you want to extend this to produce separate form classes it shouldn’t be too hard I think. It might even be as simple as just taking the resulting form, and serializing it.

    By Jani Hartikainen on Aug 20, 2011

  1. 4 Trackback(s)

  2. Oct 10, 2008: Zend_Form’s from Doctrine models: Part 2 | CodeUtopia
  3. Jan 19, 2009: Complex custom elements in Zend_Form | CodeUtopia
  4. Nov 25, 2009: 網站製作學習誌 » [Web] 連結分享
  5. Jun 11, 2011: Doctrine 2 adapter for Zend_Form model form generator | CodeUtopia - The blog of Jani Hartikainen

Post a Comment

You can use some HTML (a, em, strong, etc.). If you want to post code, use <pre lang="PHP">code here</pre> (you can replace PHP with the language you are posting)