Ask your Symfony questions! Pay money and get answers fast! (more info)

Help with filter forms (not using admin generator) Symfony

  • SOLVED

Hi,

I would like to use filter forms for my project, but documentation for this seems to be lacking. I like how filter forms work in admin generator, however for my purposes I will not be using the admin generator.

Specifically, I would like to see an example of using filter forms:
- with symfony 1.3 and doctrine
- outside of the admin generator
- involving a join, where one can search on either side of a n:m relationship
- including pagination

For the join, for example, might relate to a n:m relationship between invoices and products, with tables Invoice, InvoiceProduct, Product. One could query for invoices by invoice number, or by product sku, for example.

Once again, I want to use this WITHOUT admin generator.

To receive the award, please be sure to address the specific case described above and offer clear comments on what the specific pieces are accomplishing.

Answers (3)

2010-05-08

michalg answers:

Hello,

I've made example with everything you wanted.

This code isn't most beautiful (especially pagination) but i hope that it will allow to show you how it works.

Form is saved with get, not post, so it generates prettier url.

Page contains form and results are shown as simple list. Here's screenshot http://imagebin.org/96075

Anyway, here's code:
Schema:

Invoice:
columns:
number: { type: string(30), notnull: true }

Product:
columns:
sku: { type: string(30), notnull: true }

InvoiceProduct:
columns:
id_invoice: { type: integer, notnull: true }
id_product: { type: integer, notnull: true }
relations:
Invoice: { local: id_invoice, foreign: id }
Product: { local: id_product, foreign: id }


Fixtures:
As you see i have generated 20 invoices and 20 products. Every invoice contains those 20 products.


Invoice:
<?php for($i=0;$i<20;$i++) : ?>
invoice<?php echo $i; ?>:
number: I<?php echo str_pad($i, 3, "0", STR_PAD_LEFT)."\n"; ?>
<?php endfor; ?>

Product:
<?php for($i=0;$i<20;$i++) : ?>
product<?php echo $i; ?>:
sku: SKU<?php echo str_pad($i, 3, "0", STR_PAD_LEFT)."\n"; ?>
<?php endfor; ?>

InvoiceProduct:
<?php for($i=0;$i<20;$i++) : ?>
<?php for($i2=0;$i2<20;$i2++) : ?>
invoiceproduct<?php echo $i."-".$i2; ?>:
Invoice: invoice<?php echo $i."\n"; ?>
Product: product<?php echo $i2."\n"; ?>
<?php endfor; ?>
<?php endfor; ?>



Form code:

class FilterForm extends sfForm
{
public function configure()
{
$this->widgetSchema['number'] = new sfWidgetFormInput();
$this->widgetSchema['sku'] = new sfWidgetFormInput();

$this->validatorSchema['number'] = new sfValidatorPass();
$this->validatorSchema['sku'] = new sfValidatorPass();

// disable CSRF protection for this form, as we don't want this in url
$this->disableCSRFProtection();
}
}


Action:

class searchActions extends sfActions
{
/**
* Executes index action
*
* @param sfRequest $request A request object
*/
public function executeIndex(sfWebRequest $request)
{
$this->form = new FilterForm();
$this->result = array();
$this->page = $request->getParameter("page", 1);
$this->rowsPerPage = 5;

// binds parameters to form
$this->form->bind(array(
"number" => $request->getParameter("number"),
"sku" => $request->getParameter("sku")
));

// create query with filters
$number = $this->form->getValue("number");
$sku = $this->form->getValue("sku");
$query = Doctrine_Query::create()
->from("Invoice i")
->innerJoin("i.InvoiceProduct ip")
->innerJoin("ip.Product p");

// filters are applied only if they are filled
if ($number != "") $query->addWhere("i.number = ?", $number);
if ($sku != "") $query->addWhere("p.sku = ?", $sku);

// first get count (for pagination)
$this->count = count($query->execute(array(), Doctrine::HYDRATE_SCALAR));

// we want to limit rows, not objects
Doctrine_Manager::getInstance()->setAttribute(Doctrine::ATTR_QUERY_LIMIT, Doctrine::LIMIT_ROWS);

// set offset and limit (for page) and query for result
$query->offset(($this->page - 1) * $this->rowsPerPage);
$query->limit($this->rowsPerPage);

// query our results as scalar, as those are easier for showing as list
$this->result = $query->execute(array(), Doctrine::HYDRATE_SCALAR);

// create links for prev and next page
$this->prevPageUrl = "";
$this->nextPageUrl = "";
$params = $request->getParameterHolder()->getAll();
if ($this->page * $this->rowsPerPage < $this->count)
{
$params['page'] = $this->page + 1;
$this->nextPageUrl = $this->generateUrl("", $params);
}

if ($this->page > 1)
{
$params['page'] = $this->page -1;
$this->prevPageUrl = $this->generateUrl("", $params);
}
}

}


View (indexSucces.php):


