Development

EmbeddedForms (diff)

You must first sign up to be able to contribute.

Changes from Version 1 of EmbeddedForms

Show
Ignore:
Author:
dmccullough (IP: 128.228.113.48)
Timestamp:
08/25/10 18:44:44 (7 years ago)
Comment:

--

Legend:

Unmodified
Added
Removed
Modified
  • EmbeddedForms

    v0 v1  
     1= Introduction = 
     2 
     3Due to the heavy normalization of our database structure, our high level business objects are composed of a number of smaller, related data points that all connect to a main Id that exists in a table of only Id's (eg, agPerson). In certain instances, we can use Symfony's auto-generated lists to get data into the lower level tables, but, for sub-objects that are very complex in their own right, the use of embedded forms is necessary. Embedded Forms, at least in the way we'll need to be using them, are not an extensively documented feature Symfony. They allow you to attach the form for a sub-object of related-object into the form (and then page) of your main object. These forms then act as one form from the frontend, and add, update, or delete data from multiple database tables with one submit action. 
     4 
     5All of the research and development on embedding forms has aimed at implementing the full functionality of the agPerson business object, specifically enabling a person to have one or more names (in our test case, Given, Middle, Family, Maiden, and Alias). This documentation will use agPerson, agPersonName, agPersonNameType, and agPersonMjAgPersonName for its basis and examples. 
     6 
     7= Model Descriptions = 
     8 
     9Descriptions of the relevant models follow. 
     10 
     11== agPerson (ag_person) == 
     12 
     13This is our high-level object, the unique Id on which an agPerson and all its related values depend. agPerson is only an Id. The created_ad and updated_at columns (found on this and the other table's discussed in this document) make use of Doctrine's auto-time-stamping feature, which is set with: 
     14 {{{ 
     15 
     16    actAs: 
     17      Timestampable: 
     18}}} 
     19 
     20 {{{ 
     21 
     22 
     23    +------------+------------+------+-----+---------+----------------+ 
     24    | Field      | Type       | Null | Key | Default | Extra          | 
     25    +------------+------------+------+-----+---------+----------------+ 
     26    | id         | bigint(20) | NO   | PRI | NULL    | auto_increment | 
     27    | created_at | datetime   | NO   |     | NULL    |                | 
     28    | updated_at | datetime   | NO   |     | NULL    |                | 
     29    +------------+------------+------+-----+---------+----------------+ 
     30}}} 
     31 
     32== agPersonName (ag_person_name) == 
     33 
     34A data-dictionary for names. Each agPersonName has its string value and a unique Id. 
     35 {{{ 
     36 
     37    +-------------+-------------+------+-----+---------+----------------+ 
     38    | Field       | Type        | Null | Key | Default | Extra          | 
     39    +-------------+-------------+------+-----+---------+----------------+ 
     40    | id          | bigint(20)  | NO   | PRI | NULL    | auto_increment | 
     41    | person_name | varchar(64) | NO   | UNI | NULL    |                | 
     42    | created_at  | datetime    | NO   |     | NULL    |                | 
     43    | updated_at  | datetime    | NO   |     | NULL    |                | 
     44    +-------------+-------------+------+-----+---------+----------------+ 
     45}}} 
     46 
     47== agPersonNameType (ag_person_name_type) == 
     48 
     49A data-dictionary for name types. Each agPersonNameType has its string value and a unique Id, which allows for the assignation of various kinds of names: first, given, family, middle, alias, etc.. 
     50 {{{ 
     51 
     52    +------------------+-------------+------+-----+---------+----------------+ 
     53    | Field            | Type        | Null | Key | Default | Extra          | 
     54    +------------------+-------------+------+-----+---------+----------------+ 
     55    | id               | smallint(6) | NO   | PRI | NULL    | auto_increment | 
     56    | person_name_type | varchar(30) | NO   | UNI | NULL    |                | 
     57    | app_display      | tinyint(1)  | NO   |     | 1       |                | 
     58    | created_at       | datetime    | NO   |     | NULL    |                | 
     59    | updated_at       | datetime    | NO   |     | NULL    |                | 
     60    +------------------+-------------+------+-----+---------+----------------+ 
     61}}} 
     62 
     63== agPersonMjAgPersonName (ag_person_mj_ag_person_name) == 
     64 
     65This is the join table, where a Person is combined with its Names and Name Types to give it a full Name sub-object. An agPersonMjAgPersonName entry has its own unique Id, the unique Id of the agPerson with which it is associated (person_id), the unique Id of the agPersonName with which it is associated (person_name_id), and the unique Id of the agPersonNameType with which it is associated (person_name_type_id). 
     66 {{{ 
     67 
     68    +---------------------+-------------+------+-----+---------+----------------+ 
     69    | Field               | Type        | Null | Key | Default | Extra          | 
     70    +---------------------+-------------+------+-----+---------+----------------+ 
     71    | id                  | bigint(20)  | NO   | PRI | NULL    | auto_increment | 
     72    | person_id           | bigint(20)  | NO   | MUL | NULL    |                | 
     73    | person_name_id      | bigint(20)  | NO   | MUL | NULL    |                | 
     74    | person_name_type_id | smallint(6) | NO   | MUL | NULL    |                | 
     75    | is_primary          | tinyint(1)  | NO   |     | NULL    |                | 
     76    | created_at          | datetime    | NO   |     | NULL    |                | 
     77    | updated_at          | datetime    | NO   |     | NULL    |                | 
     78    +---------------------+-------------+------+-----+---------+----------------+ 
     79}}} 
     80 
     81= Building a Name = 
     82 
     83The full definition of an agPersonName is achieved with the ag_person_mj_ag_person_name table. The contents of a table that describes four people with three names/name-types might look like this: 
     84 
     85 {{{ 
     86 
     87    +----+-----------+----------------+---------------------+ 
     88    | id | person_id | person_name_id | person_name_type_id | 
     89    +----+-----------+----------------+---------------------+ 
     90    |  1 |         1 |              5 |                   1 | 
     91    |  2 |         1 |              3 |                   2 | 
     92    |  3 |         1 |              4 |                   3 | 
     93    +----+-----------+----------------+---------------------+ 
     94    |  4 |         2 |              8 |                   1 | 
     95    |  5 |         2 |              5 |                   2 | 
     96    |  6 |         2 |             10 |                   3 | 
     97    +----+-----------+----------------+---------------------+ 
     98    |  7 |         3 |              1 |                   1 | 
     99    |  8 |         3 |              6 |                   2 | 
     100    |  9 |         3 |              2 |                   3 | 
     101    +----+-----------+----------------+---------------------+ 
     102    | 10 |         4 |              7 |                   1 | 
     103    | 11 |         4 |             18 |                   2 | 
     104    | 12 |         4 |              5 |                   3 | 
     105    +----+-----------+----------------+---------------------+ 
     106}}} 
     107 
     108Or, if we replace the person_name_ids and person_name_type_ids with the assigned values we actually want: 
     109 {{{ 
     110 
     111    +----+-----------+----------------+---------------------+ 
     112    | id | person_id | person_name_id | person_name_type_id | 
     113    +----+-----------+----------------+---------------------+ 
     114    |  1 |         1 |         Thomas |          Given Name | 
     115    |  2 |         1 |        Michael |         Middle Name | 
     116    |  3 |         1 |          Smith |         Family Name | 
     117    +----+-----------+----------------+---------------------+ 
     118    |  4 |         2 |           John |          Given Name | 
     119    |  5 |         2 |         Thomas |         Middle Name | 
     120    |  6 |         2 |      Schneider |         Family Name | 
     121    +----+-----------+----------------+---------------------+ 
     122    |  7 |         3 |          Alice |          Given Name | 
     123    |  8 |         3 |         Louise |         Middle Name | 
     124    |  9 |         3 |         Vargas |         Family Name | 
     125    +----+-----------+----------------+---------------------+ 
     126    | 10 |         4 |           Lisa |          Given Name | 
     127    | 11 |         4 |      Elizabeth |         Middle Name | 
     128    | 12 |         4 |         Thomas |         Family Name | 
     129    +----+-----------+----------------+---------------------+ 
     130}}} 
     131     
     132Notice the reuse of the name Thomas as a Given Name, Middle Name, and Family Name. The use of the agPerson, agPersonName, and agPersonNameType allows us to avoid separate tables for different name types, and the duplication of content they would entail. 
     133 
     134= The Forms (and Embedded Forms) = 
     135 
     136== agPersonForm == 
     137 
     138To implement the creation of an agPerson along with an agPersonName for at least one agPersonNameType, you begin by making a copy of agPerson.class.php from lib/form/doctrine/ to apps/frontend/lib/form/doctrine (all the form files worked within this document will be stored in this path) and then customizing it. 
     139 
     140Unaltered: 
     141 {{{ 
     142 
     143    1 class agPersonForm extends BaseagPersonForm 
     144    2 { 
     145    3   public function configure() 
     146    4   { 
     147    5   } 
     148    6 } 
     149}}} 
     150 
     151Customized to implement embedded forms for agPersonName: 
     152 {{{ 
     153 
     154     1 class agPersonForm extends BaseagPersonForm 
     155     2 { 
     156     3   public function configure() 
     157     4   { 
     158     5     unset($this['created_at'],  
     159     6           $this['updated_at'], 
     160     7           $this['ag_nationality_list'], 
     161     8           $this['ag_religion_list'], 
     162     9           $this['ag_profession_list'], 
     163    10           $this['ag_language_list'], 
     164    11           $this['ag_country_list'], 
     165    12           $this['ag_ethnicity_list'], 
     166    13           $this['ag_sex_list'], 
     167    14           $this['ag_marital_status_list'], 
     168    15           $this['ag_import_list'], 
     169    16           $this['ag_residential_status_list'], 
     170    17           $this['ag_import_type_list'], 
     171    18           $this['ag_account_list'], 
     172    19           $this['ag_phone_contact_type_list'], 
     173    20           $this['ag_email_contact_type_list'], 
     174    21           $this['ag_address_contact_type_list'], 
     175    22           $this['ag_phone_contact_list'], 
     176    23           $this['ag_email_contact_list'], 
     177    24           $this['ag_scenario_list'], 
     178    25       $this['ag_person_name_list'], 
     179    26           $this['ag_person_name_type_list']); 
     180    27  
     181    28     $this->embedForm('name', new agEmbeddedNamesForm($this->object)); #This line embeds our name container form, agEmbeddedNamesForm, and passes the current agPerson object to it. 
     182    29   } 
     183    30  
     184    31   public function updateObjectEmbeddedForms($values, $forms = null) 
     185    32   { 
     186    33     if (is_array($forms)) 
     187    34     { 
     188    35       foreach ($forms as $key => $form) 
     189    36       { 
     190    37         if ($form instanceof agEmbeddedAgPersonMjAgPersonNameForm) 
     191    38         { 
     192    39           $formValues = isset($values[$key]) ? $values[$key] : array(); 
     193    40  
     194    41           if (agEmbeddedAgPersonMjAgPersonNameForm::formValuesAreBlank($formValues)) 
     195    42           { 
     196    43             if($id = $form->getObject()->getId()) 
     197    44             { 
     198    45               $this->object->unlink('agPersonMjAgPersonName', $id); 
     199    46               $form->getObject()->delete(); 
     200    47             } 
     201    48  
     202    49             unset($forms[$key]); 
     203    50           } 
     204    51  
     205    52           #unset($forms[$key]); 
     206    53         } 
     207    54       } 
     208    55     } 
     209    56  
     210    57     return parent::updateObjectEmbeddedForms($values, $forms); 
     211    58   } 
     212    59  
     213    60   public function saveEmbeddedForms($con = null, $forms = null) 
     214    61   { 
     215    62     if (is_array($forms)) 
     216    63     { 
     217    64       foreach ($forms as $key => $form) 
     218    65       { 
     219    66         if ($form instanceof agEmbeddedAgPersonMjAgPersonNameForm) 
     220    67         { 
     221    68           if ($form->getObject()->isModified()) 
     222    69           { 
     223    70             $form->getObject()->agPerson = $this->object; 
     224    71           } 
     225    72           else 
     226    73           { 
     227    74             unset($forms[$key]); 
     228    75           } 
     229    76         } 
     230    77       } 
     231    78     } 
     232    79  
     233    80     return parent::saveEmbeddedForms($con, $forms); 
     234    81   } 
     235    82 } 
     236}}} 
     237     
     238Line 28 is the most important part of this for now. It embeds our container (agEmbeddedNamesForm) form into the agPerson form and passes the agPerson object to it. agEmbeddedNamesForm is not based on any table or model, it is simply there to hold all of the agEmbeddedAgPersonMjAgPersonName forms, which is where the real action takes place. 
     239 
     240updateObjectEmbeddedForms and saveEmbeddedForms override Symfony's default functions of the same name. They're necessary to ensure that forms update properly and that agPersonMjAgPersonName values can be deleted from a full agPerson (if an Alias was unneeded, for example). 
     241 
     242The many fields that are unset in configure are those that Symfony automatically adds to the agPersonForm. Some have been removed because they do not need any data input from the end user (created_at, updated_at), or simply to narrow down the variables encountered while developing and testing (everything else). In the latter case, we will later remove the unset code for these fields. 
     243agEmbeddedNamesForm 
     244 
     245The next class we'll work with is the agEmbeddedNamesForm. This form is created in apps/frontend/lib/form/doctrine. 
     246 {{{ 
     247 
     248     1 class agEmbeddedNamesForm extends sfForm 
     249     2 { 
     250     3   protected $person; 
     251     4  
     252     5   public function __construct(agPerson $person) 
     253     6   { 
     254     7     $this->person = $person; 
     255     8  
     256     9     parent::__construct(); 
     257    10   } 
     258    11  
     259    12   public function configure() 
     260    13   { 
     261    14     $this->name_types = Doctrine::getTable('agPersonNameType')->createQuery('a')->execute(); 
     262    15  
     263    16     foreach ($this->name_types as $name_type) 
     264    17     { 
     265    18       $emb = new agEmbeddedAgPersonMjAgPersonNameForm(); 
     266    19       $emb->setDefault('person_name_type_id', $name_type->getId()); 
     267    20  
     268    21       foreach ($this->person->getAgPersonMjAgPersonName() as $current) 
     269    22       { 
     270    23         if ($current->getPersonNameTypeId() == $name_type->getId()) 
     271    24         { 
     272    25            $emb = new agEmbeddedAgPersonMjAgPersonNameForm($current); 
     273    26         } 
     274    27       } 
     275    28       $this->embedForm($name_type->getPersonNameType(), $emb); 
     276    29     } 
     277    30   } 
     278    31 } 
     279     
     280}}} 
     281 
     282The __construct function is the counterpart to line 28 in agPersonForm and completes the passage of the agPerson object to agEmbeddedNamesForm. 
     283 
     284The configure function is where the bulk of the action happens. Doctrine::getTable on line 14 gives this class access to all values in the ag_person_name_type table, which are needed to define labels and hidden field names for the forms that are embedded on line 28. More information on accessing an entire set of table values can be found here. 
     285 
     286The two loops in lines 16-29 handle determining the form titles and default field values and the embedding of the forms. The name_types that have been accessed on line 14 are looped through, creating a new agEmbeddedAgPersonMjAgPersonNameForm as $emb for each instance of ag_person_name_type, and assigning a default value of that instance's Id to the new agEmbeddedAgPersonMjAgPersonNameForm's person_name_type_id. 
     287 
     288The second loop that compares each of the current agPerson's agPersonMjAgPersonName objects name_type_id values to the Id of the current name_type. If the values are equal, $emb is re-assigned the value of a new agEmbeddedAgPersonMjAgPersonNameForm that has been filled with the agPersonMjAgPersonName that is being evaluated (as $current). After that, $emb is embedded, with it's current values if line 23 returns true, or as a blank form if 23 returns false. 
     289 
     290== agEmbeddedAgPersonMjAgPersonNameForm == 
     291 {{{ 
     292 
     293     1 class agEmbeddedAgPersonMjAgPersonNameForm extends agPersonMjAgPersonNameForm 
     294     2 { 
     295     3   public function configure() 
     296     4   { 
     297     5     parent::configure(); 
     298     6  
     299     7     unset($this['person_id']);   
     300     8   } 
     301     9  
     302    10   public function setup() 
     303    11   { 
     304    12     parent::setup(); 
     305    13  
     306    14      /**** 
     307    15      * setWidgets below sets the person_name_type_id to hidden. The value for this field is assigned in 
     308    16      * agEmbeddedNamesForm and is kept even though the field is hidden. widgetSchema below removes the  
     309    17      * field labels from the person_name_id fields. The table labels, generated for each value in the  
     310    18      * ag_person_name_type table, are what we want and are descriptive of the values that should be     entered  
     311    19      * in this form. 
     312    20      ****/ 
     313    21  
     314    22     $this->setWidgets(array( 
     315    23       'person_name_type_id' => new sfWidgetFormInputHidden(), 
     316    24       'person_name_id'      => new sfWidgetFormDoctrineChoice(array('model' => $this->getRelatedModelName('agPersonName'), 'add_empty' => true)), 
     317    25     )); 
     318    26     $this->widgetSchema->setLabel('person_name_id', false); 
     319    27  
     320    28     $this->setValidators(array( 
     321    29 #      'id'                  => new sfValidatorChoice(array('choices' => array($this->getObject()->get('id')), 'empty_value' => $this->getObject()->get('id'), 'required' => false)), 
     322    30 #      'person_id'           => new sfValidatorDoctrineChoice(array('model' => $this->getRelatedModelName('agPerson'))), 
     323    31       'person_name_id'      => new sfValidatorDoctrineChoice(array('model' => $this->getRelatedModelName('agPersonName'), 'required' => false)), 
     324    32       'person_name_type_id' => new sfValidatorDoctrineChoice(array('model' => $this->getRelatedModelName('agPersonNameType'))), 
     325    33 #      'is_primary'          => new sfValidatorBoolean(), 
     326    34     )); 
     327    35  
     328    36   } 
     329    37  
     330    38   public static function formValuesAreBlank(array $values) 
     331    39   { 
     332    40     $fieldNames = array_diff(Doctrine::getTable('agPersonMjAgPersonName')->getFieldNames(), array( 
     333    41       'id', 
     334    42       'person_id', 
     335    43       'person_name_type_id', 
     336    44       'is_primary', 
     337    45     )); 
     338    46  
     339    47     return parent::_formValuesAreBlank($fieldNames, $values); 
     340    48   } 
     341    49 } 
     342     
     343}}} 
     344 
     345 
     346= Reference Links = 
     347 
     348  * [http://www.blogs.uni-osnabrueck.de/rotapken/2009/03/13/symfony-merge-embedded-form/ Cybso Blog Archive » Symfony: Merge embedded Form] 
     349  * [http://www.symfony-project.org/blog/2008/11/10/call-the-expert-nested-forms-a-real-implementation/ Blog: Call the expert: Nested forms - A real implementation | symfony | Web PHP Framework] 
     350  * [http://www.symfony-project.org/blog/2008/11/12/call-the-expert-customizing-sfdoctrineguardplugin/ Blog | Call the expert: Customizing sfDoctrineGuardPlugin | symfony | Web PHP Framework] 
     351  * [http://www.symfony-project.org/more-with-symfony/1_4/en/06-Advanced-Forms/ The More with symfony book | Advanced Forms | symfony | Web PHP Framework] 
     352  * [http://www.miximum.fr/tutos/466-symfony-form-pick-or-create/ Symfony form : pick or create – Miximum] 
     353  * [http://solveme.wordpress.com/2009/06/30/foreign-key-violation-when-saving-symfony-embedded-forms/ Foreign key violation when saving embedded forms « Error Solved] 
     354  * [http://ezzatron.com/2009/12/03/expanding-forms-with-symfony-1-2-and-doctrine/ Expanding forms with symfony 1.2 and Doctrine]