Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
147 changes: 147 additions & 0 deletions docs/docs/repository-pattern/repository-pattern.md
Original file line number Diff line number Diff line change
Expand Up @@ -264,3 +264,150 @@ public function serializeIndex($request, $serialized)
}
```

## Custom routes

Laravel Restify has its own "CRUD" routes, however you're able to define your own routes right from your Repository class:

```php
/**
* Defining custom routes
*
* The default prefix of this route is the uriKey (e.g. 'restify-api/posts'),
*
* The default namespace is AppNamespace/Http/Controllers
*
* The default middlewares are the same from config('restify.middleware')
*
* However all options could be overrided by passing an $options argument
*
* @param \Illuminate\Routing\Router $router
* @param $options
*/
public static function routes(\Illuminate\Routing\Router $router, $options = [])
{
$router->get('hello-world', function () {
return 'Hello World';
});
}
```

Let's diving into a more "real life" example. Let's take the Post repository we had above:

```php
use Illuminate\Routing\Router;
use Binaryk\LaravelRestify\Repositories\Repository;

class Post extends Repository
{
/*
* @param \Illuminate\Routing\Router $router
* @param $options
*/
public static function routes(Router $router, $options = [])
{
$router->get('/{id}/kpi', 'PostController@kpi');
}

public static function uriKey()
{
return 'posts';
}
}
```

At this moment Restify built the new route as a child of the `posts`, so it has the route:

```http request
GET: /restify-api/posts/{id}/kpi
```

This route is pointing to the `PostsController`, let's define it:

```php
<?php

namespace App\Http\Controllers;

use Illuminate\Http\JsonResponse;
use Binaryk\LaravelRestify\Controllers\RestController;

class PostController extends RestController
{
/**
* Show the profile for the given user.
*
* @param int $id
* @return JsonResponse
*/
public function kpi($id)
{
//...

return $this->response();
}
}
```

### Custom prefix

As we noticed in the example above, the route is generated as a child of the current repository `uriKey` route,
however sometimes you may want to have a separate prefix, which doesn't depends of the URI of the current repository.
Restify provide you an easy of doing that, by adding default value `prefix` for the second `$options` argument:

```php
/**
* @param \Illuminate\Routing\Router $router
* @param $options
*/
public static function routes(Router $router, $options = ['prefix' => 'api',])
{
$router->get('hello-world', function () {
return 'Hello World';
});
}
````

Now the generated route will look like this:

```http request
GET: '/api/hello-world
```

With `api` as a custom prefix.


### Custom middleware

All routes declared in the `routes` method, will have the same middelwares defined in your `restify.middleware` configuration file.
Overriding default middlewares is a breeze with Restify:

```php
/**
* @param \Illuminate\Routing\Router $router
* @param $options
*/
public static function routes(Router $router, $options = ['middleware' => [CustomMiddleware::class],])
{
$router->get('hello-world', function () {
return 'Hello World';
});
}
````

In that case, the single middleware of the route will be defined by the `CustomMiddleware` class.

### Custom Namespace

By default each route defined in the `routes` method, will have the namespace `AppRootNamespace\Http\Controllers`.
You can override it easily by using `namespace` configuration key:

```php
/**
* @param \Illuminate\Routing\Router $router
* @param $options
*/
public static function routes(Router $router, $options = ['namespace' => 'App\Services',])
{
$router->get('hello-world', 'WorldController@hello');
}
````
8 changes: 0 additions & 8 deletions src/Http/Controllers/RepositoryStoreController.php
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,6 @@ public function handle(RepositoryStoreRequest $request)
*/
$repository = $request->repository();

$repository::authorizeToCreate($request);

$validator = $repository::validatorForStoring($request);

if ($validator->fails()) {
return $this->response()->invalid()->errors($validator->errors()->toArray())->respond();
}

return $request->newRepositoryWith($repository::newModel())->store($request);
}
}
30 changes: 30 additions & 0 deletions src/Repositories/Crudable.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
namespace Binaryk\LaravelRestify\Repositories;

use Binaryk\LaravelRestify\Controllers\RestResponse;
use Binaryk\LaravelRestify\Exceptions\UnauthorizedException;
use Binaryk\LaravelRestify\Http\Requests\RestifyRequest;
use Binaryk\LaravelRestify\Restify;
use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Contracts\Pagination\Paginator;
use Illuminate\Http\JsonResponse;
use Illuminate\Support\Arr;
use Illuminate\Support\Facades\DB;
use Illuminate\Validation\ValidationException;