<form class = "form" action="" method="get">
<?php echo $form; ?>
<input type="submit" value="Save" />
</form>

Count: <?php echo $count; ?><br/>
Showing: <?php echo ($page - 1) * $rowsPerPage; ?> - <?php echo min($page * $rowsPerPage, $count); ?><br/>

<?php if ($prevPageUrl != "") :?>
<a href = "<?php echo $prevPageUrl?>"> << </a>
<?php endif; ?>

<?php if ($nextPageUrl != "") :?>
<a href = "<?php echo $nextPageUrl?>"> >> </a>
<?php endif; ?>

<table>
<?php foreach($result as $row) :?>
<tr>
<td> <?php echo $row['i_number'] ?></td>
<td> <?php echo $row['p_sku'] ?></td>
</tr>
<?php endforeach; ?>
</table>


webguy comments:

Thank you for the post. I was hoping to use it more in the <em>style</em> of the admin generator, like


protected function buildQuery()
{
$tableMethod = $this->configuration->getTableMethod();
if (null === $this->filters)
{
$this->filters = $this->configuration->getFilterForm($this->getFilters());
}

$this->filters->setTableMethod($tableMethod);

$query = $this->filters->buildQuery($this->getFilters());

$this->addSortQuery($query);

$event = $this->dispatcher->filter(new sfEvent($this, 'admin.build_query'), $query);
$query = $event->getReturnValue();

return $query;
}


One of my issues is I can't make sense of the buildQuery using the joins, etc


webguy comments:

And would like for the main filter function to look along the lines of that from the admin generator,

public function executeFilter(sfWebRequest $request)
{
$this->setPage(1);

if ($request->hasParameter('_reset'))
{
$this->setFilters($this->configuration->getFilterDefaults());

$this->redirect('@contact');
}

$this->filters = $this->configuration->getFilterForm($this->getFilters());

$this->filters->bind($request->getParameter($this->filters->getName()));
if ($this->filters->isValid())
{
$this->setFilters($this->filters->getValues());

$this->redirect('@module');
}

$this->pager = $this->getPager();
$this->sort = $this->getSort();

$this->setTemplate('index');
}


michalg comments:

So, you aren't using admin generator, but you would like to have code similar to that from admin generator? Why is that?


michalg comments:

It would be possible to make those codes more similar, but they won't be same code.

Unfortunately i'm not good with admin generators, so i don't know if it would be easy to adapt code from admin generator to your purpose. For example you want to filter with join - i don't know if it can be done with admin generator, as AFAIK standard filter forms are associated to concrete model. In my example filter form isn't. So you can add fields from other tables as well - this gives more flexibility.

My code should be made prettier, for example generating query could be move from action to filter form. Pagination should also be cleaned.

Anyway, IMHO sometimes it's better to adapt working solution (like for example admin generator), but sometimes it's better to create custom one, because adapting requires more work. It all depends of your needs now and in future - if you will need to enhance filtering in future, you may need more flexible solution.


michalg comments:

Can you describe a bit more for what you need this?


michalg comments:

I've managed to add filtering with join in invoice filter form, by overriding doBuildQuery method and add new widget for product sku number.:


class InvoiceFormFilter extends BaseInvoiceFormFilter
{
public function configure()
{
$this->widgetSchema['sku'] = new sfWidgetFormInput();
$this->validatorSchema['sku'] = new sfValidatorPass();
}

public function doBuildQuery(array $values)
{
$sku = $values['sku'];

// build query
$query = parent::doBuildQuery($values);

// add join to product and filter by it
$query->innerJoin("r.InvoiceProduct ip")
->innerJoin("ip.Product p");

if ($sku != "") $query->addWhere("p.sku = ?", $sku);

return $query;
}
}


Now it's possible to find invoice by sku in admin. So only problem now is to move executeFilter/buildQuery code from admin to your module. Unfortunately i don't have more time today.


michalg comments:

Hello,

This time, code is based on code generated by admin generator.

Actions:

