8000 [Feature] Allow resource type URI fragment to be customised. · ossycodes/laravel-json-api@edd3d4f · GitHub
[go: up one dir, main page]

Skip to content

Commit edd3d4f

Browse files
committed
[Feature] Allow resource type URI fragment to be customised.
See cloudcreativity#507
1 parent a360c7c commit edd3d4f

File tree

7 files changed

+126
-19
lines changed

7 files changed

+126
-19
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,9 @@ All notable changes to this project will be documented in this file. This projec
77
### Added
88
- [#570](https://github.com/cloudcreativity/laravel-json-api/issues/570)
99
Exception parser now handles the Symfony request exception interface.
10+
- [#507](https://github.com/cloudcreativity/laravel-json-api/issues/507)
11+
Can now specify the resource type and relationship URIs when registering routes. This allows
12+
the URI fragment to be different from the resource type or relationship name.
1013

1114
### Fixed
1215
- Fixed qualifying column for morph-to-many relations. This was caused by Laravel introducing

docs/basics/routing.md

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ to it.
2222

2323
### API Route Prefix
2424

25-
When registering a JSON API, we automatically read the URL prefix and route name prefix from your
26-
[API's URL configuration](./api#url) and apply this to the route group for your API. The URL prefix in your JSON API
25+
When registering a JSON API, we automatically read the URL prefix and route name prefix from your
26+
[API's URL configuration](./api#url) and apply this to the route group for your API. The URL prefix in your JSON API
2727
config is **always** relative to the root URL on a host, i.e. from `/`.
2828
**This means when registering your routes, you need to ensure that no prefix has already been applied.**
2929

@@ -42,7 +42,7 @@ JsonApi::register('default')->withNamespace('Api')->routes(function ($api) {
4242
```
4343

4444
> We use `withNamespace()` instead of Laravel's usual `namespace()` method because `namespace` is a
45-
[Reserved Keyword](http://php.net/manual/en/reserved.keywords.php).
45+
[Reserved Keyword](http://php.net/manual/en/reserved.keywords.php).
4646

4747
## Resource Routes
4848

@@ -67,6 +67,16 @@ JsonApi::register('default')->routes(function ($api) {
6767
});
6868
```
6969

70+
By default the resource type is used as the URI fragment: i.e. the `posts` resource will have a URI of
71+
`/posts`. If you want to use a different URI fragment, use the `uri()` method. In the following example,
72+
the resource type is `posts` but the URI will be `/blog_posts`:
73+
74+
```php
75+
JsonApi::register('default')->routes(function ($api) {
76+
$api->resource('posts')->uri('blog_posts');
77+
});
78+
```
79+
7080
## Relationship Routes
7181

7282
The JSON API spec also defines routes for relationships on a resource type. There are two types of relationships:
@@ -87,6 +97,19 @@ JsonApi::register('default')->routes(function ($api) {
8797
});
8898
```
8999

100+
By default the relationship name is used as the URI fragment: i.e. for the `comments` relationship on the
101+
`posts` resource, the related resource URI is `/posts/{record}/comments`. To customise the URI framgent,
102+
use the `uri()` method. In the following example, the relationship name is `comments` but the URI
103+
will be `/posts/{record}/blog_comments`:
104+
105+
```php
106+
JsonApi::register('default')->routes(function ($api) {
107+
$api->resource('posts')->relationships(function ($relations) {
108+
$relations->hasMany('comments')->uri('blog_comments');
109+
});
110+
});
111+
```
112+
90113
### Related Resource Type
91114

92115
When registering relationship routes, it is assumed that the resource type returned in the response is the
@@ -252,7 +275,7 @@ JsonApi::register('default')->withNamespace('Api')->routes(function ($api, $rout
252275

253276
### Controller Names
254277

255-
If you call `controller()` without any arguments, we assume your controller is the camel case name version of
278+
If you call `controller()` without any arguments, we assume your controller is the camel case name version of
256279
the resource type with `Controller` on the end. I.e. `posts` would expect `PostsController` and
257280
`blog-posts` would expect `BlogPostsController`. Or if your resource type was `post`,
258281
we would guess `PostController`.
@@ -332,7 +355,7 @@ If you are using these, you will also need to refer to the *Custom Actions* sect
332355

333356
Also note that custom routes are registered *before* the routes defined by the JSON API specification,
334357
i.e. those that are added when you call `$api->resource('posts')`. You will need to ensure that your
335-
custom route definitions do not collide with these defined routes.
358+
custom route definitions do not collide with these defined routes.
336359

337360
> Generally we advise against registering custom routes. This is because the JSON API specification may
338361
have additional routes added to it in the future, which might collide with your custom routes.
@@ -396,7 +419,7 @@ does not contain an `@` symbol we add the controller name to it.
396419

397420
Secondly, if you are defining a custom relationship route, you must use the `field` method. This takes
398421
the relationship name as its first argument. The inverse resource type can be specified as the second argument,
399-
for example:
422+
for example:
400423

401424
```php
402425
JsonApi::register('default')->withNamespace('Api')->routes(function ($api) {

src/Routing/RelationshipRegistration.php

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ public function __construct(array $options = [])
3737
$this->options = $options;
3838
}
3939

40+
/**
41+
* @param string $uri
42+
* @return $this
43+
*/
44+
public function uri(string $uri): self
45+
{
46+
$this->options['relationship_uri'] = $uri;
47+
48+
return $this;
49+
}
50+
4051
/**
4152
* @param string $resourceType
4253
* @return $this

src/Routing/RelationshipsRegistrar.php

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ private function add(string $field, array $options): void
115115

116116
$this->router->group([], function () use ($field, $options, $inverse) {
117117
foreach ($options['actions'] as $action) {
118-
$this->route($field, $action, $inverse);
118+
$this->route($field, $action, $inverse, $options);
119119
}
120120
});
121121
}
@@ -125,13 +125,14 @@ private function add(string $field, array $options): void
125125
* @param string $action
126126
* @param string $inverse
127127
* the inverse resource type
128+
* @param array $options
128129
* @return Route
129130
*/
130-
private function route(string $field, string $action, string $inverse): Route
131+
private function route(string $field, string $action, string $inverse, array $options): Route
131132
{
132133
$route = $this->createRoute(
133134
$this->methodForAction($action),
134-
$this->urlForAction($field, $action),
135+
$this->urlForAction($field, $action, $options),
135136
$this->actionForRoute($field, $action)
136137
);
137138

@@ -161,24 +162,30 @@ private function hasManyActions(array $options): array
161162

162163
/**
163164
* @param string $relationship
165+
* @param array $options
164166
* @return string
165167
*/
166-
private function relatedUrl($relationship): string
168+
private function relatedUrl(string $relationship, array $options): string
167169
{
168-
return sprintf('%s/%s', $this->resourceUrl(), $relationship);
170+
return sprintf(
171+
'%s/%s',
172+
$this->resourceUrl(),
173+
$options['relationship_uri'] ?? $relationship
174+
);
169175
}
170176

171177
/**
172-
* @param $relationship
178+
* @param string $relationship
179+
* @param array $options
173180
* @return string
174181
*/
175-
private function relationshipUrl($relationship): string
182+
private function relationshipUrl(string $relationship, array $options): string
176183
{
177184
return sprintf(
178185
'%s/%s/%s',
179186
$this->resourceUrl(),
180187
ResourceRegistrar::KEYWORD_RELATIONSHIPS,
181-
$relationship
188+
$options['relationship_uri'] ?? $relationship
182189
);
183190
}
184191

@@ -194,15 +201,16 @@ private function methodForAction(string $action): string
194201
/**
195202
* @param string $field
196203
* @param string $action
204+
* @param array $options
197205
* @return string
198206
*/
199-
private function urlForAction(string $field, string $action): string
207+
private function urlForAction(string $field, string $action, array $options): string
200208
{
201209
if ('related' === $action) {
202-
return $this->relatedUrl($field);
210+
return $this->relatedUrl($field, $options);
203211
}
204212

205-
return $this->relationshipUrl($field);
213+
return $this->relationshipUrl($field, $options);
206214
}
207215

208216
/**

src/Routing/ResourceRegistrar.php

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -168,10 +168,12 @@ private function contentNegotiation(): string
168168
*/
169169
private function attributes(): array
170170
{
171+
$prefix = $this->options['resource_uri'] ?? $this->resourceType;
172+
171173
return [
172174
'middleware' => $this->middleware(),
173175
'as' => "{$this->resourceType}.",
174-
'prefix' => $this->resourceType,
176+
'prefix' => $prefix,
175177
];
176178
}
177179

src/Routing/ResourceRegistration.php

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,19 @@ public function authorizer(string $authorizer): self
102102
return $this->middleware("json-api.auth:{$authorizer}");
103103
}
104104

105+
/**
106+
* Set the URI fragment, if different from the resource type.
107+
*
108+
* @param string $uri
109+
* @return $this
110+
*/
111+
public function uri(string $uri): self
112+
{
113+
$this->options['resource_uri'] = $uri;
114+
115+
return $this;
116+
}
117+
105118
/**
106119
* Add middleware.
107120
*

tests/lib/Integration/Routing/Test.php

Lines changed: 48 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
use Illuminate\Http\Request;
2626
use Illuminate\Support\Arr;
2727
use Illuminate\Support\Facades\Route;
28+
use Illuminate\Support\Str;
2829
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
2930
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
3031

@@ -113,6 +114,52 @@ public function testFluentDefaults($method, $url, $action)
113114
$this->assertMatch($method, $url, '\\' . JsonApiController::class . $action);
114115
}
115116

117+
/**
118+
* @return array
119+
*/
120+
public function uriProvider(): array
121+
{
122+
return [
123+
'index' => ['GET', '/api/v1/blog_posts', '@index'],
124+
'create' => ['POST', '/api/v1/blog_posts', '@create'],
125+
'read' => ['GET', '/api/v1/blog_posts/1', '@read'],
126+
'update' => ['PATCH', '/api/v1/blog_posts/1', '@update'],
127+
'delete' => ['DELETE', '/api/v1/blog_posts/1', '@delete'],
128+
'has-one related' => ['GET', '/api/v1/blog_posts/1/author', '@readRelatedResource'],
129+
'has-one read' => ['GET', '/api/v1/blog_posts/1/relationships/author', '@readRelationship'],
130+
'has-one replace' => ['PATCH', '/api/v1/blog_posts/1/relationships/author', '@replaceRelationship'],
131+
'has-many related' => ['GET', '/api/v1/blog_posts/1/post_comments', '@readRelatedResource'],
132+
'has-many read' => ['GET', '/api/v1/blog_posts/1/relationships/post_comments', '@readRelationship'],
133+
'has-many replace' => ['PATCH', '/api/v1/blog_posts/1/relationships/post_comments', '@replaceRelationship'],
134+
'has-many add' => ['POST', '/api/v1/blog_posts/1/relationships/post_comments', '@addToRelationship'],
135+
'has-many remove' => ['DELETE', '/api/v1/blog_posts/1/relationships/post_comments', '@removeFromRelationship'],
136+
];
137+
}
138+
139+
/**
140+
* @param $method
141+
* @param $url
142+
* @param $action
143+
* @dataProvider uriProvider
144+
*/
145+
public function testUriIsDifferentFromResourceType(string $method, string $url, string $action): void
146+
{
147+
$this->withFluentRoutes()->routes(function (RouteRegistrar $api) {
148+
$api->resource('posts')->uri('blog_posts')->relationships(function (RelationshipsRegistration $rel) {
149+
$rel->hasOne('author');
150+
$rel->hasMany('tags');
151+
$rel->hasMany('comments')->uri('post_comments');
152+
});
153+
});
154+
155+
$route = $this->assertMatch($method, $url, '\\' . JsonApiController::class . $action);
156+
$this->assertSame('posts', $route->parameter('resource_type'));
157+
158+
if (Str::contains($url, 'post_comments')) {
159+
$this->assertSame('comments', $route->parameter('relationship_name'));
160+
}
161+
}
162+
116163
/**
117164
* @param $method
118165
* @param $url
@@ -991,7 +1038,7 @@ private function assertRoute($method, $url, $expected = 200)
9911038
*/
9921039
private function assertRoutes(array $routes)
9931040
{
994-
foreach ($routes as list($method, $url, $expected)) {
1041+
foreach ($routes as [$method, $url, $expected]) {
9951042
$this->assertRoute($method, $url, $expected);
9961043
}
9971044
}

0 commit comments

Comments
 (0)
0