diff --git a/app/config/routes.json b/app/config/routes.json new file mode 100644 index 0000000..7d185ec --- /dev/null +++ b/app/config/routes.json @@ -0,0 +1,11 @@ +{ + "routes": { + "hello": { + "path": "/hello", + "defaults": { + "_controller": "Application\\Controller\\Blog\\HelloWorldAction" + }, + "methods": [ "GET", "POST" ] + } + } +} \ No newline at end of file diff --git a/bootstrap.php b/bootstrap.php index a6d3fb3..33a1ee1 100644 --- a/bootstrap.php +++ b/bootstrap.php @@ -15,6 +15,7 @@ use Framework\Routing\Loader\CompositeFileLoader; use Framework\Routing\Loader\PhpFileLoader; use Framework\Routing\Loader\XmlFileLoader; +use Framework\Routing\Loader\JsonFileLoader; use Framework\ServiceLocator\ServiceLocator; use Framework\Session\Driver\NativeDriver; use Framework\Session\Session; @@ -32,7 +33,8 @@ \PDO::ATTR_ERRMODE => \PDO::ERRMODE_EXCEPTION, \PDO::MYSQL_ATTR_INIT_COMMAND => "SET NAMES 'UTF8'", ]); -$dic->setParameter('router.file', __DIR__.'/app/config/routes.xml'); +//$dic->setParameter('router.file', __DIR__.'/app/config/routes.xml'); +$dic->setParameter('router.file', __DIR__.'/app/config/routes.json'); $dic->setParameter('app.views_dir', __DIR__.'/app/views'); $dic->setParameter('twig.options', [ 'cache' => __DIR__.'/../app/cache/twig', @@ -70,6 +72,7 @@ $dic->register('router', function (ServiceLocator $dic) { $loader = new CompositeFileLoader(); + $loader->add(new JsonFileLoader()); $loader->add(new PhpFileLoader()); $loader->add(new XmlFileLoader()); diff --git a/composer.json b/composer.json index 6973f9b..52d530c 100644 --- a/composer.json +++ b/composer.json @@ -5,8 +5,8 @@ "license": "proprietary", "authors": [ { - "name": "Hugo Hamon", - "email": "hugo.hamon@sensiolabs.com" + "name": "Romain Oliva", + "email": "olivar.info@gmail.com" } ], "autoload": { diff --git a/src/Framework/Routing/Loader/JsonFileLoader.php b/src/Framework/Routing/Loader/JsonFileLoader.php new file mode 100644 index 0000000..8b7e5a9 --- /dev/null +++ b/src/Framework/Routing/Loader/JsonFileLoader.php @@ -0,0 +1,116 @@ +parseRoutes($path); + } + + private function parseRoutes($path) + { + $routes = new RouteCollection(); + $json = json_decode(file_get_contents($path)); + + foreach ($json->route as $route) { + $this->parseRoute($routes, $route); + } + + return $routes; + } + + private function parseRoute(RouteCollection $routes, \stdClass $route) + { + if (empty($route->name)) { + throw new \RuntimeException('Each route must have a unique name.'); + } + + $name = (string) $route->name; + if (empty($route->path)) { + throw new \RuntimeException(sprintf('Route %s must have a path.', $name)); + } + + $methods = []; + if (!empty($route->methods)) { + $methods = explode('|', $route->methods); + } + + $params = $this->parseRouteParams($route, $name); + $requirements = $this->parseRouteRequirements($route, $name); + + $routes->add($name, new Route((string) $route->path, $params, $methods, $requirements)); + } + + private function parseRouteParams(\stdClass $route, $name) + { + $params = []; + if (!isset($route->default) && !count($route->defaults)) { + return $params; + } + + foreach ($route->defaults as $key => $param) { + $params = array_merge($params, $this->parseRouteParam($name, $param, $key)); + } + + return $params; + } + + private function parseRouteParam($name, $param, $key) + { + if (empty($param)) { + throw new \RuntimeException(sprintf( + 'Parameter #%u for route %s can\'t be empty.', + $key, + $name + )); + } + + return [ (string) $key => (string) $param ]; + } + + private function parseRouteRequirements(\stdClass $route, $name) + { + $requirements = []; + if (!isset($route->requirements) || !count($route->requirements)) { + return $requirements; + } + + foreach ($route->requirements as $key => $requirement) { + $requirements = array_merge($requirements, $this->parseRouteRequirement($name, $requirement, $key)); + } + + return $requirements; + } + + private function parseRouteRequirement($name, $requirement, $key) + { + if (empty($requirement)) { + throw new \RuntimeException(sprintf( + 'Requirement #%u for route %s can\'t be empty.', + $key, + $name + )); + } + + return [ (string) $key => (string) $requirement ]; + } +} diff --git a/tests/Framework/Routing/Fixtures/invalid1.json b/tests/Framework/Routing/Fixtures/invalid1.json new file mode 100644 index 0000000..a2e066c --- /dev/null +++ b/tests/Framework/Routing/Fixtures/invalid1.json @@ -0,0 +1,11 @@ +{ + "routes": { + "": { + "path": "/", + "defaults": { + "_controller": "Application\\Controller\\Blog\\ListPostsAction" + }, + "methods": [ "GET" ] + } + } +} \ No newline at end of file diff --git a/tests/Framework/Routing/Fixtures/invalid2.json b/tests/Framework/Routing/Fixtures/invalid2.json new file mode 100644 index 0000000..ccfe4a2 --- /dev/null +++ b/tests/Framework/Routing/Fixtures/invalid2.json @@ -0,0 +1,10 @@ +{ + "routes": { + "homepage": { + "defaults": { + "_controller": "Application\\Controller\\Blog\\ListPostsAction" + }, + "methods": [ "GET" ] + } + } +} \ No newline at end of file diff --git a/tests/Framework/Routing/Fixtures/invalid3.json b/tests/Framework/Routing/Fixtures/invalid3.json new file mode 100644 index 0000000..50438e8 --- /dev/null +++ b/tests/Framework/Routing/Fixtures/invalid3.json @@ -0,0 +1,9 @@ +{ + "routes": { + "homepage": { + "path": "/", + "defaults": { "Application\\Controller\\Blog\\ListPostsAction" : "" }, + "methods": [ "GET" ] + } + } +} \ No newline at end of file diff --git a/tests/Framework/Routing/Fixtures/invalid4.json b/tests/Framework/Routing/Fixtures/invalid4.json new file mode 100644 index 0000000..fa73b4f --- /dev/null +++ b/tests/Framework/Routing/Fixtures/invalid4.json @@ -0,0 +1,12 @@ +{ + "routes": { + "homepage": { + "path": "/{lang}", + "defaults": { + "_controller": "Application\\Controller\\Blog\\ListPostsAction" + }, + "requirements": { "[a-z]{2}":"" }, + "methods": [ "GET" ] + } + } +} \ No newline at end of file diff --git a/tests/Framework/Routing/Fixtures/routes.json b/tests/Framework/Routing/Fixtures/routes.json new file mode 100644 index 0000000..e0de316 --- /dev/null +++ b/tests/Framework/Routing/Fixtures/routes.json @@ -0,0 +1,33 @@ +{ + "routes": { + "homepage": { + "path": "/", + "defaults": { + "_controller": "Application\\Controller\\Blog\\ListPostsAction" + }, + "methods": [ "GET" ] + }, + "blog": { + "path": "/blog", + "defaults": { + "_controller": "Application\\Controller\\Blog\\ListPostsAction" + }, + "methods": [ "GET" ] + }, + "blog_post": { + "path": "/blog/article-{id}.html", + "defaults": { + "_controller": "Application\\Controller\\Blog\\GetPostAction" + }, + "requirements": { "id": "\\d+" }, + "methods": [ "GET" ] + }, + "hello": { + "path": "/hello", + "defaults": { + "_controller": "Application\\Controller\\Blog\\HelloWorldAction" + }, + "methods": [ "GET" ] + } + } +} \ No newline at end of file diff --git a/tests/Framework/Routing/Loader/JsonFileLoaderTest.php b/tests/Framework/Routing/Loader/JsonFileLoaderTest.php new file mode 100644 index 0000000..4a8b87a --- /dev/null +++ b/tests/Framework/Routing/Loader/JsonFileLoaderTest.php @@ -0,0 +1,57 @@ +load('foo.txt'); + } + + /** + * @expectedException \InvalidArgumentException + */ + public function testLoadNonExistentFile() + { + $loader = new JsonFileLoader(); + $loader->load('foo.xml'); + } + + /** + * @expectedException \RuntimeException + * @dataProvider provideInvalidFile + */ + public function testLoadInvalidFile($file) + { + $loader = new JsonFileLoader(); + $loader->load(__DIR__.'/../Fixtures/'.$file); + } + + public function provideInvalidFile() + { + return [ + [ 'invalid1.json' ], + [ 'invalid2.json' ], + [ 'invalid3.json' ], + [ 'invalid4.json' ], + ]; + } + + public function testLoadFile() + { + $loader = new JsonFileLoader(); + + $this->assertInstanceOf( + RouteCollection::class, + $loader->load(__DIR__.'/../Fixtures/routes.json') + ); + } +}