class searchActions extends sfActions
{
public function executeIndex(sfWebRequest $request)
{
// sorting
if ($request->getParameter('sort') && $this->isValidSortColumn($request->getParameter('sort')))
{
$this->setSort(array($request->getParameter('sort'), $request->getParameter('sort_type')));
}

// pager
if ($request->getParameter('page'))
{
$this->setPage($request->getParameter('page'));
}

$this->pager = $this->getPager();
$this->sort = $this->getSort();
}

public function executeFilter(sfWebRequest $request)
{
$this->setPage(1);

if ($request->hasParameter('_reset'))
{
$this->setFilters($this->getFilterDefaults());

$this->redirect('search/index');
}

$this->filters = new InvoiceFormFilter($this->getFilters());

$this->filters->bind($request->getParameter($this->filters->getName()));
if ($this->filters->isValid())
{
$this->setFilters($this->filters->getValues());

$this->redirect('search/index');
}

$this->pager = $this->getPager();
$this->sort = $this->getSort();

$this->setTemplate('index');
}

protected function setPage($page)
{
$this->getUser()->setAttribute('invoice.page', $page);
}

protected function getPage()
{
return $this->getUser()->getAttribute('invoice.page', 1);
}

protected function buildQuery()
{
$this->filters = new InvoiceFormFilter($this->getFilters());

$query = $this->filters->buildQuery($this->getFilters());

$this->addSortQuery($query);

return $query;
}

protected function addSortQuery($query)
{
if (array(null, null) == ($sort = $this->getSort()))
{
return;
}

if (!in_array(strtolower($sort[1]), array('asc', 'desc')))
{
$sort[1] = 'asc';
}

$query->addOrderBy($sort[0] . ' ' . $sort[1]);
}

protected function getFilters()
{
return $this->getUser()->getAttribute('invoice.filters', $this->getFilterDefaults());
}

protected function setFilters(array $filters)
{
return $this->getUser()->setAttribute('invoice.filters', $filters);
}

protected function getPager()
{
$pager = new sfDoctrinePager(20);
$pager->setQuery($this->buildQuery());
$pager->setPage($this->getPage());
$pager->init();

return $pager;
}

protected function getSort()
{
if (null !== $sort = $this->getUser()->getAttribute('invoice.sort'))
{
return $sort;
}

$this->setSort($this->getDefaultSort());

return $this->getUser()->getAttribute('invoice.sort');
}

protected function setSort(array $sort)
{
if (null !== $sort[0] && null === $sort[1])
{
$sort[1] = 'asc';
}

$this->getUser()->setAttribute('invoice.sort', $sort);
}

// those methods are moved from configuration
protected function getDefaultSort()
{
return array(null, null);
}

public function getFilterDefaults()
{
return array();
}
}


Filter form (added functionality for SKU):

class InvoiceFormFilter extends BaseInvoiceFormFilter
{
public function configure()
{
$this->widgetSchema['sku'] = new sfWidgetFormInput();
$this->validatorSchema['sku'] = new sfValidatorPass();
}

public function doBuildQuery(array $values)
{
// create standard query for invoice
$query = parent::doBuildQuery($values);

// add our joins for product
$query->innerJoin("r.InvoiceProduct ip")
->innerJoin("ip.Product p");

if (isset($values['sku']) && $values['sku'] != "")
{
$query->addWhere("p.sku = ?", $values['sku']);
}

return $query;
}
}


indexSuccess:


<form action="<?php echo url_for('search_filter') ?>" method="post">
<?php echo $filters; ?>
<input type="submit" value="Save" />
<?php echo link_to('Reset', 'search_filter', array(), array('query_string' => '_reset', 'method' => 'post')) ?>
</form>

<?php if (!$pager->getNbResults()): ?>
<p>No results</p>
<?php else: ?>
<table cellspacing="0">
<tfoot>
<tr>
<th colspan="4">
Results <?php echo $pager->getNbResults(); ?>
<br/>
<?php if ($pager->haveToPaginate()): ?>
<a href="<?php echo url_for('search/index') ?>?page=<?php echo $pager->getPreviousPage() ?>">Previous Page</a>
<a href="<?php echo url_for('search/index') ?>?page=<?php echo $pager->getNextPage() ?>">Next Page</a>
<?php endif; ?>
</th>
</tr>
</tfoot>
<tbody>
<?php foreach ($pager->getResults() as $i => $invoice):?>
<tr>
<td><?php echo $invoice->number ?></td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
<?php endif; ?>



Routing (only what's important for this example):

search_filter:
url: /search/filter
param: { module: search, action: filter }


<strong>Remarks:</strong>
1) I've removed configuration class and moved code from it to action class.
2) User attributes aren't any more in admin namespace.
3) I've removed code for filtering events.
4) I've removed code for table method in buildQuery.
2) indexSuccess is extremaly simplified, just to show that it works. Simple listing numbers, reseting filter and prev/page is implemented. For more functionality please look at code generated by admin generator.

2010-05-07

Martin Palacio answers:

I strongly agree with you about the lack of documentation regarding form filters.

My suggestion: if you like how the admin generator handles these filters, maybe you can borrow some ideas from the generated code...
Let's say you want to search by invoice, just generate an admin module with:


./symfony doctrine:generate-admin frontend invoice


Next, generate your actual ("custom") module with


# just an example
./symfony generate:module frontend search


then look at cache/frontend/dev/modules/autoInvoice, grab the action/template code and adapt it to your needs.

I will post some code soon, but you get the idea meanwhile.

2010-05-07

casivaagustin answers:

Look, i find this post that was very help full to me but is in spanish, the sample code is easy to read. If you know something about forms this is a piece of cake.

This is for Doctrine http://axiacore.com/blog/2009/09/trabajando-con-filtros-en-symfony-ii/

And This is for Propel: http://axiacore.com/blog/2009/09/trabajando-con-filtros-en-symfony-i/

Cheers