diff --git a/CHANGELOG.md b/CHANGELOG.md index 456cfae5..092a7d53 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ Breaking changes are marked with ⚠️. **Added** - Document the `check()` method ([#294](/~https://github.com/tightenco/ziggy/pull/294)) and how to install and use Ziggy via `npm` and over a CDN ([#299](/~https://github.com/tightenco/ziggy/pull/299)) +- Add support for [custom scoped route model bindings](https://laravel.com/docs/7.x/routing#implicit-binding), e.g. `/users/{user}/posts/{post:slug}` ([#307](/~https://github.com/tightenco/ziggy/pull/307)) **Changed** diff --git a/src/RoutePayload.php b/src/RoutePayload.php index d29b52fe..f94d5324 100644 --- a/src/RoutePayload.php +++ b/src/RoutePayload.php @@ -95,6 +95,9 @@ protected function nameKeyedRoutes() return collect($route)->only(['uri', 'methods']) ->put('domain', $route->domain()) + ->when(method_exists($route, 'bindingFields'), function ($collection) use ($route) { + return $collection->put('bindings', $route->bindingFields()); + }) ->when($middleware = config('ziggy.middleware'), function ($collection) use ($middleware, $route) { if (is_array($middleware)) { return $collection->put('middleware', collect($route->middleware())->intersect($middleware)->values()); diff --git a/src/js/route.js b/src/js/route.js index ace647a3..c9aca2c4 100644 --- a/src/js/route.js +++ b/src/js/route.js @@ -67,13 +67,22 @@ class Router extends String { delete this.urlParams[keyName]; } + // The block above is what requires us to assign tagValue below + // instead of returning - if multiple *objects* are passed as + // params, numericParamIndices will be true and each object will + // be assigned above, which means !tagValue will evaluate to + // false, skipping the block below. + // If a value wasn't provided for this named parameter explicitly, // but the object that was passed contains an ID, that object // was probably a model, so we use the ID. - // Note that we are not explicitly ensuring here that the template - // doesn't have an ID param (`this.template.indexOf('{id}') == -1`) - // because we don't need to - if it does, we won't get this far. - if (!tagValue && !this.urlParams[keyName] && this.urlParams['id']) { + + let bindingKey = this.ziggy.namedRoutes[this.name]?.bindings?.[keyName]; + + if (bindingKey && !this.urlParams[keyName] && this.urlParams[bindingKey]) { + tagValue = this.urlParams[bindingKey]; + delete this.urlParams[bindingKey]; + } else if (!tagValue && !this.urlParams[keyName] && this.urlParams['id']) { tagValue = this.urlParams['id'] delete this.urlParams['id']; } @@ -97,6 +106,8 @@ class Router extends String { // If an object was passed and has an id, return it if (tagValue.id) { return encodeURIComponent(tagValue.id); + } else if (tagValue[bindingKey]) { + return encodeURIComponent(tagValue[bindingKey]) } return encodeURIComponent(tagValue); diff --git a/tests/TestCase.php b/tests/TestCase.php index 11c1b8f3..81f85a13 100644 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -16,6 +16,13 @@ protected function getPackageProviders($app) ]; } + protected function laravelVersion(int $v = null) + { + $version = (int) head(explode('.', app()->version())); + + return isset($v) ? $version >= $v : $version; + } + protected function assertJsonContains(array $haystack, array $needle) { $actual = json_encode(Arr::sortRecursive( diff --git a/tests/Unit/BladeRouteGeneratorTest.php b/tests/Unit/BladeRouteGeneratorTest.php index e8e5fc81..dff0a831 100644 --- a/tests/Unit/BladeRouteGeneratorTest.php +++ b/tests/Unit/BladeRouteGeneratorTest.php @@ -30,13 +30,19 @@ function generator_outputs_non_domain_named_routes_with_expected_structure() $generator = (new BladeRouteGenerator($router)); - $this->assertEquals([ + $expected = [ 'postComments.index' => [ 'uri' => 'posts/{post}/comments', 'methods' => ['GET', 'HEAD'], 'domain' => null, ], - ], $generator->getRoutePayload()->toArray()); + ]; + + if ($this->laravelVersion(7)) { + $expected['postComments.index']['bindings'] = []; + } + + $this->assertEquals($expected, $generator->getRoutePayload()->toArray()); } /** @test */ @@ -56,13 +62,19 @@ function generator_outputs_domain_as_defined() $generator = (new BladeRouteGenerator($router)); - $this->assertEquals([ + $expected = [ 'postComments.index' => [ 'uri' => 'posts/{post}/comments', 'methods' => ['GET', 'HEAD'], 'domain' => '{account}.myapp.com', ], - ], $generator->getRoutePayload()->toArray()); + ]; + + if ($this->laravelVersion(7)) { + $expected['postComments.index']['bindings'] = []; + } + + $this->assertEquals($expected, $generator->getRoutePayload()->toArray()); } /** @test */ diff --git a/tests/Unit/CommandRouteGeneratorTest.php b/tests/Unit/CommandRouteGeneratorTest.php index 04b7769b..84018fe8 100644 --- a/tests/Unit/CommandRouteGeneratorTest.php +++ b/tests/Unit/CommandRouteGeneratorTest.php @@ -40,7 +40,14 @@ function file_is_created_with_the_expected_structure_when_named_routes_exist() Artisan::call('ziggy:generate'); - $this->assertFileEquals('./tests/fixtures/ziggy.js', base_path('resources/js/ziggy.js')); + if ($this->laravelVersion(7)) { + $this->assertFileEquals('./tests/fixtures/ziggy.js', base_path('resources/js/ziggy.js')); + } else { + $this->assertSame( + str_replace(',"bindings":[]', '', file_get_contents(__DIR__ . '/../fixtures/ziggy.js')), + file_get_contents(base_path('resources/js/ziggy.js')) + ); + } } /** @test */ @@ -57,7 +64,14 @@ function file_is_created_with_a_custom_url() Artisan::call('ziggy:generate', ['--url' => 'http://example.org']); - $this->assertFileEquals('./tests/fixtures/custom-url.js', base_path('resources/js/ziggy.js')); + if ($this->laravelVersion(7)) { + $this->assertFileEquals('./tests/fixtures/custom-url.js', base_path('resources/js/ziggy.js')); + } else { + $this->assertSame( + str_replace(',"bindings":[]', '', file_get_contents(__DIR__ . '/../fixtures/custom-url.js')), + file_get_contents(base_path('resources/js/ziggy.js')) + ); + } } /** @test */ @@ -87,11 +101,25 @@ function file_is_created_with_the_expected_group() Artisan::call('ziggy:generate'); - $this->assertFileEquals('./tests/fixtures/ziggy.js', base_path('resources/js/ziggy.js')); + if ($this->laravelVersion(7)) { + $this->assertFileEquals('./tests/fixtures/ziggy.js', base_path('resources/js/ziggy.js')); + } else { + $this->assertSame( + str_replace(',"bindings":[]', '', file_get_contents(__DIR__ . '/../fixtures/ziggy.js')), + file_get_contents(base_path('resources/js/ziggy.js')) + ); + } Artisan::call('ziggy:generate', ['path' => 'resources/js/admin.js', '--group' => 'admin']); - $this->assertFileEquals('./tests/fixtures/admin.js', base_path('resources/js/admin.js')); + if ($this->laravelVersion(7)) { + $this->assertFileEquals('./tests/fixtures/admin.js', base_path('resources/js/admin.js')); + } else { + $this->assertSame( + str_replace(',"bindings":[]', '', file_get_contents(__DIR__ . '/../fixtures/admin.js')), + file_get_contents(base_path('resources/js/admin.js')) + ); + } } protected function tearDown(): void diff --git a/tests/Unit/RoutePayloadTest.php b/tests/Unit/RoutePayloadTest.php index 5c2dfc3f..cfd4a4d4 100644 --- a/tests/Unit/RoutePayloadTest.php +++ b/tests/Unit/RoutePayloadTest.php @@ -44,6 +44,12 @@ public function setUp(): void }) ->name('admin.users.index')->middleware('role:admin'); + if ($this->laravelVersion(7)) { + $this->router->get('/posts/{post}/comments/{comment:uuid}', function () { + return ''; + })->name('postComments.show'); + } + $this->router->getRoutes()->refreshNameLookups(); } @@ -72,6 +78,12 @@ public function only_matching_routes_included_with_include_enabled() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + } + $this->assertEquals($expected, $routes->toArray()); } @@ -95,6 +107,21 @@ public function only_matching_routes_excluded_with_exclude_enabled() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + + $expected['postComments.show'] = [ + 'uri' => 'posts/{post}/comments/{comment}', + 'methods' => ['GET', 'HEAD'], + 'domain' => null, + 'bindings' => [ + 'comment' => 'uuid', + ], + ]; + } + $this->assertEquals($expected, $routes->toArray()); } @@ -125,6 +152,12 @@ public function existence_of_only_config_causes_routes_to_be_included() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + } + $this->assertEquals($expected, $routes->toArray()); } @@ -150,6 +183,21 @@ public function existence_of_except_config_causes_routes_to_be_excluded() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + + $expected['postComments.show'] = [ + 'uri' => 'posts/{post}/comments/{comment}', + 'methods' => ['GET', 'HEAD'], + 'domain' => null, + 'bindings' => [ + 'comment' => 'uuid', + ], + ]; + } + $this->assertEquals($expected, $routes->toArray()); } @@ -196,6 +244,21 @@ public function existence_of_both_configs_returns_unfiltered_routes() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + + $expected['postComments.show'] = [ + 'uri' => 'posts/{post}/comments/{comment}', + 'methods' => ['GET', 'HEAD'], + 'domain' => null, + 'bindings' => [ + 'comment' => 'uuid', + ], + ]; + } + $this->assertEquals($expected, $routes->toArray()); } @@ -233,6 +296,12 @@ public function only_matching_routes_included_with_group_enabled() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + } + $this->assertEquals($expected, $routes->toArray()); } @@ -274,6 +343,21 @@ public function non_existence_of_group_returns_unfiltered_routes() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + + $expected['postComments.show'] = [ + 'uri' => 'posts/{post}/comments/{comment}', + 'methods' => ['GET', 'HEAD'], + 'domain' => null, + 'bindings' => [ + 'comment' => 'uuid', + ], + ]; + } + $this->assertEquals($expected, $routes->toArray()); } @@ -325,6 +409,22 @@ public function retrieves_middleware_if_config_is_set() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + + $expected['postComments.show'] = [ + 'uri' => 'posts/{post}/comments/{comment}', + 'methods' => ['GET', 'HEAD'], + 'domain' => null, + 'middleware' => [], + 'bindings' => [ + 'comment' => 'uuid', + ], + ]; + } + $this->assertEquals($expected, $routes->toArray()); } @@ -376,6 +476,22 @@ public function retrieves_only_configured_middleware() ], ]; + if ($this->laravelVersion(7)) { + foreach ($expected as $key => $route) { + $expected[$key]['bindings'] = []; + } + + $expected['postComments.show'] = [ + 'uri' => 'posts/{post}/comments/{comment}', + 'methods' => ['GET', 'HEAD'], + 'domain' => null, + 'middleware' => [], + 'bindings' => [ + 'comment' => 'uuid', + ], + ]; + } + $this->assertEquals($expected, $routes->toArray()); } } diff --git a/tests/fixtures/admin.js b/tests/fixtures/admin.js index efad952a..bef535bc 100644 --- a/tests/fixtures/admin.js +++ b/tests/fixtures/admin.js @@ -1,5 +1,5 @@ var Ziggy = { - namedRoutes: {"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"],"domain":null}}, + namedRoutes: {"admin.dashboard":{"uri":"admin","methods":["GET","HEAD"],"domain":null,"bindings":[]}}, baseUrl: 'http://myapp.com/', baseProtocol: 'http', baseDomain: 'myapp.com', diff --git a/tests/fixtures/custom-url.js b/tests/fixtures/custom-url.js index d7485fb6..d4858713 100644 --- a/tests/fixtures/custom-url.js +++ b/tests/fixtures/custom-url.js @@ -1,5 +1,5 @@ var Ziggy = { - namedRoutes: {"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"],"domain":null}}, + namedRoutes: {"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"],"domain":null,"bindings":[]}}, baseUrl: 'http://example.org/', baseProtocol: 'http', baseDomain: 'example.org', diff --git a/tests/fixtures/ziggy.js b/tests/fixtures/ziggy.js index 42b4a3cc..7ca4f899 100644 --- a/tests/fixtures/ziggy.js +++ b/tests/fixtures/ziggy.js @@ -1,5 +1,5 @@ var Ziggy = { - namedRoutes: {"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"],"domain":null}}, + namedRoutes: {"postComments.index":{"uri":"posts\/{post}\/comments","methods":["GET","HEAD"],"domain":null,"bindings":[]}}, baseUrl: 'http://myapp.com/', baseProtocol: 'http', baseDomain: 'myapp.com', diff --git a/tests/js/test.route.js b/tests/js/test.route.js index 62746fb6..fc4df46c 100644 --- a/tests/js/test.route.js +++ b/tests/js/test.route.js @@ -97,6 +97,14 @@ global.Ziggy = { methods: ['GET', 'HEAD'], domain: null }, + 'postComments.show': { + uri: 'posts/{post}/comments/{comment}', + methods: ['GET', 'HEAD'], + domain: null, + bindings: { + comment: 'uuid', + }, + }, }, baseUrl: 'http://myapp.dev/', baseProtocol: 'http', @@ -112,6 +120,24 @@ describe('route()', function() { assert.equal('http://myapp.dev/posts', route('posts.index')); }); + it('Can use custom route model binding keys', function() { + assert.equal( + route('postComments.show', [ + { id: 1, title: 'Post' }, + { uuid: 12345, title: 'Comment' } + ]), + 'http://myapp.dev/posts/1/comments/12345' + ); + + assert.equal( + route('postComments.show', [ + { id: 1, post: 'Post' }, + { uuid: 12345, comment: 'Comment' } + ]), + 'http://myapp.dev/posts/1/comments/12345' + ); + }); + it('Can handle routing for apps in a subfolder', function() { let orgBaseUrl = Ziggy.baseUrl; let orgBaseDomain = Ziggy.baseDomain;