diff --git a/.github/workflows/code_checks.yaml b/.github/workflows/code_checks.yaml new file mode 100644 index 00000000..927587d0 --- /dev/null +++ b/.github/workflows/code_checks.yaml @@ -0,0 +1,97 @@ +# .github/workflows/code_checks.yaml +name: Code_Checks + +on: ["push", "pull_request"] + +jobs: + js-tests: + name: "JS Tests" + runs-on: ubuntu-20.04 + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Install JS dependencies + run: | + cd Resources && npm install + + - name: Run JS tests + run: | + cd Resources && npm run test + phpunit: + name: "PHP ${{ matrix.php }} + ${{ matrix.dependencies }} dependencies + Symfony ${{ matrix.symfony }}" + runs-on: ubuntu-20.04 + strategy: + fail-fast: false + matrix: + php: ['7.1', '7.2', '7.3', '7.4', '8.0'] + dependencies: [highest] + symfony: ['*'] + include: + # Minimum supported dependencies with the oldest supported PHP version + - php: '7.1' + dependencies: lowest + symfony: '*' + + # Minimum supported dependencies with the latest supported PHP version + - php: '8.0' + dependencies: lowest + symfony: '*' + + - php: '8.0' + dependencies: highest + symfony: '*' + + # Test each supported Symfony version with lowest supported PHP version + - php: '7.1' + dependencies: highest + symfony: '3.4.*' + + - php: '7.1' + dependencies: highest + symfony: '4.4.*' + + - php: '7.2' + dependencies: highest + symfony: '5.4.*' + + - php: '7.3' + dependencies: highest + symfony: '5.4.*' + + - php: '7.4' + dependencies: highest + symfony: '5.4.*' + steps: + - name: Checkout + uses: actions/checkout@v2 + + - name: Setup PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + + - name: Require Symfony version + if: matrix.symfony != '*' + run: | + composer global require --no-interaction --no-progress symfony/flex:^1.11 + composer config extra.symfony.require ${{ matrix.symfony }} + + - name: Update project dependencies + uses: ramsey/composer-install@v1 + with: + dependency-versions: ${{ matrix.dependencies }} + + - name: Cache PHPUnit + uses: actions/cache@v2 + with: + path: vendor/bin/.phpunit + key: ${{ runner.os }}-phpunit-${{ matrix.php }} + + - name: Install PHPUnit + run: vendor/bin/simple-phpunit install + + - name: Run PHPUnit tests + env: + SYMFONY_DEPRECATIONS_HELPER: max[self]=0 + run: vendor/bin/simple-phpunit -v \ No newline at end of file diff --git a/.gitignore b/.gitignore index 2b6cfe3b..b6fb3940 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ /composer.phar /vendor/ /node_modules/ +/.phpunit/ +/.phpunit.result.cache \ No newline at end of file diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 7b1bd269..00000000 --- a/.travis.yml +++ /dev/null @@ -1,40 +0,0 @@ -language: php - -php: - - 5.3 - - 5.4 - - 5.6 - - 7.0 - - hhvm - -cache: - directories: - - $HOME/.composer/cache/files - -matrix: - include: - - php: 5.6 - env: SYMFONY_VERSION='2.3.*' - - php: 5.6 - env: SYMFONY_VERSION='2.5.*' - - php: 5.6 - env: SYMFONY_VERSION='2.8.*' - - php: 5.6 - env: SYMFONY_VERSION='3.0.*' - -sudo: false - -cache: - directory: - - $HOME/.composer/cache - -before_install: - - sh -c 'if [ "$SYMFONY_VERSION" != "" ]; then composer require --no-update symfony/symfony=$SYMFONY_VERSION; fi;' - -install: - - composer install - - npm install google-closure-library - -script: - - phpunit --coverage-text - - phantomjs Resources/js/run_jsunit.js Resources/js/router_test.html diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..d502070b --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,72 @@ +# Changelog + +## v2.8.0 - 2021-12-15 +- Fix expose: false behavior ([#404](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/404)) +- Fix dump using domains ([#410](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/410)) +- Fix docs links ([#412](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/412)) +- Replace Travis with Github actions ([#414](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/414)) + +## v2.7.0 - 2020-11-22 +- Add support for PHP 8 ([#399](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/399)) + +## v2.6.0 - 2020-05-20 +- [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)) +- Fixed issue with creating absolute instead of relative path on hosts with differing ports ([#391](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/391)) + +## v2.5.4 - 2020-04-15 +- Fix duplicated port in absolute path ([#381](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/381)) + +## v2.5.3 - 2020-01-13 +- Rervert fall back to current domain when baseurl is missing or empty in json ([#374](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/374)) + +## v2.5.2 - 2020-01-12 +- Fall back to current domain when baseurl is missing or empty in json ([#371](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/371)) +- Upgrade gulp to version 4 ([#372](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/372)) + +## v2.5.1 - 2019-12-02 +- [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)) + +## v2.5.0 - 2019-12-01 +- [BC break] Add support for Symfony 5, drop support for PHP5, drop support for Symfony 2 ([#366](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/366)) +- Fix absolute url generation including ports ([#361](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/361)) +- Fix cache for exposed routes in debug mode ([#362](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/362)) + +## v2.4.0 - 2019-08-10 +- Add Symfony 4.1 localized routes support ([#334](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/334)) +- Add documentation remarks on JMSI18nRoutingBundle compatibility ([#352](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/352)) + +## v2.3.1 - 2019-06-17 +- Fix regex pattern to match whole url pattern ([#350](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/350)) +- Small documentation update + +## v2.3.0 - 2019-02-03 +- Add routing-sf4.xml to move towards Symfony >4.1 syntax +- Add functionality to granularly expose routes based on domains ([#346](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/346)) +- Small cleanup and textual fix + +## v2.2.2 - 2018-11-28 +- Fix Symfony 4.2 deprecation +- Add setRoutingData to typescript definition + +## v2.2.1 - 2018-09-29 +- Add support for a different port + +## v2.2.0 - 2018-02-07 +- Refactor JavaScript code to improve webpack compatibility + +## v2.1.1 - 2017-12-13 +- Fix SF <4 compatibility ([#306](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/306)) + +## v2.1.0 - 2017-12-13 +- Add Symfony 4 compatibility ([#300](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/300)) +- Add JSON dump functionality ([#302](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/302)) +- Fix bug denormalizing empty routing collections from cache +- Update documentation for Symfony 3 ([#273](https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/pull/273)) + +## v2.0.0 - 2017-11-08 +- Add Symfony 3.* compatibility +- Added `--pretty-print` option to `fos:js-routing:dump`-command, making the resulting javascript pretty-printed +- Removed SF 2.1 backwards compatibility code +- Add automatic injection of `locale` parameter +- Added functionality to change the used router service +- Added normalizer classes diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index ff60002c..1576229f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -35,30 +35,42 @@ Before running the test suite, execute the following Composer command to install the dependencies used by the bundle: ```bash -$ composer install --dev +$ composer update ``` Then, execute the tests executing: ```bash -$ phpunit +$ ./phpunit ``` ### JavaScript Test Suite -First, install [PhantomJS](http://phantomjs.org/) and [Google Closure -Library](https://github.com/google/closure-library): +First, install [PhantomJS](http://phantomjs.org/) (see the website for further +details or simply use your favourite package manager) and the development dependencies using: ```bash -$ npm install google-closure-library +$ cd Resources +$ npm install ``` -Run the JS test suite with: +then run the JS test suite with: ```bash -$ phantomjs Resources/js/run_jsunit.js Resources/js/router_test.html +$ npm run test ``` +Because the current test suite runs against the built javascript a build is automatically +run first (see 'Compiling the JavaScript files' below for further details). You can +explicitly run only the test suite with: + +```bash +$ phantomjs js/run_jsunit.js js/router_test.html +``` + +Alternatively you can open `Resources/js/router_test.html` in your browser which +runs the same test suite with a graphical output. + Compiling the JavaScript files ------------------------------ @@ -67,19 +79,23 @@ Compiling the JavaScript files > We already provide a compiled version of the JavaScript; this section is only > relevant if you want to make changes to this script. -In order to re-compile the JavaScript source files that we ship with this -bundle, you need the Google Closure Tools. You need the -[plovr](http://plovr.com/download.html) tool, which is a Java ARchive, so you -also need a working Java environment. You can re-compile the JavaScript with the -following command: +This project is using [Gulp](https://gulpjs.com/) to compile JavaScript files. +In order to use Gulp you must install both [node](https://nodejs.org/en/) and +[npm](https://www.npmjs.com/). + +If you are not familiar with using Gulp, it is recommended that you review this +[An Introduction to Gulp.js](https://www.sitepoint.com/introduction-gulp-js/) +tutorial which will guide you through the process of getting node and npm installed. + +Once you have node and npm installed: ```bash -$ java -jar plovr.jar build Resources/config/plovr/compile.js +$ cd Resources +$ npm install ``` -Alternatively, you can use the JMSGoogleClosureBundle. If you install this -bundle, you can re-compile the JavaScript with the following command: +Then to perform a build ```bash -$ php app/console plovr:build @FOSJsRoutingBundle/compile.js +$ npm run build ``` diff --git a/Command/DumpCommand.php b/Command/DumpCommand.php index 16f82294..864f37e8 100644 --- a/Command/DumpCommand.php +++ b/Command/DumpCommand.php @@ -11,34 +11,53 @@ namespace FOS\JsRoutingBundle\Command; +use FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractorInterface; use FOS\JsRoutingBundle\Response\RoutesResponse; -use Symfony\Bundle\FrameworkBundle\Command\ContainerAwareCommand; +use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Serializer\SerializerInterface; /** * Dumps routes to the filesystem. * * @author Benjamin Dulau */ -class DumpCommand extends ContainerAwareCommand +class DumpCommand extends Command { - /** - * @var string - */ - private $targetPath; + protected static $defaultName = 'fos:js-routing:dump'; /** - * @var \FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractorInterface + * @var ExposedRoutesExtractorInterface */ private $extractor; /** - * @var \Symfony\Component\Serializer\SerializerInterface + * @var SerializerInterface */ private $serializer; + /** + * @var string + */ + private $projectDir; + + /** + * @var string + */ + private $requestContextBaseUrl; + + public function __construct(ExposedRoutesExtractorInterface $extractor, SerializerInterface $serializer, $projectDir, $requestContextBaseUrl = null) + { + $this->extractor = $extractor; + $this->serializer = $serializer; + $this->projectDir = $projectDir; + $this->requestContextBaseUrl = $requestContextBaseUrl; + + parent::__construct(); + } + protected function configure() { $this @@ -51,6 +70,13 @@ protected function configure() 'Callback function to pass the routes as an argument.', 'fos.Router.setData' ) + ->addOption( + 'format', + null, + InputOption::VALUE_REQUIRED, + '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', + 'js' + ) ->addOption( 'target', null, @@ -70,26 +96,34 @@ protected function configure() InputOption::VALUE_NONE, 'Pretty print the JSON.' ) + ->addOption( + 'domain', + null, + InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, + 'Specify expose domain', + array() + ) ; } - protected function initialize(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - parent::initialize($input, $output); - - $this->targetPath = $input->getOption('target') ?: - sprintf('%s/../web/js/fos_js_routes.js', $this->getContainer()->getParameter('kernel.root_dir')); + if(!in_array($input->getOption('format'), array('js', 'json'))) { + $output->writeln('Invalid format specified. Use js or json.'); + return 1; + } - $this->extractor = $this->getContainer()->get('fos_js_routing.extractor'); - $this->serializer = $this->getContainer()->get('fos_js_routing.serializer'); - } + $callback = $input->getOption('callback'); + if(empty($callback)) { + $output->writeln('If you include --callback it must not be empty. Do you perhaps want --format=json'); + return 1; + } - protected function execute(InputInterface $input, OutputInterface $output) - { $output->writeln('Dumping exposed routes.'); $output->writeln(''); $this->doDump($input, $output); + return 0; } /** @@ -100,17 +134,29 @@ protected function execute(InputInterface $input, OutputInterface $output) */ private function doDump(InputInterface $input, OutputInterface $output) { - if (!is_dir($dir = dirname($this->targetPath))) { + $domain = $input->getOption('domain'); + + $extractor = $this->extractor; + $serializer = $this->serializer; + $targetPath = $input->getOption('target') ?: + sprintf( + '%s/web/js/fos_js_routes%s.%s', + $this->projectDir, + empty($domain) ? '' : ('_' . implode('_', $domain)), + $input->getOption('format') + ); + + if (!is_dir($dir = dirname($targetPath))) { $output->writeln('[dir+] ' . $dir); if (false === @mkdir($dir, 0777, true)) { throw new \RuntimeException('Unable to create directory ' . $dir); } } - $output->writeln('[file+] ' . $this->targetPath); + $output->writeln('[file+] ' . $targetPath); - $baseUrl = $this->getContainer()->hasParameter('fos_js_routing.request_context_base_url') ? - $this->getContainer()->getParameter('fos_js_routing.request_context_base_url') : + $baseUrl = null !== $this->requestContextBaseUrl ? + $this->requestContextBaseUrl : $this->extractor->getBaseUrl() ; @@ -120,22 +166,27 @@ private function doDump(InputInterface $input, OutputInterface $output) $params = array(); } - $content = $this->serializer->serialize( + $content = $serializer->serialize( new RoutesResponse( $baseUrl, - $this->extractor->getRoutes(), - $this->extractor->getPrefix($input->getOption('locale')), - $this->extractor->getHost(), - $this->extractor->getScheme() + $extractor->getRoutes(), + $extractor->getPrefix($input->getOption('locale')), + $extractor->getHost(), + $extractor->getPort(), + $extractor->getScheme(), + $input->getOption('locale'), + $domain ), 'json', $params ); - $content = sprintf("%s(%s);", $input->getOption('callback'), $content); + if('js' == $input->getOption('format')) { + $content = sprintf("%s(%s);", $input->getOption('callback'), $content); + } - if (false === @file_put_contents($this->targetPath, $content)) { - throw new \RuntimeException('Unable to write file ' . $this->targetPath); + if (false === @file_put_contents($targetPath, $content)) { + throw new \RuntimeException('Unable to write file ' . $targetPath); } } } diff --git a/Command/RouterDebugExposedCommand.php b/Command/RouterDebugExposedCommand.php index e3d203c5..0e2e1cd7 100644 --- a/Command/RouterDebugExposedCommand.php +++ b/Command/RouterDebugExposedCommand.php @@ -12,29 +12,52 @@ namespace FOS\JsRoutingBundle\Command; use FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractorInterface; -use Symfony\Bundle\FrameworkBundle\Command\RouterDebugCommand; use Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper; +use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Routing\Route; +use Symfony\Component\Routing\RouterInterface; +use Symfony\Component\Routing\RouteCollection; /** * A console command for retrieving information about exposed routes. * * @author William DURAND */ -class RouterDebugExposedCommand extends RouterDebugCommand +class RouterDebugExposedCommand extends Command { + protected static $defaultName = 'fos:js-routing:debug'; + + private $extractor; + + private $router; + + public function __construct(ExposedRoutesExtractorInterface $extractor, RouterInterface $router) + { + $this->extractor = $extractor; + $this->router = $router; + + parent::__construct(); + } + + /** * {@inheritdoc} */ protected function configure() { - parent::configure(); - $this + ->setDefinition(array( + new InputArgument('name', InputArgument::OPTIONAL, 'A route name'), + new InputOption('show-controllers', null, InputOption::VALUE_NONE, 'Show assigned controllers in overview'), + new InputOption('format', null, InputOption::VALUE_REQUIRED, 'The output format (txt, xml, json, or md)', 'txt'), + new InputOption('raw', null, InputOption::VALUE_NONE, 'To output raw route(s)'), + new InputOption('domain', null, InputOption::VALUE_OPTIONAL | InputOption::VALUE_IS_ARRAY, 'Specify expose domain', array()) + )) ->setName('fos:js-routing:debug') - ->setAliases(array()) // reset the aliases used by the parent command in Symfony 2.6+ ->setDescription('Displays currently exposed routes for an application') ->setHelp(<<fos:js-routing:debug command displays an application's routes which will be available via JavaScript. @@ -53,46 +76,58 @@ protected function configure() /** * @see Command */ - protected function execute(InputInterface $input, OutputInterface $output) + protected function execute(InputInterface $input, OutputInterface $output): int { - /** @var ExposedRoutesExtractorInterface $extractor */ - $extractor = $this->getContainer()->get('fos_js_routing.extractor'); - if ($name = $input->getArgument('name')) { /** @var Route $route */ - $route = $this->getContainer()->get('router')->getRouteCollection()->get($name); + $route = $this->router->getRouteCollection()->get($name); if (!$route) { throw new \InvalidArgumentException(sprintf('The route "%s" does not exist.', $name)); } - if (!$extractor->isRouteExposed($route, $name)) { + if (!$this->extractor->isRouteExposed($route, $name)) { throw new \InvalidArgumentException(sprintf('The route "%s" was found, but it is not an exposed route.', $name)); } - if (!class_exists('Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper')) { - // BC layer for Symfony 2.3 - $this->outputRoute($output, $name); - } else { - $helper = new DescriptorHelper(); - $helper->describe($output, $route, array( - 'format' => $input->getOption('format'), - 'raw_text' => $input->getOption('raw'), - 'show_controllers' => $input->getOption('show-controllers'), - )); - } + $helper = new DescriptorHelper(); + $helper->describe($output, $route, array( + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'show_controllers' => $input->getOption('show-controllers'), + )); } else { - if (!class_exists('Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper')) { - // BC layer for Symfony 2.3 - $this->outputRoutes($output, $extractor->getRoutes()); - } else { - $helper = new DescriptorHelper(); - $helper->describe($output, $extractor->getRoutes(), array( - 'format' => $input->getOption('format'), - 'raw_text' => $input->getOption('raw'), - 'show_controllers' => $input->getOption('show-controllers'), - )); + $helper = new DescriptorHelper(); + $helper->describe($output, $this->getRoutes($input->getOption('domain')), array( + 'format' => $input->getOption('format'), + 'raw_text' => $input->getOption('raw'), + 'show_controllers' => $input->getOption('show-controllers'), + )); + } + return 0; + } + + protected function getRoutes($domain = array()) + { + $routes = $this->extractor->getRoutes(); + + if (empty($domain)) { + return $routes; + } + + $targetRoutes = new RouteCollection(); + + foreach ($routes as $name => $route) { + + $expose = $route->getOption('expose'); + $expose = is_string($expose) ? ($expose === 'true' ? 'default' : $expose) : 'default'; + + if (in_array($expose, $domain, true)) { + $targetRoutes->add($name, $route); } + } + + return $targetRoutes; } } diff --git a/Controller/Controller.php b/Controller/Controller.php index aea785f0..9f220789 100644 --- a/Controller/Controller.php +++ b/Controller/Controller.php @@ -68,7 +68,7 @@ public function __construct($serializer, ExposedRoutesExtractorInterface $expose */ public function indexAction(Request $request, $_format) { - $session = $request->getSession(); + $session = $request->hasSession() ? $request->getSession() : null; if ($request->hasPreviousSession() && $session->getFlashBag() instanceof AutoExpireFlashBag) { // keep current flashes for one more request if using AutoExpireFlashBag @@ -77,7 +77,7 @@ public function indexAction(Request $request, $_format) $cache = new ConfigCache($this->exposedRoutesExtractor->getCachePath($request->getLocale()), $this->debug); - if (!$cache->isFresh()) { + if (!$cache->isFresh() || $this->debug) { $exposedRoutes = $this->exposedRoutesExtractor->getRoutes(); $serializedRoutes = $this->serializer->serialize($exposedRoutes, 'json'); $cache->write($serializedRoutes, $this->exposedRoutesExtractor->getResources()); @@ -96,8 +96,10 @@ public function indexAction(Request $request, $_format) $exposedRoutes, $this->exposedRoutesExtractor->getPrefix($request->getLocale()), $this->exposedRoutesExtractor->getHost(), + $this->exposedRoutesExtractor->getPort(), $this->exposedRoutesExtractor->getScheme(), - $request->getLocale() + $request->getLocale(), + $request->query->has('domain') ? explode(',', $request->query->get('domain')) : array() ); $content = $this->serializer->serialize($routesResponse, 'json'); diff --git a/DependencyInjection/Configuration.php b/DependencyInjection/Configuration.php index 39275a6c..8f0a210b 100644 --- a/DependencyInjection/Configuration.php +++ b/DependencyInjection/Configuration.php @@ -28,9 +28,15 @@ class Configuration implements ConfigurationInterface */ public function getConfigTreeBuilder() { - $builder = new TreeBuilder(); + $builder = new TreeBuilder('fos_js_routing'); + if (\method_exists($builder, 'getRootNode')) { + $rootNode = $builder->getRootNode(); + } else { + // BC layer for symfony/config 4.1 and older + $rootNode = $builder->root('fos_js_routing'); + } - $builder->root('fos_js_routing') + $rootNode ->children() ->scalarNode('serializer')->cannotBeEmpty()->end() ->arrayNode('routes_to_expose') diff --git a/DependencyInjection/FOSJsRoutingExtension.php b/DependencyInjection/FOSJsRoutingExtension.php index 50c12cbb..1746f8cc 100644 --- a/DependencyInjection/FOSJsRoutingExtension.php +++ b/DependencyInjection/FOSJsRoutingExtension.php @@ -53,9 +53,10 @@ public function load(array $configs, ContainerBuilder $container) ->getDefinition('fos_js_routing.extractor') ->replaceArgument(1, $config['routes_to_expose']); - if (isset($config['request_context_base_url'])) { - $container->setParameter('fos_js_routing.request_context_base_url', $config['request_context_base_url']); - } + $container->setParameter( + 'fos_js_routing.request_context_base_url', + $config['request_context_base_url'] ? $config['request_context_base_url'] : null + ); if (isset($config['cache_control'])) { $config['cache_control']['enabled'] = true; diff --git a/Extractor/ExposedRoutesExtractor.php b/Extractor/ExposedRoutesExtractor.php index 15933617..90faf8c9 100644 --- a/Extractor/ExposedRoutesExtractor.php +++ b/Extractor/ExposedRoutesExtractor.php @@ -38,25 +38,37 @@ class ExposedRoutesExtractor implements ExposedRoutesExtractorInterface */ protected $bundles; + /** + * @var string + */ + protected $pattern; + /** * @var array */ - protected $routesToExpose; + protected $availableDomains; /** * Default constructor. * - * @param RouterInterface $router The router. - * @param array $routesToExpose Some route names to expose. - * @param string $cacheDir - * @param array $bundles list of loaded bundles to check when generating the prefix + * @param RouterInterface $router The router. + * @param array $routesToExpose Some route names to expose. + * @param string $cacheDir + * @param array $bundles list of loaded bundles to check when generating the prefix + * + * @throws \Exception */ - public function __construct(RouterInterface $router, array $routesToExpose = array(), $cacheDir, $bundles = array()) + public function __construct(RouterInterface $router, array $routesToExpose, $cacheDir, $bundles = array()) { $this->router = $router; - $this->routesToExpose = $routesToExpose; $this->cacheDir = $cacheDir; $this->bundles = $bundles; + + $domainPatterns = $this->extractDomainPatterns($routesToExpose); + + $this->availableDomains = array_keys($domainPatterns); + + $this->pattern = $this->buildPattern($domainPatterns); } /** @@ -69,9 +81,32 @@ public function getRoutes() /** @var Route $route */ foreach ($collection->all() as $name => $route) { - if ($this->isRouteExposed($route, $name)) { - $routes->add($name, $route); + + if ($route->hasOption('expose')) { + + $expose = $route->getOption('expose'); + + if ($expose !== false && $expose !== 'false') { + $routes->add($name, $route); + } + continue; } + + preg_match('#^' . $this->pattern . '$#', $name, $matches); + + if (count($matches) === 0) { + continue; + } + + $domain = $this->getDomainByRouteMatches($matches, $name); + + if (is_null($domain)) { + continue; + } + + $route = clone $route; + $route->setOption('expose', $domain); + $routes->add($name, $route); } return $routes; @@ -104,14 +139,26 @@ public function getHost() { $requestContext = $this->router->getContext(); - $host = $requestContext->getHost(); + $host = $requestContext->getHost() . + ('' === $this->getPort() ? $this->getPort() : ':' . $this->getPort()); + + return $host; + } + /** + * {@inheritDoc} + */ + public function getPort() + { + $requestContext = $this->router->getContext(); + + $port=""; if ($this->usesNonStandardPort()) { $method = sprintf('get%sPort', ucfirst($requestContext->getScheme())); - $host .= ':' . $requestContext->$method(); + $port = $requestContext->$method(); } - return $host; + return $port; } /** @@ -154,26 +201,71 @@ public function getResources() */ public function isRouteExposed(Route $route, $name) { - $pattern = $this->buildPattern(); + if (false === $route->hasOption('expose')) { + return ('' !== $this->pattern && preg_match('#^' . $this->pattern . '$#', $name)); + } + + $status = $route->getOption('expose'); + return ($status !== false && $status !== 'false'); + } + + protected function getDomainByRouteMatches($matches, $name) + { + $matches = array_filter($matches, function($match) { + return !empty($match); + }); + + $matches = array_flip(array_intersect_key($matches, array_flip($this->availableDomains))); + + return isset($matches[$name]) ? $matches[$name] : null; + } + + protected function extractDomainPatterns($routesToExpose) + { + $domainPatterns = array(); + + foreach ($routesToExpose as $item) { + + if (is_string($item)) { + $domainPatterns['default'][] = $item; + continue; + } + + if (is_array($item) && is_string($item['pattern'])) { + + if (!isset($item['domain'])) { + $domainPatterns['default'][] = $item['pattern']; + continue; + } elseif (is_string($item['domain'])) { + $domainPatterns[$item['domain']][] = $item['pattern']; + continue; + } - return true === $route->getOption('expose') - || 'true' === $route->getOption('expose') - || ('' !== $pattern && preg_match('#' . $pattern . '#', $name)); + } + + throw new \Exception('routes_to_expose definition is invalid'); + } + + return $domainPatterns; } /** * Convert the routesToExpose array in a regular expression pattern * + * @param $domainPatterns * @return string + * @throws \Exception */ - protected function buildPattern() + protected function buildPattern($domainPatterns) { $patterns = array(); - foreach ($this->routesToExpose as $toExpose) { - $patterns[] = '(' . $toExpose . ')'; + + foreach ($domainPatterns as $domain => $items) { + + $patterns[] = '(?P<' . $domain . '>' . implode('|', $items) . ')'; } - return implode($patterns, '|'); + return implode('|', $patterns); } /** diff --git a/Extractor/ExposedRoutesExtractorInterface.php b/Extractor/ExposedRoutesExtractorInterface.php index 2db32f63..cb09bf2a 100644 --- a/Extractor/ExposedRoutesExtractorInterface.php +++ b/Extractor/ExposedRoutesExtractorInterface.php @@ -49,6 +49,13 @@ public function getPrefix($locale); */ public function getHost(); + /** + * Get the port from RequestContext, only if non standard port (Eg: "8080") + * + * @return string + */ + public function getPort(); + /** * Get the scheme from RequestContext * diff --git a/README.md b/README.md index 2e157289..aab57688 100644 --- a/README.md +++ b/README.md @@ -4,6 +4,8 @@ FOSJsRoutingBundle [![Build Status](https://secure.travis-ci.org/FriendsOfSymfony/FOSJsRoutingBundle.png?branch=master)](http://travis-ci.org/FriendsOfSymfony/FOSJsRoutingBundle) +[![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) + This bundle allows you to expose your routing in your JavaScript code. 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. @@ -12,7 +14,7 @@ This is a port of the _symfony 1.x_ plugin: [chCmsExposeRoutingPlugin](https://g Documentation ------------- -For documentation, see: +For documentation, see: [Resources/doc/index.rst](Resources/doc/index.rst). [https://symfony.com/doc/master/bundles/FOSJsRoutingBundle/index.html](https://symfony.com/doc/master/bundles/FOSJsRoutingBundle/index.html) diff --git a/Resources/.gitignore b/Resources/.gitignore new file mode 100644 index 00000000..ea0b6b75 --- /dev/null +++ b/Resources/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +yarn.lock +package-lock.json diff --git a/Resources/config/controllers.xml b/Resources/config/controllers.xml index fe516d90..ecc7e546 100644 --- a/Resources/config/controllers.xml +++ b/Resources/config/controllers.xml @@ -6,7 +6,7 @@ FOS\JsRoutingBundle\Controller\Controller - + %fos_js_routing.cache_control% diff --git a/Resources/config/routing/routing-sf4.xml b/Resources/config/routing/routing-sf4.xml new file mode 100644 index 00000000..e75a19ad --- /dev/null +++ b/Resources/config/routing/routing-sf4.xml @@ -0,0 +1,11 @@ + + + + + fos_js_routing.controller::indexAction + js + js|json + + diff --git a/Resources/config/services.xml b/Resources/config/services.xml index 2413a361..efdd6715 100644 --- a/Resources/config/services.xml +++ b/Resources/config/services.xml @@ -8,11 +8,24 @@ - + %kernel.cache_dir% %kernel.bundles% + + + + + %kernel.project_dir% + %fos_js_routing.request_context_base_url% + + + + + + + diff --git a/Resources/doc/commands.rst b/Resources/doc/commands.rst index 8308e56c..0318fbfe 100644 --- a/Resources/doc/commands.rst +++ b/Resources/doc/commands.rst @@ -10,7 +10,11 @@ to combine the routes with the other JavaScript files in assetic. .. code-block:: bash + # Symfony 2 $ php app/console fos:js-routing:dump + + # Symfony 3 + $ php bin/console fos:js-routing:dump Instead of the line @@ -53,6 +57,10 @@ This command lists all exposed routes: .. code-block:: bash + # Symfony 2 $ php app/console fos:js-routing:debug [name] + + # Symfony 3 + $ php bin/console fos:js-routing:debug [name] .. _`Configuring The Request Context Globally`: http://symfony.com/doc/current/cookbook/console/sending_emails.html#configuring-the-request-context-globally diff --git a/Resources/doc/index.rst b/Resources/doc/index.rst index 31e4554e..dff0999b 100644 --- a/Resources/doc/index.rst +++ b/Resources/doc/index.rst @@ -4,9 +4,6 @@ FOSJsRoutingBundle This bundle allows to expose Symfony Routes to JavaScript, so you can generate relative or absolute URLs in the browser using the same routes as in the backend. -.. toctree:: - :maxdepth: 1 - - installation - usage - commands +* `Installation `_ +* `Usage `_ +* `Commands `_ diff --git a/Resources/doc/installation.rst b/Resources/doc/installation.rst index 4c6107aa..e304135c 100644 --- a/Resources/doc/installation.rst +++ b/Resources/doc/installation.rst @@ -45,15 +45,18 @@ in the ``app/AppKernel.php`` file of your project: Step 3: Register the Routes --------------------------- -Load the bundle's routing definition in the application (usually in the -``app/config/routing.yml`` file): +Load the bundle's routing definition in the application: .. code-block:: yaml - # app/config/routing.yml + # Symfony 2 + 3: app/config/routing.yml fos_js_routing: resource: "@FOSJsRoutingBundle/Resources/config/routing/routing.xml" + # Symfony 4: config/routes/fos_js_routing.yml + fos_js_routing: + resource: "@FOSJsRoutingBundle/Resources/config/routing/routing-sf4.xml" + Step 4: Publish the Assets -------------------------- @@ -66,5 +69,8 @@ Execute the following command to publish the assets required by the bundle: # Symfony 3 $ php bin/console assets:install --symlink web + + # Symfony 4 + $ php bin/console assets:install --symlink public .. _`installation chapter`: https://getcomposer.org/doc/00-intro.md diff --git a/Resources/doc/usage.rst b/Resources/doc/usage.rst index 3b94784d..2383bc96 100644 --- a/Resources/doc/usage.rst +++ b/Resources/doc/usage.rst @@ -1,25 +1,55 @@ Usage ===== -Add these two lines in your layout: +In applications not using webpack add these two lines in your layout: -.. configuration-block:: +**With Twig:** - .. code-block:: html+twig +.. code-block:: twig - - + + - .. code-block:: html+php +**With PHP:** - - +.. code-block:: html+php + + + .. note:: - If you are not using Twig, then it is no problem. What you need is to add + If you are not using Twig, then it is no problem. What you need is the two JavaScript files above loaded at some point in your web page. + +If you are using webpack and Encore to package your assets you will need to use the dump command +and export your routes to json, this command will create a json file into the ``web/js`` folder: + +.. code-block:: bash + + # Symfony 3 + bin/console fos:js-routing:dump --format=json + +If you are using Flex, probably you want to dump your routes into the ``public`` folder +instead of ``web``, to achieve this you can set the ``target`` parameter: + +.. code-block:: bash + + # Symfony Flex + bin/console fos:js-routing:dump --format=json --target=public/js/fos_js_routes.json + +Then within your JavaScript development you can use: + +.. code-block:: javascript + + const routes = require('../../public/js/fos_js_routes.json'); + import Routing from '../../vendor/friendsofsymfony/jsrouting-bundle/Resources/public/js/router.min.js'; + + Routing.setRoutingData(routes); + Routing.generate('rep_log_list'); + + Generating URIs --------------- @@ -37,44 +67,46 @@ Or if you want to generate **absolute URLs**: Assuming some route definitions: -.. configuration-block:: - - .. code-block:: php-annotations - - // src/AppBundle/Controller/DefaultController.php - - /** - * @Route("/foo/{id}/bar", options={"expose"=true}, name="my_route_to_expose") - */ - public function indexAction($foo) { - // ... - } - - /** - * @Route("/blog/{page}", - * defaults = { "page" = 1 }, - * options = { "expose" = true }, - * name = "my_route_to_expose_with_defaults", - * ) - */ - public function blogAction($page) { - // ... - } - - .. code-block:: yaml - - # app/config/routing.yml - my_route_to_expose: - pattern: /foo/{id}/bar - defaults: { _controller: AppBundle:Default:index } - options: - expose: true - - my_route_to_expose_with_defaults: - pattern: /blog/{page} - defaults: { _controller: AppBundle:Default:blog, page: 1 } - options: - expose: true +**With annotations:** + +.. code-block:: php + + // src/AppBundle/Controller/DefaultController.php + + /** + * @Route("/foo/{id}/bar", options={"expose"=true}, name="my_route_to_expose") + */ + public function indexAction($foo) { + // ... + } + + /** + * @Route("/blog/{page}", + * defaults = { "page" = 1 }, + * options = { "expose" = true }, + * name = "my_route_to_expose_with_defaults", + * ) + */ + public function blogAction($page) { + // ... + } + +**With YAML:** + +.. code-block:: yaml + + # app/config/routing.yml + my_route_to_expose: + pattern: /foo/{id}/bar + defaults: { _controller: AppBundle:Default:index } + options: + expose: true + + my_route_to_expose_with_defaults: + pattern: /blog/{page} + defaults: { _controller: AppBundle:Default:blog, page: 1 } + options: + expose: true You can use the ``generate()`` method that way: @@ -112,6 +144,24 @@ Moreover, you can configure a list of routes to expose in ``app/config/config.ym These routes will be added to the exposed routes. You can use regular expression patterns if you don't want to list all your routes name by name. +.. note:: + + 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: + +.. code-block:: yaml + + # app/config/config.yml + fos_js_routing: + routes_to_expose: [ en__RG__route_1, en__RG__route_2, ... ] + +.. code-block:: yaml + + # app/config/config.yml + fos_js_routing: + routes_to_expose: [ '[a-z]{2}__RG__route_1', '[a-z]{2}__RG__route_2', ... ] + +Note that `Symfony 4.1 added support for internationalized routes`_ out-of-the-box. + You can prevent to expose a route by configuring it as below: .. code-block:: yaml @@ -151,3 +201,6 @@ You can enable HTTP caching as below: smaxage: null # integer value, e.g. 300 expires: null # anything that can be fed to "new \DateTime($expires)", e.g. "5 minutes" vary: [] # string or array, e.g. "Cookie" or [ Cookie, Accept ] + +.. _`JMSI18nRoutingBundle`: https://github.com/schmittjoh/JMSI18nRoutingBundle +.. _`Symfony 4.1 added support for internationalized routes`: https://symfony.com/blog/new-in-symfony-4-1-internationalized-routing diff --git a/Resources/gulpfile.js b/Resources/gulpfile.js new file mode 100755 index 00000000..ec2aeda4 --- /dev/null +++ b/Resources/gulpfile.js @@ -0,0 +1,20 @@ +const gulp = require('gulp'); +const babel = require('gulp-babel'); +const rename = require('gulp-rename'); +const uglify = require('gulp-uglify'); +const wrap = require('gulp-wrap'); + +gulp.task('js', function() { + return gulp.src('js/router.js') + .pipe(babel({ + presets: ["es2015"], + plugins: ["transform-object-assign"] + })) + .pipe(wrap({ src: 'js/router.template.js' })) + .pipe(gulp.dest('public/js')) + .pipe(rename({ extname: '.min.js' })) + .pipe(uglify()) + .pipe(gulp.dest('public/js')); +}); + +gulp.task('default', gulp.series('js')); diff --git a/Resources/js/export.js b/Resources/js/export.js deleted file mode 100644 index 66eb01cc..00000000 --- a/Resources/js/export.js +++ /dev/null @@ -1,31 +0,0 @@ -/** - * @fileoverview This file is the entry point for the compiler. - * - * You can compile this script by running (assuming you have JMSGoogleClosureBundle installed): - * - * php app/console plovr:build @FOSJsRoutingBundle/compile.js - */ - -goog.require('fos.Router'); - -goog.exportSymbol('fos.Router', fos.Router); -goog.exportSymbol('fos.Router.setData', function(data) { - var router = fos.Router.getInstance(); - router.setBaseUrl(/** @type {string} */ (data['base_url'])); - router.setRoutes(/** @type {Object.} */ (data['routes'])); - if ('prefix' in data) { - router.setPrefix(/** @type {string} */ (data['prefix'])); - } - router.setHost(/** @type {string} */ (data['host'])); - router.setScheme(/** @type {string} */ (data['scheme'])); -}); -goog.exportProperty(fos.Router, 'getInstance', fos.Router.getInstance); -goog.exportProperty(fos.Router.prototype, 'setRoutes', fos.Router.prototype.setRoutes); -goog.exportProperty(fos.Router.prototype, 'getRoutes', fos.Router.prototype.getRoutes); -goog.exportProperty(fos.Router.prototype, 'setBaseUrl', fos.Router.prototype.setBaseUrl); -goog.exportProperty(fos.Router.prototype, 'getBaseUrl', fos.Router.prototype.getBaseUrl); -goog.exportProperty(fos.Router.prototype, 'generate', fos.Router.prototype.generate); -goog.exportProperty(fos.Router.prototype, 'setPrefix', fos.Router.prototype.setPrefix); -goog.exportProperty(fos.Router.prototype, 'getRoute', fos.Router.prototype.getRoute); - -window['Routing'] = fos.Router.getInstance(); diff --git a/Resources/js/externs.js b/Resources/js/externs.js deleted file mode 100644 index 010d8d93..00000000 --- a/Resources/js/externs.js +++ /dev/null @@ -1,10 +0,0 @@ -/** - * @fileoverview This file contains some properties which we don't - * want the compiler to rename. - */ -var externs = { - tokens: '', - defaults: '', - requirements: '', - hosttokens: '' -}; diff --git a/Resources/js/router.js b/Resources/js/router.js index f27a8d46..df57c0d6 100644 --- a/Resources/js/router.js +++ b/Resources/js/router.js @@ -1,185 +1,242 @@ -goog.provide('fos.Router'); - -goog.require('goog.structs.Map'); -goog.require('goog.array'); -goog.require('goog.object'); -goog.require('goog.uri.utils'); +'use strict'; /** - * @constructor - * @param {fos.Router.Context=} opt_context - * @param {Object.=} opt_routes + * @fileoverview This file defines the Router class. + * + * You can compile this file by running the following command from the Resources folder: + * + * npm install && npm run build */ -fos.Router = function(opt_context, opt_routes) { - this.context_ = opt_context || {base_url: '', prefix: '', host: '', scheme: ''}; - this.setRoutes(opt_routes || {}); -}; -goog.addSingletonGetter(fos.Router); /** - * @typedef {{ - * tokens: (Array.>), - * defaults: (Object.), - * requirements: Object, - * hosttokens: (Array.) - * }} + * Class Router */ -fos.Router.Route; +class Router { + + /** + * @constructor + * @param {Router.Context=} context + * @param {Object.=} routes + */ + constructor(context, routes) { + this.context_ = context || {base_url: '', prefix: '', host: '', port: '', scheme: '', locale: ''}; + this.setRoutes(routes || {}); + } -/** - * @typedef {{ - * base_url: (string) - * }} - */ -fos.Router.Context; + /** + * Returns the current instance. + * @returns {Router} + */ + static getInstance() { + return Routing; + } -/** - * @param {Object.} routes - */ -fos.Router.prototype.setRoutes = function(routes) { - this.routes_ = new goog.structs.Map(routes); -}; + /** + * Configures the current Router instance with the provided data. + * @param {Object} data + */ + static setData(data) { + let router = Router.getInstance(); -/** - * @return {Object.} routes - */ -fos.Router.prototype.getRoutes = function() { - return this.routes_; -}; + router.setRoutingData(data); + } -/** - * @param {string} baseUrl - */ -fos.Router.prototype.setBaseUrl = function(baseUrl) { - this.context_.base_url = baseUrl; -}; + /** + * Sets data for the current instance + * @param {Object} data + */ + setRoutingData(data) { + this.setBaseUrl(data['base_url']); + this.setRoutes(data['routes']); -/** - * @return {string} - */ -fos.Router.prototype.getBaseUrl = function() { - return this.context_.base_url; -}; + if ('prefix' in data) { + this.setPrefix(data['prefix']); + } + if ('port' in data) { + this.setPort(data['port']); + } + if ('locale' in data) { + this.setLocale(data['locale']); + } -/** - * @param {string} prefix - */ -fos.Router.prototype.setPrefix = function(prefix) { - this.context_.prefix = prefix; -}; + this.setHost(data['host']); + this.setScheme(data['scheme']); + } -/** - * @param {string} scheme - */ -fos.Router.prototype.setScheme = function(scheme) { - this.context_.scheme = scheme; -}; + /** + * @param {Object.} routes + */ + setRoutes(routes) { + this.routes_ = Object.freeze(routes); + } -/** - * @return {string} - */ -fos.Router.prototype.getScheme = function() { - return this.context_.scheme; -}; + /** + * @return {Object.} routes + */ + getRoutes() { + return this.routes_; + } -/** - * @param {string} host - */ -fos.Router.prototype.setHost = function(host) { - this.context_.host = host; -}; + /** + * @param {string} baseUrl + */ + setBaseUrl(baseUrl) { + this.context_.base_url = baseUrl; + } -/** - * @return {string} - */ -fos.Router.prototype.getHost = function() { - return this.context_.host; -}; + /** + * @return {string} + */ + getBaseUrl() { + return this.context_.base_url; + } + /** + * @param {string} prefix + */ + setPrefix(prefix) { + this.context_.prefix = prefix; + } -/** - * Builds query string params added to a URL. - * Port of jQuery's $.param() function, so credit is due there. - * - * @param {string} prefix - * @param {Array|Object|string} params - * @param {Function} add - */ -fos.Router.prototype.buildQueryParams = function(prefix, params, add) { - var self = this; - var name; - var rbracket = new RegExp(/\[\]$/); - - if (params instanceof Array) { - goog.array.forEach(params, function(val, i) { - if (rbracket.test(prefix)) { - add(prefix, val); - } else { - self.buildQueryParams(prefix + '[' + (typeof val === 'object' ? i : '') + ']', val, add); + /** + * @param {string} scheme + */ + setScheme(scheme) { + this.context_.scheme = scheme; + } + + /** + * @return {string} + */ + getScheme() { + return this.context_.scheme; + } + + /** + * @param {string} host + */ + setHost(host) { + this.context_.host = host; + } + + /** + * @return {string} + */ + getHost() { + return this.context_.host; + } + + /** + * @param {string} port + */ + setPort(port) { + this.context_.port = port; + } + + /** + * @return {string} + */ + getPort() { + return this.context_.port; + }; + + /** + * @param {string} locale + */ + setLocale(locale) { + this.context_.locale = locale; + } + + /** + * @return {string} + */ + getLocale() { + return this.context_.locale; + }; + + /** + * Builds query string params added to a URL. + * Port of jQuery's $.param() function, so credit is due there. + * + * @param {string} prefix + * @param {Array|Object|string} params + * @param {Function} add + */ + buildQueryParams(prefix, params, add) { + let name; + let rbracket = new RegExp(/\[\]$/); + + if (params instanceof Array) { + params.forEach((val, i) => { + if (rbracket.test(prefix)) { + add(prefix, val); + } else { + this.buildQueryParams(prefix + '[' + (typeof val === 'object' ? i : '') + ']', val, add); + } + }); + } else if (typeof params === 'object') { + for (name in params) { + this.buildQueryParams(prefix + '[' + name + ']', params[name], add); } - }); - } else if (typeof params === 'object') { - for (name in params) { - this.buildQueryParams(prefix + '[' + name + ']', params[name], add); + } else { + add(prefix, params); } - } else { - add(prefix, params); } -}; -/** - * Returns a raw route object. - * - * @param {string} name - * @return {fos.Router.Route} - */ -fos.Router.prototype.getRoute = function(name) { - var prefixedName = this.context_.prefix + name; - if (!this.routes_.containsKey(prefixedName)) { - // Check first for default route before failing - if (!this.routes_.containsKey(name)) { - throw new Error('The route "' + name + '" does not exist.'); + /** + * Returns a raw route object. + * + * @param {string} name + * @return {Router.Route} + */ + getRoute(name) { + let prefixedName = this.context_.prefix + name; + let sf41i18nName = name + '.' + this.context_.locale; + let prefixedSf41i18nName = this.context_.prefix + name + '.' + this.context_.locale; + let variants = [prefixedName, sf41i18nName, prefixedSf41i18nName, name]; + + for (let i in variants) { + if (variants[i] in this.routes_) { + return this.routes_[variants[i]]; + } } - } else { - name = prefixedName; - } - return (this.routes_.get(name)); -}; + throw new Error('The route "' + name + '" does not exist.'); + } + /** + * Generates the URL for a route. + * + * @param {string} name + * @param {Object.} opt_params + * @param {boolean} absolute + * @return {string} + */ + generate(name, opt_params, absolute = false) { + let route = (this.getRoute(name)), + params = opt_params || {}, + unusedParams = Object.assign({}, params), + url = '', + optional = true, + host = '', + port = (typeof this.getPort() == "undefined" || this.getPort() === null) ? '' : this.getPort(); + + route.tokens.forEach((token) => { + if ('text' === token[0]) { + url = Router.encodePathComponent(token[1]) + url; + optional = false; -/** - * Generates the URL for a route. - * - * @param {string} name - * @param {Object.} opt_params - * @param {boolean} absolute - * @return {string} - */ -fos.Router.prototype.generate = function(name, opt_params, absolute) { - var route = (this.getRoute(name)), - params = opt_params || {}, - unusedParams = goog.object.clone(params), - url = '', - optional = true, - host = ''; - - goog.array.forEach(route.tokens, function(token) { - if ('text' === token[0]) { - url = token[1] + url; - optional = false; - - return; - } + return; + } - if ('variable' === token[0]) { - var hasDefault = goog.object.containsKey(route.defaults, token[3]); - if (false === optional || !hasDefault || (goog.object.containsKey(params, token[3]) && params[token[3]] != route.defaults[token[3]])) { - var value; + if ('variable' === token[0]) { + let hasDefault = route.defaults && (token[3] in route.defaults); + if (false === optional || !hasDefault || ((token[3] in params) && params[token[3]] != route.defaults[token[3]])) { + let value; - if (goog.object.containsKey(params, token[3])) { + if (token[3] in params) { value = params[token[3]]; - goog.object.remove(unusedParams, token[3]); + delete unusedParams[token[3]]; } else if (hasDefault) { value = route.defaults[token[3]]; } else if (optional) { @@ -188,10 +245,10 @@ fos.Router.prototype.generate = function(name, opt_params, absolute) { throw new Error('The route "' + name + '" requires the parameter "' + token[3] + '".'); } - var empty = true === value || false === value || '' === value; + let empty = true === value || false === value || '' === value; if (!empty || !optional) { - var encodedValue = encodeURIComponent(value).replace(/%2F/g, '/'); + let encodedValue = Router.encodePathComponent(value); if ('null' === encodedValue && null === value) { encodedValue = ''; @@ -201,69 +258,150 @@ fos.Router.prototype.generate = function(name, opt_params, absolute) { } optional = false; - } else if (hasDefault) { - goog.object.remove(unusedParams, token[3]); + } else if (hasDefault && (token[3] in unusedParams)) { + delete unusedParams[token[3]]; } return; + } + + throw new Error('The token type "' + token[0] + '" is not supported.'); + }); + + if (url === '') { + url = '/'; } - throw new Error('The token type "' + token[0] + '" is not supported.'); - }); + route.hosttokens.forEach((token) => { + let value; - if (url === '') { - url = '/'; - } + if ('text' === token[0]) { + host = token[1] + host; - goog.array.forEach(route.hosttokens, function (token) { - var value; + return; + } - if ('text' === token[0]) { - host = token[1] + host; + if ('variable' === token[0]) { + if (token[3] in params) { + value = params[token[3]]; + delete unusedParams[token[3]]; + } else if (route.defaults && (token[3] in route.defaults)) { + value = route.defaults[token[3]]; + } + + host = token[1] + value + host; + } + }); + // Foo-bar! + url = this.context_.base_url + url; - return; + if (route.requirements && ("_scheme" in route.requirements) && this.getScheme() != route.requirements["_scheme"]) { + const currentHost = host || this.getHost(); + + url = route.requirements["_scheme"] + "://" + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } else if ("undefined" !== typeof route.schemes && "undefined" !== typeof route.schemes[0] && this.getScheme() !== route.schemes[0]) { + const currentHost = host || this.getHost(); + + url = route.schemes[0] + "://" + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } else if (host && this.getHost() !== host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port)) { + url = this.getScheme() + "://" + host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } else if (absolute === true) { + url = this.getScheme() + "://" + this.getHost() + (this.getHost().indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; } - if ('variable' === token[0]) { - if (goog.object.containsKey(params, token[3])) { - value = params[token[3]]; - goog.object.remove(unusedParams, token[3]); - } else if (goog.object.containsKey(route.defaults, token[3])) { - value = route.defaults[token[3]]; + if (Object.keys(unusedParams).length > 0) { + let prefix; + let queryParams = []; + let add = (key, value) => { + // if value is a function then call it and assign it's return value as value + value = (typeof value === 'function') ? value() : value; + + // change null to empty string + value = (value === null) ? '' : value; + + queryParams.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value)); + }; + + for (prefix in unusedParams) { + this.buildQueryParams(prefix, unusedParams[prefix], add); } - host = token[1] + value + host; + url = url + '?' + queryParams.join('&'); } - }); - - url = this.context_.base_url + url; - if (goog.object.containsKey(route.requirements, "_scheme") && this.getScheme() != route.requirements["_scheme"]) { - url = route.requirements["_scheme"] + "://" + (host || this.getHost()) + url; - } else if (host && this.getHost() !== host) { - url = this.getScheme() + "://" + host + url; - } else if (absolute === true) { - url = this.getScheme() + "://" + this.getHost() + url; + + return url; } - if (goog.object.getCount(unusedParams) > 0) { - var prefix; - var queryParams = []; - var add = function(key, value) { - // if value is a function then call it and assign it's return value as value - value = (typeof value === 'function') ? value() : value; + /** + * Returns the given string encoded to mimic Symfony URL generator. + * + * @param {string} value + * @return {string} + */ + static customEncodeURIComponent(value) { + return encodeURIComponent(value) + .replace(/%2F/g, '/') + .replace(/%40/g, '@') + .replace(/%3A/g, ':') + .replace(/%21/g, '!') + .replace(/%3B/g, ';') + .replace(/%2C/g, ',') + .replace(/%2A/g, '*') + .replace(/\(/g, '%28') + .replace(/\)/g, '%29') + .replace(/'/g, '%27') + ; + } - // change null to empty string - value = (value === null) ? '' : value; + /** + * Returns the given path properly encoded to mimic Symfony URL generator. + * + * @param {string} value + * @return {string} + */ + static encodePathComponent(value) { + return Router.customEncodeURIComponent(value) + .replace(/%3D/g, '=') + .replace(/%2B/g, '+') + .replace(/%21/g, '!') + .replace(/%7C/g, '|') + ; + } - queryParams.push(encodeURIComponent(key) + '=' + encodeURIComponent(value)); - }; + /** + * Returns the given query parameter or value properly encoded to mimic Symfony URL generator. + * + * @param {string} value + * @return {string} + */ + static encodeQueryComponent(value) { + return Router.customEncodeURIComponent(value) + .replace(/%3F/g, '?') + ; + } - for (prefix in unusedParams) { - this.buildQueryParams(prefix, unusedParams[prefix], add); - } +} - url = url + '?' + queryParams.join('&').replace(/%20/g, '+'); - } +/** + * @typedef {{ + * tokens: (Array.>), + * defaults: (Object.), + * requirements: Object, + * hosttokens: (Array.) + * }} + */ +Router.Route; - return url; -}; +/** + * @typedef {{ + * base_url: (string) + * }} + */ +Router.Context; + +/** + * Router singleton. + * @const + * @type {Router} + */ +const Routing = new Router(); diff --git a/Resources/js/router.template.js b/Resources/js/router.template.js new file mode 100644 index 00000000..8a01ea0a --- /dev/null +++ b/Resources/js/router.template.js @@ -0,0 +1,22 @@ +(function (root, factory) { + var routing = factory(); + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], routing.Routing); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = routing.Routing; + } else { + // Browser globals (root is window) + root.Routing = routing.Routing; + root.fos = { + Router: routing.Router + }; + } +}(this, function () { + <%= contents %> + + return { Router: Router, Routing: Routing }; +})); diff --git a/Resources/js/router.test.js b/Resources/js/router.test.js index 7fd20a64..ca897a74 100644 --- a/Resources/js/router.test.js +++ b/Resources/js/router.test.js @@ -1,4 +1,5 @@ goog.require('goog.testing.jsunit'); +goog.require('goog.structs.Map'); function testGenerate() { var router = new fos.Router({base_url: ''}, { @@ -78,6 +79,21 @@ function testGenerateUsesHostWhenTheSameSchemeRequirementGiven() { assertEquals('http://otherhost/foo/bar', router.generate('homepage')); } +function testGenerateUsesHostWhenTheSameSchemeGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [['text', 'otherhost']], + schemes: ['http'], + methods: [] + } + }); + + assertEquals('http://otherhost/foo/bar', router.generate('homepage')); +} + function testGenerateUsesHostWhenAnotherSchemeRequirementGiven() { var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http"}, { homepage: { @@ -91,6 +107,21 @@ function testGenerateUsesHostWhenAnotherSchemeRequirementGiven() { assertEquals('https://otherhost/foo/bar', router.generate('homepage')); } +function testGenerateUsesHostWhenAnotherSchemeGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [['text', 'otherhost']], + schemes: ['https'], + methods: [] + } + }); + + assertEquals('https://otherhost/foo/bar', router.generate('homepage')); +} + function testGenerateSupportsHostPlaceholders() { var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http"}, { homepage: { @@ -152,6 +183,32 @@ function testGenerateUsesAbsoluteUrl() { assertEquals('http://localhost/foo/bar', router.generate('homepage', [], true)); } +function testGenerateUsesAbsoluteUrlWithGivenPort() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http", port: "8000"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [] + } + }); + + assertEquals('http://localhost:8000/foo/bar', router.generate('homepage', [], true)); +} + +function testGenerateUsesAbsoluteUrlWithGivenPortAndHostWithPort() { + var router = new fos.Router({base_url: '/foo', host: "localhost:8080", scheme: "http", port: "8080"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [] + } + }); + + assertEquals('http://localhost:8080/foo/bar', router.generate('homepage', [], true)); +} + function testGenerateUsesAbsoluteUrlWhenSchemeRequirementGiven() { var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http"}, { homepage: { @@ -165,6 +222,77 @@ function testGenerateUsesAbsoluteUrlWhenSchemeRequirementGiven() { assertEquals('http://localhost/foo/bar', router.generate('homepage', [], true)); } +function testGenerateUsesAbsoluteUrlWithGivenPortWhenSchemeRequirementGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http", port: "8080"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {"_scheme": "http"}, + hosttokens: [] + } + }); + + assertEquals('http://localhost:8080/foo/bar', router.generate('homepage', [], true)); +} + +function testGenerateUsesAbsoluteUrlWithGivenPortWhenSchemeRequirementAndHostWithPortGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost:8080", scheme: "http", port: "8080"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {"_scheme": "http"}, + hosttokens: [] + } + }); + + assertEquals('http://localhost:8080/foo/bar', router.generate('homepage', [], true)); +} + +function testGenerateUsesAbsoluteUrlWhenSchemeGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [], + schemes: ['http'], + methods: [] + } + }); + + assertEquals('http://localhost/foo/bar', router.generate('homepage', [], true)); +} + +function testGenerateUsesAbsoluteUrlWithGivenPortWhenSchemeGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http", port:"1234"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [], + schemes: ['http'], + methods: [] + } + }); + + assertEquals('http://localhost:1234/foo/bar', router.generate('homepage', [], true)); +} + +function testGenerateUsesAbsoluteUrlWithGivenPortWhenSchemeAndHostWithPortGiven() { + var router = new fos.Router({base_url: '/foo', host: "localhost:8080", scheme: "http", port:"8080"}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {}, + requirements: {}, + hosttokens: [], + schemes: ['http'], + methods: [] + } + }); + + assertEquals('http://localhost:8080/foo/bar', router.generate('homepage', [], true)); +} + function testGenerateWithOptionalTrailingParam() { var router = new fos.Router({base_url: ''}, { posts: { @@ -231,7 +359,7 @@ function testGenerateWithExtraParamsDeep() { } }); - assertEquals('/baz?foo%5B%5D=1&foo%5B1%5D%5B%5D=1&foo%5B1%5D%5B%5D=2&foo%5B1%5D%5B%5D=3&foo%5B1%5D%5B%5D=foo&foo%5B%5D=3&foo%5B%5D=4&foo%5B%5D=bar&foo%5B5%5D%5B%5D=1&foo%5B5%5D%5B%5D=2&foo%5B5%5D%5B%5D=3&foo%5B5%5D%5B%5D=baz&baz%5Bfoo%5D=bar+foo&baz%5Bbar%5D=baz&bob=cat', router.generate('foo', { + assertEquals('/baz?foo%5B%5D=1&foo%5B1%5D%5B%5D=1&foo%5B1%5D%5B%5D=2&foo%5B1%5D%5B%5D=3&foo%5B1%5D%5B%5D=foo&foo%5B%5D=3&foo%5B%5D=4&foo%5B%5D=bar&foo%5B5%5D%5B%5D=1&foo%5B5%5D%5B%5D=2&foo%5B5%5D%5B%5D=3&foo%5B5%5D%5B%5D=baz&baz%5Bfoo%5D=bar%20foo&baz%5Bbar%5D=baz&bob=cat', router.generate('foo', { bar: 'baz', // valid param, not included in the query string foo: [1, [1, 2, 3, 'foo'], 3, 4, 'bar', [1, 2, 3, 'baz']], baz: { @@ -242,6 +370,28 @@ function testGenerateWithExtraParamsDeep() { })); } +function testUrlEncoding() { + // This test was copied from Symfony URL Generator + + // This tests the encoding of reserved characters that are used for delimiting of URI components (defined in RFC 3986) + // and other special ASCII chars. These chars are tested as static text path, variable path and query param. + var chars = '@:[]/()*\'" +,;-._~&$<>|{}%\\^`!?foo=bar#id'; + + var router = new fos.Router({base_url: '/app.php'}, { + posts: { + tokens: [['variable', '/', '.+', 'varpath'], ['text', '/'+chars]], + defaults: {}, + requirements: {}, + hosttokens: [] + } + }); + + assertEquals( + '/app.php/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id/@:%5B%5D/%28%29*%27%22%20+,;-._~%26%24%3C%3E|%7B%7D%25%5C%5E%60!%3Ffoo=bar%23id?query=@:%5B%5D/%28%29*%27%22%20%2B,;-._~%26%24%3C%3E%7C%7B%7D%25%5C%5E%60!?foo%3Dbar%23id', + router.generate('posts', {varpath: chars, query: chars}) + ); +} + function testGenerateThrowsErrorWhenRequiredParameterWasNotGiven() { var router = new fos.Router({base_url: ''}, { foo: { @@ -281,7 +431,7 @@ function testGetBaseUrl() { } function testGeti18n() { - var router = new fos.Router({base_url: '/foo', prefix: 'en__RG__'}, { + var router = new fos.Router({base_url: '/foo', prefix: 'en__RG__', locale: 'en'}, { en__RG__homepage: { tokens: [['text', '/bar']], defaults: {}, @@ -299,14 +449,29 @@ function testGeti18n() { defaults: {}, requirements: {}, hosttokens: [] + }, + "login.en": { + tokens: [['text', '/en/login']], + defaults: {}, + requirements: {}, + hosttokens: [] + }, + "login.es": { + tokens: [['text', '/es/login']], + defaults: {}, + requirements: {}, + hosttokens: [] } }); assertEquals('/foo/bar', router.generate('homepage')); assertEquals('/foo/admin', router.generate('_admin')); + assertEquals('/foo/en/login', router.generate('login')); router.setPrefix('es__RG__'); + router.setLocale('es'); assertEquals('/foo/es/bar', router.generate('homepage')); + assertEquals('/foo/es/login', router.generate('login')); } function testGetRoute() { @@ -341,7 +506,7 @@ function testGetRoutes() { blog: 'test' }); - assertObjectEquals(expected, router.getRoutes()); + assertObjectEquals(expected.toObject(), router.getRoutes()); } function testGenerateWithNullValue() { @@ -360,3 +525,34 @@ function testGenerateWithNullValue() { assertEquals('/blog-post//10', router.generate('posts', { page: null, id: 10 })); } + +function testGenerateWithPort() { + var router = new fos.Router({base_url: '/foo', host: "localhost", scheme: "http", port: 443}, { + homepage: { + tokens: [['text', '/bar']], + defaults: {subdomain: 'api'}, + requirements: {}, + hosttokens: [ + ['text', '.localhost'], + ['variable', '', '', 'subdomain'] + ] + } + }); + + assertEquals('http://api.localhost:443/foo/bar', router.generate('homepage')); +} + +// Regression test for issue #384 (https://github.com/FriendsOfSymfony/FOSJsRoutingBundle/issues/384) +function testGenerateWithPortInHost() { + var router = new fos.Router({base_url: '', host: "my-host.loc:81", scheme: "http", port: 81}, { + homepage: { + tokens: [['text', "\/foo\/"]], + defaults: [], + requirements: {}, + hosttokens: [["text", "my-host.loc"]], + methods: ["GET", "POST"], + } + }); + + assertEquals('/foo/', router.generate('homepage')); +} \ No newline at end of file diff --git a/Resources/js/router_test.html b/Resources/js/router_test.html index ff30ecdf..bc4a6e65 100644 --- a/Resources/js/router_test.html +++ b/Resources/js/router_test.html @@ -5,8 +5,8 @@ Router Test - - + + diff --git a/Resources/js/run_jsunit.js b/Resources/js/run_jsunit.js index 393554f0..515e276c 100644 --- a/Resources/js/run_jsunit.js +++ b/Resources/js/run_jsunit.js @@ -1,5 +1,5 @@ function waitFor(testFx, onReady, timeOutMillis) { - var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timout is 3s + var maxtimeOutMillis = timeOutMillis ? timeOutMillis : 3001, //< Default Max Timeout is 3s start = new Date().getTime(), condition = false, interval = setInterval(function() { @@ -21,7 +21,9 @@ function waitFor(testFx, onReady, timeOutMillis) { }, 100); //< repeat check every 250ms } -if (phantom.args.length === 0) { +var system = require('system'); + +if (system.args.length <= 1) { console.log('Usage: phantomjs run_jsunit.js '); phantom.exit(); } else { @@ -31,7 +33,7 @@ if (phantom.args.length === 0) { console.log(msg); }; - page.open(phantom.args[0], function(status) { + page.open(system.args[1], function(status) { if (status === 'success') { waitFor(function() { return page.evaluate(function() { @@ -39,7 +41,7 @@ if (phantom.args.length === 0) { }); }, function() { var exitCode = page.evaluate(function() { - return G_testRunner.isSuccess() ? 0 : 1; + return G_testRunner.testCase.isSuccess() ? 0 : 1; }); phantom.exit(exitCode); }); diff --git a/Resources/package.json b/Resources/package.json new file mode 100755 index 00000000..eb4deb1c --- /dev/null +++ b/Resources/package.json @@ -0,0 +1,48 @@ +{ + "name": "fos-router", + "version": "2.2.0", + "description": "A pretty nice way to use the routes generated by the FOSJsRoutingBundle in your JavaScript.", + "keywords": [ + "router", + "symfony" + ], + "license": "MIT", + "author": { + "name": "FriendsOfSymfony Community", + "url": "https://github.com/friendsofsymfony/FOSJsRoutingBundle/contributors" + }, + "contributors": [ + { + "name": "William Durand", + "email": "will+git@drnd.me" + }, + { + "name": "Bruno Sampaio", + "email": "bens.sampaio@gmail.com" + } + ], + "files": [ + "public/js/router.js", + "public/js/router.min.js", + "ts/router.d.ts" + ], + "main": "public/js/router.js", + "devDependencies": { + "babel-plugin-transform-object-assign": "^6.22.0", + "babel-polyfill": "^6.9.1", + "babel-preset-es2015": "^6.9.0", + "babel-register": "^6.11.6", + "google-closure-library": "^20171203.0.0", + "gulp": "^4.0.2", + "gulp-babel": "^6.1.2", + "gulp-rename": "^1.2.2", + "gulp-uglify": "^1.5.4", + "gulp-wrap": "^0.13.0", + "jasmine": "^2.4.1" + }, + "scripts": { + "build": "gulp", + "test": "npm run build && phantomjs js/run_jsunit.js js/router_test.html" + }, + "dependencies": {} +} diff --git a/Resources/public/js/router.js b/Resources/public/js/router.js index 7aec4ee7..ae1b2d36 100644 --- a/Resources/public/js/router.js +++ b/Resources/public/js/router.js @@ -1,12 +1,496 @@ +(function (root, factory) { + var routing = factory(); + if (typeof define === 'function' && define.amd) { + // AMD. Register as an anonymous module. + define([], routing.Routing); + } else if (typeof module === 'object' && module.exports) { + // Node. Does not work with strict CommonJS, but + // only CommonJS-like environments that support module.exports, + // like Node. + module.exports = routing.Routing; + } else { + // Browser globals (root is window) + root.Routing = routing.Routing; + root.fos = { + Router: routing.Router + }; + } +}(this, function () { + 'use strict'; + /** - * Portions of this code are from the Google Closure Library, - * received from the Closure Authors under the Apache 2.0 license. + * @fileoverview This file defines the Router class. * - * All other code is (C) FriendsOfSymfony and subject to the MIT license. - */ -(function() {var f=!1,i,k=this;function l(a,c){var b=a.split("."),d=k;!(b[0]in d)&&d.execScript&&d.execScript("var "+b[0]);for(var e;b.length&&(e=b.shift());)!b.length&&void 0!==c?d[e]=c:d=d[e]?d[e]:d[e]={}};var m=Array.prototype,n=m.forEach?function(a,c,b){m.forEach.call(a,c,b)}:function(a,c,b){for(var d=a.length,e="string"==typeof a?a.split(""):a,g=0;g=} routes + */ + function Router(context, routes) { + _classCallCheck(this, Router); + + this.context_ = context || { base_url: '', prefix: '', host: '', port: '', scheme: '', locale: '' }; + this.setRoutes(routes || {}); + } + + /** + * Returns the current instance. + * @returns {Router} + */ + + + _createClass(Router, [{ + key: 'setRoutingData', + + + /** + * Sets data for the current instance + * @param {Object} data + */ + value: function setRoutingData(data) { + this.setBaseUrl(data['base_url']); + this.setRoutes(data['routes']); + + if ('prefix' in data) { + this.setPrefix(data['prefix']); + } + if ('port' in data) { + this.setPort(data['port']); + } + if ('locale' in data) { + this.setLocale(data['locale']); + } + + this.setHost(data['host']); + this.setScheme(data['scheme']); + } + + /** + * @param {Object.} routes + */ + + }, { + key: 'setRoutes', + value: function setRoutes(routes) { + this.routes_ = Object.freeze(routes); + } + + /** + * @return {Object.} routes + */ + + }, { + key: 'getRoutes', + value: function getRoutes() { + return this.routes_; + } + + /** + * @param {string} baseUrl + */ + + }, { + key: 'setBaseUrl', + value: function setBaseUrl(baseUrl) { + this.context_.base_url = baseUrl; + } + + /** + * @return {string} + */ + + }, { + key: 'getBaseUrl', + value: function getBaseUrl() { + return this.context_.base_url; + } + + /** + * @param {string} prefix + */ + + }, { + key: 'setPrefix', + value: function setPrefix(prefix) { + this.context_.prefix = prefix; + } + + /** + * @param {string} scheme + */ + + }, { + key: 'setScheme', + value: function setScheme(scheme) { + this.context_.scheme = scheme; + } + + /** + * @return {string} + */ + + }, { + key: 'getScheme', + value: function getScheme() { + return this.context_.scheme; + } + + /** + * @param {string} host + */ + + }, { + key: 'setHost', + value: function setHost(host) { + this.context_.host = host; + } + + /** + * @return {string} + */ + + }, { + key: 'getHost', + value: function getHost() { + return this.context_.host; + } + + /** + * @param {string} port + */ + + }, { + key: 'setPort', + value: function setPort(port) { + this.context_.port = port; + } + + /** + * @return {string} + */ + + }, { + key: 'getPort', + value: function getPort() { + return this.context_.port; + } + }, { + key: 'setLocale', + + + /** + * @param {string} locale + */ + value: function setLocale(locale) { + this.context_.locale = locale; + } + + /** + * @return {string} + */ + + }, { + key: 'getLocale', + value: function getLocale() { + return this.context_.locale; + } + }, { + key: 'buildQueryParams', + + + /** + * Builds query string params added to a URL. + * Port of jQuery's $.param() function, so credit is due there. + * + * @param {string} prefix + * @param {Array|Object|string} params + * @param {Function} add + */ + value: function buildQueryParams(prefix, params, add) { + var _this = this; + + var name = void 0; + var rbracket = new RegExp(/\[\]$/); + + if (params instanceof Array) { + params.forEach(function (val, i) { + if (rbracket.test(prefix)) { + add(prefix, val); + } else { + _this.buildQueryParams(prefix + '[' + ((typeof val === 'undefined' ? 'undefined' : _typeof(val)) === 'object' ? i : '') + ']', val, add); + } + }); + } else if ((typeof params === 'undefined' ? 'undefined' : _typeof(params)) === 'object') { + for (name in params) { + this.buildQueryParams(prefix + '[' + name + ']', params[name], add); + } + } else { + add(prefix, params); + } + } + + /** + * Returns a raw route object. + * + * @param {string} name + * @return {Router.Route} + */ + + }, { + key: 'getRoute', + value: function getRoute(name) { + var prefixedName = this.context_.prefix + name; + var sf41i18nName = name + '.' + this.context_.locale; + var prefixedSf41i18nName = this.context_.prefix + name + '.' + this.context_.locale; + var variants = [prefixedName, sf41i18nName, prefixedSf41i18nName, name]; + + for (var i in variants) { + if (variants[i] in this.routes_) { + return this.routes_[variants[i]]; + } + } + + throw new Error('The route "' + name + '" does not exist.'); + } + + /** + * Generates the URL for a route. + * + * @param {string} name + * @param {Object.} opt_params + * @param {boolean} absolute + * @return {string} + */ + + }, { + key: 'generate', + value: function generate(name, opt_params) { + var absolute = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : false; + + var route = this.getRoute(name), + params = opt_params || {}, + unusedParams = _extends({}, params), + url = '', + optional = true, + host = '', + port = typeof this.getPort() == "undefined" || this.getPort() === null ? '' : this.getPort(); + + route.tokens.forEach(function (token) { + if ('text' === token[0]) { + url = Router.encodePathComponent(token[1]) + url; + optional = false; + + return; + } + + if ('variable' === token[0]) { + var hasDefault = route.defaults && token[3] in route.defaults; + if (false === optional || !hasDefault || token[3] in params && params[token[3]] != route.defaults[token[3]]) { + var value = void 0; + + if (token[3] in params) { + value = params[token[3]]; + delete unusedParams[token[3]]; + } else if (hasDefault) { + value = route.defaults[token[3]]; + } else if (optional) { + return; + } else { + throw new Error('The route "' + name + '" requires the parameter "' + token[3] + '".'); + } + + var empty = true === value || false === value || '' === value; + + if (!empty || !optional) { + var encodedValue = Router.encodePathComponent(value); + + if ('null' === encodedValue && null === value) { + encodedValue = ''; + } + + url = token[1] + encodedValue + url; + } + + optional = false; + } else if (hasDefault && token[3] in unusedParams) { + delete unusedParams[token[3]]; + } + + return; + } + + throw new Error('The token type "' + token[0] + '" is not supported.'); + }); + + if (url === '') { + url = '/'; + } + + route.hosttokens.forEach(function (token) { + var value = void 0; + + if ('text' === token[0]) { + host = token[1] + host; + + return; + } + + if ('variable' === token[0]) { + if (token[3] in params) { + value = params[token[3]]; + delete unusedParams[token[3]]; + } else if (route.defaults && token[3] in route.defaults) { + value = route.defaults[token[3]]; + } + + host = token[1] + value + host; + } + }); + // Foo-bar! + url = this.context_.base_url + url; + + if (route.requirements && "_scheme" in route.requirements && this.getScheme() != route.requirements["_scheme"]) { + var currentHost = host || this.getHost(); + + url = route.requirements["_scheme"] + "://" + currentHost + (currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } else if ("undefined" !== typeof route.schemes && "undefined" !== typeof route.schemes[0] && this.getScheme() !== route.schemes[0]) { + var _currentHost = host || this.getHost(); + + url = route.schemes[0] + "://" + _currentHost + (_currentHost.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } else if (host && this.getHost() !== host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port)) { + url = this.getScheme() + "://" + host + (host.indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } else if (absolute === true) { + url = this.getScheme() + "://" + this.getHost() + (this.getHost().indexOf(':' + port) > -1 || '' === port ? '' : ':' + port) + url; + } + + if (Object.keys(unusedParams).length > 0) { + var prefix = void 0; + var queryParams = []; + var add = function add(key, value) { + // if value is a function then call it and assign it's return value as value + value = typeof value === 'function' ? value() : value; + + // change null to empty string + value = value === null ? '' : value; + + queryParams.push(Router.encodeQueryComponent(key) + '=' + Router.encodeQueryComponent(value)); + }; + + for (prefix in unusedParams) { + this.buildQueryParams(prefix, unusedParams[prefix], add); + } + + url = url + '?' + queryParams.join('&'); + } + + return url; + } + + /** + * Returns the given string encoded to mimic Symfony URL generator. + * + * @param {string} value + * @return {string} + */ + + }], [{ + key: 'getInstance', + value: function getInstance() { + return Routing; + } + + /** + * Configures the current Router instance with the provided data. + * @param {Object} data + */ + + }, { + key: 'setData', + value: function setData(data) { + var router = Router.getInstance(); + + router.setRoutingData(data); + } + }, { + key: 'customEncodeURIComponent', + value: function customEncodeURIComponent(value) { + return encodeURIComponent(value).replace(/%2F/g, '/').replace(/%40/g, '@').replace(/%3A/g, ':').replace(/%21/g, '!').replace(/%3B/g, ';').replace(/%2C/g, ',').replace(/%2A/g, '*').replace(/\(/g, '%28').replace(/\)/g, '%29').replace(/'/g, '%27'); + } + + /** + * Returns the given path properly encoded to mimic Symfony URL generator. + * + * @param {string} value + * @return {string} + */ + + }, { + key: 'encodePathComponent', + value: function encodePathComponent(value) { + return Router.customEncodeURIComponent(value).replace(/%3D/g, '=').replace(/%2B/g, '+').replace(/%21/g, '!').replace(/%7C/g, '|'); + } + + /** + * Returns the given query parameter or value properly encoded to mimic Symfony URL generator. + * + * @param {string} value + * @return {string} + */ + + }, { + key: 'encodeQueryComponent', + value: function encodeQueryComponent(value) { + return Router.customEncodeURIComponent(value).replace(/%3F/g, '?'); + } + }]); + + return Router; +}(); + +/** + * @typedef {{ + * tokens: (Array.>), + * defaults: (Object.), + * requirements: Object, + * hosttokens: (Array.) + * }} + */ + + +Router.Route; + +/** + * @typedef {{ + * base_url: (string) + * }} + */ +Router.Context; + +/** + * Router singleton. + * @const + * @type {Router} + */ +var Routing = new Router(); + + return { Router: Router, Routing: Routing }; +})); \ No newline at end of file diff --git a/Resources/public/js/router.min.js b/Resources/public/js/router.min.js new file mode 100644 index 00000000..0b97ba8b --- /dev/null +++ b/Resources/public/js/router.min.js @@ -0,0 +1 @@ +!function(e,t){var n=t();"function"==typeof define&&define.amd?define([],n.Routing):"object"==typeof module&&module.exports?module.exports=n.Routing:(e.Routing=n.Routing,e.fos={Router:n.Router})}(this,function(){"use strict";function e(e,t){if(!(e instanceof t))throw new TypeError("Cannot call a class as a function")}var t=Object.assign||function(e){for(var t=1;t2&&void 0!==arguments[2]&&arguments[2],i=this.getRoute(e),u=n||{},s=t({},u),c="",a=!0,l="",f="undefined"==typeof this.getPort()||null===this.getPort()?"":this.getPort();if(i.tokens.forEach(function(t){if("text"===t[0])return c=r.encodePathComponent(t[1])+c,void(a=!1);{if("variable"!==t[0])throw new Error('The token type "'+t[0]+'" is not supported.');var n=i.defaults&&t[3]in i.defaults;if(!1===a||!n||t[3]in u&&u[t[3]]!=i.defaults[t[3]]){var o=void 0;if(t[3]in u)o=u[t[3]],delete s[t[3]];else{if(!n){if(a)return;throw new Error('The route "'+e+'" requires the parameter "'+t[3]+'".')}o=i.defaults[t[3]]}var l=!0===o||!1===o||""===o;if(!l||!a){var f=r.encodePathComponent(o);"null"===f&&null===o&&(f=""),c=t[1]+f+c}a=!1}else n&&t[3]in s&&delete s[t[3]]}}),""===c&&(c="/"),i.hosttokens.forEach(function(e){var t=void 0;return"text"===e[0]?void(l=e[1]+l):void("variable"===e[0]&&(e[3]in u?(t=u[e[3]],delete s[e[3]]):i.defaults&&e[3]in i.defaults&&(t=i.defaults[e[3]]),l=e[1]+t+l))}),c=this.context_.base_url+c,i.requirements&&"_scheme"in i.requirements&&this.getScheme()!=i.requirements._scheme){var h=l||this.getHost();c=i.requirements._scheme+"://"+h+(h.indexOf(":"+f)>-1||""===f?"":":"+f)+c}else if("undefined"!=typeof i.schemes&&"undefined"!=typeof i.schemes[0]&&this.getScheme()!==i.schemes[0]){var p=l||this.getHost();c=i.schemes[0]+"://"+p+(p.indexOf(":"+f)>-1||""===f?"":":"+f)+c}else l&&this.getHost()!==l+(l.indexOf(":"+f)>-1||""===f?"":":"+f)?c=this.getScheme()+"://"+l+(l.indexOf(":"+f)>-1||""===f?"":":"+f)+c:o===!0&&(c=this.getScheme()+"://"+this.getHost()+(this.getHost().indexOf(":"+f)>-1||""===f?"":":"+f)+c);if(Object.keys(s).length>0){var d=void 0,y=[],v=function(e,t){t="function"==typeof t?t():t,t=null===t?"":t,y.push(r.encodeQueryComponent(e)+"="+r.encodeQueryComponent(t))};for(d in s)this.buildQueryParams(d,s[d],v);c=c+"?"+y.join("&")}return c}}],[{key:"getInstance",value:function(){return i}},{key:"setData",value:function(e){var t=r.getInstance();t.setRoutingData(e)}},{key:"customEncodeURIComponent",value:function(e){return encodeURIComponent(e).replace(/%2F/g,"/").replace(/%40/g,"@").replace(/%3A/g,":").replace(/%21/g,"!").replace(/%3B/g,";").replace(/%2C/g,",").replace(/%2A/g,"*").replace(/\(/g,"%28").replace(/\)/g,"%29").replace(/'/g,"%27")}},{key:"encodePathComponent",value:function(e){return r.customEncodeURIComponent(e).replace(/%3D/g,"=").replace(/%2B/g,"+").replace(/%21/g,"!").replace(/%7C/g,"|")}},{key:"encodeQueryComponent",value:function(e){return r.customEncodeURIComponent(e).replace(/%3F/g,"?")}}]),r}();r.Route,r.Context;var i=new r;return{Router:r,Routing:i}}); \ No newline at end of file diff --git a/Resources/ts/router.d.ts b/Resources/ts/router.d.ts index 5bc47381..5506fe14 100644 --- a/Resources/ts/router.d.ts +++ b/Resources/ts/router.d.ts @@ -30,8 +30,17 @@ declare module FOS { base_url:string; } + export interface RoutingData { + base_url:string; + routes:RoutesMap; + prefix?:string; + host:string; + scheme:string; + } + export interface Router { new(opt_context?:Context, opt_routes?:RoutesMap):Router; + setRoutingData(data:RoutingData):void; setRoutes(routes:RoutesMap):void; getRoutes():RoutesMap; setBaseUrl(base_url:string):void; diff --git a/Response/RoutesResponse.php b/Response/RoutesResponse.php index 4acd6e56..5308a23a 100644 --- a/Response/RoutesResponse.php +++ b/Response/RoutesResponse.php @@ -19,17 +19,22 @@ class RoutesResponse private $routes; private $prefix; private $host; + private $port; private $scheme; private $locale; + private $domains; - public function __construct($baseUrl, RouteCollection $routes = null, $prefix = null, $host = null, $scheme = null, $locale = null) + public function __construct($baseUrl, RouteCollection $routes = null, $prefix = null, $host = null, $port = null, + $scheme = null, $locale = null, $domains = array()) { $this->baseUrl = $baseUrl; $this->routes = $routes ?: new RouteCollection(); $this->prefix = $prefix; $this->host = $host; + $this->port = $port; $this->scheme = $scheme; $this->locale = $locale; + $this->domains = $domains; } public function getBaseUrl() @@ -41,11 +46,25 @@ public function getRoutes() { $exposedRoutes = array(); foreach ($this->routes->all() as $name => $route) { + + if (!$route->hasOption('expose')) { + $domain = 'default'; + } else { + $domain = $route->getOption('expose'); + $domain = is_string($domain) ? ($domain === 'true' ? 'default' : $domain) : 'default'; + } + + + if (count($this->domains) === 0) { + if ($domain !== 'default') { + continue; + } + } elseif (!in_array($domain, $this->domains, true)) { + continue; + } + $compiledRoute = $route->compile(); - $defaults = array_intersect_key( - $route->getDefaults(), - array_fill_keys($compiledRoute->getVariables(), null) - ); + $defaults = $route->getDefaults(); if (!isset($defaults['_locale']) && in_array('_locale', $compiledRoute->getVariables())) { $defaults['_locale'] = $this->locale; @@ -56,6 +75,8 @@ public function getRoutes() 'defaults' => $defaults, 'requirements' => $route->getRequirements(), 'hosttokens' => method_exists($compiledRoute, 'getHostTokens') ? $compiledRoute->getHostTokens() : array(), + 'methods' => $route->getMethods(), + 'schemes' => $route->getSchemes(), ); } @@ -72,8 +93,18 @@ public function getHost() return $this->host; } + public function getPort() + { + return $this->port; + } + public function getScheme() { return $this->scheme; } + + public function getLocale() + { + return $this->locale; + } } diff --git a/Serializer/Denormalizer/RouteCollectionDenormalizer.php b/Serializer/Denormalizer/RouteCollectionDenormalizer.php index e5ef38ad..e7042f76 100644 --- a/Serializer/Denormalizer/RouteCollectionDenormalizer.php +++ b/Serializer/Denormalizer/RouteCollectionDenormalizer.php @@ -19,6 +19,7 @@ class RouteCollectionDenormalizer implements DenormalizerInterface { /** * {@inheritDoc} + * @return mixed */ public function denormalize($data, $class, $format = null, array $context = array()) { @@ -43,11 +44,15 @@ public function denormalize($data, $class, $format = null, array $context = arra /** * {@inheritDoc} */ - public function supportsDenormalization($data, $type, $format = null) + public function supportsDenormalization($data, $type, $format = null): bool { if (!is_array($data)) { return false; } + + if (count($data) < 1) { + return true; + } $values = current($data); diff --git a/Serializer/Normalizer/RouteCollectionNormalizer.php b/Serializer/Normalizer/RouteCollectionNormalizer.php index 330f7c37..74f1fcb6 100644 --- a/Serializer/Normalizer/RouteCollectionNormalizer.php +++ b/Serializer/Normalizer/RouteCollectionNormalizer.php @@ -21,6 +21,7 @@ class RouteCollectionNormalizer implements NormalizerInterface { /** * {@inheritDoc} + * @return array|string|int|float|bool|\ArrayObject|null */ public function normalize($data, $format = null, array $context = array()) { @@ -45,7 +46,7 @@ public function normalize($data, $format = null, array $context = array()) /** * {@inheritDoc} */ - public function supportsNormalization($data, $format = null) + public function supportsNormalization($data, $format = null): bool { return $data instanceof RouteCollection; } diff --git a/Serializer/Normalizer/RoutesResponseNormalizer.php b/Serializer/Normalizer/RoutesResponseNormalizer.php index ca659d19..dc7acb12 100644 --- a/Serializer/Normalizer/RoutesResponseNormalizer.php +++ b/Serializer/Normalizer/RoutesResponseNormalizer.php @@ -21,6 +21,7 @@ class RoutesResponseNormalizer implements NormalizerInterface { /** * {@inheritdoc} + * @return array|string|int|float|bool|\ArrayObject|null */ public function normalize($data, $format = null, array $context = array()) { @@ -29,14 +30,16 @@ public function normalize($data, $format = null, array $context = array()) 'routes' => $data->getRoutes(), 'prefix' => $data->getPrefix(), 'host' => $data->getHost(), + 'port' => $data->getPort(), 'scheme' => $data->getScheme(), + 'locale' => $data->getLocale(), ); } /** * {@inheritDoc} */ - public function supportsNormalization($data, $format = null) + public function supportsNormalization($data, $format = null): bool { return $data instanceof RoutesResponse; } diff --git a/Tests/Command/DumpCommandTest.php b/Tests/Command/DumpCommandTest.php index dcaa614d..6d8da188 100644 --- a/Tests/Command/DumpCommandTest.php +++ b/Tests/Command/DumpCommandTest.php @@ -12,18 +12,17 @@ namespace FOS\JsRoutingBundle\Tests\Command; use FOS\JsRoutingBundle\Command\DumpCommand; +use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; -class DumpCommandTest extends \PHPUnit_Framework_TestCase +class DumpCommandTest extends TestCase { - protected $container; protected $extractor; protected $router; + private $serializer; - public function setUp() + public function setUp(): void { - $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $this->extractor = $this->getMockBuilder('FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractor') ->disableOriginalConstructor() ->getMock(); @@ -39,50 +38,28 @@ public function setUp() public function testExecute() { - $this->container->expects($this->at(0)) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - $this->serializer->expects($this->once()) ->method('serialize') ->will($this->returnValue('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""}')); - $this->container->expects($this->at(1)) - ->method('get') - ->with('fos_js_routing.serializer') - ->will($this->returnValue($this->serializer)); - - $command = new DumpCommand(); - $command->setContainer($this->container); + $command = new DumpCommand($this->extractor, $this->serializer, '/root/dir'); $tester = new CommandTester($command); $tester->execute(array('--target' => '/tmp/dump-command-test')); - $this->assertContains('Dumping exposed routes.', $tester->getDisplay()); - $this->assertContains('[file+] /tmp/dump-command-test', $tester->getDisplay()); + $this->assertStringContainsString('Dumping exposed routes.', $tester->getDisplay()); + $this->assertStringContainsString('[file+] /tmp/dump-command-test', $tester->getDisplay()); $this->assertEquals('fos.Router.setData({"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""});', file_get_contents('/tmp/dump-command-test')); } public function testExecuteCallbackOption() { - $this->container->expects($this->at(0)) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - $this->serializer->expects($this->once()) ->method('serialize') ->will($this->returnValue('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""}')); - $this->container->expects($this->at(1)) - ->method('get') - ->with('fos_js_routing.serializer') - ->will($this->returnValue($this->serializer)); - - $command = new DumpCommand(); - $command->setContainer($this->container); + $command = new DumpCommand($this->extractor, $this->serializer, '/root/dir'); $tester = new CommandTester($command); $tester->execute(array( @@ -90,47 +67,53 @@ public function testExecuteCallbackOption() '--callback' => 'test', )); - $this->assertContains('Dumping exposed routes.', $tester->getDisplay()); - $this->assertContains('[file+] /tmp/dump-command-test', $tester->getDisplay()); + $this->assertStringContainsString('Dumping exposed routes.', $tester->getDisplay()); + $this->assertStringContainsString('[file+] /tmp/dump-command-test', $tester->getDisplay()); $this->assertEquals('test({"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""});', file_get_contents('/tmp/dump-command-test')); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Unable to create directory /../web/js - */ + public function testExecuteFormatOption() + { + $json = '{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""}'; + + $this->serializer->expects($this->once()) + ->method('serialize') + ->will($this->returnValue($json)); + + $command = new DumpCommand($this->extractor, $this->serializer, '/root/dir'); + + $tester = new CommandTester($command); + $tester->execute(array( + '--target' => '/tmp/dump-command-test', + '--format' => 'json', + )); + + $this->assertStringContainsString('Dumping exposed routes.', $tester->getDisplay()); + $this->assertStringContainsString('[file+] /tmp/dump-command-test', $tester->getDisplay()); + + $this->assertEquals($json, file_get_contents('/tmp/dump-command-test')); + } + public function testExecuteUnableToCreateDirectory() { - $command = new DumpCommand(); - $command->setContainer($this->container); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to create directory /root/dir/web/js'); + $command = new DumpCommand($this->extractor, $this->serializer, '/root/dir'); $tester = new CommandTester($command); $tester->execute(array()); } - /** - * @expectedException \RuntimeException - * @expectedExceptionMessage Unable to write file /tmp - */ public function testExecuteUnableToWriteFile() { - $this->container->expects($this->at(0)) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('Unable to write file /tmp'); $this->serializer->expects($this->once()) ->method('serialize') ->will($this->returnValue('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""}')); - $this->container->expects($this->at(1)) - ->method('get') - ->with('fos_js_routing.serializer') - ->will($this->returnValue($this->serializer)); - - $command = new DumpCommand(); - $command->setContainer($this->container); + $command = new DumpCommand($this->extractor, $this->serializer, '/root/dir'); $tester = new CommandTester($command); $tester->execute(array('--target' => '/tmp')); diff --git a/Tests/Command/RouterDebugExposedCommandTest.php b/Tests/Command/RouterDebugExposedCommandTest.php index b2ccb3f2..bc4ec293 100644 --- a/Tests/Command/RouterDebugExposedCommandTest.php +++ b/Tests/Command/RouterDebugExposedCommandTest.php @@ -12,20 +12,18 @@ namespace FOS\JsRoutingBundle\Tests\Command; use FOS\JsRoutingBundle\Command\RouterDebugExposedCommand; +use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Tester\CommandTester; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; -class RouterDebugExposedCommandTest extends \PHPUnit_Framework_TestCase +class RouterDebugExposedCommandTest extends TestCase { - protected $container; protected $extractor; protected $router; - public function setUp() + public function setUp(): void { - $this->container = $this->getMock('Symfony\Component\DependencyInjection\ContainerInterface'); - $this->extractor = $this->getMockBuilder('FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractor') ->disableOriginalConstructor() ->getMock(); @@ -37,41 +35,29 @@ public function setUp() public function testExecute() { - if (!class_exists('Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper')) { - $this->markTestSkipped('2.3 BC is not tested'); - } - $routes = new RouteCollection(); $routes->add('literal', new Route('/literal')); $routes->add('blog_post', new Route('/blog-post/{slug}')); $routes->add('list', new Route('/literal')); - $this->container->expects($this->once()) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - $this->extractor->expects($this->once()) ->method('getRoutes') ->will($this->returnValue($routes)); - $command = new RouterDebugExposedCommand(); - $command->setContainer($this->container); + $command = new RouterDebugExposedCommand($this->extractor, $this->router); $tester = new CommandTester($command); $tester->execute(array()); - $this->assertRegExp('/literal(.*ANY){3}.*\/literal/', $tester->getDisplay()); - $this->assertRegExp('/blog_post(.*ANY){3}.*\/blog-post\/{slug}/', $tester->getDisplay()); - $this->assertRegExp('/list(.*ANY){3}.*\/literal/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/literal(.*ANY){3}.*\/literal/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/blog_post(.*ANY){3}.*\/blog-post\/{slug}/', $tester->getDisplay()); + $this->assertMatchesRegularExpression('/list(.*ANY){3}.*\/literal/', $tester->getDisplay()); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage The route "foobar" does not exist. - */ public function testExecuteWithNameUnknown() { + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The route "foobar" does not exist.'); $routes = new RouteCollection(); $routes->add('literal', new Route('/literal')); $routes->add('blog_post', new Route('/blog-post/{slug}')); @@ -81,33 +67,16 @@ public function testExecuteWithNameUnknown() ->method('getRouteCollection') ->will($this->returnValue($routes)); - $this->container->expects($this->at(0)) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - - $this->container->expects($this->at(1)) - ->method('get') - ->with('router') - ->will($this->returnValue($this->router)); - - $command = new RouterDebugExposedCommand(); - $command->setContainer($this->container); + $command = new RouterDebugExposedCommand($this->extractor, $this->router); $tester = new CommandTester($command); $tester->execute(array('name' => 'foobar')); } - /** - * @expectedException InvalidArgumentException - * @expectedExceptionMessage The route "literal" was found, but it is not an exposed route. - */ public function testExecuteWithNameNotExposed() { - if (!class_exists('Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper')) { - $this->markTestSkipped('2.3 BC is not tested'); - } - + $this->expectException(\InvalidArgumentException::class); + $this->expectExceptionMessage('The route "literal" was found, but it is not an exposed route.'); $routes = new RouteCollection(); $routes->add('literal', new Route('/literal')); $routes->add('blog_post', new Route('/blog-post/{slug}')); @@ -117,18 +86,7 @@ public function testExecuteWithNameNotExposed() ->method('getRouteCollection') ->will($this->returnValue($routes)); - $this->container->expects($this->at(0)) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - - $this->container->expects($this->at(1)) - ->method('get') - ->with('router') - ->will($this->returnValue($this->router)); - - $command = new RouterDebugExposedCommand(); - $command->setContainer($this->container); + $command = new RouterDebugExposedCommand($this->extractor, $this->router); $tester = new CommandTester($command); $tester->execute(array('name' => 'literal')); @@ -136,10 +94,6 @@ public function testExecuteWithNameNotExposed() public function testExecuteWithName() { - if (!class_exists('Symfony\Bundle\FrameworkBundle\Console\Helper\DescriptorHelper')) { - $this->markTestSkipped('2.3 BC is not tested'); - } - $routes = new RouteCollection(); $routes->add('literal', new Route('/literal', array(), array(), array('exposed' => true))); $routes->add('blog_post', new Route('/blog-post/{slug}')); @@ -153,22 +107,11 @@ public function testExecuteWithName() ->method('isRouteExposed') ->will($this->returnValue(true)); - $this->container->expects($this->at(0)) - ->method('get') - ->with('fos_js_routing.extractor') - ->will($this->returnValue($this->extractor)); - - $this->container->expects($this->at(1)) - ->method('get') - ->with('router') - ->will($this->returnValue($this->router)); - - $command = new RouterDebugExposedCommand(); - $command->setContainer($this->container); + $command = new RouterDebugExposedCommand($this->extractor, $this->router); $tester = new CommandTester($command); $tester->execute(array('name' => 'literal')); - $this->assertContains('exposed: true', $tester->getDisplay()); + $this->assertStringContainsString('exposed: true', $tester->getDisplay()); } } diff --git a/Tests/Controller/ControllerTest.php b/Tests/Controller/ControllerTest.php index 98af6438..b7f525bf 100644 --- a/Tests/Controller/ControllerTest.php +++ b/Tests/Controller/ControllerTest.php @@ -15,22 +15,24 @@ use FOS\JsRoutingBundle\Serializer\Denormalizer\RouteCollectionDenormalizer; use FOS\JsRoutingBundle\Serializer\Normalizer\RouteCollectionNormalizer; use FOS\JsRoutingBundle\Serializer\Normalizer\RoutesResponseNormalizer; +use PHPUnit\Framework\TestCase; use Symfony\Component\HttpFoundation\Request; +use Symfony\Component\HttpKernel\Exception\HttpException; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Serializer\Encoder\JsonEncoder; use Symfony\Component\Serializer\Serializer; -class ControllerTest extends \PHPUnit_Framework_TestCase +class ControllerTest extends TestCase { private $cachePath; - public function setUp() + public function setUp(): void { $this->cachePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'fosJsRouting' . DIRECTORY_SEPARATOR . 'data.json'; } - public function tearDown() + public function tearDown(): void { unlink($this->cachePath); } @@ -48,7 +50,7 @@ public function testIndexAction() $response = $controller->indexAction($this->getRequest('/'), 'json'); - $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""}', $response->getContent()); + $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"blog":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); } public function testIndexActionWithLocalizedRoutes() @@ -64,7 +66,7 @@ public function testIndexActionWithLocalizedRoutes() $response = $controller->indexAction($this->getRequest('/'), 'json'); - $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]},"blog":{"tokens":[["variable","\/","[^\/]++","_locale"],["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":{"_locale":"en"},"requirements":[],"hosttokens":[["text","localhost"]]}},"prefix":"","host":"","scheme":""}', $response->getContent()); + $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"blog":{"tokens":[["variable","\/","[^\/]++","_locale"],["variable","\/","[^\/]++","slug"],["text","\/blog-post"]],"defaults":{"_locale":"en"},"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); } public function testConfigCache() @@ -78,11 +80,11 @@ public function testConfigCache() ); $response = $controller->indexAction($this->getRequest('/'), 'json'); - $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]}},"prefix":"","host":"","scheme":""}', $response->getContent()); + $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); // second call should serve the cached content $response = $controller->indexAction($this->getRequest('/'), 'json'); - $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[]}},"prefix":"","host":"","scheme":""}', $response->getContent()); + $this->assertEquals('{"base_url":"","routes":{"literal":{"tokens":[["text","\/homepage"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); } /** @@ -94,7 +96,7 @@ public function testGenerateWithCallback($callback) $response = $controller->indexAction($this->getRequest('/', 'GET', array('callback' => $callback)), 'json'); $this->assertEquals( - sprintf('/**/%s({"base_url":"","routes":[],"prefix":"","host":"","scheme":""});', $callback), + sprintf('/**/%s({"base_url":"","routes":[],"prefix":"","host":"","port":null,"scheme":"","locale":"en"});', $callback), $response->getContent() ); } @@ -107,11 +109,9 @@ public static function dataProviderForTestGenerateWithCallback() ); } - /** - * @expectedException \Symfony\Component\HttpKernel\Exception\HttpException - */ public function testGenerateWithInvalidCallback() { + $this->expectException(HttpException::class); $controller = new Controller($this->getSerializer(), $this->getExtractor()); $controller->indexAction($this->getRequest('/', 'GET', array('callback' => '(function xss(x) {evil()})')), 'json'); } @@ -121,7 +121,7 @@ public function testIndexActionWithoutRoutes() $controller = new Controller($this->getSerializer(), $this->getExtractor(), array(), sys_get_temp_dir()); $response = $controller->indexAction($this->getRequest('/'), 'json'); - $this->assertEquals('{"base_url":"","routes":[],"prefix":"","host":"","scheme":""}', $response->getContent()); + $this->assertEquals('{"base_url":"","routes":[],"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); $this->assertEquals(200, $response->getStatusCode()); $this->assertEquals('application/json', $response->headers->get('Content-Type')); @@ -156,13 +156,56 @@ public function testCacheControl() $this->assertEquals(456, $response->headers->getCacheControlDirective('s-maxage')); } + public function testExposeDomain() + { + $routes = new RouteCollection(); + $routes->add('homepage', new Route('/')); + $routes->add('admin_index', new Route('/admin', array(), array(), + array('expose' => 'admin'))); + $routes->add('admin_pages', new Route('/admin/path', array(), array(), + array('expose' => 'admin'))); + $routes->add('blog_index', new Route('/blog', array(), array(), + array('expose' => 'blog'), 'localhost')); + $routes->add('blog_post', new Route('/blog/{slug}', array(), array(), + array('expose' => 'blog'), 'localhost')); + + $controller = new Controller( + $this->getSerializer(), + $this->getExtractor($routes) + ); + + $response = $controller->indexAction($this->getRequest('/'), 'json'); + + $this->assertEquals('{"base_url":"","routes":{"homepage":{"tokens":[["text","\/"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); + + $response = $controller->indexAction($this->getRequest('/', + 'GET', array('domain' => 'admin')), 'json'); + + $this->assertEquals('{"base_url":"","routes":{"admin_index":{"tokens":[["text","\/admin"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"admin_pages":{"tokens":[["text","\/admin\/path"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); + + $response = $controller->indexAction($this->getRequest('/', + 'GET', array('domain' => 'blog')), 'json'); + + $this->assertEquals('{"base_url":"","routes":{"blog_index":{"tokens":[["text","\/blog"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]},"blog_post":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); + + $response = $controller->indexAction($this->getRequest('/', + 'GET', array('domain' => 'admin,blog')), 'json'); + + $this->assertEquals('{"base_url":"","routes":{"admin_index":{"tokens":[["text","\/admin"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"admin_pages":{"tokens":[["text","\/admin\/path"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"blog_index":{"tokens":[["text","\/blog"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]},"blog_post":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); + + $response = $controller->indexAction($this->getRequest('/', + 'GET', array('domain' => 'default,admin,blog')), 'json'); + + $this->assertEquals('{"base_url":"","routes":{"homepage":{"tokens":[["text","\/"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"admin_index":{"tokens":[["text","\/admin"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"admin_pages":{"tokens":[["text","\/admin\/path"]],"defaults":[],"requirements":[],"hosttokens":[],"methods":[],"schemes":[]},"blog_index":{"tokens":[["text","\/blog"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]},"blog_post":{"tokens":[["variable","\/","[^\/]++","slug"],["text","\/blog"]],"defaults":[],"requirements":[],"hosttokens":[["text","localhost"]],"methods":[],"schemes":[]}},"prefix":"","host":"","port":null,"scheme":"","locale":"en"}', $response->getContent()); + } + private function getExtractor(RouteCollection $exposedRoutes = null, $baseUrl = '') { if (null === $exposedRoutes) { $exposedRoutes = new RouteCollection(); } - $extractor = $this->getMock('FOS\\JsRoutingBundle\\Extractor\\ExposedRoutesExtractorInterface'); + $extractor = $this->getMockBuilder('FOS\\JsRoutingBundle\\Extractor\\ExposedRoutesExtractorInterface')->getMock(); $extractor ->expects($this->any()) ->method('getRoutes') diff --git a/Tests/DependencyInjection/FOSJsRoutingExtensionTest.php b/Tests/DependencyInjection/FOSJsRoutingExtensionTest.php index 7be59dc9..d7dc45b9 100644 --- a/Tests/DependencyInjection/FOSJsRoutingExtensionTest.php +++ b/Tests/DependencyInjection/FOSJsRoutingExtensionTest.php @@ -12,11 +12,12 @@ namespace FOS\JsRoutingBundle\Tests\DependencyInjection; use FOS\JsRoutingBundle\DependencyInjection\FOSJsRoutingExtension; +use PHPUnit\Framework\TestCase; use Symfony\Component\DependencyInjection\ContainerBuilder; -class FOSJsRoutingExtensionTest extends \PHPUnit_Framework_TestCase +class FOSJsRoutingExtensionTest extends TestCase { - public function setUp() + public function setUp(): void { if (!class_exists('Symfony\Component\DependencyInjection\ContainerBuilder')) { $this->markTestSkipped('The DependencyInjection component is not available.'); diff --git a/Tests/Extractor/ExposedRoutesExtractorTest.php b/Tests/Extractor/ExposedRoutesExtractorTest.php index 3d8cfc92..06b270d8 100644 --- a/Tests/Extractor/ExposedRoutesExtractorTest.php +++ b/Tests/Extractor/ExposedRoutesExtractorTest.php @@ -12,6 +12,7 @@ namespace FOS\JsRoutingBundle\Tests\Extractor; use FOS\JsRoutingBundle\Extractor\ExposedRoutesExtractor; +use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\RequestContext; use Symfony\Component\Routing\Route; use Symfony\Component\Routing\RouteCollection; @@ -21,11 +22,11 @@ * * @author William DURAND */ -class ExposedRoutesExtractorTest extends \PHPUnit_Framework_TestCase +class ExposedRoutesExtractorTest extends TestCase { private $cacheDir; - public function setUp() + public function setUp(): void { if (!class_exists('Symfony\\Component\\Routing\\Route')) { $this->markTestSkipped('The Routing component is not available.'); @@ -43,6 +44,9 @@ public function testGetRoutes() $router = $this->getRouter($expected); $extractor = new ExposedRoutesExtractor($router, array('.*'), $this->cacheDir, array()); + + $expected->addOptions(array('expose' => 'default')); + $this->assertEquals($expected, $extractor->getRoutes()); } @@ -56,30 +60,32 @@ public function testGetRoutesWithPatterns() $router = $this->getRouter($expected); $extractor = new ExposedRoutesExtractor($router, array('hello_.*'), $this->cacheDir, array()); - $this->assertEquals(3, count($extractor->getRoutes()), '3 routes match the pattern: "hello_.*"'); + $this->assertCount(3, $extractor->getRoutes(), '3 routes match the pattern: "hello_.*"'); $extractor = new ExposedRoutesExtractor($router, array('hello_[0-9]{3}'), $this->cacheDir, array()); - $this->assertEquals(1, count($extractor->getRoutes()), '1 routes match the pattern: "hello_[0-9]{3}"'); + $this->assertCount(1, $extractor->getRoutes(), '1 routes match the pattern: "hello_[0-9]{3}"'); $extractor = new ExposedRoutesExtractor($router, array('hello_[0-9]{4}'), $this->cacheDir, array()); - $this->assertEquals(0, count($extractor->getRoutes()), '1 routes match the pattern: "hello_[0-9]{4}"'); + $this->assertCount(0, $extractor->getRoutes(), '1 routes match the pattern: "hello_[0-9]{4}"'); $extractor = new ExposedRoutesExtractor($router, array('hello_.+o.+'), $this->cacheDir, array()); - $this->assertEquals(2, count($extractor->getRoutes()), '2 routes match the pattern: "hello_.+o.+"'); + $this->assertCount(2, $extractor->getRoutes(), '2 routes match the pattern: "hello_.+o.+"'); $extractor = new ExposedRoutesExtractor($router, array('hello_.+o.+', 'hello_123'), $this->cacheDir, array()); - $this->assertEquals(3, count($extractor->getRoutes()), '3 routes match patterns: "hello_.+o.+" and "hello_123"'); + $this->assertCount(3, $extractor->getRoutes(), '3 routes match patterns: "hello_.+o.+" and "hello_123"'); $extractor = new ExposedRoutesExtractor($router, array('hello_.+o.+', 'hello_$'), $this->cacheDir, array()); - $this->assertEquals(2, count($extractor->getRoutes()), '2 routes match patterns: "hello_.+o.+" and "hello_"'); + $this->assertCount(2, $extractor->getRoutes(), '2 routes match patterns: "hello_.+o.+" and "hello_"'); $extractor = new ExposedRoutesExtractor($router, array(), $this->cacheDir, array()); - $this->assertEquals(0, count($extractor->getRoutes()), 'No patterns so no matched routes'); + $this->assertCount(0, $extractor->getRoutes(), 'No patterns so no matched routes'); } public function testGetCachePath() { - $router = $this->getMock('Symfony\\Component\\Routing\\Router', array(), array(), '', false); + $router = $this->getMockBuilder('Symfony\\Component\\Routing\\Router') + ->disableOriginalConstructor() + ->getMock(); $extractor = new ExposedRoutesExtractor($router, array(), $this->cacheDir, array()); $this->assertEquals($this->cacheDir . DIRECTORY_SEPARATOR . 'fosJsRouting' . DIRECTORY_SEPARATOR . 'data.json', $extractor->getCachePath('')); @@ -92,7 +98,9 @@ public function testGetHostOverHttp($host, $httpPort, $expected) { $requestContext = new RequestContext('/app_dev.php', 'GET', $host, 'http', $httpPort); - $router = $this->getMock('Symfony\\Component\\Routing\\Router', array(), array(), '', false); + $router = $this->getMockBuilder('Symfony\\Component\\Routing\\Router') + ->disableOriginalConstructor() + ->getMock(); $router->expects($this->atLeastOnce()) ->method('getContext') ->will($this->returnValue($requestContext)); @@ -120,7 +128,9 @@ public function testGetHostOverHttps($host, $httpsPort, $expected) { $requestContext = new RequestContext('/app_dev.php', 'GET', $host, 'https', 80, $httpsPort); - $router = $this->getMock('Symfony\\Component\\Routing\\Router', array(), array(), '', false); + $router = $this->getMockBuilder('Symfony\\Component\\Routing\\Router') + ->disableOriginalConstructor() + ->getMock(); $router->expects($this->atLeastOnce()) ->method('getContext') ->will($this->returnValue($requestContext)); @@ -130,6 +140,33 @@ public function testGetHostOverHttps($host, $httpsPort, $expected) $this->assertEquals($expected, $extractor->getHost()); } + public function testExposeFalse() + { + $expected = new RouteCollection(); + $expected->add('user_public_1', new Route('/user/public/1')); + $expected->add('user_public_2', new Route('/user/public/2', [], [], ['expose' => true])); + $expected->add('user_public_3', new Route('/user/public/3', [], [], ['expose' => 'true'])); + $expected->add('user_public_4', new Route('/user/public/4', [], [], ['expose' => 'default'])); + $expected->add('user_public_5', new Route('/user/public/5', [], [], ['expose' => 'group_name'])); + $expected->add('user_secret_1', new Route('/user/secret/1', [], [], ['expose' => false])); + $expected->add('user_secret_2', new Route('/user/secret/2', [], [], ['expose' => 'false'])); + + $router = $this->getRouter($expected); + $extractor = new ExposedRoutesExtractor($router, array('user_.+'), $this->cacheDir, []); + + $this->assertCount(5, $extractor->getRoutes()); + + foreach ($expected->all() as $name => $route) { + + if (in_array($name, ['user_secret_1', 'user_secret_2'])) { + $this->assertFalse($extractor->isRouteExposed($route, $name)); + } else { + $this->assertTrue($extractor->isRouteExposed($route, $name)); + } + + } + } + /** * @return array */ @@ -148,7 +185,9 @@ public function provideTestGetHostOverHttps() */ private function getRouter(RouteCollection $routes) { - $router = $this->getMock('Symfony\\Component\\Routing\\Router', array(), array(), '', false); + $router = $this->getMockBuilder('Symfony\\Component\\Routing\\Router') + ->disableOriginalConstructor() + ->getMock(); $router ->expects($this->atLeastOnce()) ->method('getRouteCollection') diff --git a/Tests/Serializer/Normalizer/RouteCollectionNormalizerTest.php b/Tests/Serializer/Normalizer/RouteCollectionNormalizerTest.php index 08d856bd..ea7108d3 100644 --- a/Tests/Serializer/Normalizer/RouteCollectionNormalizerTest.php +++ b/Tests/Serializer/Normalizer/RouteCollectionNormalizerTest.php @@ -12,13 +12,14 @@ namespace FOS\JsRoutingBundle\Tests\Serializer\Normalizer; use FOS\JsRoutingBundle\Serializer\Normalizer\RouteCollectionNormalizer; +use PHPUnit\Framework\TestCase; use Symfony\Component\Routing\RouteCollection; use Symfony\Component\Routing\Route; /** * Class RouteCollectionNormalizerTest */ -class RouteCollectionNormalizerTest extends \PHPUnit_Framework_TestCase +class RouteCollectionNormalizerTest extends TestCase { public function testSupportsNormalization() { diff --git a/Tests/Serializer/Normalizer/RoutesResponseNormalizerTest.php b/Tests/Serializer/Normalizer/RoutesResponseNormalizerTest.php index 83acfc2d..c73c7b75 100644 --- a/Tests/Serializer/Normalizer/RoutesResponseNormalizerTest.php +++ b/Tests/Serializer/Normalizer/RoutesResponseNormalizerTest.php @@ -13,8 +13,9 @@ use FOS\JsRoutingBundle\Serializer\Normalizer\RouteCollectionNormalizer; use FOS\JsRoutingBundle\Serializer\Normalizer\RoutesResponseNormalizer; +use PHPUnit\Framework\TestCase; -class RoutesResponseNormalizerTest extends \PHPUnit_Framework_TestCase +class RoutesResponseNormalizerTest extends TestCase { public function testSupportsNormalization() { @@ -54,12 +55,18 @@ public function testNormalize() ->method('getScheme') ->will($this->returnValue('scheme')); + $response->expects($this->once()) + ->method('getLocale') + ->will($this->returnValue('locale')); + $expected = array( 'base_url' => 'baseUrl', 'routes' => array(), 'prefix' => 'prefix', 'host' => 'host', + 'port' => null, 'scheme' => 'scheme', + 'locale' => 'locale', ); $this->assertSame($expected, $normalizer->normalize($response)); diff --git a/composer.json b/composer.json index 74c3e934..7c01c04f 100644 --- a/composer.json +++ b/composer.json @@ -16,22 +16,25 @@ } ], "require": { - "php": ">=5.3.2", - "symfony/framework-bundle": "~2.3||~3.0", - "symfony/serializer": "~2.0||~3.0", - "symfony/console": "~2.3||~3.0", - "willdurand/jsonp-callback-validator": "~1.0" + "php": "^7.1|^8.0", + "symfony/framework-bundle": "~3.4|^4.4.20|^5.0", + "symfony/serializer": "~3.4|^4.4.20|^5.0", + "symfony/console": "~3.4|^4.4.20|^5.0", + "willdurand/jsonp-callback-validator": "~1.1" }, "require-dev": { - "symfony/expression-language": "~2.4||~3.0", - "symfony/phpunit-bridge": "~2.7||~3.0" + "symfony/expression-language": "~3.4|^4.4.20|^5.0", + "symfony/phpunit-bridge": "^5.3" }, "autoload": { - "psr-4": { "FOS\\JsRoutingBundle\\": "" } + "psr-4": { "FOS\\JsRoutingBundle\\": "" }, + "exclude-from-classmap": [ + "/Tests/" + ] }, "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-master": "2.x-dev" } } } diff --git a/phpunit b/phpunit new file mode 100755 index 00000000..f9243bcb --- /dev/null +++ b/phpunit @@ -0,0 +1,9 @@ +#!/usr/bin/env php +