Skip to content

Commit d0167e8

Browse files
author
Lupacescu Eduard
authored
Custom routes (#92)
* Custom routes * Apply fixes from StyleCI (#91) * Documented custom routes * Adapt store integration test * Apply fixes from StyleCI (#93)
1 parent 67a075f commit d0167e8

File tree

12 files changed

+461
-31
lines changed

12 files changed

+461
-31
lines changed

docs/docs/repository-pattern/repository-pattern.md

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -264,3 +264,150 @@ public function serializeIndex($request, $serialized)
264264
}
265265
```
266266

267+
## Custom routes
268+
269+
Laravel Restify has its own "CRUD" routes, however you're able to define your own routes right from your Repository class:
270+
271+
```php
272+
/**
273+
* Defining custom routes
274+
*
275+
* The default prefix of this route is the uriKey (e.g. 'restify-api/posts'),
276+
*
277+
* The default namespace is AppNamespace/Http/Controllers
278+
*
279+
* The default middlewares are the same from config('restify.middleware')
280+
*
281+
* However all options could be overrided by passing an $options argument
282+
*
283+
* @param \Illuminate\Routing\Router $router
284+
* @param $options
285+
*/
286+
public static function routes(\Illuminate\Routing\Router $router, $options = [])
287+
{
288+
$router->get('hello-world', function () {
289+
return 'Hello World';
290+
});
291+
}
292+
```
293+
294+
Let's diving into a more "real life" example. Let's take the Post repository we had above:
295+
296+
```php
297+
use Illuminate\Routing\Router;
298+
use Binaryk\LaravelRestify\Repositories\Repository;
299+
300+
class Post extends Repository
301+
{
302+
/*
303+
* @param \Illuminate\Routing\Router $router
304+
* @param $options
305+
*/
306+
public static function routes(Router $router, $options = [])
307+
{
308+
$router->get('/{id}/kpi', 'PostController@kpi');
309+
}
310+
311+
public static function uriKey()
312+
{
313+
return 'posts';
314+
}
315+
}
316+
```
317+
318+
At this moment Restify built the new route as a child of the `posts`, so it has the route:
319+
320+
```http request
321+
GET: /restify-api/posts/{id}/kpi
322+
```
323+
324+
This route is pointing to the `PostsController`, let's define it:
325+
326+
```php
327+
<?php
328+
329+
namespace App\Http\Controllers;
330+
331+
use Illuminate\Http\JsonResponse;
332+
use Binaryk\LaravelRestify\Controllers\RestController;
333+
334+
class PostController extends RestController
335+
{
336+
/**
337+
* Show the profile for the given user.
338+
*
339+
* @param int $id
340+
* @return JsonResponse
341+
*/
342+
public function kpi($id)
343+
{
344+
//...
345+
346+
return $this->response();
347+
}
348+
}
349+
```
350+
351+
### Custom prefix
352+
353+
As we noticed in the example above, the route is generated as a child of the current repository `uriKey` route,
354+
however sometimes you may want to have a separate prefix, which doesn't depends of the URI of the current repository.
355+
Restify provide you an easy of doing that, by adding default value `prefix` for the second `$options` argument:
356+
357+
```php
358+
/**
359+
* @param \Illuminate\Routing\Router $router
360+
* @param $options
361+
*/
362+
public static function routes(Router $router, $options = ['prefix' => 'api',])
363+
{
364+
$router->get('hello-world', function () {
365+
return 'Hello World';
366+
});
367+
}
368+
````
369+
370+
Now the generated route will look like this:
371+
372+
```http request
373+
GET: '/api/hello-world
374+
```
375+
376+
With `api` as a custom prefix.
377+
378+
379+
### Custom middleware
380+
381+
All routes declared in the `routes` method, will have the same middelwares defined in your `restify.middleware` configuration file.
382+
Overriding default middlewares is a breeze with Restify:
383+
384+
```php
385+
/**
386+
* @param \Illuminate\Routing\Router $router
387+
* @param $options
388+
*/
389+
public static function routes(Router $router, $options = ['middleware' => [CustomMiddleware::class],])
390+
{
391+
$router->get('hello-world', function () {
392+
return 'Hello World';
393+
});
394+
}
395+
````
396+
397+
In that case, the single middleware of the route will be defined by the `CustomMiddleware` class.
398+
399+
### Custom Namespace
400+
401+
By default each route defined in the `routes` method, will have the namespace `AppRootNamespace\Http\Controllers`.
402+
You can override it easily by using `namespace` configuration key:
403+
404+
```php
405+
/**
406+
* @param \Illuminate\Routing\Router $router
407+
* @param $options
408+
*/
409+
public static function routes(Router $router, $options = ['namespace' => 'App\Services',])
410+
{
411+
$router->get('hello-world', 'WorldController@hello');
412+
}
413+
````

src/Http/Controllers/RepositoryStoreController.php

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,6 @@ public function handle(RepositoryStoreRequest $request)
3232
*/
3333
$repository = $request->repository();
3434

35-
$repository::authorizeToCreate($request);
36-
37-
$validator = $repository::validatorForStoring($request);
38-
39-
if ($validator->fails()) {
40-
return $this->response()->invalid()->errors($validator->errors()->toArray())->respond();
41-
}
42-
4335
return $request->newRepositoryWith($repository::newModel())->store($request);
4436
}
4537
}

