Symfony Routes in Vue.js (SPA)
This article discusses how to pass URLs from Vue.js to Symfony with simple rerouting options. We discuss what each platform is and how to connect them.
Join the DZone community and get the full member experience.
Join For FreeSymfony comes with the "out-of-box" routing logic, which works flawlessly when using it in PHP or in Twig. However, once the frontend control is fully handled to the Vue.js (SPA), the solution for passing the URLs to the JavaScript is no longer an option.
The official idea/way of passing the URLs has been described in the Symfony documentation in the section “Generating URLs in JavaScript”, which is simply:
x
const route = "{{ path('blog_show', {slug: 'my-blog-post'})|escape('js') }}";
With a new project (or more like an extension of an already existing project), I started to wonder if there is actually a way to share Symfony routes in Vue.js. I’ve already in a big, over 2 years old private project, in which the urls are simply hardcoded in the frontend — with this I wanted to find a way out to replicate the routing in the Vue.js.
Actually the solution for this is pretty simple, and allows to easily replicate the url generating logic available in php:
xxxxxxxxxx
$this->router->generate('user_profile', [
'username' => $user->getUsername(),
]);
Handling Routes for Vue.js
The difference in Routes Structure
The foremost important difference between Symfony and Vue.js routes is the way that parameters in URLs are handled.
While Symfony uses this annotation:
xxxxxxxxxx
"/module/passwords/group/{id}"
Vue.js uses this one:
xxxxxxxxxx
"/module/passwords/group/:id"
Generating Routes for Vue.js
All available routes can be loaded and afterwards inserted into the json
file, which is a perfect solution to read later on with Vue.js. For this, I’ve created a Symfony Command:
xxxxxxxxxx
<php
namespace App\Command\Frontend;
use App\Controller\Core\ConfigLoader;
use App\Controller\Core\Services;
use Exception;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use Symfony\Component\Routing\RouterInterface;
use TypeError;
/**
* This command handles building json file which consist of backed (symfony) routes,
* This way the urls can be changed any moment without the need to update these also on front,
* Just like it works in symfony - names must be always changed manually, same goes to logic related to parameters
*
* Class BuildRoutingMatrixCommand
* @package App\Command\Frontend
*/
class BuildRoutingMatrixCommand extends Command
{
protected static $defaultName = 'pms-io:build-frontend-routing-file';
/**
* @var Services $services
*/
private Services $services;
/**
* @var SymfonyStyle $io
*/
private SymfonyStyle $io;
/**
* @var RouterInterface $router
*/
private RouterInterface $router;
/**
* @var ConfigLoader $configLoader
*/
private ConfigLoader $configLoader;
public function __construct(
Services $services,
RouterInterface $router,
ConfigLoader $configLoader,
string $name = null)
{
parent::__construct($name);
$this->configLoader = $configLoader;
$this->services = $services;
$this->router = $router;
}
/**
* Initialize the logic
*
* @param InputInterface $input
* @param OutputInterface $output
*/
public function initialize(InputInterface $input, OutputInterface $output)
{
$this->io = new SymfonyStyle($input, $output);
}
/**
* Execute the main logic
*
* @param InputInterface $input
* @param OutputInterface $output
* @return int
* @throws Exception
*/
public function execute(InputInterface $input, OutputInterface $output): int
{
$this->services->getLoggerService()->getLogger()->info("Started building routing file for frontend");
{
try {
$routesNamesToPaths = [];
$routesCollection = $this->router->getRouteCollection()->all();
foreach ($routesCollection as $routeName => $route) {
$routesNamesToPaths[$routeName] = $this->normalizePathForVueRouter($route->getPath());
}
$jsonRoutesMatrix = json_encode($routesNamesToPaths);
file_put_contents($this->configLoader->getConfigLoaderPaths()->getRoutingFrontendFilePath(), $jsonRoutesMatrix);
} catch (Exception | TypeError $e) {
$message = "Something went wrong while building the file";
$this->services->getLoggerService()->logException($e, [
"info" => $message,
"calledFrom" => __CLASS__,
]);
throw new Exception($message);
}
}
$this->services->getLoggerService()->getLogger()->info("Started building routing file for frontend");
return Command::SUCCESS;
}
/**
* Handles transforming paths to make them work with vue
*/
private function normalizePathForVueRouter(string $path): string
{
//While Symfony uses {param}, vue router uses :param
$normalizedPath = preg_replace("#\{(.*)\}#", ":$1", $path);
return $normalizedPath;
}
}
Afterwards all that has to be done is loading the generated file in the Typescript Class:
xxxxxxxxxx
import * as routes from '../../../../config/frontend/routes.json';
and adding a method to load the url by route name and to replace the provided parameters:
xxxxxxxxxx
/**
* Will get url path for route name
* Exception is thrown is none match is found
*
* @param routeName - name of the searched route
* @param routeParameters - array of parameters that need to be replaced in the route
* if not matching parameter is found then warning log is thrown and next
* parameter will be processed
*/
public static getPathForName(routeName: string, routeParameters: Object = {}): string
{
// get route
let matchingRoutePath = routes[routeName];
if( StringUtils.isEmptyString(matchingRoutePath) ){
throw {
"info" : "No matching route was route was found for given name",
"searchedName" : routeName,
}
}
// replace params
let keys = Object.keys(routeParameters);
keys.forEach( (parameter) => {
if( !matchingRoutePath.includes(":" + parameter) ){
console.warn({
"info" : "Provided path does not contain given parameter",
"parameter" : parameter,
})
}else{
let value = routeParameters[parameter];
matchingRoutePath = matchingRoutePath.replace(":" + parameter, value);
}
})
return matchingRoutePath;
}
It’s possible to make things even better just by adding the constants in Typescript (static readonly):
xxxxxxxxxx
/**
* @description This class contains definitions of INTERNAL api routes defined on backend side
* there is no way to pass this via templates etc so whenever a route is being changed in the symfony
* it also has to be updated here.
*
* This solution was added to avoid for example calling routing api, or having string hardcoded in
* all the places.
*/
export default class SymfonyRoutes {
/**
* @description route used to fetch notes for given category id
*/
static readonly ROUTE_NAME_GET_NOTES_FOR_CATEGORY_ID = "module_notes_get_for_category";
static readonly ROUTE_GET_NOTES_FOR_CATEGORY_ID_PARAM_CATEGORY_ID = "categoryId";
Conclusion
If Symfony is properly configured, and the route file is json
instead of yaml/yml
then the routes loading will work "out-of-box." However, in my case and as I see in other people's projects the,Annotation
is the favorite way to define a route.
Pros
- Unified routes in frontend and backend.
Cons
- Necessity to call the command upon adding new routes.
Published at DZone with permission of Dariusz Włodarczyk. See the original article here.
Opinions expressed by DZone contributors are their own.
Comments