Integrating FOSJsRoutingBundle

Open Components/RepLogApp.js and search for Routing:

... lines 1 - 8
class RepLogApp {
... lines 10 - 43
loadRepLogs() {
$.ajax({
url: Routing.generate('rep_log_list'),
... lines 47 - 50
})
}
... lines 53 - 194
}
... lines 196 - 214

Guess what? This Routing variable is a global variable. Boo! It's our last one. In templates/, open the base layout:

... lines 1 - 96
{% block javascripts %}
<script src="{{ asset('bundles/fosjsrouting/js/router.js') }}"></script>
<script src="{{ path('fos_js_routing_js', { callback: 'fos.Router.setData' }) }}"></script>
... lines 100 - 101
{% endblock %}
... lines 103 - 106

Other than a polyfill - which we won't talk about - there are only two script tags left. These give us the Router variable and they come from FOSJsRoutingBundle: a really cool bundle that allows you to generate URLs from Symfony routes in JavaScript.

Our goal is clear: refactor our code so that we can require the Router instead of relying on the global variable.

Requiring the router.js File

The first interesting thing is that this is not a Node package. Nope, it's a normal PHP package that happens to have a JavaScript file inside. But, that doesn't really make any difference... except that the path for it is ugly: it lives in vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.js. Wow! Ok then: const Routing = require(), then go up a few directories and follow the path: vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.js:

... lines 1 - 5
const Routing = require('../../../vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js');
... lines 7 - 215

Simple enough! Let's try it! In your browser, refresh! Bah! Error!

The route rep_log_list does not exist

Booo! This error comes from inside the Router. Here's what's going on: this JavaScript library is more complex than most. The first script tag gives us the Router variable. But the second executes a dynamic endpoint that fetches a JSON list of the route information and then sets that on the router.

When we simply require the router... we do get the Router object... but it has no routes! So the question is: how can we get the dynamic route info so that it can be set into the router?

Actually, this is possible! If you look at the Usage section of the bundle's docs, it talks about how to integrate with Webpack Encore. Basically, by running a bin/console command, you can dump your route information to a static JSON file. Then, you can require that JSON from Webpack and set it on the Router. Oh, and don't worry about this import syntax - it's basically the same as require(), and we'll talk about it next.

So this is really cool! It shows how you can even require JSON files from JavaScript! But... it has a downside: each time you add a new route, you need to re-run the command. That can be a pain during development. It's still a great option - and is a bit faster on production - but it does have that weakness.

Creating the Fake Router Module

And there is another option. It's not quite as fancy or awesome... but it's easier. Inside assets/js/Components, create a new file called Routing.js. Inside, um, just say, module.exports = window.Routing:

/**
* For now, we rely on the router.js script tag to be included
* in the layout. This is just a helper module to get that object.
*/
module.exports = window.Routing;

Yep! We are going to continue using the global variable. But now, we can at least require this file from everywhere else so that our code looks more responsible: const Routing = require('./Routing'):

... lines 1 - 5
const Routing = require('./Routing');
... lines 7 - 215

And now, when we refresh, it works. The cool thing about this hacky solution is that if you want to change to the better solution later, it's easy! Just put the correct code in Router.js, and everything will already be using it. Nice!

Leave a comment!

  • 2018-08-15 Diego Aguiar

    Hey Carlos Eduardo

    Nice findings! But you may not want to "expose" all your routes, so what you can do is to expose manually each of the routes that you want them as public, and then run this command (so your routes file gets updated)
    bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json

    Probably there is a way to not have to run that command every time you add a new route but at the moment I don't know how.

    Cheers!

  • 2018-08-14 Carlos Eduardo

    Ok, I think I found the problem. Here it is:

    Checking how the FOSJsRoutingBundle I found the ExposedRoutesExtractor.php , that checks if "isRouteExposed" on "getRoutes()" method.

    I found that I needed to expose my routes by setting the 'exposed' = true on the @Route, like this:

    /**
    *
    * @Route("/bse/pessoa/findByNome/{str}", name="bse_pessoa_findByNome", methods={"GET"}, options = { "expose" = true })
    *
    */
    public function findByNome($str = null)
    {
    [...]

    But, even after that, FOSJsRoutingBundle continues not showing anyone.

    In this Issue (https://github.com/FriendsO..., they said that routes defined by annotations (FOSRest routes???) was not caught by this ExposedRoutesExtractor, only if they were exposed in yaml.

    So I dit it. On config/routes/annotations.yaml I added:

    controllers:
    resource: ../../src/Controller/
    type: annotation
    options:
    expose: true

    Voila.

    php bin/console fos:js-routing:debug now shows all my routes.

  • 2018-08-14 Carlos Eduardo

    It generates the file, but it results like this:

    {"base_url":"","routes":[],"prefix":"","host":"localhost","scheme":"http"}

    When I use the "php bin/console debug:router" it shows me all the routes that I've defined.

    Maybe the documentation on https://symfony.com/doc/mas... is outdated? Look at the step 2, for example. I don´t think that now with Symfony4 I need to change this AppKernel.php file, even because it doesn´t exists. Maybe now it is the Kernel.php file:

    public function registerBundles()
    {
    $contents = require $this->getProjectDir().'/config/bundles.php';
    foreach ($contents as $class => $envs) {
    if (isset($envs['all']) || isset($envs[$this->environment])) {
    yield new $class();
    }
    }
    }

    From this method, I went to /config/bundles.php and the FOS\JsRoutingBundle\FOSJsRoutingBundle it´s already there:

    [...]
    FOS\JsRoutingBundle\FOSJsRoutingBundle::class => ['all' => true],

    And app/config/routing.yml also doesn´t exists anymore. I think that it was replaced by config/routes/fos_js_routing.yaml, since it has the same content that was mentioned:

    fos_js_routing:
    resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml"

    There´s more people saying the same:

    https://github.com/FriendsO...

    Maybe anyone here knows the answer?? Thanks

  • 2018-08-13 Diego Aguiar

    Hey Carlos Eduardo

    Yea, that bundle works in Symfony4 since this version: https://github.com/FriendsO...

    Probably you forgot to specify routes as public? https://symfony.com/doc/mas...

    Cheers!

  • 2018-08-13 Carlos Eduardo

    Does this really works with Symfony 4?? Because this:

    php bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json

    is not working for me here. The file is empty.