How to add an autocomplete field in a Symfony2 form for a collection and using Propel?

16,497

The solution I ultimately went for is slightly different from my previous answer. I ended up using a "text" field type instead of a "collection" field type in my "ReaderType" form, since I ended up using the Loopj Tokeninput jQuery plugin which allows for selecting multiple objects (e.g. "Favorite Book") to associate to my main object (e.g. "Reader" object) in the form. Considering that, I created a "Data Transformer" for transforming the objects' ids passed (in a comma separated list in the text field) into a Propel Object Collection. The code is shared as follows, including a sample ajax object controller.

The key part of the "ReaderType" form looks as follows:

$transformer = new BooksToIdsTransformer();
$builder->add(
    $builder->create('books', 'text', array(
        'required' => false,
    ))->addModelTransformer($transformer)
);

The "Data Transformer" file looks like this:

// src/MyVendor/MyBundle/Form/DataTransformer/BooksToIdsTransformer.php
namespace MyVendor\MyBundle\Form\DataTransformer;

use \PropelObjectCollection;
use Symfony\Component\Form\DataTransformerInterface;
use Symfony\Component\Form\Exception\UnexpectedTypeException;
use MyVendor\MyBundle\Model\BookQuery;

class BooksToIdsTransformer implements DataTransformerInterface
{
    public function transform($books)
    {
        if (null === $books) {
            return "";
        }

        if (!$books instanceof PropelObjectCollection) {
            throw new UnexpectedTypeException($books, '\PropelObjectCollection');
        }
        $idsArray = array();
        foreach ($books as $book) {
            $idsArray[] = $book->getId();
        }
        $ids = implode(",", $idsArray);
        return $ids;
    }

    public function reverseTransform($ids)
    {
        $books = new PropelObjectCollection();

        if ('' === $ids || null === $ids) {
            return $books;
        }

        if (!is_string($ids)) {
            throw new UnexpectedTypeException($ids, 'string');
        }
        $idsArray = explode(",", $ids);
        $idsArray = array_filter ($idsArray, 'is_numeric');
        foreach ($idsArray as $id) {
            $books->append(BookQuery::create()->findOneById($id));
        }
        return $books;
    }
}

The ajax controller that returns a json collection of "books" to the Tokeninput autocomplete function is as follows:

namespace MyVendor\MyBundle\Controller;

use Symfony\Bundle\FrameworkBundle\Controller\Controller;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use MyVendor\MyBundle\Model\BookQuery;


class ClassAjaxController extends Controller
{
    public function bookAction(Request $request)
    {
        $value = $request->get('q');

        $books = BookQuery::create()->findByName('%'.$value.'%');

        $json = array();
        foreach ($books as $book) {
            $json[] = array(
                'id' => $book->getId(),
                'name' => $book->getName()
            );
        }

        $response = new Response();
        $response->setContent(json_encode($json));

        return $response;
    }
}

And finally, the router in the "routing.yml" file:

ajax_book:
    pattern:  /ajax_book
    defaults: { _controller: MySiteBundle:ClassAjax:book }
Share:
16,497

Related videos on Youtube

RayOnAir
Author by

RayOnAir

Updated on October 09, 2022

Comments

  • RayOnAir
    RayOnAir over 1 year

    I'm using Symfony 2.1 forms with PropelBundle and I'm trying to refactor a form that had a drop-down list of objects (to select from) to instead use a jquery autocomplete field (working with AJAX). For the dropdown list I was using the following code (which worked perfectly for the drop-down) in my form type:

    $builder->add('books', 'collection', array(
        'type'          => 'model',
        'options'       => array(
            'class'     => 'MyVendor\MyBundle\Model\Book',
            'property'  => 'title',
        ),
        'allow_add'     => true,
        'allow_delete'  => true,
        'by_reference'  => false,
        'required'      => false,
    ));
    

    For the sake of giving a little context, let's say we are creating a new "Reader" object and that we would like to select the Reader's favorite books from a list of available "Book" objects. A collection type is used so that many "favorite books" can be selected in the new "Reader" form. Now, I would like to change the above to use autocomplete. For doing so, I tried to implement a Data Transformer to be able to get a Book object from a simple text field that could be used for the Autocomplete function to pass the Book ID as indicated in the answer to this Question. However, I was not able to figure out how to make the Data Transformer work with a collection type and Propel classes. I created a BookToIdTransformer class as indicated in the Symfony Cookbook and tried the following in the "ReaderType" file:

    $transformer = new BookToIdTransformer();
    $builder->add(
            $builder->create('books', 'collection', array(
                'type'          => 'text',
                'allow_add'     => true,
                'allow_delete'  => true,
                'by_reference'  => false,
                'required'      => false,
            ))->addModelTransformer($transformer)
    );
    

    With the above, I get a "Call to undefined method: getId" exception (apparently the Transformer expects a PropelCollection of Books, not a single Book object..). Does anyone know how to go about it? or let me know if there are other ways to implement the autocomplete in Symfony using Propel and allowing for selecting multiple objects (e.g. a collection of books)?

  • Narretz
    Narretz over 11 years
    Thanks for the thorough explanation. If you have many Controller actions that should return JSON responses, I recommend FOSRestBundle
  • timaschew
    timaschew almost 11 years
    can you add a snippet of your view / form, please?
  • RayOnAir
    RayOnAir almost 11 years
    @timaschew if I remember well (I ended up not needing it), the code in the view is just the standard one used for symfony forms. The field is a regular text input. The autocomplete functionality is added to that input by the Tokeninput plugin using javascript using the id of the input: $("#my-text-input").tokenInput("/url/to/your/script/");