DQL, Creating and Saving New Objects, and Passing Information Between Forms

With a highly normalized database/schema like the one used in Agasti, form submission for the creation of a complete, highly normalized object has required a great deal of research into and configuration of Symfony's forms.

The framework conveniently provides list-boxes for many-to-many relationshops on the newSuccess.php and editSuccess.php pages of modules based on models that have these many-to-many relationships. Unfortunately, in some of the more complex cases (such as between agPerson, agPersonName, and agPersonNameType), these lists do not work together in a way that ensures the proper relationships are created in the database.

This makes sense when you consider the models we are working with: agPerson (a unique ID to identify an agPerson object), agPersonName (a lookup table populated with name values of all types: Bart, Lisa, Homer, Marge, Wiggum, Smithers, Spike, Stinky, etc.) and agPersonNameType (definitions of name type: Given/First, Middle, Family/Last, Alias, Maiden, Nickname, etc.). Each of these three models can have many-to-many relations with the other two, and these relations are made in the agPersonMjAgPersonName table. To illustrate how this works, take a look at these simplified tables:

agPerson    agPersonName            agPersonNameType
+----+      +----+-------------+    +----+------------------+
| id |      | id | person_name |    | id | person_name_type |
+----+      +----+-------------+    +----+------------------+
|  1 |      |  1 | John        |    |  1 | given            |
|  2 |      |  2 | Lisa        |    |  2 | middle           |
|  3 |      |  3 | Anderson    |    |  3 | family           |
+----+      |  4 | Frederick   |    +----+------------------+
            |  5 | David       |
            +----+-------------+
            
agPersonMjAgPersonName
+----+-----------+------------------+---------------------+
| id | person_id | person_name_id   | person_name_type_id |
+----+-----------+------------------+---------------------+
|  1 |  1        |  5 (David)        | 1 (Given)          |
|  2 |  1        |  3 (Anderson)     | 3 (Family)         |
+----+-----------+------------------+---------------------+
|  3 |  2        |  2  (Lisa)       | 1 (Given)           |
|  4 |  2        |  4  (Frederick)  | 3 (Family)          |
+----+-----------+------------------+---------------------+
|  5 |  3        |  4  (Frederick)  | 1 (Given)           |
|  6 |  3        |  1  (John)       | 2 (Middle)          |
|  7 |  3        |  5  (David)      | 3 (Family)          |
+----+-----------+------------------+---------------------+

(Divisions between ''person_id'' sections have been made and the values of ''person_name'' and ''person_name_type'' have been appended to their IDs for ease of interpretation.)

Entire names for three people are defined above: David Anderson, Lisa Frederick, and Frederick John David. David and Lise use only two of the available name types, while Frederick uses all three. Also note the use of Frederick and David as both Given and Family names, the normalization of the Agasti database allows for this, and thus prevents the duplification of name data. These relations would be impossible to create accurately if an end-user were simply to choose an equal number of agPersonName values and agPersonNameType values from select-lists: while the relation between agPerson both name tables would be made correctly, there is no way to determine the relations between each agPersonName and each agPersonNameType.

To solve this problem, we've used the Embedded Forms feature of Symfony, and made some modifications to the way they're handled.

This is the code in apps/frontend/lib/forms/doctrine/agPersonForm.class.php the embeds our container form:

$emb = new agEmbeddedNamesForm($this->object);
$this->embedForm('name',$emb); 

And this is from apps/frontend/lib/forms/doctrine/agEmbeddedNamesForm (the container form embedded in the lines above):

$emb = new agEmbeddedAgPersonNameForm();#agEmbeddedAgPersonMjAgPersonNameForm();
$type = new agEmbeddedAgPersonMjAgPersonNameForm();
$emb->embedForm($name_type->getPersonNameType(), $type);
$this->embedForm($name_type->getPersonNameType(), $emb);

The above code is used in a loop that creates a new agEmbeddedAgPersonNameForm (as $emb) for each agPersonNameType. In addtion, a new agEmbeddedAgPersonMjAgPersonNameForme (as $type) is embedded within each of the agEmbeddedAgPersonNameForm forms.

Our modifications to the way embedded forms are handles takes place in agPersonForm.class.php. Here's where it gets fun:

  public function saveEmbeddedForms($con = null, $forms = null)
  {
    if (is_array($forms))
    {
      foreach ($forms as $key => $form)
      {
        if ($form instanceof agEmbeddedAgPersonNameForm)
        {
          if ($form->getObject()->isModified())
          {
            $eforms = $form->getEmbeddedForms();

            foreach ($eforms as $kay => $eform)
            {
              if ($eform instanceof agEmbeddedAgPersonMjAgPersonNameForm)
              {
                if ($eform->getObject()->isModified())
                {
                  $name_lookup = $form->getObject()->person_name;
                  $q = Doctrine_Query::create()
                      ->select('a.id')
                      ->from('agPersonName a')
                      ->where('a.person_name = ?', $name_lookup);
                                
                  if($queried = $q->fetchOne())
                  {
                     $name_id = $queried->get('id');
                     $eform->getObject()->person_name_id=$name_id;
                     $eform->getObject()->person_id = $this->getObject()->id;
                     $form->saveEmbeddedForms();
                     unset($forms[$key]);
                  }
                  elseif(!$queried = $q->fetchOne())
                  {
                     $new_name = new agPersonName();
                     $new_name->person_name = $form->getObject()->person_name;
                     $new_name->save();
                     $eform->getObject()->person_name_id = $new_name->id;
                     $eform->getObject()->person_id = $this->getObject()->id;
                     $form->saveEmbeddedForms();
                  }
                }
              }
            }
          }
          else
          {
            unset($forms[$key]);
          }
        }
      }
    }

    return parent::saveEmbeddedForms($con, $forms);
  }

We're doing a couple of neat things here, namely, we look up the value entered into the person_name field from the agEmbeddedPersonNameForm against that tables current values in the database. This table is made available with a Doctrine_Query (created at $q = Doctrine_Query::create()), and accessed with the calls to fetchOne() in the if and elseif statements. In the if statement, if the name value exists, we return its ID and pass it to the agEmbeddedPersonMjAgPersonNameForm ($eform). The new agPerson and agPersonMjAgPersonName objects are then saved via $form→saveEmbeddedForms. After that, we actually remove the agEmbeddedAgPersonNameForm from our array of $forms (via unset($forms[$key])), since we don't want or need to create a new agPersonName object.

What's most important though is the elseif statement and the introduction of an agPersonName object as the variable $new_name. In the elseif, the submitted name value was not matched to any currently in the database. The new agPersonName object is initialized as $new_name, and then assigned the submitted name value (retrieved with $form→getObject()→person_name).

The next line, $new_name→save(), is what gets the newly submitted name value into the database, and does so without saving all the embedded forms. This way, the name value is in the database, and it and it's ID are both usable by the current form. When $form→saveEmbeddedForms() is called, the ID value of the new agPersonName object is used to populate the agPersonMjAgPersonName table and ensure that the proper mapping is done between agPerson, agPersonName, and agPersonNameType. $form→saveEmbeddedForms saves the $eform embedded within $form, and $eform is an instance of agPersonEmbeddedAgPersonMjAgPersonNameForm, populated with the person_id of the agPerson being created, the name_id of the agPersonName saved with $new_name→save(), and the name_type_id assigned to the embedded form upon page generation by code in apps/frontend/lib/forms/doctrine/agEmbeddedNamesForm.


QR Code
QR Code agasti:mayon:dql_creating_and_saving_new_objects_and_passing_information_between_forms (generated for current page)