Symfony2 dynamic routing to one controller action with multiple optional parameters

routingsymfonyurl

I am still quite new to Symfony2 and having trouble getting my head around the following problem.

I have a main browse Action in a Controller with the following routes defined (in the controller):

/**
 * @Route("/browse")
 * @Route("/browse/{page}")
 * @Route("/browse/c/{category}/{categoryName}")
 * @Route("/browse/c/{category}/{categoryName}/{page}")
 * @Route("/browse/c/{category}/b/{brand}/{page}")
 * @Route("/browse/b/{brand}")
 * @Route("/browse/b/{brand}/{page}")
 * @Template()
 */
public function browseAction($category = 0, $page = 1, $brand = 0) {

The above routing works with no problems.

The problem is generating urls from twig views or view helpers.

I would have liked to be able to do the following in a view helper:

{{ url('browse', {'brand': '123'}) }}

This works ok with the following in routing.yml:

browse:
  pattern: /browse/b/{brand}
  defaults: { _controller: MyCoreBundle:Browse:browse } 

I then tried:

browse:
  pattern: /browse/b/{brand}
  pattern: /browse/c/{category}/b/{brand}
  defaults: { _controller: MyCoreBundle:Browse:browse } 

But only the last pattern seems to apply and trying to use the following would throw an error:

{{ url('browse', {'brand': '123'}) }}

I realise I can create multiple individual routes in routing.yml that are uniquely named. But that means depending on the variables that will be used I need to specify a different route name which would get very messy quickly.

I also tried:

browse:
  pattern: /browse/c/{category}/b/{brand}/{page}
  defaults: { _controller: MyCoreBundle:Browse:browse } 

With:

{{ url('browse', {'brand': '123', 'category':'', 'page': '1'}) }}

But that threw an error saying category was not in the correct format..

Am I missing something here? Can somebody point me in the right direction? Do I maybe need to create a twig extension that can take all variables and construct the URL based on input?

Best Solution

I ended up creating a twig extension. It is long winded and involves passing route_params and query_params around but it works.

So in my controller I needed to get route and query parameters:

$routeParams = $this->get('request')->attributes->get('_route_params');
$queryParams = $this->get('request')->query->all();

Then pass to view:

return array('products' => $products,  'mfdFacets' => $mfdFacets, 'routeParams' => $routeParams, 'queryParams' => $queryParams);

Then in my browse.html.twig I call a view helper and pass route and query parameters:

{% render controller("MyCoreBundle:Helper:menumfd", {'mfdFacets': mfdFacets, 'routeParams': routeParams, 'queryParams': queryParams }) %}

Then in the helper Controller:

   /**
     * @Route("/helper/menu/module/mfd")
     * @Template()
     */
    public function menumfdAction($mfdFacets, $routeParams, $queryParams) {
    $manufacturers = $this->get("my.manufacturers")->makeNamedArray($mfdFacets);
    return array('manufacturers' => $manufacturers, 'routeParams' => $routeParams, 'queryParams' => $queryParams);
    }

Then in helper view:

<ul>
{% for mfd in manufacturers %}

    <li><a href="{{ mybrowseroute("brand", mfd.id, "brandName", mfd.name, routeParams, queryParams) }}">{{ mfd.name | raw }} ({{ mfd.count }})</a></li>

{% endfor %}
</ul>

Then the Twig Extension (service?) class:

<?

namespace My\Bundle\ServiceBundle\Twig\Extension;

use Symfony\Component\HttpKernel\KernelInterface;
use Symfony\Component\DependencyInjection\Container;
use Symfony\Component\HttpFoundation\Request;

class MyRouterExtension extends \Twig_Extension {

    protected $container;

    public function __construct(Container $container) {
    $this->container = $container;
    }

    public function getFunctions() {
    return array(
        'mybrowseroute' => new \Twig_Function_Method($this, 'myBrowseRoute')
    );
    }

    public function myBrowseRoute($label, $value, $label2, $value2, $routeParams, $queryParams) {


    #print_r($routeParams);
    $route_array = array("category", "categoryName", "brand", "brandName");

    ## Value 1
    if (array_key_exists($label, $routeParams)) {
        $routeParams["$label"] = $value;
    } else {
        if (in_array($label, $route_array)) {
            $routeParams["$label"] = $value;
        }
    }
    if (array_key_exists($label, $queryParams)) {
        $queryParams["$label"] = $value;
    } else {
        if (!array_key_exists($label, $route_array) && !array_key_exists($label, $routeParams)) {
            $queryParams["$label"] = $value;
        }
    }

    ## Value 2
    if (array_key_exists($label, $routeParams)) {
        $routeParams["$label2"] = $value2;
    } else {
        if (in_array($label, $route_array)) {
            $routeParams["$label2"] = $value2;
        }
    }
    if (array_key_exists($label2, $queryParams)) {
        $queryParams["$label2"] = $value2;
    } else {
        if (!array_key_exists($label2, $route_array) && !array_key_exists($label2, $routeParams)) {
            $queryParams["$label2"] = $value2;
        }
    }

    ## Generate URL string

    $base_route = $this->container->get('router')->generate("browse");
    $routeString = $base_route;


    if (array_key_exists("category", $routeParams)) {
        $routeString .= "/c/" . $routeParams["category"];
    }
    if (array_key_exists("categoryName", $routeParams)) {
        $routeString .= "/" . urlencode($routeParams["categoryName"]);
    }
    if (array_key_exists("brand", $routeParams)) {
        $routeString .= "/b/" . $routeParams["brand"];
    }
    if (array_key_exists("brandName", $routeParams)) {
        $routeString .= "/" . urlencode($routeParams["brandName"]);
    }

    # Page
    $routeString .= '/1';

    $i = 1;
    foreach($queryParams as $qLabel => $qValue){
        if($i == 1){
            $routeString .= "?$qLabel=$qValue";
        } else {
            $routeString .= "&$qLabel=$qValue";
        }
        $i++;
    }

    return $routeString;
    }

    public function getName() {
    return 'my_router';
    }

}

Which requires the following in routing.yml:

browse:
      pattern: /browse
      defaults: { _controller: MyCoreBundle:Browse:browse } 

And in services.yml:

services:
    my.router:
    class: My\Bundle\ServiceBundle\Twig\Extension\MyRouterExtension
    arguments: ['@service_container']
    tags:
        - { name: twig.extension }

If you only want pass 1 key value pair in the view helper you can just use:

<li><a href="{{ mybrowseroute("brand", mfd.id, '', '', routeParams, queryParams) }}">{{ mfd.name | raw }} ({{ mfd.count }})</a></li>