3
votes

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?

3

3 Answers

3
votes

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>
1
votes

Debug the routes and you will see every route annotation gets a unique name

php app/console router:debug

You can set individual names with the name parameter in the annotation. Take a a look at the documentation.

@Route("/browse/{page}", name="browse_with_page") 

Creating a dedicated twig extension for your case could be another solution. Or maybe this blog post an the mentioned could be interesting.

EDIT: you can minimize you routes, as the page parameter can be optional.

@Route("/browse/c/{category}/{categoryName}")
@Route("/browse/c/{category}/{categoryName}/{page}")

is the same as and can be called both ways.

@Route("/browse/c/{category}/{categoryName}/{page}", defaults={"page" = 1})

Test it on the console, the same route should match.

php app:console router:match /browse/c/1/foobar
php app:console router:match /browse/c/1/foobar/2
-2
votes

First of all, this is not dynamic routing. This is static routing with multiple patterns. There is a distinct difference.

Second, you will need to create a specifically-defined route for each pattern, with unique names for each. However you could default the page parameter to 1 to make it optional, reducing the number of patterns defined.

Third, when you need to generate these links in twig, simply use the unique name defined above, with the required parameters.

Finally, with your last example, category is a required parameter, and you are passing a blank value to it. This would create "/browse/c//b/123/1", which is an invalid route for Symfony2 to handle.