A few months ago I wrote an article about how to edit the sfFormExtraPlugin to respect metadata for ‘associated’ (selected) items. Back then I was able to get this to work ‘out-of-the-box’ because MySQL was playing nicely, but after a few weeks I found that the sort order wasn’t being respected (something arbitrary about mysql’s uuid).

Being that I don’t care much for ‘unexplained’ behavior I decided to force the a sort order by adding a new ‘sort_id’ (metadata) column on the linking table (LinkingAuthorBook). Now with the schema ready to support the saving of sort_id, the trick is how to save the sort_id during the form save process, which is what this article will demonstrate.

This article was written for use with Symfony 1.4, but it shouldn’t be a problem applying it to older versions as well.

We will be using the schema outlined here as a basis for this article, so be sure you have your schema in place and model up-to-date before continuing.  If you are following this article verbatim you will be making these changes to lib/form/doctrine/AuthorForm.class.php, otherwise if you are using an existing codebase, you will need to decide where the following changes will be made.

This article assumes the importance of saving books linked to an author in chronological order, which is to demonstrate the importance of maintaining an order, how you implement the following ides within your own projects is entirely up to you. Also, I tried to make the following ideas as simple as possible, so hopefully they aren’t too hard to understand.

Override doSave()

Pretty much a verbatim cut and paste from the base class (lib/form/doctrine/base/BaseLinkingAuthorBook.class.php), so we can add our own custom functions. Note that I am calling on the parent’s parent method vs the parent method because we don’t want to do the work twice.

//lib/form/doctrine/AuthorForm.class.php

protected function doSave($con = null)
{
    $this->saveBookList($con); //Default - Found in BaseLinkingAuthorBook.class.php

    BaseFormDoctrine::doSave($con); //Notice we aren't calling parent::doSave()

    LinkingAuthorBook::saveSortIds($this->getObject()->getId(), $this->getLinkingAuthorBookSortIds()); //Why we overrode, this is our custom stuff
}

Override updateDefaultsFromObject()

This method retrieves the default values saved to an existing object, by overriding it we can dictate how we want the data pulled. By default the data is  retrieved via ‘getPrimaryKeys()’, if you notice below we are using a custom getter instead.

//lib/form/doctrine/AuthorForm.class.php

public function updateDefaultsFromObject()
{
    BaseFormDoctrine::updateDefaultsFromObject(); //Notice, no call to parent::updateDefaultsFromObject()

    if (isset($this->widgetSchema['book_list']))
    {
        $this->setDefault('book_list', LinkingAuthorBook::getPrimaryKeysByAuthorIdOrderBySortId($this->object->getId()); //We are using a custom getter vs the default $this->object->Book->getPrimaryKeys()
    }
}

Add custom primary keys getter

The following method will return primary keys based ordered by sort_id (asc), which is used by updateDefaultsFromObject().

//lib/model/doctrine/LinkingAuthorBook.class.php

public static function getPrimaryKeysByAuthorIdOrderBySortId($authorId)
{
    $primaryKeys = array();

    $q =
        Doctrine_Query::create()
            ->from('LinkingAuthorBook link')
            ->where('link.author_id = ?', array($authorId))
            ->orderBy('link.sort_id');

    $results = $q->execute();

    foreach ($results as $result) {
        $primaryKeys[] = $result->getBookId();
    }

    return $primaryKeys;
}

Override saveBookList()

This is a verbatim copy and paste from the base class except for the section marked as ‘Sort Ids’. Be sure you add it exactly as shown below. DO NOT COPY the following code verbatim, the code before and after the ‘Sort Ids’ block is there for reference only.

//lib/form/doctrine/AuthorForm.class.php

public function saveBookList($con = null)
{
    if (!is_array($values))
    {
        $values = array();
    }

    //-----
    //Sort Ids
    //-----
    $count = 1;

    foreach ($values as $value)
    {
        $sortIds[$value] = $count++;
    }

    $this->setLinkingAuthorBookSortIds($sortIds); //The reason for a custom member, setter, and getter
    //-----

    $unlink = array_diff($existing, $values);
}

Add member var, setter and getter

Using member var, setter and getter will allow us to save the sortids when the saveBookList() method is called, which we can reference later (during doSave()).

//lib/form/doctrine/AuthorForm.class.php

/**
 * This var will store linking book author sort ids
 *     Spelled out in case order of entry is critical for other linking stuff
 *
 * @var array
 */
 protected $linkingAuthorBookSortIds;

 /**
 * Setter for linking drink instruction sort ids
 *
 * @param array $linkingAuthorBookSortIds
 */
 public function setLinkingAuthorBookSortIds($sortIds)
 {
     $this->linkingAuthorBookSortIds = $sortIds;
 }

 /**
 * Getter for linking drink instruction sort ids
 *
 * @return array
 */
 public function getLinkingAuthorBookSortIds()
 {
     return $this->linkingAuthorBookSortIds;
 }

Make changes to sfFormExtraPlugin

Follow the instructions outlined in the following article. Basically you will need to make some changes to the render() method to ensure it respects the order in which you retrieved ‘associated’ values (the reason why we overrode updateDefaultsFromObject()).

Custom method to update rows

You want to add this method to your lib/model/doctrine/LinkingAuthorBook.class.php file. This is the method that will actually update the rows with their corresponding sort_ids.

//lib/model/doctrine/LinkingAuthorBook.class.php

public static function saveSortIds($authorId, $sortIds)
{
    if (empty($authorId))
    {
        throw new Exception('Invalid authorId');
    }

    elseif (empty($sortIds))
    {
        throw new Exception('Invalid sortIds');
    }

    $q =
        Doctrine_Query::create()
            ->update('LinkingAuthorBook link');

    foreach ($sortIds as $bookId => $sortId)
    {
        $q
            ->set('link.sort_id', $sortId)
            ->where('link.author_id = ?', array($authorId))
            ->andWhere('link.book_id = ?', array($bookId));

        $q->execute();
    }
}

Conclusion

At this point you should be noticing that rows in your linking table are being saved with sort_id, which are then displayed in correct order  in the ‘associated’ section of your double lists. If not, check the changes you made, the first thing I was looked into was to be sure the data was being saved in the database correctly. If they aren’t, check the changes you made to the AuthorForm.class.php and LinkingAuthorBook.class.php files.

Hope this helped!