src/Repositories/Crudable.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,10 +3,13 @@
33
namespace Binaryk\LaravelRestify\Repositories;
44

55
use Binaryk\LaravelRestify\Controllers\RestResponse;
6+
use Binaryk\LaravelRestify\Exceptions\UnauthorizedException;
67
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
78
use Binaryk\LaravelRestify\Restify;
9+
use Illuminate\Auth\Access\AuthorizationException;
810
use Illuminate\Contracts\Pagination\Paginator;
911
use Illuminate\Http\JsonResponse;
12+
use Illuminate\Support\Arr;
1013
use Illuminate\Support\Facades\DB;
1114
use Illuminate\Validation\ValidationException;
1215

@@ -57,6 +60,18 @@ public function show(RestifyRequest $request, $repositoryId)
5760
*/
5861
public function store(RestifyRequest $request)
5962
{
63+
try {
64+
$this->allowToStore($request);
65+
} catch (AuthorizationException | UnauthorizedException $e) {
66+
return $this->response()->setData([
67+
'errors' => Arr::wrap($e->getMessage()),
68+
])->setStatusCode(RestResponse::REST_RESPONSE_FORBIDDEN_CODE);
69+
} catch (ValidationException $e) {
70+
return $this->response()->setData([
71+
'errors' => $e->errors(),
72+
])->setStatusCode(RestResponse::REST_RESPONSE_INVALID_CODE);
73+
}
74+
6075
$model = DB::transaction(function () use ($request) {
6176
$model = self::fillWhenStore(
6277
$request, self::newModel()
@@ -127,6 +142,21 @@ public function allowToUpdate(RestifyRequest $request)
127142
$validator->validate();
128143
}
129144

145+
/**
146+
* @param RestifyRequest $request
147+
* @return mixed
148+
* @throws \Illuminate\Auth\Access\AuthorizationException
149+
* @throws ValidationException
150+
*/
151+
public function allowToStore(RestifyRequest $request)
152+
{
153+
self::authorizeToCreate($request);
154+
155+
$validator = self::validatorForStoring($request);
156+
157+
$validator->validate();
158+
}
159+
130160
/**
131161
* @param RestifyRequest $request
132162
* @throws \Illuminate\Auth\Access\AuthorizationException

src/Repositories/Repository.php

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
1111
use Illuminate\Database\Eloquent\Builder;
1212
use Illuminate\Database\Eloquent\Model;
13+
use Illuminate\Routing\Router;
1314
use Illuminate\Support\Collection;
1415
use Illuminate\Support\Str;
1516

@@ -158,4 +159,33 @@ public static function resolveWith($model)
158159

159160
return $self->withResource($model);
160161
}
162+
163+
/**
164+
* Handle dynamic static method calls into the method.
165+
*
166+
* @param string $method
167+
* @param array $parameters
168+
* @return mixed
169+
*/
170+
public static function __callStatic($method, $parameters)
171+
{
172+
return (new static)->$method(...$parameters);
173+
}
174+
175+
/**
176+
* Defining custom routes.
177+
*
178+
* The prefix of this route is the uriKey (e.g. 'restify-api/orders'),
179+
* The namespace is Http/Controllers
180+
* Middlewares are the same from config('restify.middleware').
181+
*
182+
* However all options could be customized by passing an $options argument
183+
*
184+
* @param Router $router
185+
* @param $options
186+
*/
187+
public static function routes(Router $router, $options = [])
188+
{
189+
// override for custom routes
190+
}
161191
}

src/RestifyServiceProvider.php

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@
22

33
namespace Binaryk\LaravelRestify;
44

5-
use Illuminate\Support\Arr;
65
use Illuminate\Support\Facades\Route;
76
use Illuminate\Support\ServiceProvider;
7+
use ReflectionClass;
88

99
/**
1010
* This provider is injected in console context by the main provider or by the RestifyInjector
@@ -34,27 +34,37 @@ protected function registerRoutes()
3434
'middleware' => config('restify.middleware', []),
3535
];
3636

37-
$this->customDefinitions($config)
37+
$this->customDefinitions()
3838
->defaultRoutes($config);
3939
}
4040

4141
/**
42-
* @param $config
4342
* @return RestifyServiceProvider
4443
*/
45-
public function customDefinitions($config)
44+
public function customDefinitions()
4645
{
47-
collect(Restify::$repositories)->filter(function ($repository) {
48-
return isset($repository::$middleware) || isset($repository::$prefix);
49-
})
50-
->each(function ($repository) use ($config) {
51-
$config['middleware'] = array_merge(config('restify.middleware', []), Arr::wrap($repository::$middleware));
52-
$config['prefix'] = Restify::path($repository::$prefix);
46+
collect(Restify::$repositories)->each(function ($repository) {
47+
$config = [
48+
'namespace' => trim(app()->getNamespace(), '\\').'\Http\Controllers',
49+
'as' => '',
50+
'prefix' => Restify::path($repository::uriKey()),
51+
'middleware' => config('restify.middleware', []),
52+
];
53+
54+
$reflector = new ReflectionClass($repository);
55+
56+
$method = $reflector->getMethod('routes');
5357

54-
Route::group($config, function () {
55-
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
56-
});
58+
$parameters = $method->getParameters();
59+
60+
if (count($parameters) === 2 && $parameters[1] instanceof \ReflectionParameter) {
61+
$config = array_merge($config, $parameters[1]->getDefaultValue());
62+
}
63+
64+
Route::group($config, function ($router) use ($repository) {
65+
$repository::routes($router);
5766
});
67+
});
5868

5969
return $this;
6070
}

src/Traits/AuthorizableModels.php

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ public function authorizedToView(Request $request)
107107
*/
108108
public static function authorizeToCreate(Request $request)
109109
{
110-
throw_unless(static::authorizedToCreate($request), AuthorizationException::class);
110+
throw_unless(static::authorizedToCreate($request), AuthorizationException::class, 'Unauthorized to create.');
111111
}
112112

113113
/**
@@ -119,7 +119,7 @@ public static function authorizeToCreate(Request $request)
119119
public static function authorizedToCreate(Request $request)
120120
{
121121
if (static::authorizable()) {
122-
return Gate::check('create', get_class(static::newModel()));
122+
return Gate::check('create', static::$model);
123123
}
124124

125125
return true;

tests/Controllers/RepositoryStoreControllerTest.php

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@
22

33
namespace Binaryk\LaravelRestify\Tests\Controllers;
44

5+
use Binaryk\LaravelRestify\Tests\Fixtures\Post;
6+
use Binaryk\LaravelRestify\Tests\Fixtures\PostPolicy;
57
use Binaryk\LaravelRestify\Tests\IntegrationTest;
8+
use Illuminate\Support\Facades\Gate;
69

710
/**
811
* @author Eduard Lupacescu <[email protected]>
@@ -12,6 +15,7 @@ class RepositoryStoreControllerTest extends IntegrationTest
1215
protected function setUp(): void
1316
{
1417
parent::setUp();
18+
$this->authenticate();
1519
}
1620

1721
public function test_basic_validation_works()
@@ -23,12 +27,25 @@ public function test_basic_validation_works()
2327
->assertJson([
2428
'errors' => [
2529
'description' => [
26-
'Description field is required bro.',
30+
'Description field is required',
2731
],
2832
],
2933
]);
3034
}
3135

36+
public function test_unauthorized_store()
37+
{
38+
$_SERVER['restify.user.creatable'] = false;
39+
40+
Gate::policy(Post::class, PostPolicy::class);
41+
42+
$this->withExceptionHandling()->post('/restify-api/posts', [
43+
'title' => 'Title',
44+
'description' => 'Title',
45+
])->assertStatus(403)
46+
->assertJson(['errors' => ['Unauthorized to create.']]);
47+
}
48+
3249
public function test_success_storing()
3350
{
3451
$user = $this->mockUsers()->first();

0 commit comments

Comments
 (0)