Browse Source

Merge pull request #11688 from cakephp/3.next-reverse-routing-syntax

Implementing reverse routing syntax
Mark Story 8 years ago
parent
commit
e09ff9ba5f

+ 48 - 2
src/Routing/RouteBuilder.php

@@ -586,6 +586,7 @@ class RouteBuilder
             'routeClass' => $this->_routeClass,
         ];
 
+        $target = $this->parseDefaults($target);
         $target['_method'] = $method;
 
         $route = $this->_makeRoute($template, $target, $options);
@@ -693,7 +694,7 @@ class RouteBuilder
      * The above route will only be matched for GET requests. POST requests will fail to match this route.
      *
      * @param string $route A string describing the template of the route
-     * @param array $defaults An array describing the default route parameters. These parameters will be used by default
+     * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default
      *   and can supply routing parameters that are not dynamic. See above.
      * @param array $options An array matching the named elements in the route to regular expressions which that
      *   element should match. Also contains additional parameters such as which routed parameters should be
@@ -703,8 +704,9 @@ class RouteBuilder
      * @throws \InvalidArgumentException
      * @throws \BadMethodCallException
      */
-    public function connect($route, array $defaults = [], array $options = [])
+    public function connect($route, $defaults = [], array $options = [])
     {
+        $defaults = $this->parseDefaults($defaults);
         if (!isset($options['action']) && !isset($defaults['action'])) {
             $defaults['action'] = 'index';
         }
@@ -730,6 +732,50 @@ class RouteBuilder
     }
 
     /**
+     * Parse the defaults if they're a string
+     *
+     * @param string|array $defaults Defaults array from the connect() method.
+     * @return string|array
+     */
+    protected static function parseDefaults($defaults)
+    {
+        if (!is_string($defaults)) {
+            return $defaults;
+        }
+
+        $regex = '/(?:([a-zA-Z0-9\/]*)\.)?([a-zA-Z0-9\/]*?)(?:\/)?([a-zA-Z0-9]*):{2}([a-zA-Z0-9_]*)/i';
+        if (preg_match($regex, $defaults, $matches)) {
+            unset($matches[0]);
+            $matches = array_filter($matches, function ($value) {
+                return $value !== '' && $value !== '::';
+            });
+
+            // Intentionally incomplete switch
+            switch (count($matches)) {
+                case 2:
+                    return [
+                        'controller' => $matches[3],
+                        'action' => $matches[4]
+                    ];
+                case 3:
+                    return [
+                        'prefix' => strtolower($matches[2]),
+                        'controller' => $matches[3],
+                        'action' => $matches[4]
+                    ];
+                case 4:
+                    return [
+                        'plugin' => $matches[1],
+                        'prefix' => strtolower($matches[2]),
+                        'controller' => $matches[3],
+                        'action' => $matches[4]
+                    ];
+            }
+        }
+        throw new RuntimeException("Could not parse `{$defaults}` route destination string.");
+    }
+
+    /**
      * Create a route object, or return the provided object.
      *
      * @param string|\Cake\Routing\Route\Route $route The route template or route object.

+ 2 - 1
src/Routing/Router.php

@@ -19,6 +19,7 @@ use Cake\Http\ServerRequest;
 use Cake\Routing\Exception\MissingRouteException;
 use Cake\Utility\Inflector;
 use Psr\Http\Message\ServerRequestInterface;
+use RuntimeException;
 
 /**
  * Parses the request URL into controller, action, and parameters. Uses the connected routes
@@ -191,7 +192,7 @@ class Router
      * Compatibility proxy to \Cake\Routing\RouteBuilder::connect() in the `/` scope.
      *
      * @param string $route A string describing the template of the route
-     * @param array $defaults An array describing the default route parameters. These parameters will be used by default
+     * @param array|string $defaults An array describing the default route parameters. These parameters will be used by default
      *   and can supply routing parameters that are not dynamic. See above.
      * @param array $options An array matching the named elements in the route to regular expressions which that
      *   element should match. Also contains additional parameters such as which routed parameters should be

+ 84 - 2
tests/TestCase/Routing/RouteBuilderTest.php

@@ -22,6 +22,7 @@ use Cake\Routing\Route\InflectedRoute;
 use Cake\Routing\Route\RedirectRoute;
 use Cake\Routing\Route\Route;
 use Cake\TestSuite\TestCase;
+use RuntimeException;
 
 /**
  * RouteBuilder test case
@@ -185,6 +186,65 @@ class RouteBuilderTest extends TestCase
     }
 
     /**
+     * Test connect() with short string syntax
+     *
+     * @return void
+     */
+    public function testConnectShortStringInvalid()
+    {
+        $this->expectException(RuntimeException::class);
+        $routes = new RouteBuilder($this->collection, '/');
+        $routes->connect('/my-articles/view', 'Articles:no');
+    }
+
+    /**
+     * Test connect() with short string syntax
+     *
+     * @return void
+     */
+    public function testConnectShortString()
+    {
+        $routes = new RouteBuilder($this->collection, '/');
+        $routes->connect('/my-articles/view', 'Articles::view');
+        $expected = [
+            'pass' => [],
+            'controller' => 'Articles',
+            'action' => 'view',
+            'plugin' => null,
+            '_matchedRoute' => '/my-articles/view'
+        ];
+        $this->assertEquals($expected, $this->collection->parse('/my-articles/view'));
+
+        $url = $expected['_matchedRoute'];
+        unset($expected['_matchedRoute']);
+        $this->assertEquals($url, '/' . $this->collection->match($expected, []));
+    }
+
+    /**
+     * Test connect() with short string syntax
+     *
+     * @return void
+     */
+    public function testConnectShortStringPluginPrefix()
+    {
+        $routes = new RouteBuilder($this->collection, '/');
+        $routes->connect('/admin/blog/articles/view', 'Vendor/Blog.Management/Admin/Articles::view');
+        $expected = [
+            'pass' => [],
+            'plugin' => 'Vendor/Blog',
+            'prefix' => 'management/admin',
+            'controller' => 'Articles',
+            'action' => 'view',
+            '_matchedRoute' => '/admin/blog/articles/view'
+        ];
+        $this->assertEquals($expected, $this->collection->parse('/admin/blog/articles/view'));
+
+        $url = $expected['_matchedRoute'];
+        unset($expected['_matchedRoute']);
+        $this->assertEquals($url, '/' . $this->collection->match($expected, []));
+    }
+
+    /**
      * Test if a route name already exist
      *
      * @return void
@@ -192,11 +252,9 @@ class RouteBuilderTest extends TestCase
     public function testNameExists()
     {
         $routes = new RouteBuilder($this->collection, '/l', ['prefix' => 'api']);
-
         $this->assertFalse($routes->nameExists('myRouteName'));
 
         $routes->connect('myRouteUrl', ['action' => 'index'], ['_name' => 'myRouteName']);
-
         $this->assertTrue($routes->nameExists('myRouteName'));
     }
 
@@ -1035,6 +1093,30 @@ class RouteBuilderTest extends TestCase
     }
 
     /**
+     * Test that the HTTP method helpers create the right kind of routes.
+     *
+     * @dataProvider httpMethodProvider
+     * @return void
+     */
+    public function testHttpMethodsStringTarget($method)
+    {
+        $routes = new RouteBuilder($this->collection, '/', [], ['namePrefix' => 'app:']);
+        $route = $routes->{strtolower($method)}(
+            '/bookmarks/:id',
+            'Bookmarks::view',
+            'route-name'
+        );
+        $this->assertInstanceOf(Route::class, $route, 'Should return a route');
+        $this->assertSame($method, $route->defaults['_method']);
+        $this->assertSame('app:route-name', $route->options['_name']);
+        $this->assertSame('/bookmarks/:id', $route->template);
+        $this->assertEquals(
+            ['plugin' => null, 'controller' => 'Bookmarks', 'action' => 'view', '_method' => $method],
+            $route->defaults
+        );
+    }
+
+    /**
      * Integration test for http method helpers and route fluent method
      *
      * @return void

+ 21 - 0
tests/TestCase/Routing/RouterTest.php

@@ -3423,6 +3423,27 @@ class RouterTest extends TestCase
     }
 
     /**
+     * test connect() with short string syntax
+     *
+     * @return void
+     */
+    public function testConnectShortStringSyntax()
+    {
+        Router::connect('/admin/articles/view', 'Admin/Articles::view');
+        $result = Router::parseRequest($this->makeRequest('/admin/articles/view', 'GET'));
+        $expected = [
+            'pass' => [],
+            'prefix' => 'admin',
+            'controller' => 'Articles',
+            'action' => 'view',
+            'plugin' => null,
+            '_matchedRoute' => '/admin/articles/view'
+
+        ];
+        $this->assertEquals($result, $expected);
+    }
+
+    /**
      * Connect some fallback routes for testing router behavior.
      *
      * @return void