Expand Down Expand Up @@ -57,6 +60,18 @@ public function show(RestifyRequest $request, $repositoryId)
*/
public function store(RestifyRequest $request)
{
try {
$this->allowToStore($request);
} catch (AuthorizationException | UnauthorizedException $e) {
return $this->response()->setData([
'errors' => Arr::wrap($e->getMessage()),
])->setStatusCode(RestResponse::REST_RESPONSE_FORBIDDEN_CODE);
} catch (ValidationException $e) {
return $this->response()->setData([
'errors' => $e->errors(),
])->setStatusCode(RestResponse::REST_RESPONSE_INVALID_CODE);
}

$model = DB::transaction(function () use ($request) {
$model = self::fillWhenStore(
$request, self::newModel()
Expand Down Expand Up @@ -127,6 +142,21 @@ public function allowToUpdate(RestifyRequest $request)
$validator->validate();
}

/**
* @param RestifyRequest $request
* @return mixed
* @throws \Illuminate\Auth\Access\AuthorizationException
* @throws ValidationException
*/
public function allowToStore(RestifyRequest $request)
{
self::authorizeToCreate($request);

$validator = self::validatorForStoring($request);

$validator->validate();
}

/**
* @param RestifyRequest $request
* @throws \Illuminate\Auth\Access\AuthorizationException
Expand Down
30 changes: 30 additions & 0 deletions src/Repositories/Repository.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
use Illuminate\Contracts\Pagination\LengthAwarePaginator;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Routing\Router;
use Illuminate\Support\Collection;
use Illuminate\Support\Str;

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

return $self->withResource($model);
}

/**
* Handle dynamic static method calls into the method.
*
* @param string $method
* @param array $parameters
* @return mixed
*/
public static function __callStatic($method, $parameters)
{
return (new static)->$method(...$parameters);
}

/**
* Defining custom routes.
*
* The prefix of this route is the uriKey (e.g. 'restify-api/orders'),
* The namespace is Http/Controllers
* Middlewares are the same from config('restify.middleware').
*
* However all options could be customized by passing an $options argument
*
* @param Router $router
* @param $options
*/
public static function routes(Router $router, $options = [])
{
// override for custom routes
}
}
36 changes: 23 additions & 13 deletions src/RestifyServiceProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@

namespace Binaryk\LaravelRestify;

use Illuminate\Support\Arr;
use Illuminate\Support\Facades\Route;
use Illuminate\Support\ServiceProvider;
use ReflectionClass;

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

$this->customDefinitions($config)
$this->customDefinitions()
->defaultRoutes($config);
}

/**
* @param $config
* @return RestifyServiceProvider
*/
public function customDefinitions($config)
public function customDefinitions()
{
collect(Restify::$repositories)->filter(function ($repository) {
return isset($repository::$middleware) || isset($repository::$prefix);
})
->each(function ($repository) use ($config) {
$config['middleware'] = array_merge(config('restify.middleware', []), Arr::wrap($repository::$middleware));
$config['prefix'] = Restify::path($repository::$prefix);
collect(Restify::$repositories)->each(function ($repository) {
$config = [
'namespace' => trim(app()->getNamespace(), '\\').'\Http\Controllers',
'as' => '',
'prefix' => Restify::path($repository::uriKey()),
'middleware' => config('restify.middleware', []),
];

$reflector = new ReflectionClass($repository);

$method = $reflector->getMethod('routes');

Route::group($config, function () {
$this->loadRoutesFrom(__DIR__.'/../routes/api.php');
});
$parameters = $method->getParameters();

if (count($parameters) === 2 && $parameters[1] instanceof \ReflectionParameter) {
$config = array_merge($config, $parameters[1]->getDefaultValue());
}

Route::group($config, function ($router) use ($repository) {
$repository::routes($router);
});
});

return $this;
}
Expand Down
4 changes: 2 additions & 2 deletions src/Traits/AuthorizableModels.php
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ public function authorizedToView(Request $request)
*/
public static function authorizeToCreate(Request $request)
{
throw_unless(static::authorizedToCreate($request), AuthorizationException::class);
throw_unless(static::authorizedToCreate($request), AuthorizationException::class, 'Unauthorized to create.');
}

/**
Expand All @@ -119,7 +119,7 @@ public static function authorizeToCreate(Request $request)
public static function authorizedToCreate(Request $request)
{
if (static::authorizable()) {
return Gate::check('create', get_class(static::newModel()));
return Gate::check('create', static::$model);
}

return true;
Expand Down
19 changes: 18 additions & 1 deletion tests/Controllers/RepositoryStoreControllerTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@

namespace Binaryk\LaravelRestify\Tests\Controllers;

use Binaryk\LaravelRestify\Tests\Fixtures\Post;
use Binaryk\LaravelRestify\Tests\Fixtures\PostPolicy;
use Binaryk\LaravelRestify\Tests\IntegrationTest;
use Illuminate\Support\Facades\Gate;

/**
* @author Eduard Lupacescu <eduard.lupacescu@binarcode.com>
Expand All @@ -12,6 +15,7 @@ class RepositoryStoreControllerTest extends IntegrationTest
protected function setUp(): void
{
parent::setUp();
$this->authenticate();
}

public function test_basic_validation_works()
Expand All @@ -23,12 +27,25 @@ public function test_basic_validation_works()
->assertJson([
'errors' => [
'description' => [
'Description field is required bro.',
'Description field is required',
],
],
]);
}

public function test_unauthorized_store()
{
$_SERVER['restify.user.creatable'] = false;

Gate::policy(Post::class, PostPolicy::class);

$this->withExceptionHandling()->post('/restify-api/posts', [
'title' => 'Title',
'description' => 'Title',
])->assertStatus(403)
->assertJson(['errors' => ['Unauthorized to create.']]);
}

public function test_success_storing()
{
$user = $this->mockUsers()->first();
Expand Down
Loading