Para comenzar la categoría del maravilloso framework PHP Symfony, que mejor que hacerlo que con su generador de backend/administrador. Cuando generamos una relación de uno a muchos (one-to-many), Symfony, por defecto, nos crea un formulario con un campo select para que escojamos la clave foránea. Este comportamiento es más que correcto. Pero, ¿qué ocurre cuando ese combo desplegable contiene más de 10.000 registros?

Para solucionar este problema, voy a explicar una funcionalidad que he utilizado estos últimos días (ya la había usado en otros proyectos anteriormente) y que consiste en convertir un despegable en un campo con auto-completado.

Antes de nada, y como requisito, debemos tener instalado el plugin sfFormExtraPlugin. En caso de que no lo tengamos instalado, tan solo debemos ir a la página web del plugin, y seguir las instrucciones de instalación. Una vez instalado, podremos continuar implementando nuestra nueva funcionalidad.

Para ello, debemos añadir lo siguiente en el formulario donde queramos el auto-completado. Yo voy a seguir el ejemplo de una relación entre un libro y su autor. Aclarar que en este caso, un libro solo tendrá un autor, y un autor podrá haber escrito más de un libro.

<?php
// lib/form/doctrine/BookForm.class.php
$this->getWidget('author_id')->setOption('renderer_class', 'sfWidgetFormDoctrineJQueryAutocompleter');
$this->getWidget('author_id')->setOption('renderer_options', array(
    'model' => 'Author',
    'url' => sfContext::getInstance()->getRouting()->generate('ajax_authors')
));

Con este sencillo código, y gracias al widget sfWidgetFormDoctrineJQueryAutocompleter, habremos convertido nuestro desplegable en un campo de texto. Ahora nos queda ver cómo devolveremos los autores. Si nos fijamos, vemos una ruta "ajax_authors" que tendremos que definir en nuestro routing.yml. Esta ruta será una petición GET a la acción del módulo que nosotros indiquemos. Por ejemplo:

ajax_authors:
    url:   /authors/ajax
    param: { module: author, action: ajax }

Una vez definida la ruta, vamos a implementar el código necesario para que todo funcione.

<?php
// apps/application/modules/author/actions/actions.class.php
public function executeAjax($request)
{
    $this->getResponse()->setContentType('application/json');

    $query = AuthorTable::getInstance()->ajaxAuthorsQuery(
        $request->getParameter('q'),
        $request->getParameter('limit')
    );

    $authors = array();
    foreach ($query as $author) {
        $authors[$author->getId()] = (string) $author;
    }
    
    return $this->renderText(json_encode($authors));
}

En este caso, vemos que el método que realiza la consulta está en la clase AuthorTable. Podríamos haber hecho la lógica aquí mismo, pero por limpieza y organización, la hemos sacado fuera. Este es el código del método ajaxAuthorsQuery, al cual se le pasan los parámetros de qué/quién estamos buscando, y cuantos resultados nos mostrará (por defecto, 10).

<?php
// lib/model/doctrine/AuthorTable.class.php
public function ajaxAuthorsQuery($q, $limit)
{
    $q = self::getInstance()->createQuery('a')
        ->addWhere('a.name LIKE ?', '%'. $q .'%')
        ->orderby('a.name ASC')
        ->limit($limit);

    return $q->execute();
}

Y con esto tendríamos implementado nuestro campo con auto-completado. Me parece la mejor forma de asignar un valor que ya existe en nuestra base de datos cuando el conjunto de posibles resultados es muy grande.