├── CHANGELOG.md ├── CONTRIBUTING.md ├── Command ├── DumpCommand.php └── RouterDebugExposedCommand.php ├── Controller └── Controller.php ├── DependencyInjection ├── Configuration.php └── FOSJsRoutingExtension.php ├── Extractor ├── ExposedRoutesExtractor.php └── ExposedRoutesExtractorInterface.php ├── FOSJsRoutingBundle.php ├── README.md ├── Resources ├── config │ ├── controllers.xml │ ├── plovr │ │ └── compile.js │ ├── routing │ │ └── routing-sf4.xml │ ├── serializer.xml │ └── services.xml ├── doc │ ├── commands.rst │ ├── index.rst │ ├── installation.rst │ └── usage.rst ├── gulpfile.js ├── js │ ├── router.template.js │ └── router.ts ├── meta │ └── LICENSE ├── package.json ├── public │ └── js │ │ ├── router.js │ │ └── router.min.js ├── ts │ ├── router.d.ts │ ├── router.test-d.ts │ └── routes.json ├── tsconfig.json └── webpack │ └── FosRouting.js ├── Response └── RoutesResponse.php ├── Serializer ├── Denormalizer │ └── RouteCollectionDenormalizer.php └── Normalizer │ ├── RouteCollectionNormalizer.php │ └── RoutesResponseNormalizer.php ├── Util └── CacheControlConfig.php └── composer.json /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## v3.5.0 - 2024-01-23 4 | - Fix TypeScript error when verbatimModuleSyntax is enabled ([#476](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/476)) 5 | - Define RoutesResponse as a Service ([#474](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/474)) 6 | - Ignore session in stateless requests ([#468](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/468)) 7 | - Add option to skip registering compile hooks ([#462](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/462)) 8 | 9 | ## v3.4.1 - 2023-12-15 10 | - fix: do not use BannerPlugin but newer webpack-inject-plugin instead to fix vulnerability 11 | 12 | ## v3.4.0 - 2023-12-12 13 | - Allow Symfony 7.0 ([#471](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/471)) 14 | - fix: remove webpack-inject-plugin dependency ([#464](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/464)) 15 | - Docs only: remove $ so gitclip works ([#472](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/472)) 16 | - Docs only: Update console note ([#463](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/463)) 17 | 18 | ## v3.3.0 - 2023-07-04 19 | - add support for Windows when using the webpack plugin ([#444](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/444)) 20 | - Add PHP 8.2 tests ([#449](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/449)) 21 | - Phpunit config file migration ([#450](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/450))) 22 | - Deprecation fixes (PHP 8 ([#451](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/451)) and Symfony 6.3 ([#460](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/460))) 23 | - JSON Callback validator static call instead of new object ([#458](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/458)) 24 | - Optimize package size by excluding tests ([#457](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/457)) 25 | 26 | ## v3.2.1 - 2022-07-01 27 | - fix for webpack plugin: fosRoute.json dir created at root ([#443](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/443)) 28 | 29 | ## v3.2.0 - 2022-06-30 30 | - [BC break] Use Symfony Flex default path. Will break if you're still using the `web` directory and not defining the path ([#433](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/433)) 31 | - Add webpack plugin to automatically load the routes with no user interactions ([#429](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/429)) 32 | - Changed ExposedRoutesExtractor to handle mkdir warnings ([#434](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/434)) 33 | - Handle nullable route defaults ([#436](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/436)) 34 | - Fix Symfony 6.1 deprecations ([#439](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/439)) 35 | 36 | ## v3.1.1 - 2022-03-02 37 | - Allow willdurand/jsonp-callback-validator v2 ([#430](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/430)) 38 | - Use latest PHP 8.0 features ([#432](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/432)) 39 | 40 | ## v3.1.0 - 2021-12-28 41 | - Improve documentation for dump command when using locales ([#426](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/426)) 42 | - Add support for explicit default inclusion ([#423](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/423)) 43 | 44 | ## v3.0.0 - 2021-12-15 45 | - Migrate router implementation to TS ([#406](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/406)) 46 | - Allow Symfony 6 ([#408](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/408)) 47 | - [BC break] Drop support for PHP <8.0 and Symfony <5.4, add typing to all classes 48 | - Add documentation for attributes 49 | 50 | ## v2.8.0 - 2021-12-15 51 | - Fix expose: false behavior ([#404](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/404)) 52 | - Fix dump using domains ([#410](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/410)) 53 | - Fix docs links ([#412](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/412)) 54 | - Replace Travis with Github actions ([#414](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/414)) 55 | 56 | ## v2.7.0 - 2020-11-22 57 | - Add support for PHP 8 ([#399](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/399)) 58 | 59 | ## v2.6.0 - 2020-05-20 60 | - [BC break] Fix URL encoding to mimic Symfony URL Generator (this might change behavior for special characters, it should be in line with Symfony Router though) ([#387](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/387)) 61 | - Fixed issue with creating absolute instead of relative path on hosts with differing ports ([#391](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/391)) 62 | 63 | ## v2.5.4 - 2020-04-15 64 | - Fix duplicated port in absolute path ([#381](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/381)) 65 | 66 | ## v2.5.3 - 2020-01-13 67 | - Rervert fall back to current domain when baseurl is missing or empty in json ([#374](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/374)) 68 | 69 | ## v2.5.2 - 2020-01-12 70 | - Fall back to current domain when baseurl is missing or empty in json ([#371](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/371)) 71 | - Upgrade gulp to version 4 ([#372](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/372)) 72 | 73 | ## v2.5.1 - 2019-12-02 74 | - [BC break] Fix root dir deprecation and fix PHP 7.4 deprecation (drops Symfony < 3.3 support) ([#369](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/369)) 75 | 76 | ## v2.5.0 - 2019-12-01 77 | - [BC break] Add support for Symfony 5, drop support for PHP5, drop support for Symfony 2 ([#366](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/366)) 78 | - Fix absolute url generation including ports ([#361](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/361)) 79 | - Fix cache for exposed routes in debug mode ([#362](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/362)) 80 | 81 | ## v2.4.0 - 2019-08-10 82 | - Add Symfony 4.1 localized routes support ([#334](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/334)) 83 | - Add documentation remarks on JMSI18nRoutingBundle compatibility ([#352](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/352)) 84 | 85 | ## v2.3.1 - 2019-06-17 86 | - Fix regex pattern to match whole url pattern ([#350](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/350)) 87 | - Small documentation update 88 | 89 | ## v2.3.0 - 2019-02-03 90 | - Add routing-sf4.xml to move towards Symfony >4.1 syntax 91 | - Add functionality to granularly expose routes based on domains ([#346](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/346)) 92 | - Small cleanup and textual fix 93 | 94 | ## v2.2.2 - 2018-11-28 95 | - Fix Symfony 4.2 deprecation 96 | - Add setRoutingData to typescript definition 97 | 98 | ## v2.2.1 - 2018-09-29 99 | - Add support for a different port 100 | 101 | ## v2.2.0 - 2018-02-07 102 | - Refactor JavaScript code to improve webpack compatibility 103 | 104 | ## v2.1.1 - 2017-12-13 105 | - Fix SF <4 compatibility ([#306](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/306)) 106 | 107 | ## v2.1.0 - 2017-12-13 108 | - Add Symfony 4 compatibility ([#300](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/300)) 109 | - Add JSON dump functionality ([#302](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/302)) 110 | - Fix bug denormalizing empty routing collections from cache 111 | - Update documentation for Symfony 3 ([#273](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/273)) 112 | 113 | ## v2.0.0 - 2017-11-08 114 | - Add Symfony 3.* compatibility 115 | - Added `--pretty-print` option to `fos:js-routing:dump`-command, making the resulting javascript pretty-printed 116 | - Removed SF 2.1 backwards compatibility code 117 | - Add automatic injection of `locale` parameter 118 | - Added functionality to change the used router service 119 | - Added normalizer classes 120 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ============ 3 | 4 | First of all, **thank you** for contributing, **you are awesome**! 5 | 6 | Here are a few rules to follow in order to ease code reviews, and discussions 7 | before maintainers accept and merge your work: 8 | 9 | * You MUST follow the [PSR-1](http://www.php-fig.org/psr/1/) and 10 | [PSR-2](http://www.php-fig.org/psr/2/) recommendations. Use the [PHP-CS-Fixer 11 | tool](http://cs.sensiolabs.org/) to fix the syntax of your code automatically. 12 | * You MUST run the test suite. 13 | * You MUST write (or update) unit tests. 14 | * You SHOULD write documentation. 15 | 16 | Please, write [commit messages that make 17 | sense](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), 18 | and [rebase your branch](http://git-scm.com/book/en/Git-Branching-Rebasing) 19 | before submitting your Pull Request. 20 | 21 | One may ask you to [squash your 22 | commits](http://gitready.com/advanced/2009/02/10/squashing-commits-with-rebase.html) 23 | too. This is used to "clean" your Pull Request before merging it (we don't want 24 | commits such as `fix tests`, `fix 2`, `fix 3`, etc.). 25 | 26 | Also, while creating your Pull Request on GitHub, you MUST write a description 27 | which gives the context and/or explains why you are creating it. 28 | 29 | Thank you! 30 | 31 | Running tests 32 | ------------- 33 | 34 | Before running the test suite, execute the following Composer command to install 35 | the dependencies used by the bundle: 36 | 37 | ```bash 38 | $ composer update 39 | ``` 40 | 41 | Then, execute the tests executing: 42 | 43 | ```bash 44 | $ ./phpunit 45 | ``` 46 | 47 | ### JavaScript Test Suite 48 | 49 | First, install [PhantomJS](http://phantomjs.org/) (see the website for further 50 | details or simply use your favourite package manager) and the development dependencies using: 51 | 52 | ```bash 53 | $ cd Resources 54 | $ npm install 55 | ``` 56 | 57 | then run the JS test suite with: 58 | 59 | ```bash 60 | $ npm run test 61 | ``` 62 | 63 | Because the current test suite runs against the built javascript a build is automatically 64 | run first (see 'Compiling the JavaScript files' below for further details). You can 65 | explicitly run only the test suite with: 66 | 67 | ```bash 68 | $ phantomjs js/run_jsunit.js js/router_test.html 69 | ``` 70 | 71 | Alternatively you can open `Resources/js/router_test.html` in your browser which 72 | runs the same test suite with a graphical output. 73 | 74 | Compiling the JavaScript files 75 | ------------------------------ 76 | 77 | > **NOTE** 78 | > 79 | > We already provide a compiled version of the JavaScript; this section is only 80 | > relevant if you want to make changes to this script. 81 | 82 | This project is using [Gulp](https://gulpjs.com/) to compile JavaScript files. 83 | In order to use Gulp you must install both [node](https://nodejs.org/en/) and 84 | [npm](https://www.npmjs.com/). 85 | 86 | If you are not familiar with using Gulp, it is recommended that you review this 87 | [An Introduction to Gulp.js](https://www.sitepoint.com/introduction-gulp-js/) 88 | tutorial which will guide you through the process of getting node and npm installed. 89 | 90 | Once you have node and npm installed: 91 | 92 | ```bash 93 | $ cd Resources 94 | $ npm install 95 | ``` 96 | 97 | Then to perform a build 98 | 99 | ```bash 100 | $ npm run build 101 | ``` 102 | -------------------------------------------------------------------------------- /Command/DumpCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Command; 15 | 16 | use FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractorInterface; 17 | use FOS\JsRoutingBundle\Response\RoutesResponse; 18 | use Symfony\Component\Console\Attribute\AsCommand; 19 | use Symfony\Component\Console\Command\Command; 20 | use Symfony\Component\Console\Input\InputInterface; 21 | use Symfony\Component\Console\Input\InputOption; 22 | use Symfony\Component\Console\Output\OutputInterface; 23 | use Symfony\Component\Serializer\SerializerInterface; 24 | 25 | /** 26 | * Dumps routes to the filesystem. 27 | * 28 | * @author Benjamin Dulau 29 | */ 30 | #[AsCommand('fos:js-routing:dump', 'Dumps exposed routes to the filesystem')] 31 | class DumpCommand extends Command 32 | { 33 | public function __construct( 34 | private RoutesResponse $routesResponse, 35 | private ExposedRoutesExtractorInterface $extractor, 36 | private SerializerInterface $serializer, 37 | private string $projectDir, 38 | private ?string $requestContextBaseUrl = null, 39 | ) { 40 | parent::__construct(); 41 | } 42 | 43 | protected function configure(): void 44 | { 45 | $this 46 | ->setName('fos:js-routing:dump') 47 | ->setDescription('Dumps exposed routes to the filesystem') 48 | ->addOption( 49 | 'callback', 50 | null, 51 | InputOption::VALUE_REQUIRED, 52 | 'Callback function to pass the routes as an argument.', 53 | 'fos.Router.setData' 54 | ) 55 | ->addOption( 56 | 'format', 57 | null, 58 | InputOption::VALUE_REQUIRED, 59 | 'Format to output routes in. js to wrap the response in a callback, json for raw json output. Callback is ignored when format is json', 60 | 'js' 61 | ) 62 | ->addOption( 63 | 'target', 64 | null, 65 | InputOption::VALUE_OPTIONAL, 66 | 'Override the target file to dump routes in.' 67 | ) 68 | ->addOption( 69 | 'locale', 70 | null, 71 | InputOption::VALUE_OPTIONAL, 72 | 'Set locale to be used with JMSI18nRoutingBundle.', 73 | '' 74 | ) 75 | ->addOption( 76 | 'pretty-print', 77 | 'p', 78 | InputOption::VALUE_NONE, 79 | 'Pretty print the JSON.' 80 | ) 81 | ->addOption( 82 | 'domain', 83 | null, 84 | InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 85 | 'Specify expose domain', 86 | [] 87 | ) 88 | ; 89 | } 90 | 91 | protected function execute(InputInterface $input, OutputInterface $output): int 92 | { 93 | if (!in_array($input->getOption('format'), ['js', 'json'])) { 94 | $output->writeln('Invalid format specified. Use js or json.'); 95 | 96 | return 1; 97 | } 98 | 99 | $callback = $input->getOption('callback'); 100 | if (empty($callback)) { 101 | $output->writeln('If you include --callback it must not be empty. Do you perhaps want --format=json'); 102 | 103 | return 1; 104 | } 105 | 106 | $output->writeln('Dumping exposed routes.'); 107 | $output->writeln(''); 108 | 109 | $this->doDump($input, $output); 110 | 111 | return 0; 112 | } 113 | 114 | /** 115 | * Performs the routes dump. 116 | */ 117 | private function doDump(InputInterface $input, OutputInterface $output): void 118 | { 119 | $domain = $input->getOption('domain'); 120 | 121 | $extractor = $this->extractor; 122 | $serializer = $this->serializer; 123 | $targetPath = $input->getOption('target') ?: 124 | sprintf( 125 | '%s/public/js/fos_js_routes%s.%s', 126 | $this->projectDir, 127 | empty($domain) ? '' : ('_'.implode('_', $domain)), 128 | $input->getOption('format') 129 | ); 130 | 131 | if (!is_dir($dir = dirname($targetPath))) { 132 | $output->writeln('[dir+] '.$dir); 133 | if (false === @mkdir($dir, 0777, true)) { 134 | throw new \RuntimeException('Unable to create directory '.$dir); 135 | } 136 | } 137 | 138 | $output->writeln('[file+] '.$targetPath); 139 | 140 | $baseUrl = $this->requestContextBaseUrl ?? $this->extractor->getBaseUrl() 141 | ; 142 | 143 | if ($input->getOption('pretty-print')) { 144 | $params = ['json_encode_options' => JSON_PRETTY_PRINT]; 145 | } else { 146 | $params = []; 147 | } 148 | 149 | $this->routesResponse->setBaseUrl($baseUrl); 150 | $this->routesResponse->setRoutes($extractor->getRoutes()); 151 | $this->routesResponse->setPrefix($extractor->getPrefix($input->getOption('locale'))); 152 | $this->routesResponse->setHost($extractor->getHost()); 153 | $this->routesResponse->setPort($extractor->getPort()); 154 | $this->routesResponse->setScheme($extractor->getScheme()); 155 | $this->routesResponse->setLocale($input->getOption('locale')); 156 | $this->routesResponse->setDomains($domain); 157 | 158 | $content = $serializer->serialize($this->routesResponse, 'json', $params); 159 | 160 | if ('js' == $input->getOption('format')) { 161 | $content = sprintf('%s(%s);', $input->getOption('callback'), $content); 162 | } 163 | 164 | if (false === @file_put_contents($targetPath, $content)) { 165 | throw new \RuntimeException('Unable to write file '.$targetPath); 166 | } 167 | } 168 | } 169 | -------------------------------------------------------------------------------- /Command/RouterDebugExposedCommand.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Command; 15 | 16 | use FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractorInterface; 17 | use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; 18 | use Symfony\Component\Console\Attribute\AsCommand; 19 | use Symfony\Component\Console\Command\Command; 20 | use Symfony\Component\Console\Input\InputArgument; 21 | use Symfony\Component\Console\Input\InputInterface; 22 | use Symfony\Component\Console\Input\InputOption; 23 | use Symfony\Component\Console\Output\OutputInterface; 24 | use Symfony\Component\Routing\Route; 25 | use Symfony\Component\Routing\RouteCollection; 26 | use Symfony\Component\Routing\RouterInterface; 27 | 28 | /** 29 | * A console command for retrieving information about exposed routes. 30 | * 31 | * @author William DURAND 32 | */ 33 | #[AsCommand('fos:js-routing:debug', 'Displays currently exposed routes for an application')] 34 | class RouterDebugExposedCommand extends Command 35 | { 36 | public function __construct(private ExposedRoutesExtractorInterface $extractor, private RouterInterface $router) 37 | { 38 | parent::__construct(); 39 | } 40 | 41 | /** 42 | * {@inheritdoc} 43 | */ 44 | protected function configure(): void 45 | { 46 | $this 47 | ->setDefinition([ 48 | new InputArgument('name', InputArgument::OPTIONAL, 'A route name'), 49 | new InputOption('show-controllers', null, InputOption::VALUE_NONE, 'Show assigned controllers in overview'), 50 | new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), 51 | new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), 52 | new InputOption('domain', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify expose domain', []), 53 | ]) 54 | ->setName('fos:js-routing:debug') 55 | ->setDescription('Displays currently exposed routes for an application') 56 | ->setHelp(<<fos:js-routing:debug command displays an application's routes which will be available via JavaScript. 58 | 59 | php app/console fos:js-routing:debug 60 | 61 | You can alternatively specify a route name as an argument to get more info about that specific route: 62 | 63 | php app/console fos:js-routing:debug my_route 64 | 65 | EOF 66 | ) 67 | ; 68 | } 69 | 70 | protected function execute(InputInterface $input, OutputInterface $output): int 71 | { 72 | if ($name = $input->getArgument('name')) { 73 | /** @var Route $route */ 74 | $route = $this->router->getRouteCollection()->get($name); 75 | 76 | if (!$route) { 77 | throw new \InvalidArgumentException(sprintf('The route "%s" does not exist.', $name)); 78 | } 79 | 80 | if (!$this->extractor->isRouteExposed($route, $name)) { 81 | throw new \InvalidArgumentException(sprintf('The route "%s" was found, but it is not an exposed route.', $name)); 82 | } 83 | 84 | $helper = new DescriptorHelper(); 85 | $helper->describe($output, $route, [ 86 | 'format' => $input->getOption('format'), 87 | 'raw_text' => $input->getOption('raw'), 88 | 'show_controllers' => $input->getOption('show-controllers'), 89 | ]); 90 | } else { 91 | $helper = new DescriptorHelper(); 92 | $helper->describe($output, $this->getRoutes($input->getOption('domain')), [ 93 | 'format' => $input->getOption('format'), 94 | 'raw_text' => $input->getOption('raw'), 95 | 'show_controllers' => $input->getOption('show-controllers'), 96 | ]); 97 | } 98 | 99 | return 0; 100 | } 101 | 102 | protected function getRoutes($domain = []): RouteCollection 103 | { 104 | $routes = $this->extractor->getRoutes(); 105 | 106 | if (empty($domain)) { 107 | return $routes; 108 | } 109 | 110 | $targetRoutes = new RouteCollection(); 111 | 112 | foreach ($routes as $name => $route) { 113 | $expose = $route->getOption('expose'); 114 | $expose = is_string($expose) ? ('true' === $expose ? 'default' : $expose) : 'default'; 115 | 116 | if (in_array($expose, $domain, true)) { 117 | $targetRoutes->add($name, $route); 118 | } 119 | } 120 | 121 | return $targetRoutes; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Controller/Controller.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Controller; 15 | 16 | use FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractorInterface; 17 | use FOS\JsRoutingBundle\Response\RoutesResponse; 18 | use FOS\JsRoutingBundle\Util\CacheControlConfig; 19 | use Symfony\Component\Config\ConfigCache; 20 | use Symfony\Component\HttpFoundation\Request; 21 | use Symfony\Component\HttpFoundation\Response; 22 | use Symfony\Component\HttpFoundation\Session\Flash\AutoExpireFlashBag; 23 | use Symfony\Component\HttpKernel\Exception\BadRequestHttpException; 24 | 25 | /** 26 | * Controller class. 27 | * 28 | * @author William DURAND 29 | */ 30 | class Controller 31 | { 32 | protected CacheControlConfig $cacheControlConfig; 33 | 34 | /** 35 | * Default constructor. 36 | * 37 | * @param object $serializer Any object with a serialize($data, $format) method 38 | * @param ExposedRoutesExtractorInterface $exposedRoutesExtractor the extractor service 39 | * @param bool $debug 40 | */ 41 | public function __construct( 42 | private RoutesResponse $routesResponse, 43 | private mixed $serializer, 44 | private ExposedRoutesExtractorInterface $exposedRoutesExtractor, 45 | array $cacheControl = [], 46 | private bool $debug = false, 47 | ) { 48 | $this->cacheControlConfig = new CacheControlConfig($cacheControl); 49 | } 50 | 51 | public function indexAction(Request $request, $_format): Response 52 | { 53 | if (!$request->attributes->getBoolean('_stateless') && $request->hasSession() 54 | && ($session = $request->getSession())->isStarted() && $session->getFlashBag() instanceof AutoExpireFlashBag 55 | ) { 56 | // keep current flashes for one more request if using AutoExpireFlashBag 57 | $session->getFlashBag()->setAll($session->getFlashBag()->peekAll()); 58 | } 59 | 60 | $cache = new ConfigCache($this->exposedRoutesExtractor->getCachePath($request->getLocale()), $this->debug); 61 | 62 | if (!$cache->isFresh() || $this->debug) { 63 | $exposedRoutes = $this->exposedRoutesExtractor->getRoutes(); 64 | $serializedRoutes = $this->serializer->serialize($exposedRoutes, 'json'); 65 | $cache->write($serializedRoutes, $this->exposedRoutesExtractor->getResources()); 66 | } else { 67 | $path = method_exists($cache, 'getPath') ? $cache->getPath() : (string) $cache; 68 | $serializedRoutes = file_get_contents($path); 69 | $exposedRoutes = $this->serializer->deserialize( 70 | $serializedRoutes, 71 | 'Symfony\Component\Routing\RouteCollection', 72 | 'json' 73 | ); 74 | } 75 | 76 | $this->routesResponse->setBaseUrl($this->exposedRoutesExtractor->getBaseUrl()); 77 | $this->routesResponse->setRoutes($exposedRoutes); 78 | $this->routesResponse->setPrefix($this->exposedRoutesExtractor->getPrefix($request->getLocale())); 79 | $this->routesResponse->setHost($this->exposedRoutesExtractor->getHost()); 80 | $this->routesResponse->setPort($this->exposedRoutesExtractor->getPort()); 81 | $this->routesResponse->setScheme($this->exposedRoutesExtractor->getScheme()); 82 | $this->routesResponse->setLocale($request->getLocale()); 83 | $this->routesResponse->setDomains($request->query->has('domain') ? explode(',', $request->query->get('domain')) : []); 84 | 85 | $content = $this->serializer->serialize($this->routesResponse, 'json'); 86 | 87 | if (null !== $callback = $request->query->get('callback')) { 88 | if (!\JsonpCallbackValidator::validate($callback)) { 89 | throw new BadRequestHttpException('Invalid JSONP callback value'); 90 | } 91 | 92 | $content = '/**/'.$callback.'('.$content.');'; 93 | } 94 | 95 | $response = new Response($content, 200, ['Content-Type' => $request->getMimeType($_format)]); 96 | $this->cacheControlConfig->apply($response); 97 | 98 | return $response; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /DependencyInjection/Configuration.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\DependencyInjection; 15 | 16 | use Symfony\Component\Config\Definition\Builder\TreeBuilder; 17 | use Symfony\Component\Config\Definition\ConfigurationInterface; 18 | 19 | /** 20 | * Configuration class. 21 | * 22 | * @author William DURAND 23 | */ 24 | class Configuration implements ConfigurationInterface 25 | { 26 | /** 27 | * Generates the configuration tree builder. 28 | * 29 | * @return TreeBuilder The tree builder 30 | */ 31 | public function getConfigTreeBuilder(): TreeBuilder 32 | { 33 | $builder = new TreeBuilder('fos_js_routing'); 34 | 35 | $rootNode = $builder->getRootNode(); 36 | 37 | $rootNode 38 | ->children() 39 | ->scalarNode('serializer')->cannotBeEmpty()->end() 40 | ->arrayNode('routes_to_expose') 41 | ->beforeNormalization() 42 | ->ifTrue(fn ($v) => !is_array($v)) 43 | ->then(fn ($v) => [$v]) 44 | ->end() 45 | ->prototype('scalar')->end() 46 | ->end() 47 | ->scalarNode('router')->defaultValue('router')->end() 48 | ->scalarNode('request_context_base_url')->defaultNull()->end() 49 | ->arrayNode('cache_control') 50 | ->children() 51 | ->booleanNode('public')->defaultFalse()->end() 52 | ->scalarNode('expires')->defaultNull()->end() 53 | ->scalarNode('maxage')->defaultNull()->end() 54 | ->scalarNode('smaxage')->defaultNull()->end() 55 | ->arrayNode('vary') 56 | ->beforeNormalization() 57 | ->ifTrue(fn ($v) => !is_array($v)) 58 | ->then(fn ($v) => [$v]) 59 | ->end() 60 | ->prototype('scalar')->end() 61 | ->end() 62 | ->end() 63 | ->end() 64 | ->end(); 65 | 66 | return $builder; 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /DependencyInjection/FOSJsRoutingExtension.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\DependencyInjection; 15 | 16 | use Symfony\Component\Config\Definition\Processor; 17 | use Symfony\Component\Config\FileLocator; 18 | use Symfony\Component\DependencyInjection\Alias; 19 | use Symfony\Component\DependencyInjection\ContainerBuilder; 20 | use Symfony\Component\DependencyInjection\Extension\Extension; 21 | use Symfony\Component\DependencyInjection\Loader\XmlFileLoader; 22 | 23 | /** 24 | * @author William DURAND 25 | */ 26 | class FOSJsRoutingExtension extends Extension 27 | { 28 | public function load(array $configs, ContainerBuilder $container): void 29 | { 30 | $processor = new Processor(); 31 | $configuration = new Configuration(); 32 | $config = $processor->processConfiguration($configuration, $configs); 33 | 34 | $loader = new XmlFileLoader($container, new FileLocator(__DIR__.'/../Resources/config')); 35 | $loader->load('services.xml'); 36 | $loader->load('controllers.xml'); 37 | 38 | if (isset($config['serializer'])) { 39 | $container->setAlias('fos_js_routing.serializer', new Alias($config['serializer'], false)); 40 | } else { 41 | $loader->load('serializer.xml'); 42 | } 43 | 44 | $container->setAlias( 45 | 'fos_js_routing.router', 46 | new Alias($config['router'], false) 47 | ); 48 | $container 49 | ->getDefinition('fos_js_routing.extractor') 50 | ->replaceArgument(1, $config['routes_to_expose']); 51 | 52 | $container->setParameter( 53 | 'fos_js_routing.request_context_base_url', 54 | $config['request_context_base_url'] ?: null 55 | ); 56 | 57 | if (isset($config['cache_control'])) { 58 | $config['cache_control']['enabled'] = true; 59 | } else { 60 | $config['cache_control'] = ['enabled' => false]; 61 | } 62 | 63 | $container->setParameter('fos_js_routing.cache_control', $config['cache_control']); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /Extractor/ExposedRoutesExtractor.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Extractor; 15 | 16 | use JMS\I18nRoutingBundle\Router\I18nLoader; 17 | use Symfony\Component\Routing\Route; 18 | use Symfony\Component\Routing\RouteCollection; 19 | use Symfony\Component\Routing\RouterInterface; 20 | 21 | /** 22 | * @author William DURAND 23 | */ 24 | class ExposedRoutesExtractor implements ExposedRoutesExtractorInterface 25 | { 26 | protected string $pattern; 27 | 28 | protected array $availableDomains; 29 | 30 | /** 31 | * Default constructor. 32 | * 33 | * @param array $routesToExpose some route names to expose 34 | * @param array $bundles list of loaded bundles to check when generating the prefix 35 | * 36 | * @throws \Exception 37 | */ 38 | public function __construct(private RouterInterface $router, array $routesToExpose, private string $cacheDir, private array $bundles = []) 39 | { 40 | $domainPatterns = $this->extractDomainPatterns($routesToExpose); 41 | 42 | $this->availableDomains = array_keys($domainPatterns); 43 | 44 | $this->pattern = $this->buildPattern($domainPatterns); 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function getRoutes(): RouteCollection 51 | { 52 | $collection = $this->router->getRouteCollection(); 53 | $routes = new RouteCollection(); 54 | 55 | /** @var Route $route */ 56 | foreach ($collection->all() as $name => $route) { 57 | if ($route->hasOption('expose')) { 58 | $expose = $route->getOption('expose'); 59 | 60 | if (false !== $expose && 'false' !== $expose) { 61 | $routes->add($name, $route); 62 | } 63 | continue; 64 | } 65 | 66 | preg_match('#^'.$this->pattern.'$#', $name, $matches); 67 | 68 | if (0 === count($matches)) { 69 | continue; 70 | } 71 | 72 | $domain = $this->getDomainByRouteMatches($matches, $name); 73 | 74 | if (is_null($domain)) { 75 | continue; 76 | } 77 | 78 | $route = clone $route; 79 | $route->setOption('expose', $domain); 80 | $routes->add($name, $route); 81 | } 82 | 83 | return $routes; 84 | } 85 | 86 | /** 87 | * {@inheritDoc} 88 | */ 89 | public function getBaseUrl(): string 90 | { 91 | return $this->router->getContext()->getBaseUrl() ?: ''; 92 | } 93 | 94 | /** 95 | * {@inheritDoc} 96 | */ 97 | public function getPrefix(string $locale): string 98 | { 99 | if (isset($this->bundles['JMSI18nRoutingBundle'])) { 100 | return $locale.I18nLoader::ROUTING_PREFIX; 101 | } 102 | 103 | return ''; 104 | } 105 | 106 | /** 107 | * {@inheritDoc} 108 | */ 109 | public function getHost(): string 110 | { 111 | $requestContext = $this->router->getContext(); 112 | 113 | $host = $requestContext->getHost(). 114 | ('' === $this->getPort() ? $this->getPort() : ':'.$this->getPort()); 115 | 116 | return $host; 117 | } 118 | 119 | /** 120 | * {@inheritDoc} 121 | */ 122 | public function getPort(): ?string 123 | { 124 | $requestContext = $this->router->getContext(); 125 | 126 | $port = ''; 127 | if ($this->usesNonStandardPort()) { 128 | $method = sprintf('get%sPort', ucfirst($requestContext->getScheme())); 129 | $port = (string) $requestContext->$method(); 130 | } 131 | 132 | return $port; 133 | } 134 | 135 | /** 136 | * {@inheritDoc} 137 | */ 138 | public function getScheme(): string 139 | { 140 | return $this->router->getContext()->getScheme(); 141 | } 142 | 143 | /** 144 | * {@inheritDoc} 145 | */ 146 | public function getCachePath(?string $locale = null): string 147 | { 148 | $cachePath = $this->cacheDir.DIRECTORY_SEPARATOR.'fosJsRouting'; 149 | if (!file_exists($cachePath)) { 150 | if (false === @mkdir($cachePath)) { 151 | throw new \RuntimeException('Unable to create Cache directory ' . $cachePath); 152 | } 153 | } 154 | 155 | if (isset($this->bundles['JMSI18nRoutingBundle'])) { 156 | $cachePath = $cachePath.DIRECTORY_SEPARATOR.'data.'.$locale.'.json'; 157 | } else { 158 | $cachePath = $cachePath.DIRECTORY_SEPARATOR.'data.json'; 159 | } 160 | 161 | return $cachePath; 162 | } 163 | 164 | /** 165 | * {@inheritDoc} 166 | */ 167 | public function getResources(): array 168 | { 169 | return $this->router->getRouteCollection()->getResources(); 170 | } 171 | 172 | /** 173 | * {@inheritDoc} 174 | */ 175 | public function isRouteExposed(Route $route, $name): bool 176 | { 177 | if (false === $route->hasOption('expose')) { 178 | return '' !== $this->pattern && preg_match('#^'.$this->pattern.'$#', $name); 179 | } 180 | 181 | $status = $route->getOption('expose'); 182 | 183 | return false !== $status && 'false' !== $status; 184 | } 185 | 186 | protected function getDomainByRouteMatches($matches, $name): int|string|null 187 | { 188 | $matches = array_filter($matches, fn ($match) => !empty($match)); 189 | 190 | $matches = array_flip(array_intersect_key($matches, array_flip($this->availableDomains))); 191 | 192 | return $matches[$name] ?? null; 193 | } 194 | 195 | protected function extractDomainPatterns($routesToExpose): array 196 | { 197 | $domainPatterns = []; 198 | 199 | foreach ($routesToExpose as $item) { 200 | if (is_string($item)) { 201 | $domainPatterns['default'][] = $item; 202 | continue; 203 | } 204 | 205 | if (is_array($item) && is_string($item['pattern'])) { 206 | if (!isset($item['domain'])) { 207 | $domainPatterns['default'][] = $item['pattern']; 208 | continue; 209 | } elseif (is_string($item['domain'])) { 210 | $domainPatterns[$item['domain']][] = $item['pattern']; 211 | continue; 212 | } 213 | } 214 | 215 | throw new \Exception('routes_to_expose definition is invalid'); 216 | } 217 | 218 | return $domainPatterns; 219 | } 220 | 221 | /** 222 | * Convert the routesToExpose array in a regular expression pattern. 223 | * 224 | * @throws \Exception 225 | */ 226 | protected function buildPattern(array $domainPatterns): string 227 | { 228 | $patterns = []; 229 | 230 | foreach ($domainPatterns as $domain => $items) { 231 | $patterns[] = '(?P<'.$domain.'>'.implode('|', $items).')'; 232 | } 233 | 234 | return implode('|', $patterns); 235 | } 236 | 237 | /** 238 | * Check whether server is serving this request from a non-standard port. 239 | */ 240 | private function usesNonStandardPort(): bool 241 | { 242 | return $this->usesNonStandardHttpPort() || $this->usesNonStandardHttpsPort(); 243 | } 244 | 245 | /** 246 | * Check whether server is serving HTTP over a non-standard port. 247 | */ 248 | private function usesNonStandardHttpPort(): bool 249 | { 250 | return 'http' === $this->getScheme() && '80' != $this->router->getContext()->getHttpPort(); 251 | } 252 | 253 | /** 254 | * Check whether server is serving HTTPS over a non-standard port. 255 | */ 256 | private function usesNonStandardHttpsPort(): bool 257 | { 258 | return 'https' === $this->getScheme() && '443' != $this->router->getContext()->getHttpsPort(); 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /Extractor/ExposedRoutesExtractorInterface.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Extractor; 15 | 16 | use Symfony\Component\Config\Resource\ResourceInterface; 17 | use Symfony\Component\Routing\Route; 18 | use Symfony\Component\Routing\RouteCollection; 19 | 20 | /** 21 | * @author William DURAND 22 | */ 23 | interface ExposedRoutesExtractorInterface 24 | { 25 | /** 26 | * Returns a collection of exposed routes. 27 | */ 28 | public function getRoutes(): RouteCollection; 29 | 30 | /** 31 | * Return the Base URL. 32 | */ 33 | public function getBaseUrl(): string; 34 | 35 | /** 36 | * Get the route prefix to use, i.e. the language if JMSI18nRoutingBundle is active. 37 | */ 38 | public function getPrefix(string $locale): string; 39 | 40 | /** 41 | * Get the host and applicable port from RequestContext. 42 | */ 43 | public function getHost(): string; 44 | 45 | /** 46 | * Get the port from RequestContext, only if non standard port (Eg: "8080"). 47 | */ 48 | public function getPort(): ?string; 49 | 50 | /** 51 | * Get the scheme from RequestContext. 52 | */ 53 | public function getScheme(): string; 54 | 55 | /** 56 | * Get the cache path for this request. 57 | * 58 | * @param string|null $locale the request locale 59 | */ 60 | public function getCachePath(?string $locale): string; 61 | 62 | /** 63 | * Return an array of routing resources. 64 | * 65 | * @return ResourceInterface[] 66 | */ 67 | public function getResources(): array; 68 | 69 | /** 70 | * Tell whether a route should be considered as exposed. 71 | */ 72 | public function isRouteExposed(Route $route, string $name): bool; 73 | } 74 | -------------------------------------------------------------------------------- /FOSJsRoutingBundle.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle; 15 | 16 | use Symfony\Component\HttpKernel\Bundle\Bundle; 17 | 18 | /** 19 | * FOSJsRoutingBundle class. 20 | * 21 | * @author William DURAND 22 | */ 23 | class FOSJsRoutingBundle extends Bundle 24 | { 25 | } 26 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | FOSJsRoutingBundle 2 | ================== 3 | 4 | [![Build 5 | Status](https://secure.travis-ci.org/FriendsOfSymfony/FOSJsRoutingBundle.png?branch=master)](http://travis-ci.org/FriendsOfSymfony/FOSJsRoutingBundle) 6 | 7 | [![Join the chat at https://gitter.im/FOSJsRoutingBundle/Lobby](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/FOSJsRoutingBundle/Lobby?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 8 | 9 | This bundle allows you to expose your routing in your JavaScript code. 10 | That means you'll be able to generate URL with given parameters like you can do with the Router component provided in the Symfony2 core. 11 | 12 | This is a port of the _symfony 1.x_ plugin: [chCmsExposeRoutingPlugin](https://github.com/themouette/chCmsExposeRoutingPlugin). 13 | 14 | Documentation 15 | ------------- 16 | 17 | For documentation, see: [Resources/doc/index.rst](Resources/doc/index.rst). 18 | 19 | [https://symfony.com/doc/master/bundles/FOSJsRoutingBundle/index.html](https://symfony.com/doc/master/bundles/FOSJsRoutingBundle/index.html) 20 | 21 | Contributing 22 | ------------ 23 | 24 | See 25 | [CONTRIBUTING](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/blob/master/CONTRIBUTING.md) 26 | file. 27 | 28 | Original Credits 29 | ---------------- 30 | 31 | * William DURAND as main author. 32 | * Julien MUETTON (Carpe Hora) for the inspiration. 33 | 34 | License 35 | ------- 36 | 37 | This bundle is released under the MIT license. See the complete license in the 38 | bundle: 39 | 40 | Resources/meta/LICENSE 41 | -------------------------------------------------------------------------------- /Resources/config/controllers.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | FOS\JsRoutingBundle\Controller\Controller 7 | 8 | 9 | 10 | 11 | 12 | 13 | %fos_js_routing.cache_control% 14 | %kernel.debug% 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /Resources/config/plovr/compile.js: -------------------------------------------------------------------------------- 1 | { 2 | "id": "router", 3 | "paths": ["../../js"], 4 | "mode": "ADVANCED", 5 | "level": "VERBOSE", 6 | "inputs": "../../js/export.js", 7 | "externs": "../../js/externs.js", 8 | 9 | "define": { 10 | "goog.DEBUG": false 11 | }, 12 | 13 | "type-prefixes-to-strip": ["goog.debug", "goog.asserts", "goog.assert", "console"], 14 | "name-suffixes-to-strip": ["logger", "logger_"], 15 | 16 | "output-file": "../../public/js/router.js", 17 | "output-wrapper": "/**\n * Portions of this code are from the Google Closure Library,\n * received from the Closure Authors under the Apache 2.0 license.\n *\n * All other code is (C) FriendsOfSymfony and subject to the MIT license.\n */\n(function() {%output%})();", 18 | 19 | "pretty-print": false, 20 | "debug": false 21 | } 22 | -------------------------------------------------------------------------------- /Resources/config/routing/routing-sf4.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | fos_js_routing.controller::indexAction 8 | js 9 | js|json 10 | 11 | 12 | -------------------------------------------------------------------------------- /Resources/config/serializer.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FOS\JsRoutingBundle\Serializer\Normalizer\RouteCollectionNormalizer 8 | FOS\JsRoutingBundle\Serializer\Normalizer\RoutesResponseNormalizer 9 | 10 | FOS\JsRoutingBundle\Serializer\Denormalizer\RouteCollectionDenormalizer 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /Resources/config/services.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractor 8 | FOS\JsRoutingBundle\Response\RoutesResponse 9 | 10 | 11 | 12 | 13 | 14 | 15 | %kernel.cache_dir% 16 | %kernel.bundles% 17 | 18 | 19 | 20 | 21 | 22 | 23 | %kernel.project_dir% 24 | %fos_js_routing.request_context_base_url% 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /Resources/doc/commands.rst: -------------------------------------------------------------------------------- 1 | Commands 2 | ======== 3 | 4 | fos:js-routing:dump 5 | ------------------- 6 | 7 | This command dumps the route information into a file so that instead of having 8 | the controller generated JavaScript, you can use a normal file. This also allows 9 | to combine the routes with the other JavaScript files in assetic. 10 | 11 | .. code-block:: bash 12 | 13 | $ php bin/console fos:js-routing:dump 14 | 15 | Instead of the line 16 | 17 | .. code-block:: twig 18 | 19 | 20 | 21 | you now include this as 22 | 23 | .. code-block:: html 24 | 25 | 26 | 27 | Or inside assetic, do 28 | 29 | .. code-block:: twig 30 | 31 | {% javascripts filter='?yui_js' 32 | 'bundles/fosjsrouting/js/router.js' 33 | 'js/fos_js_routes.js' 34 | %} 35 | 36 | {% endjavascripts %} 37 | 38 | .. caution:: 39 | 40 | You should follow the Symfony documentation about generating URLs 41 | in the console: `Forcing HTTPS on Generated URLs`_, as the console is unaware 42 | of the host/port combination you use during a request. You can also set the 43 | `HTTP_HOST` environment variable to hold your hostname including the port you 44 | use (i.e. `localhost:8443`). You can also use the `setHost` and `setPort` 45 | methods on the `Router` object to set it at runtime. 46 | 47 | .. tip:: 48 | 49 | If you are using JMSI18nRoutingBundle, you need to run the command with the 50 | ``--locale`` parameter and a custom ``--target`` once for each locale you use. 51 | Then adjust your include path accordingly. Note that you can only load the dump 52 | of one locale at once in your html as each following dump would overwrite the 53 | data of the previous one. 54 | 55 | fos:js-routing:debug 56 | -------------------- 57 | 58 | This command lists all exposed routes: 59 | 60 | .. code-block:: bash 61 | 62 | # Symfony 3 63 | $ php bin/console fos:js-routing:debug [name] 64 | 65 | .. _`Forcing HTTPS on Generated URLs`: https://symfony.com/doc/current/routing.html#forcing-https-on-generated-urls 66 | -------------------------------------------------------------------------------- /Resources/doc/index.rst: -------------------------------------------------------------------------------- 1 | FOSJsRoutingBundle 2 | ================== 3 | 4 | This bundle allows to expose Symfony Routes to JavaScript, so you can generate 5 | relative or absolute URLs in the browser using the same routes as in the backend. 6 | 7 | * `Installation `_ 8 | * `Usage `_ 9 | * `Commands `_ 10 | -------------------------------------------------------------------------------- /Resources/doc/installation.rst: -------------------------------------------------------------------------------- 1 | Installation 2 | ============ 3 | 4 | Step 1: Download the Bundle 5 | --------------------------- 6 | 7 | Open a command console, enter your project directory and execute the 8 | following command to download the latest stable version of this bundle: 9 | 10 | .. code-block:: bash 11 | 12 | composer require friendsofsymfony/jsrouting-bundle 13 | 14 | This command requires you to have Composer installed globally, as explained 15 | in the `installation chapter`_ of the Composer documentation. 16 | 17 | If you're using Symfony Flex, you can ignore the following steps as they will be executed automatically. 18 | 19 | Step 2: Enable the Bundle 20 | ------------------------- 21 | 22 | Then, enable the bundle by adding it to the list of registered bundles 23 | in the ``config/bundles.php`` file of your project: 24 | 25 | .. code-block:: php 26 | 27 | ['all' => true], 35 | // ... 36 | ]; 37 | 38 | Step 3: Register the Routes 39 | --------------------------- 40 | 41 | Load the bundle's routing definition in the application: 42 | 43 | .. code-block:: yaml 44 | 45 | fos_js_routing: 46 | resource: "@FOSJsRoutingBundle/Resources/config/routing/routing-sf4.xml" 47 | 48 | Step 4: Publish the Assets 49 | -------------------------- 50 | 51 | Execute the following command to publish the assets required by the bundle: 52 | 53 | .. code-block:: bash 54 | 55 | php bin/console assets:install --symlink public 56 | 57 | .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md 58 | 59 | Step 5: If you are using webpack, install the npm package locally 60 | ----------------------------------------------------------------- 61 | .. code-block:: bash 62 | 63 | yarn add -D ./vendor/friendsofsymfony/jsrouting-bundle/Resources/ 64 | -------------------------------------------------------------------------------- /Resources/doc/usage.rst: -------------------------------------------------------------------------------- 1 | Usage 2 | ===== 3 | 4 | In applications not using webpack add these two lines in your layout: 5 | 6 | **With Twig:** 7 | 8 | .. code-block:: twig 9 | 10 | 11 | 12 | 13 | **With PHP:** 14 | 15 | .. code-block:: html+php 16 | 17 | 18 | 19 | 20 | .. note:: 21 | 22 | If you are not using Twig, then it is no problem. What you need is 23 | the two JavaScript files above loaded at some point in your web page. 24 | 25 | 26 | If you are using webpack and Encore to package your assets you can use the webpack plugin included in this package 27 | 28 | .. code-block:: js 29 | 30 | const FosRouting = require('fos-router/webpack/FosRouting'); 31 | //... 32 | Encore 33 | .addPlugin(new FosRouting()) 34 | 35 | Then use it simply by importing ``import Routing from 'fos-router';`` in your js or ts code 36 | 37 | The plugin hooks into the webpack `build` and `watch` process and triggers the `fos:js-routing:dump` command automatically, 38 | once routes have been changed. 39 | 40 | To avoid that, e.g. when building the frontend on a machine or docker image/layer, where no PHP is present, you can configure the 41 | plugin to use a static dumped `routes.json` and suppress automatic recompilation of the file, by passing some options to the plugin: 42 | 43 | .. code-block:: js 44 | 45 | const FosRouting = require('fos-router/webpack/FosRouting'); 46 | //... 47 | Encore 48 | .addPlugin(new FosRouting( 49 | { target: './assets/js/routes.json' }, // <- path to dumped routes.json 50 | false // <- set false to suppress automatic recompilation of the file 51 | ) 52 | ) 53 | 54 | Alternatively you can use the dump command 55 | and export your routes to json, this command will create a json file into the ``public/js`` folder: 56 | 57 | .. code-block:: bash 58 | 59 | bin/console fos:js-routing:dump --format=json --target=assets/js/routes.json 60 | 61 | If you are not using Flex, probably you want to dump your routes into the ``web`` folder 62 | instead of ``public``, to achieve this you can set the ``target`` parameter: 63 | 64 | .. code-block:: bash 65 | 66 | # Symfony Flex 67 | bin/console fos:js-routing:dump --format=json --target=web/js/fos_js_routes.json 68 | 69 | Then within your JavaScript development you can use: 70 | 71 | .. code-block:: javascript 72 | 73 | const routes = require('../../public/js/fos_js_routes.json'); 74 | import Routing from '../../vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'; 75 | 76 | Routing.setRoutingData(routes); 77 | Routing.generate('rep_log_list'); 78 | 79 | 80 | Generating URIs 81 | --------------- 82 | 83 | It's as simple as calling: 84 | 85 | .. code-block:: javascript 86 | 87 | Routing.generate('route_name', /* your params */) 88 | 89 | Or if you want to generate **absolute URLs**: 90 | 91 | .. code-block:: javascript 92 | 93 | Routing.generate('route_name', /* your params */, true) 94 | 95 | Assuming some route definitions: 96 | 97 | **With attributes:** 98 | 99 | .. code-block:: php 100 | 101 | // src/AppBundle/Controller/DefaultController.php 102 | 103 | #[Route(path: '/foo/{id}/bar', name: 'my_route_to_expose', options: ['expose' => true])] 104 | public function indexAction($foo) { 105 | // ... 106 | } 107 | 108 | #[Route(path: '/blog/{page}', name: 'my_route_to_expose_with_defaults', options: ['expose' => true], defaults: ['page' => 1])] 109 | public function blogAction($page) { 110 | // ... 111 | } 112 | 113 | **With YAML:** 114 | 115 | .. code-block:: yaml 116 | 117 | # app/config/routing.yml 118 | my_route_to_expose: 119 | pattern: /foo/{id}/bar 120 | defaults: { _controller: AppBundle:Default:index } 121 | options: 122 | expose: true 123 | 124 | my_route_to_expose_with_defaults: 125 | pattern: /blog/{page} 126 | defaults: { _controller: AppBundle:Default:blog, page: 1 } 127 | options: 128 | expose: true 129 | 130 | **With annotations (deprecated):** 131 | 132 | .. code-block:: php 133 | 134 | // src/AppBundle/Controller/DefaultController.php 135 | 136 | /** 137 | * @Route("/foo/{id}/bar", options={"expose"=true}, name="my_route_to_expose") 138 | */ 139 | public function indexAction($foo) { 140 | // ... 141 | } 142 | 143 | /** 144 | * @Route("/blog/{page}", 145 | * defaults = { "page" = 1 }, 146 | * options = { "expose" = true }, 147 | * name = "my_route_to_expose_with_defaults", 148 | * ) 149 | */ 150 | public function blogAction($page) { 151 | // ... 152 | } 153 | 154 | 155 | 156 | 157 | You can use the ``generate()`` method that way: 158 | 159 | .. code-block:: javascript 160 | 161 | Routing.generate('my_route_to_expose', { id: 10 }); 162 | // will result in /foo/10/bar 163 | 164 | Routing.generate('my_route_to_expose', { id: 10, foo: "bar" }); 165 | // will result in /foo/10/bar?foo=bar 166 | 167 | $.get(Routing.generate('my_route_to_expose', { id: 10, foo: "bar" })); 168 | // will call /foo/10/bar?foo=bar 169 | 170 | Routing.generate('my_route_to_expose_with_defaults'); 171 | // will result in /blog/1 172 | 173 | Routing.generate('my_route_to_expose_with_defaults', { id: 2 }); 174 | // will result in /blog/2 175 | 176 | Routing.generate('my_route_to_expose_with_defaults', { foo: "bar" }); 177 | // will result in /blog/1?foo=bar 178 | 179 | Routing.generate('my_route_to_expose_with_defaults', { id: 2, foo: "bar" }); 180 | // will result in /blog/2?foo=bar 181 | 182 | Moreover, you can configure a list of routes to expose in ``app/config/config.yml``: 183 | 184 | .. code-block:: yaml 185 | 186 | # app/config/config.yml 187 | fos_js_routing: 188 | routes_to_expose: [ route_1, route_2, ... ] 189 | 190 | These routes will be added to the exposed routes. You can use regular expression 191 | patterns if you don't want to list all your routes name by name. 192 | 193 | .. note:: 194 | 195 | If you're using `JMSI18nRoutingBundle`_ for your internationalized routes, your exposed routes must now match the bundle locale-prefixed routes, so you could either specify each locale by hand in the routes names, or use a regular expression to match all of your locales at once: 196 | 197 | .. code-block:: yaml 198 | 199 | # app/config/config.yml 200 | fos_js_routing: 201 | routes_to_expose: [ en__RG__route_1, en__RG__route_2, ... ] 202 | 203 | .. code-block:: yaml 204 | 205 | # app/config/config.yml 206 | fos_js_routing: 207 | routes_to_expose: [ '[a-z]{2}__RG__route_1', '[a-z]{2}__RG__route_2', ... ] 208 | 209 | Note that `Symfony 4.1 added support for internationalized routes`_ out-of-the-box. 210 | 211 | You can prevent to expose a route by configuring it as below: 212 | 213 | .. code-block:: yaml 214 | 215 | # app/config/routing.yml 216 | my_very_secret_route: 217 | pattern: /admin 218 | defaults: { _controller: HelloBundle:Admin:index } 219 | options: 220 | expose: false 221 | 222 | Router service 223 | -------------- 224 | 225 | By default, this bundle exports routes from the default service `router`. You 226 | can configure a different router service if needed: 227 | 228 | .. code-block:: yaml 229 | 230 | # app/config/config.yml 231 | fos_js_routing: 232 | router: my_router_service 233 | 234 | HTTP Caching 235 | ------------ 236 | 237 | You can enable HTTP caching as below: 238 | 239 | .. code-block:: yaml 240 | 241 | # app/config/config.yml 242 | fos_js_routing: 243 | cache_control: 244 | # All are optional, defaults shown 245 | public: false # can be true (public) or false (private) 246 | maxage: null # integer value, e.g. 300 247 | smaxage: null # integer value, e.g. 300 248 | expires: null # anything that can be fed to "new \DateTime($expires)", e.g. "5 minutes" 249 | vary: [] # string or array, e.g. "Cookie" or [ Cookie, Accept ] 250 | 251 | .. _`JMSI18nRoutingBundle`: https://github.com/schmittjoh/JMSI18nRoutingBundle 252 | .. _`Symfony 4.1 added support for internationalized routes`: https://symfony.com/blog/new-in-symfony-4-1-internationalized-routing 253 | -------------------------------------------------------------------------------- /Resources/gulpfile.js: -------------------------------------------------------------------------------- 1 | const gulp = require('gulp'); 2 | const rename = require('gulp-rename'); 3 | const uglify = require('gulp-uglify'); 4 | const wrap = require('gulp-wrap'); 5 | const ts = require('gulp-typescript') 6 | 7 | gulp.task('ts', function() { 8 | return gulp.src('js/router.ts') 9 | .pipe(ts({ 10 | noImplicitAny: true, 11 | })) 12 | .pipe(gulp.dest('public/js')); 13 | }); 14 | 15 | gulp.task('min', function() { 16 | return gulp.src('public/js/!(*.min).js') 17 | .pipe(wrap({ src: 'js/router.template.js' })) 18 | .pipe(gulp.dest('public/js')) 19 | .pipe(rename({ extname: '.min.js' })) 20 | .pipe(uglify()) 21 | .pipe(gulp.dest('public/js')); 22 | }); 23 | 24 | gulp.task('default', gulp.series('ts', 'min')); 25 | -------------------------------------------------------------------------------- /Resources/js/router.template.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | var routing = factory(); 3 | if (typeof define === 'function' && define.amd) { 4 | // AMD. Register as an anonymous module. 5 | define([], routing.Routing); 6 | } else if (typeof module === 'object' && module.exports) { 7 | // Node. Does not work with strict CommonJS, but 8 | // only CommonJS-like environments that support module.exports, 9 | // like Node. 10 | module.exports = routing.Routing; 11 | } else { 12 | // Browser globals (root is window) 13 | root.Routing = routing.Routing; 14 | root.fos = { 15 | Router: routing.Router 16 | }; 17 | } 18 | }(this, function () { 19 | var exports = {}; 20 | <%= contents %> 21 | 22 | return { Router: exports.Router, Routing: exports.Routing }; 23 | })); 24 | -------------------------------------------------------------------------------- /Resources/js/router.ts: -------------------------------------------------------------------------------- 1 | export interface RouteDefaults { 2 | [index: string]: string | null; 3 | } 4 | 5 | export interface RouteRequirements { 6 | [index: string]: string; 7 | } 8 | 9 | export interface RouteParams { 10 | [index: string]: any; 11 | } 12 | 13 | export interface QueryParamAddFunction { 14 | (prefix: string, params: any): void; 15 | } 16 | 17 | export interface Route { 18 | tokens: (string|boolean)[][]; 19 | defaults: undefined[] | RouteDefaults; 20 | requirements: undefined[] | RouteRequirements; 21 | hosttokens: string[][]; 22 | schemes: string[]; 23 | methods: string[]; 24 | } 25 | 26 | export interface RoutesMap { 27 | [index: string]: Route; 28 | } 29 | 30 | export interface Context { 31 | base_url: string; 32 | prefix: string; 33 | host: string; 34 | port: string | null; 35 | scheme: string; 36 | locale: string | null; 37 | } 38 | 39 | export interface RoutingData { 40 | base_url: string; 41 | routes: RoutesMap; 42 | prefix?: string; 43 | host: string; 44 | port?: string | null; 45 | scheme?: string; 46 | locale?: string | null; 47 | } 48 | 49 | export class Router { 50 | private context_: Context; 51 | private routes_!: RoutesMap; 52 | 53 | static getInstance(): Router { 54 | return Routing; 55 | } 56 | 57 | static setData(data: RoutingData): void { 58 | const router = Router.getInstance(); 59 | 60 | router.setRoutingData(data); 61 | } 62 | 63 | constructor(context?: Context, routes?: RoutesMap) { 64 | this.context_ = context || { base_url: '', prefix: '', host: '', port: '', scheme: '', locale: '' }; 65 | this.setRoutes(routes || {}); 66 | } 67 | 68 | setRoutingData(data: RoutingData): void { 69 | this.setBaseUrl(data['base_url']); 70 | this.setRoutes(data['routes']); 71 | 72 | if (typeof data.prefix !== 'undefined') { 73 | this.setPrefix(data['prefix']); 74 | } 75 | if (typeof data.port !== 'undefined') { 76 | this.setPort(data['port']); 77 | } 78 | if (typeof data.locale !== 'undefined') { 79 | this.setLocale(data['locale']); 80 | } 81 | 82 | this.setHost(data['host']); 83 | 84 | if (typeof data.scheme !== 'undefined') { 85 | this.setScheme(data['scheme']); 86 | } 87 | } 88 | 89 | setRoutes(routes: RoutesMap): void { 90 | this.routes_ = Object.freeze(routes); 91 | } 92 | 93 | getRoutes(): RoutesMap { 94 | return this.routes_; 95 | } 96 | 97 | setBaseUrl(baseUrl: string): void { 98 | this.context_.base_url = baseUrl; 99 | } 100 | 101 | getBaseUrl(): string { 102 | return this.context_.base_url; 103 | } 104 | 105 | setPrefix(prefix: string): void { 106 | this.context_.prefix = prefix; 107 | } 108 | 109 | setScheme(scheme: string): void { 110 | this.context_.scheme = scheme; 111 | } 112 | 113 | getScheme(): string { 114 | return this.context_.scheme; 115 | } 116 | 117 | setHost(host: string): void { 118 | this.context_.host = host; 119 | } 120 | 121 | getHost(): string { 122 | return this.context_.host; 123 | } 124 | 125 | setPort(port: string | null) { 126 | this.context_.port = port; 127 | } 128 | 129 | getPort(): string | null { 130 | return this.context_.port; 131 | }; 132 | 133 | setLocale(locale: string | null) { 134 | this.context_.locale = locale; 135 | } 136 | 137 | getLocale(): string | null { 138 | return this.context_.locale; 139 | }; 140 | 141 | /** 142 | * Builds query string params added to a URL. 143 | * Port of jQuery's $.param() function, so credit is due there. 144 | */ 145 | buildQueryParams(prefix: string, params: any, add: QueryParamAddFunction): void { 146 | let name; 147 | let rbracket = new RegExp(/\[\]$/); 148 | 149 | if (params instanceof Array) { 150 | params.forEach((val, i) => { 151 | if (rbracket.test(prefix)) { 152 | add(prefix, val); 153 | } else { 154 | this.buildQueryParams(prefix + '[' + (typeof val === 'object' ? i : '') + ']', val, add); 155 | } 156 | }); 157 | } else if (typeof params === 'object') { 158 | for (name in params) { 159 | this.buildQueryParams(prefix + '[' + name + ']', params[name], add); 160 | } 161 | } else { 162 | add(prefix, params); 163 | } 164 | } 165 | 166 | /** 167 | * Returns a raw route object. 168 | */ 169 | getRoute(name: string): Route { 170 | let prefixedName = this.context_.prefix + name; 171 | let sf41i18nName = name + '.' + this.context_.locale; 172 | let prefixedSf41i18nName = this.context_.prefix + name + '.' + this.context_.locale; 173 | let variants = [prefixedName, sf41i18nName, prefixedSf41i18nName, name]; 174 | 175 | for (let i in variants) { 176 | if (variants[i] in this.routes_) { 177 | return this.routes_[variants[i]]; 178 | } 179 | } 180 | 181 | throw new Error('The route "' + name + '" does not exist.'); 182 | } 183 | 184 | /** 185 | * Generates the URL for a route. 186 | */ 187 | generate(name: string, opt_params?: RouteParams, absolute?: boolean): string { 188 | let route = (this.getRoute(name)); 189 | let params = opt_params || {}; 190 | let unusedParams = Object.assign({}, params); 191 | let url = ''; 192 | let optional = true; 193 | let host = ''; 194 | let port = (typeof this.getPort() == 'undefined' || this.getPort() === null) ? '' : this.getPort(); 195 | 196 | route.tokens.forEach((token) => { 197 | if ('text' === token[0] && typeof token[1] === 'string') { 198 | url = Router.encodePathComponent(token[1]) + url; 199 | optional = false; 200 | 201 | return; 202 | } 203 | 204 | if ('variable' === token[0]) { 205 | if (token.length === 6 && token[5] === true) { // Sixth part of the token array indicates if it should be included in case of defaults 206 | optional = false; 207 | } 208 | let hasDefault = route.defaults && !Array.isArray(route.defaults) && typeof token[3] === 'string' && (token[3] in route.defaults); 209 | if (false === optional || !hasDefault || ((typeof token[3] === 'string' && token[3] in params) && !Array.isArray(route.defaults) && params[token[3]] != route.defaults[token[3]])) { 210 | let value; 211 | 212 | if (typeof token[3] === 'string' && token[3] in params) { 213 | value = params[token[3]]; 214 | delete unusedParams[token[3]]; 215 | } else if (typeof token[3] === 'string' && hasDefault && !Array.isArray(route.defaults)) { 216 | value = route.defaults[token[3]]; 217 | } else if (optional) { 218 | return; 219 | } else { 220 | throw new Error('The route "' + name + '" requires the parameter "' + token[3] + '".'); 221 | } 222 | 223 | let empty = true === value || false === value || '' === value; 224 | 225 | if (!empty || !optional) { 226 | let encodedValue = Router.encodePathComponent(value); 227 | 228 | if ('null' === encodedValue && null === value) { 229 | encodedValue = ''; 230 | } 231 | 232 | url = token[1] + encodedValue + url; 233 | } 234 | 235 | optional = false; 236 | } else if (hasDefault && (typeof token[3] === 'string' && token[3] in unusedParams)) { 237 | delete unusedParams[token[3]]; 238 | } 239 | 240 | return; 241 | } 242 | 243 | throw new Error('The token type "' + token[0] + '" is not supported.'); 244 | }); 245 | 246 | if (url === '') { 247 | url = '/'; 248 | } 249 | 250 | route.hosttokens.forEach((token) => { 251 | let value; 252 | 253 | if ('text' === token[0]) { 254 | host = token[1] + host; 255 | 256 | return; 257 | } 258 | 259 | if ('variable' === token[0]) { 260 | if (token[3] in params) { 261 | value = params[token[3]]; 262 | delete unusedParams[token[3]]; 263 | } else if (route.defaults && !Array.isArray(route.defaults) && (token[3] in route.defaults)) { 264 | value = route.defaults[token[3]]; 265 | } 266 | 267 | host = token[1] + value + host; 268 | } 269 | }); 270 | 271 | url = this.context_.base_url + url; 272 | 273 | if (route.requirements && ('_scheme' in route.requirements) && this.getScheme() != route.requirements['_scheme']) { 274 | const currentHost = host || this.getHost(); 275 | 276 | url = route.requirements['_scheme'] + '://' + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 277 | } else if ('undefined' !== typeof route.schemes && 'undefined' !== typeof route.schemes[0] && this.getScheme() !== route.schemes[0]) { 278 | const currentHost = host || this.getHost(); 279 | 280 | url = route.schemes[0] + '://' + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 281 | } else if (host && this.getHost() !== host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port)) { 282 | url = this.getScheme() + '://' + host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 283 | } else if (absolute === true) { 284 | url = this.getScheme() + '://' + this.getHost() + (this.getHost().indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 285 | } 286 | 287 | if (Object.keys(unusedParams).length > 0) { 288 | let queryParams: string[] = []; 289 | let add = (key: string, value: string|(() => string)) => { 290 | // if value is a function then call it and assign it's return value as value 291 | value = (typeof value === 'function') ? value() : value; 292 | 293 | // change null to empty string 294 | value = (value === null) ? '' : value; 295 | 296 | queryParams.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value)); 297 | }; 298 | 299 | for (const prefix in unusedParams) { 300 | if(unusedParams.hasOwnProperty(prefix)) { 301 | this.buildQueryParams(prefix, unusedParams[prefix], add); 302 | } 303 | } 304 | 305 | url = url + '?' + queryParams.join('&'); 306 | } 307 | 308 | return url; 309 | } 310 | 311 | /** 312 | * Returns the given string encoded to mimic Symfony URL generator. 313 | */ 314 | static customEncodeURIComponent(value: string): string { 315 | return encodeURIComponent(value) 316 | .replace(/%2F/g, '/') 317 | .replace(/%40/g, '@') 318 | .replace(/%3A/g, ':') 319 | .replace(/%21/g, '!') 320 | .replace(/%3B/g, ';') 321 | .replace(/%2C/g, ',') 322 | .replace(/%2A/g, '*') 323 | .replace(/\(/g, '%28') 324 | .replace(/\)/g, '%29') 325 | .replace(/'/g, '%27') 326 | ; 327 | } 328 | 329 | /** 330 | * Returns the given path properly encoded to mimic Symfony URL generator. 331 | */ 332 | static encodePathComponent(value: string): string { 333 | return Router.customEncodeURIComponent(value) 334 | .replace(/%3D/g, '=') 335 | .replace(/%2B/g, '+') 336 | .replace(/%21/g, '!') 337 | .replace(/%7C/g, '|') 338 | ; 339 | } 340 | 341 | /** 342 | * Returns the given query parameter or value properly encoded to mimic Symfony URL generator. 343 | */ 344 | static encodeQueryComponent(value: string): string { 345 | return Router.customEncodeURIComponent(value) 346 | .replace(/%3F/g, '?') 347 | ; 348 | } 349 | } 350 | 351 | export const Routing = new Router(); 352 | 353 | export default Routing; 354 | -------------------------------------------------------------------------------- /Resources/meta/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) FriendsOfSymfony 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is furnished 8 | to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /Resources/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "fos-router", 3 | "version": "2.5.0", 4 | "description": "A pretty nice way to use the routes generated by the FOSJsRoutingBundle in your JavaScript.", 5 | "keywords": [ 6 | "router", 7 | "symfony" 8 | ], 9 | "license": "MIT", 10 | "author": { 11 | "name": "FriendsOfSymfony Community", 12 | "url": "https://github.com/friendsofsymfony/FOSJsRoutingBundle/contributors" 13 | }, 14 | "contributors": [ 15 | { 16 | "name": "William Durand", 17 | "email": "will+git@drnd.me" 18 | }, 19 | { 20 | "name": "Bruno Sampaio", 21 | "email": "bens.sampaio@gmail.com" 22 | } 23 | ], 24 | "main": "public/js/router.js", 25 | "files": [ 26 | "webpack/FosRouting.js", 27 | "public/js/router.js", 28 | "public/js/router.min.js", 29 | "ts/router.d.ts" 30 | ], 31 | "types": "ts/router.d.ts", 32 | "devDependencies": { 33 | "@types/node": "^14.18.10", 34 | "google-closure-library": "^20220104.0.0", 35 | "gulp": "^4.0.2", 36 | "gulp-rename": "^2.0.0", 37 | "gulp-uglify": "^3.0.2", 38 | "gulp-typescript": "^6.0.0-alpha.1", 39 | "gulp-wrap": "^0.15.0", 40 | "jasmine": "^4.0.2", 41 | "tsd": "^0.19.1", 42 | "typescript": "^4.5.5" 43 | }, 44 | "scripts": { 45 | "build": "gulp && npm run build:types", 46 | "build:types": "tsc --declaration --emitDeclarationOnly", 47 | "test": "npm run build && npm run test:types && phantomjs js/run_jsunit.js js/router_test.html", 48 | "test:types": "tsd", 49 | "prepublish": "npm run build" 50 | }, 51 | "dependencies": { 52 | "@bpnetguy/webpack-inject-plugin": "^2.0.4" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /Resources/public/js/router.js: -------------------------------------------------------------------------------- 1 | (function (root, factory) { 2 | var routing = factory(); 3 | if (typeof define === 'function' && define.amd) { 4 | // AMD. Register as an anonymous module. 5 | define([], routing.Routing); 6 | } else if (typeof module === 'object' && module.exports) { 7 | // Node. Does not work with strict CommonJS, but 8 | // only CommonJS-like environments that support module.exports, 9 | // like Node. 10 | module.exports = routing.Routing; 11 | } else { 12 | // Browser globals (root is window) 13 | root.Routing = routing.Routing; 14 | root.fos = { 15 | Router: routing.Router 16 | }; 17 | } 18 | }(this, function () { 19 | var exports = {}; 20 | "use strict"; 21 | exports.__esModule = true; 22 | exports.Routing = exports.Router = void 0; 23 | var Router = /** @class */ (function () { 24 | function Router(context, routes) { 25 | this.context_ = context || { base_url: '', prefix: '', host: '', port: '', scheme: '', locale: '' }; 26 | this.setRoutes(routes || {}); 27 | } 28 | Router.getInstance = function () { 29 | return exports.Routing; 30 | }; 31 | Router.setData = function (data) { 32 | var router = Router.getInstance(); 33 | router.setRoutingData(data); 34 | }; 35 | Router.prototype.setRoutingData = function (data) { 36 | this.setBaseUrl(data['base_url']); 37 | this.setRoutes(data['routes']); 38 | if (typeof data.prefix !== 'undefined') { 39 | this.setPrefix(data['prefix']); 40 | } 41 | if (typeof data.port !== 'undefined') { 42 | this.setPort(data['port']); 43 | } 44 | if (typeof data.locale !== 'undefined') { 45 | this.setLocale(data['locale']); 46 | } 47 | this.setHost(data['host']); 48 | if (typeof data.scheme !== 'undefined') { 49 | this.setScheme(data['scheme']); 50 | } 51 | }; 52 | Router.prototype.setRoutes = function (routes) { 53 | this.routes_ = Object.freeze(routes); 54 | }; 55 | Router.prototype.getRoutes = function () { 56 | return this.routes_; 57 | }; 58 | Router.prototype.setBaseUrl = function (baseUrl) { 59 | this.context_.base_url = baseUrl; 60 | }; 61 | Router.prototype.getBaseUrl = function () { 62 | return this.context_.base_url; 63 | }; 64 | Router.prototype.setPrefix = function (prefix) { 65 | this.context_.prefix = prefix; 66 | }; 67 | Router.prototype.setScheme = function (scheme) { 68 | this.context_.scheme = scheme; 69 | }; 70 | Router.prototype.getScheme = function () { 71 | return this.context_.scheme; 72 | }; 73 | Router.prototype.setHost = function (host) { 74 | this.context_.host = host; 75 | }; 76 | Router.prototype.getHost = function () { 77 | return this.context_.host; 78 | }; 79 | Router.prototype.setPort = function (port) { 80 | this.context_.port = port; 81 | }; 82 | Router.prototype.getPort = function () { 83 | return this.context_.port; 84 | }; 85 | ; 86 | Router.prototype.setLocale = function (locale) { 87 | this.context_.locale = locale; 88 | }; 89 | Router.prototype.getLocale = function () { 90 | return this.context_.locale; 91 | }; 92 | ; 93 | /** 94 | * Builds query string params added to a URL. 95 | * Port of jQuery's $.param() function, so credit is due there. 96 | */ 97 | Router.prototype.buildQueryParams = function (prefix, params, add) { 98 | var _this = this; 99 | var name; 100 | var rbracket = new RegExp(/\[\]$/); 101 | if (params instanceof Array) { 102 | params.forEach(function (val, i) { 103 | if (rbracket.test(prefix)) { 104 | add(prefix, val); 105 | } 106 | else { 107 | _this.buildQueryParams(prefix + '[' + (typeof val === 'object' ? i : '') + ']', val, add); 108 | } 109 | }); 110 | } 111 | else if (typeof params === 'object') { 112 | for (name in params) { 113 | this.buildQueryParams(prefix + '[' + name + ']', params[name], add); 114 | } 115 | } 116 | else { 117 | add(prefix, params); 118 | } 119 | }; 120 | /** 121 | * Returns a raw route object. 122 | */ 123 | Router.prototype.getRoute = function (name) { 124 | var prefixedName = this.context_.prefix + name; 125 | var sf41i18nName = name + '.' + this.context_.locale; 126 | var prefixedSf41i18nName = this.context_.prefix + name + '.' + this.context_.locale; 127 | var variants = [prefixedName, sf41i18nName, prefixedSf41i18nName, name]; 128 | for (var i in variants) { 129 | if (variants[i] in this.routes_) { 130 | return this.routes_[variants[i]]; 131 | } 132 | } 133 | throw new Error('The route "' + name + '" does not exist.'); 134 | }; 135 | /** 136 | * Generates the URL for a route. 137 | */ 138 | Router.prototype.generate = function (name, opt_params, absolute) { 139 | var route = (this.getRoute(name)); 140 | var params = opt_params || {}; 141 | var unusedParams = Object.assign({}, params); 142 | var url = ''; 143 | var optional = true; 144 | var host = ''; 145 | var port = (typeof this.getPort() == 'undefined' || this.getPort() === null) ? '' : this.getPort(); 146 | route.tokens.forEach(function (token) { 147 | if ('text' === token[0] && typeof token[1] === 'string') { 148 | url = Router.encodePathComponent(token[1]) + url; 149 | optional = false; 150 | return; 151 | } 152 | if ('variable' === token[0]) { 153 | if (token.length === 6 && token[5] === true) { // Sixth part of the token array indicates if it should be included in case of defaults 154 | optional = false; 155 | } 156 | var hasDefault = route.defaults && !Array.isArray(route.defaults) && typeof token[3] === 'string' && (token[3] in route.defaults); 157 | if (false === optional || !hasDefault || ((typeof token[3] === 'string' && token[3] in params) && !Array.isArray(route.defaults) && params[token[3]] != route.defaults[token[3]])) { 158 | var value = void 0; 159 | if (typeof token[3] === 'string' && token[3] in params) { 160 | value = params[token[3]]; 161 | delete unusedParams[token[3]]; 162 | } 163 | else if (typeof token[3] === 'string' && hasDefault && !Array.isArray(route.defaults)) { 164 | value = route.defaults[token[3]]; 165 | } 166 | else if (optional) { 167 | return; 168 | } 169 | else { 170 | throw new Error('The route "' + name + '" requires the parameter "' + token[3] + '".'); 171 | } 172 | var empty = true === value || false === value || '' === value; 173 | if (!empty || !optional) { 174 | var encodedValue = Router.encodePathComponent(value); 175 | if ('null' === encodedValue && null === value) { 176 | encodedValue = ''; 177 | } 178 | url = token[1] + encodedValue + url; 179 | } 180 | optional = false; 181 | } 182 | else if (hasDefault && (typeof token[3] === 'string' && token[3] in unusedParams)) { 183 | delete unusedParams[token[3]]; 184 | } 185 | return; 186 | } 187 | throw new Error('The token type "' + token[0] + '" is not supported.'); 188 | }); 189 | if (url === '') { 190 | url = '/'; 191 | } 192 | route.hosttokens.forEach(function (token) { 193 | var value; 194 | if ('text' === token[0]) { 195 | host = token[1] + host; 196 | return; 197 | } 198 | if ('variable' === token[0]) { 199 | if (token[3] in params) { 200 | value = params[token[3]]; 201 | delete unusedParams[token[3]]; 202 | } 203 | else if (route.defaults && !Array.isArray(route.defaults) && (token[3] in route.defaults)) { 204 | value = route.defaults[token[3]]; 205 | } 206 | host = token[1] + value + host; 207 | } 208 | }); 209 | url = this.context_.base_url + url; 210 | if (route.requirements && ('_scheme' in route.requirements) && this.getScheme() != route.requirements['_scheme']) { 211 | var currentHost = host || this.getHost(); 212 | url = route.requirements['_scheme'] + '://' + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 213 | } 214 | else if ('undefined' !== typeof route.schemes && 'undefined' !== typeof route.schemes[0] && this.getScheme() !== route.schemes[0]) { 215 | var currentHost = host || this.getHost(); 216 | url = route.schemes[0] + '://' + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 217 | } 218 | else if (host && this.getHost() !== host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port)) { 219 | url = this.getScheme() + '://' + host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 220 | } 221 | else if (absolute === true) { 222 | url = this.getScheme() + '://' + this.getHost() + (this.getHost().indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; 223 | } 224 | if (Object.keys(unusedParams).length > 0) { 225 | var queryParams_1 = []; 226 | var add = function (key, value) { 227 | // if value is a function then call it and assign it's return value as value 228 | value = (typeof value === 'function') ? value() : value; 229 | // change null to empty string 230 | value = (value === null) ? '' : value; 231 | queryParams_1.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value)); 232 | }; 233 | for (var prefix in unusedParams) { 234 | if (unusedParams.hasOwnProperty(prefix)) { 235 | this.buildQueryParams(prefix, unusedParams[prefix], add); 236 | } 237 | } 238 | url = url + '?' + queryParams_1.join('&'); 239 | } 240 | return url; 241 | }; 242 | /** 243 | * Returns the given string encoded to mimic Symfony URL generator. 244 | */ 245 | Router.customEncodeURIComponent = function (value) { 246 | return encodeURIComponent(value) 247 | .replace(/%2F/g, '/') 248 | .replace(/%40/g, '@') 249 | .replace(/%3A/g, ':') 250 | .replace(/%21/g, '!') 251 | .replace(/%3B/g, ';') 252 | .replace(/%2C/g, ',') 253 | .replace(/%2A/g, '*') 254 | .replace(/\(/g, '%28') 255 | .replace(/\)/g, '%29') 256 | .replace(/'/g, '%27'); 257 | }; 258 | /** 259 | * Returns the given path properly encoded to mimic Symfony URL generator. 260 | */ 261 | Router.encodePathComponent = function (value) { 262 | return Router.customEncodeURIComponent(value) 263 | .replace(/%3D/g, '=') 264 | .replace(/%2B/g, '+') 265 | .replace(/%21/g, '!') 266 | .replace(/%7C/g, '|'); 267 | }; 268 | /** 269 | * Returns the given query parameter or value properly encoded to mimic Symfony URL generator. 270 | */ 271 | Router.encodeQueryComponent = function (value) { 272 | return Router.customEncodeURIComponent(value) 273 | .replace(/%3F/g, '?'); 274 | }; 275 | return Router; 276 | }()); 277 | exports.Router = Router; 278 | exports.Routing = new Router(); 279 | exports["default"] = exports.Routing; 280 | 281 | 282 | return { Router: exports.Router, Routing: exports.Routing }; 283 | })); -------------------------------------------------------------------------------- /Resources/public/js/router.min.js: -------------------------------------------------------------------------------- 1 | !function(e){(t={}).__esModule=!0,t.Routing=t.Router=void 0,o=function(){function l(e,t){this.context_=e||{base_url:"",prefix:"",host:"",port:"",scheme:"",locale:""},this.setRoutes(t||{})}return l.getInstance=function(){return t.Routing},l.setData=function(e){l.getInstance().setRoutingData(e)},l.prototype.setRoutingData=function(e){this.setBaseUrl(e.base_url),this.setRoutes(e.routes),void 0!==e.prefix&&this.setPrefix(e.prefix),void 0!==e.port&&this.setPort(e.port),void 0!==e.locale&&this.setLocale(e.locale),this.setHost(e.host),void 0!==e.scheme&&this.setScheme(e.scheme)},l.prototype.setRoutes=function(e){this.routes_=Object.freeze(e)},l.prototype.getRoutes=function(){return this.routes_},l.prototype.setBaseUrl=function(e){this.context_.base_url=e},l.prototype.getBaseUrl=function(){return this.context_.base_url},l.prototype.setPrefix=function(e){this.context_.prefix=e},l.prototype.setScheme=function(e){this.context_.scheme=e},l.prototype.getScheme=function(){return this.context_.scheme},l.prototype.setHost=function(e){this.context_.host=e},l.prototype.getHost=function(){return this.context_.host},l.prototype.setPort=function(e){this.context_.port=e},l.prototype.getPort=function(){return this.context_.port},l.prototype.setLocale=function(e){this.context_.locale=e},l.prototype.getLocale=function(){return this.context_.locale},l.prototype.buildQueryParams=function(o,e,n){var t,r=this,s=new RegExp(/\[\]$/);if(e instanceof Array)e.forEach(function(e,t){s.test(o)?n(o,e):r.buildQueryParams(o+"["+("object"==typeof e?t:"")+"]",e,n)});else if("object"==typeof e)for(t in e)this.buildQueryParams(o+"["+t+"]",e[t],n);else n(o,e)},l.prototype.getRoute=function(e){var t,o=[this.context_.prefix+e,e+"."+this.context_.locale,this.context_.prefix+e+"."+this.context_.locale,e];for(t in o)if(o[t]in this.routes_)return this.routes_[o[t]];throw new Error('The route "'+e+'" does not exist.')},l.prototype.generate=function(r,e,p){var t,s=this.getRoute(r),i=e||{},u=Object.assign({},i),c="",a=!0,o="",e=void 0===this.getPort()||null===this.getPort()?"":this.getPort();if(s.tokens.forEach(function(e){if("text"===e[0]&&"string"==typeof e[1])return c=l.encodePathComponent(e[1])+c,void(a=!1);if("variable"!==e[0])throw new Error('The token type "'+e[0]+'" is not supported.');6===e.length&&!0===e[5]&&(a=!1);var t=s.defaults&&!Array.isArray(s.defaults)&&"string"==typeof e[3]&&e[3]in s.defaults;if(!1===a||!t||"string"==typeof e[3]&&e[3]in i&&!Array.isArray(s.defaults)&&i[e[3]]!=s.defaults[e[3]]){var o,n=void 0;if("string"==typeof e[3]&&e[3]in i)n=i[e[3]],delete u[e[3]];else{if("string"!=typeof e[3]||!t||Array.isArray(s.defaults)){if(a)return;throw new Error('The route "'+r+'" requires the parameter "'+e[3]+'".')}n=s.defaults[e[3]]}(!0===n||!1===n||""===n)&&a||(o=l.encodePathComponent(n),c=e[1]+(o="null"===o&&null===n?"":o)+c),a=!1}else t&&"string"==typeof e[3]&&e[3]in u&&delete u[e[3]]}),""===c&&(c="/"),s.hosttokens.forEach(function(e){var t;"text"!==e[0]?"variable"===e[0]&&(e[3]in i?(t=i[e[3]],delete u[e[3]]):s.defaults&&!Array.isArray(s.defaults)&&e[3]in s.defaults&&(t=s.defaults[e[3]]),o=e[1]+t+o):o=e[1]+o}),c=this.context_.base_url+c,s.requirements&&"_scheme"in s.requirements&&this.getScheme()!=s.requirements._scheme?(t=o||this.getHost(),c=s.requirements._scheme+"://"+t+(-1(Router.getInstance()); 7 | expectType(Routing); 8 | 9 | expectType(Routing.getRoutes()); 10 | expectType(Routing.getRoute('homepage')); 11 | 12 | expectType(Routing.getBaseUrl()); 13 | Routing.setBaseUrl(''); 14 | 15 | expectType(Routing.getScheme()); 16 | Routing.setScheme('https'); 17 | 18 | expectType(Routing.getHost()); 19 | Routing.setHost('localhost'); 20 | 21 | expectType(Routing.getPort()); 22 | Routing.setPort('1234'); 23 | 24 | expectType(Routing.getLocale()); 25 | Routing.setLocale('en'); 26 | 27 | Routing.setRoutingData(routes); 28 | Routing.setRoutingData({ 29 | base_url: '', 30 | routes: { 31 | homepage: { tokens: [['text', '/']], defaults: [], requirements: [], hosttokens: [], methods: [], schemes: [], }, 32 | admin_index: { tokens: [['text', '/admin']], defaults: [], requirements: [], hosttokens: [], methods: [], schemes: [], }, 33 | admin_pages: { tokens: [['text', '/admin/path']], defaults: [], requirements: [], hosttokens: [], methods: [], schemes: [], }, 34 | blog_index: { tokens: [['text', '/blog']], defaults: [], requirements: [], hosttokens: [['text', 'localhost']], methods: [], schemes: [], }, 35 | blog_post: { 36 | tokens: [ 37 | ['variable', '/', '[^/]++', 'slug'], 38 | ['text', '/blog'], 39 | ], 40 | defaults: [], 41 | requirements: [], 42 | hosttokens: [['text', 'localhost']], 43 | methods: [], 44 | schemes: [], 45 | }, 46 | users_delete: { 47 | tokens: [ 48 | ['text', '/delete'], 49 | ['variable', '/', '[^/]++', 'id', true], 50 | ['text', '/users'] 51 | ], 52 | defaults: [], 53 | requirements: [], 54 | hosttokens: [], 55 | methods: [ 56 | 'DELETE' 57 | ], 58 | schemes: [] 59 | }, 60 | feed_post: { 61 | tokens: [ 62 | ['variable', '.', 'js|json', '_format', true], 63 | ['text', '/feed/post'] 64 | ], 65 | defaults: { 66 | _format: 'xml', 67 | }, 68 | requirements: { 69 | _format: 'xml|json', 70 | }, 71 | hosttokens: [], 72 | methods: ['GET'], 73 | schemes: [], 74 | }, 75 | }, 76 | prefix: '', 77 | host: '', 78 | port: null, 79 | scheme: '', 80 | locale: 'en', 81 | }); 82 | 83 | expectType(Routing.generate('homepage')); 84 | expectType(Routing.generate('blog_post', { 85 | slug: 'my-blog-post', 86 | })); 87 | expectType(Routing.generate('users_delete', { 88 | id: 123, 89 | })); 90 | -------------------------------------------------------------------------------- /Resources/ts/routes.json: -------------------------------------------------------------------------------- 1 | { 2 | "base_url": "", 3 | "routes": { 4 | "feed_post": { 5 | "tokens": [ 6 | [ 7 | "variable", 8 | ".", 9 | "js|json", 10 | "_format", 11 | true 12 | ], 13 | ["text", "/feed/post"] 14 | ], 15 | "defaults": { 16 | "_format": "xml" 17 | }, 18 | "requirements": { 19 | "_format": "xml|json" 20 | }, 21 | "hosttokens": [], 22 | "methods": [ 23 | "GET" 24 | ], 25 | "schemes": [] 26 | } 27 | }, 28 | "prefix": "", 29 | "host": "localhost", 30 | "port": "", 31 | "scheme": "https", 32 | "locale": null 33 | } 34 | -------------------------------------------------------------------------------- /Resources/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "declaration": true, 4 | "declarationDir": "ts", 5 | "resolveJsonModule": true, 6 | "esModuleInterop": true 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /Resources/webpack/FosRouting.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @author Adrien Foulon 3 | */ 4 | const fs = require('fs'); 5 | const path = require('path'); 6 | const util = require('util'); 7 | 8 | const InjectPlugin = require('@bpnetguy/webpack-inject-plugin').default; 9 | 10 | const execFile = util.promisify(require('child_process').execFile); 11 | const readFile = util.promisify(fs.readFile); 12 | const rmFile = util.promisify(fs.rm); 13 | const writeFile = util.promisify(fs.writeFile); 14 | const makeDir = util.promisify(fs.mkdir) 15 | 16 | class FosRouting { 17 | default = { 18 | locale: '', 19 | prettyPrint: false, 20 | domain: [], 21 | php: 'php' 22 | }; 23 | 24 | constructor(options = {}, registerCompileHooks = true) { 25 | this.options = Object.assign({target: 'var/cache/fosRoutes.json'}, this.default, options, {format: 'json'}); 26 | this.finalTarget = path.resolve(process.cwd(), this.options.target); 27 | this.options.target = path.resolve(process.cwd(), this.options.target.replace(/\.json$/, '.tmp.json')); 28 | 29 | if (this.options.target === this.finalTarget) { 30 | this.options.target += '.tmp'; 31 | } 32 | this.registerCompileHooks = registerCompileHooks; 33 | } 34 | 35 | // Values don't need to be escaped because node already does that 36 | shellArg(key, value) { 37 | key = this.kebabize(key); 38 | return typeof value === 'boolean' ? (value ? '--' + key : '') : '--' + key + '=' + value; 39 | } 40 | 41 | kebabize(str) { 42 | return str.split('').map((letter, idx) => { 43 | return letter.toUpperCase() === letter 44 | ? `${idx !== 0 ? '-' : ''}${letter.toLowerCase()}` 45 | : letter; 46 | }).join(''); 47 | } 48 | 49 | apply(compiler) { 50 | let prevContent = null; 51 | try { 52 | fs.readFileSync(this.finalTarget); 53 | } catch (e) { 54 | } 55 | const compile = async (comp, callback) => { 56 | const args = Object.keys(this.options).reduce((pass, key) => { 57 | const val = this.options[key]; 58 | if (val !== this.default[key]) { 59 | if (Array.isArray(val)) { 60 | pass.push(...val.map((v) => this.shellArg(key, v))); 61 | } else { 62 | pass.push(this.shellArg(key, val)); 63 | } 64 | } 65 | return pass; 66 | }, []); 67 | await execFile(this.options.php, ['bin/console', 'fos:js-routing:dump', ...args]); 68 | try { 69 | const content = await readFile(this.options.target); 70 | await rmFile(this.options.target); 71 | if (!prevContent || content.compare(prevContent) !== 0) { 72 | await makeDir(path.dirname(this.finalTarget), {recursive: true}); 73 | await writeFile(this.finalTarget, content); 74 | prevContent = content; 75 | if (comp.modifiedFiles && !comp.modifiedFiles.has(this.finalTarget)) { 76 | comp.modifiedFiles.add(this.finalTarget); 77 | } 78 | } 79 | } catch (e) { 80 | const logger = compiler.getInfrastructureLogger('FosRouting'); 81 | logger.error(e.toString()); 82 | } 83 | callback(); 84 | }; 85 | 86 | if (this.registerCompileHooks === true) { 87 | compiler.hooks.beforeRun.tapAsync('RouteDump', compile); 88 | compiler.hooks.watchRun.tapAsync('RouteDump_Watch', (comp, callback) => { 89 | if (!comp.modifiedFiles || !comp.modifiedFiles.has(this.finalTarget)) { 90 | compile(comp, callback); 91 | } else { 92 | callback(); 93 | } 94 | }); 95 | } 96 | 97 | new InjectPlugin(() => { 98 | return 'import Routing from "fos-router";' + 99 | 'import routes from '+JSON.stringify(this.finalTarget)+';' + 100 | 'Routing.setRoutingData(routes);'; 101 | }).apply(compiler); 102 | } 103 | } 104 | 105 | module.exports = FosRouting; 106 | module.exports.default = FosRouting; 107 | -------------------------------------------------------------------------------- /Response/RoutesResponse.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Response; 15 | 16 | use Symfony\Component\Routing\RouteCollection; 17 | 18 | class RoutesResponse 19 | { 20 | protected $routes; 21 | 22 | public function __construct( 23 | protected ?string $baseUrl = null, 24 | ?RouteCollection $routes = null, 25 | protected ?string $prefix = null, 26 | protected ?string $host = null, 27 | protected ?string $port = null, 28 | protected ?string $scheme = null, 29 | protected ?string $locale = null, 30 | protected array $domains = [], 31 | ) { 32 | $this->routes = $routes ?: new RouteCollection(); 33 | } 34 | 35 | public function getRoutes(): array 36 | { 37 | $exposedRoutes = []; 38 | 39 | foreach ($this->routes->all() as $name => $route) { 40 | if (!$route->hasOption('expose')) { 41 | $domain = 'default'; 42 | } else { 43 | $domain = $route->getOption('expose'); 44 | $domain = is_string($domain) ? ('true' === $domain ? 'default' : $domain) : 'default'; 45 | } 46 | 47 | if (0 === count($this->domains)) { 48 | if ('default' !== $domain) { 49 | continue; 50 | } 51 | } elseif (!in_array($domain, $this->domains, true)) { 52 | continue; 53 | } 54 | 55 | $compiledRoute = $route->compile(); 56 | $defaults = array_intersect_key( 57 | $route->getDefaults(), 58 | array_fill_keys($compiledRoute->getVariables(), null) 59 | ); 60 | 61 | if (!isset($defaults['_locale']) && in_array('_locale', $compiledRoute->getVariables())) { 62 | $defaults['_locale'] = $this->locale; 63 | } 64 | 65 | $exposedRoutes[$name] = [ 66 | 'tokens' => $compiledRoute->getTokens(), 67 | 'defaults' => $defaults, 68 | 'requirements' => $route->getRequirements(), 69 | 'hosttokens' => method_exists($compiledRoute, 'getHostTokens') ? $compiledRoute->getHostTokens() : [], 70 | 'methods' => $route->getMethods(), 71 | 'schemes' => $route->getSchemes(), 72 | ]; 73 | } 74 | 75 | return $exposedRoutes; 76 | } 77 | 78 | public function setRoutes(RouteCollection $routes): void 79 | { 80 | $this->routes = $routes; 81 | } 82 | 83 | public function getBaseUrl(): string 84 | { 85 | return $this->baseUrl; 86 | } 87 | 88 | public function setBaseUrl(string $baseUrl): void 89 | { 90 | $this->baseUrl = $baseUrl; 91 | } 92 | 93 | public function getPrefix(): ?string 94 | { 95 | return $this->prefix; 96 | } 97 | 98 | public function setPrefix(?string $prefix): void 99 | { 100 | $this->prefix = $prefix; 101 | } 102 | 103 | public function getHost(): ?string 104 | { 105 | return $this->host; 106 | } 107 | 108 | public function setHost(?string $host): void 109 | { 110 | $this->host = $host; 111 | } 112 | 113 | public function getPort(): ?string 114 | { 115 | return $this->port; 116 | } 117 | 118 | public function setPort(?string $port): void 119 | { 120 | $this->port = $port; 121 | } 122 | 123 | public function getScheme(): ?string 124 | { 125 | return $this->scheme; 126 | } 127 | 128 | public function setScheme(?string $scheme): void 129 | { 130 | $this->scheme = $scheme; 131 | } 132 | 133 | public function getLocale(): ?string 134 | { 135 | return $this->locale; 136 | } 137 | 138 | public function setLocale(?string $locale): void 139 | { 140 | $this->locale = $locale; 141 | } 142 | 143 | public function getDomains(): array 144 | { 145 | return $this->domains; 146 | } 147 | 148 | public function setDomains(array $domains): void 149 | { 150 | $this->domains = $domains; 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /Serializer/Denormalizer/RouteCollectionDenormalizer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Serializer\Denormalizer; 15 | 16 | use Symfony\Component\Routing\Route; 17 | use Symfony\Component\Routing\RouteCollection; 18 | use Symfony\Component\Serializer\Normalizer\DenormalizerInterface; 19 | 20 | class RouteCollectionDenormalizer implements DenormalizerInterface 21 | { 22 | /** 23 | * {@inheritDoc} 24 | */ 25 | public function denormalize(mixed $data, string $type, ?string $format = null, array $context = []): RouteCollection 26 | { 27 | $collection = new RouteCollection(); 28 | 29 | foreach ($data as $name => $values) { 30 | $collection->add($name, new Route( 31 | $values['path'], 32 | $values['defaults'], 33 | $values['requirements'], 34 | $values['options'], 35 | $values['host'], 36 | $values['schemes'], 37 | $values['methods'], 38 | $values['condition'] 39 | )); 40 | } 41 | 42 | return $collection; 43 | } 44 | 45 | /** 46 | * {@inheritDoc} 47 | */ 48 | public function supportsDenormalization(mixed $data, string $type, ?string $format = null, array $context = []): bool 49 | { 50 | if (!is_array($data)) { 51 | return false; 52 | } 53 | 54 | if (count($data) < 1) { 55 | return true; 56 | } 57 | 58 | $values = current($data); 59 | 60 | foreach (['path', 'defaults', 'requirements', 'options', 'host', 'schemes', 'methods', 'condition'] as $key) { 61 | if (!isset($values[$key])) { 62 | return false; 63 | } 64 | } 65 | 66 | return true; 67 | } 68 | 69 | public function getSupportedTypes(?string $format): array 70 | { 71 | return ['*' => false]; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Serializer/Normalizer/RouteCollectionNormalizer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Serializer\Normalizer; 15 | 16 | use Symfony\Component\Routing\RouteCollection; 17 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 18 | 19 | /** 20 | * Class RouteCollectionNormalizer. 21 | */ 22 | class RouteCollectionNormalizer implements NormalizerInterface 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function normalize(mixed $object, ?string $format = null, array $context = []): array 28 | { 29 | $collection = []; 30 | 31 | foreach ($object->all() as $name => $route) { 32 | $collection[$name] = [ 33 | 'path' => $route->getPath(), 34 | 'host' => $route->getHost(), 35 | 'defaults' => $route->getDefaults(), 36 | 'requirements' => $route->getRequirements(), 37 | 'options' => $route->getOptions(), 38 | 'schemes' => $route->getSchemes(), 39 | 'methods' => $route->getMethods(), 40 | 'condition' => method_exists($route, 'getCondition') ? $route->getCondition() : '', 41 | ]; 42 | } 43 | 44 | return $collection; 45 | } 46 | 47 | /** 48 | * {@inheritDoc} 49 | */ 50 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 51 | { 52 | return $data instanceof RouteCollection; 53 | } 54 | 55 | public function getSupportedTypes(?string $format): array 56 | { 57 | return [RouteCollection::class => true]; 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /Serializer/Normalizer/RoutesResponseNormalizer.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Serializer\Normalizer; 15 | 16 | use FOS\JsRoutingBundle\Response\RoutesResponse; 17 | use Symfony\Component\Serializer\Normalizer\NormalizerInterface; 18 | 19 | /** 20 | * Class RoutesResponseNormalizer. 21 | */ 22 | class RoutesResponseNormalizer implements NormalizerInterface 23 | { 24 | /** 25 | * {@inheritDoc} 26 | */ 27 | public function normalize(mixed $object, ?string $format = null, array $context = []): array 28 | { 29 | return [ 30 | 'base_url' => $object->getBaseUrl(), 31 | 'routes' => $object->getRoutes(), 32 | 'prefix' => $object->getPrefix(), 33 | 'host' => $object->getHost(), 34 | 'port' => $object->getPort(), 35 | 'scheme' => $object->getScheme(), 36 | 'locale' => $object->getLocale(), 37 | ]; 38 | } 39 | 40 | /** 41 | * {@inheritDoc} 42 | */ 43 | public function supportsNormalization(mixed $data, ?string $format = null, array $context = []): bool 44 | { 45 | return $data instanceof RoutesResponse; 46 | } 47 | 48 | public function getSupportedTypes(?string $format): array 49 | { 50 | return [RoutesResponse::class => true]; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /Util/CacheControlConfig.php: -------------------------------------------------------------------------------- 1 | 9 | * 10 | * For the full copyright and license information, please view the LICENSE 11 | * file that was distributed with this source code. 12 | */ 13 | 14 | namespace FOS\JsRoutingBundle\Util; 15 | 16 | use Symfony\Component\HttpFoundation\Response; 17 | use Symfony\Component\HttpKernel\EventListener\AbstractSessionListener; 18 | 19 | class CacheControlConfig 20 | { 21 | public function __construct(private array $parameters = []) 22 | { 23 | } 24 | 25 | public function apply(Response $response): void 26 | { 27 | if (empty($this->parameters['enabled'])) { 28 | return; 29 | } 30 | 31 | $response->headers->set(AbstractSessionListener::NO_AUTO_CACHE_CONTROL_HEADER, 'true'); 32 | 33 | $this->parameters['public'] ? $response->setPublic() : $response->setPrivate(); 34 | 35 | if (is_int($this->parameters['maxage'])) { 36 | $response->setMaxAge($this->parameters['maxage']); 37 | } 38 | 39 | if (is_int($this->parameters['smaxage'])) { 40 | $response->setSharedMaxAge($this->parameters['smaxage']); 41 | } 42 | 43 | if (null !== $this->parameters['expires']) { 44 | $response->setExpires(new \DateTime($this->parameters['expires'])); 45 | } 46 | 47 | if (!empty($this->parameters['vary'])) { 48 | $response->setVary($this->parameters['vary']); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "friendsofsymfony/jsrouting-bundle", 3 | "type": "symfony-bundle", 4 | "description": "A pretty nice way to expose your Symfony routing to client applications.", 5 | "keywords": [ "js routing", "routing", "javascript" ], 6 | "homepage": "http://friendsofsymfony.github.com", 7 | "license": "MIT", 8 | "authors": [ 9 | { 10 | "name": "William Durand", 11 | "email": "will+git@drnd.me" 12 | }, 13 | { 14 | "name": "FriendsOfSymfony Community", 15 | "homepage": "https://github.com/friendsofsymfony/FOSJsRoutingBundle/contributors" 16 | } 17 | ], 18 | "require": { 19 | "php": "^8.0", 20 | "symfony/framework-bundle": "^5.4|^6.0|^7.0", 21 | "symfony/serializer": "^5.4|^6.0.1|^7.0", 22 | "symfony/console": "^5.4|^6.0|^7.0", 23 | "willdurand/jsonp-callback-validator": "~1.1|^2.0" 24 | }, 25 | "require-dev": { 26 | "symfony/expression-language": "^5.4|^6.0|^7.0", 27 | "symfony/phpunit-bridge": "^5.4|^6.0|^7.0" 28 | }, 29 | "autoload": { 30 | "psr-4": { "FOS\\JsRoutingBundle\\": "" }, 31 | "exclude-from-classmap": [ 32 | "/Tests/" 33 | ] 34 | }, 35 | "extra": { 36 | "branch-alias": { 37 | "dev-master": "3.x-dev" 38 | } 39 | } 40 | } 41 | --------------------------------------------------------------------------------