Skip to content

Commit 23468da

Browse files
authored
Merge pull request #1 from softonic/feature/Extract-classes-to-library
Extracted classes to library
2 parents fc93bcb + 9500fb0 commit 23468da

File tree

11 files changed

+466
-17
lines changed

11 files changed

+466
-17
lines changed

CODEOWNERS

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
* @<AUTHOR>
1+
* @xaviapa

README.md

Lines changed: 78 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,38 +1,106 @@
1-
<PACKAGE-NAME>
1+
REST API nested resources
22
====================
33

4-
[![Latest Version](https://img.shields.io/github/release/softonic/<PACKAGE-ID>.svg?style=flat-square)](https://github.com/softonic/<PACKAGE-ID>/releases)
4+
[![Latest Version](https://img.shields.io/github/release/softonic/rest-api-nested-resources.svg?style=flat-square)](https://github.com/softonic/rest-api-nested-resources/releases)
55
[![Software License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE.md)
6-
[![Build Status](https://github.com/softonic/<PACKAGE-ID>/actions/workflows/build.yml/badge.svg)](https://github.com/softonic/<PACKAGE-ID>/actions/workflows/build.yml)
7-
[![Total Downloads](https://img.shields.io/packagist/dt/softonic/<PACKAGE-ID>.svg?style=flat-square)](https://packagist.org/packages/softonic/<PACKAGE-ID>)
8-
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/softonic/<PACKAGE-ID>.svg?style=flat-square)](http://isitmaintained.com/project/softonic/<PACKAGE-ID> "Average time to resolve an issue")
9-
[![Percentage of issues still open](http://isitmaintained.com/badge/open/softonic/<PACKAGE-ID>.svg?style=flat-square)](http://isitmaintained.com/project/softonic/<PACKAGE-ID> "Percentage of issues still open")
6+
[![Build Status](https://github.com/softonic/rest-api-nested-resources/actions/workflows/build.yml/badge.svg)](https://github.com/softonic/rest-api-nested-resources/actions/workflows/build.yml)
7+
[![Total Downloads](https://img.shields.io/packagist/dt/softonic/rest-api-nested-resources.svg?style=flat-square)](https://packagist.org/packages/softonic/rest-api-nested-resources)
8+
[![Average time to resolve an issue](http://isitmaintained.com/badge/resolution/softonic/rest-api-nested-resources.svg?style=flat-square)](http://isitmaintained.com/project/softonic/rest-api-nested-resources "Average time to resolve an issue")
9+
[![Percentage of issues still open](http://isitmaintained.com/badge/open/softonic/rest-api-nested-resources.svg?style=flat-square)](http://isitmaintained.com/project/softonic/rest-api-nested-resources "Percentage of issues still open")
1010

11-
<DESCRIPTION>
11+
Utilities to work with REST APIs with nested resources
1212

1313
Main features
1414
-------------
1515

16-
*
16+
* MultiKeyModel: allows to have nested resources with composite primary keys
17+
* EnsureModelExists: middleware to validate that a resource exists (used to ensure that a parent resource exists)
18+
* EnsureModelDoesNotExist: middleware to validate that the resource we want to create doesn't already exist
19+
* SubstituteBindings: a personalization of the Laravel's SubstituteBindings middleware to work with nested resources
20+
* SplitPutPatchVerbs: trait that allows the controller to split the "update" method into "modify" (PATCH) and "replace" (PUT) CRUDL methods
1721

1822
Installation
1923
-------------
2024

2125
You can require the last version of the package using composer
2226
```bash
23-
composer require softonic/<PACKAGE-ID>
27+
composer require softonic/rest-api-nested-resources
2428
```
2529

2630
### Configuration
2731

32+
* MultiKeyModel
2833
```php
34+
class UserCommentModel extends MultiKeyModel
35+
{
36+
/**
37+
* Identifiers to be hashed and used in the real primary and foreign keys.
38+
*/
39+
protected static array $generatedIds = [
40+
'id_user_comment' => [
41+
'id_user',
42+
'id_comment',
43+
],
44+
];
45+
}
46+
```
47+
48+
* EnsureModelExists and EnsureModelDoesNotExist
49+
```php
50+
class UserCommentController extends Controller
51+
{
52+
protected function setMiddlewares(Request $request)
53+
{
54+
$this->middleware(
55+
'App\Http\Middleware\EnsureModelExists:App\Models\User,id_user',
56+
['only' => ['store', 'update']]
57+
);
58+
59+
$this->middleware(
60+
'App\Http\Middleware\EnsureModelDoesNotExist:App\Models\UserComment,id_user,id_comment',
61+
['only' => 'store']
62+
);
63+
}
64+
}
65+
```
2966

67+
* SubstituteBindings
68+
```php
69+
use App\Models\UserComment;
70+
71+
class UserCommentController extends Controller
72+
{
73+
public function show(UserComment $userComment)
74+
{
75+
...
76+
}
77+
}
78+
```
79+
80+
* SplitPutPatchVerbs
81+
```php
82+
use App\Models\UserComment;
83+
84+
class UserCommentController extends Controller
85+
{
86+
use SplitPutPatchVerbs;
87+
88+
public function modify(UserComment $userComment, Request $request)
89+
{
90+
...
91+
}
92+
93+
public function replace(Request $request, string $id_user, string $id_comment)
94+
{
95+
...
96+
}
97+
}
3098
```
3199

32100
Testing
33101
-------
34102

35-
`softonic/<PACKAGE-ID>` has a [PHPUnit](https://phpunit.de) test suite, and a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/).
103+
`softonic/rest-api-nested-resources` has a [PHPUnit](https://phpunit.de) test suite, and a coding style compliance test suite using [PHP CS Fixer](http://cs.sensiolabs.org/).
36104

37105
To run the tests, run the following command from the project folder.
38106

composer.json

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
11
{
2-
"name": "softonic/<PACKAGE-ID>",
2+
"name": "softonic/rest-api-nested-resources\n",
33
"type": "library",
4-
"description" : "<DESCRIPTION>",
4+
"description" : "Utilities to work with REST APIs with nested resources",
55
"keywords": [],
66
"license": "Apache-2.0",
7-
"homepage": "https://github.com/softonic/<PACKAGE-ID>",
7+
"homepage": "https://github.com/softonic/rest-api-nested-resources",
88
"support": {
9-
"issues": "https://github.com/softonic/<PACKAGE-ID>/issues"
9+
"issues": "https://github.com/softonic/rest-api-nested-resources/issues"
1010
},
1111
"require": {
1212
"php": ">=8.0"
@@ -20,12 +20,12 @@
2020
},
2121
"autoload": {
2222
"psr-4": {
23-
"Softonic\\<NAMESPACE>\\": "src/"
23+
"Softonic\\RestApiNestedResources\\": "src/"
2424
}
2525
},
2626
"autoload-dev": {
2727
"psr-4": {
28-
"Softonic\\<NAMESPACE>\\": "tests/"
28+
"Softonic\\RestApiNestedResources\\": "tests/"
2929
}
3030
},
3131
"scripts": {

src/.gitkeep

Whitespace-only changes.
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
<?php
2+
3+
namespace Softonic\RestApiNestedResources\Http\Middleware;
4+
5+
use Illuminate\Http\Request;
6+
use Illuminate\Support\Arr;
7+
use Softonic\RestApiNestedResources\Http\Traits\PathParameters;
8+
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
9+
10+
/**
11+
* Middleware that checks if model does not exist.
12+
*/
13+
class EnsureModelDoesNotExist
14+
{
15+
use PathParameters;
16+
17+
public function handle(Request $request, callable $next, string $modelClass, ...$fieldsToCheck)
18+
{
19+
$parametersToCheck = $this->getParametersToCheck($request, $fieldsToCheck);
20+
21+
$found = (bool)($modelClass)::where($parametersToCheck)
22+
->count();
23+
24+
if ($found) {
25+
throw new ConflictHttpException(
26+
"{$modelClass} resource already exists for " . json_encode($parametersToCheck, JSON_THROW_ON_ERROR)
27+
);
28+
}
29+
30+
return $next($request);
31+
}
32+
33+
private function getParametersToCheck(Request $request, array $fieldsToCheck): array
34+
{
35+
$pathParameters = $this->getPathParameters($request);
36+
37+
$parameters = array_merge($request->all(), $pathParameters);
38+
39+
return Arr::only($parameters, $fieldsToCheck);
40+
}
41+
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
<?php
2+
3+
namespace Softonic\RestApiNestedResources\Http\Middleware;
4+
5+
use Illuminate\Http\Request;
6+
use Softonic\RestApiNestedResources\Http\Traits\PathParameters;
7+
use Softonic\RestApiNestedResources\PreProcessors\EnsureModelExists as EnsureModelExistsProcessor;
8+
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;
9+
10+
/**
11+
* Middleware that checks if model exists.
12+
*/
13+
class EnsureModelExists
14+
{
15+
use PathParameters;
16+
17+
public function __construct(private EnsureModelExistsProcessor $ensureModelExists)
18+
{
19+
}
20+
21+
/**
22+
* @throws ConflictHttpException
23+
*/
24+
public function handle(Request $request, callable $next, string $modelClass, ...$fieldsToCheck)
25+
{
26+
$pathParameters = $this->getPathParameters($request);
27+
28+
$parameters = array_merge($request->all(), $pathParameters);
29+
30+
$this->ensureModelExists->process($modelClass, $fieldsToCheck, $parameters);
31+
32+
return $next($request);
33+
}
34+
}
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
namespace Softonic\RestApiNestedResources\Http\Middleware;
4+
5+
use Closure;
6+
use Illuminate\Container\Container;
7+
use Illuminate\Contracts\Routing\Registrar;
8+
use Illuminate\Contracts\Routing\UrlRoutable;
9+
use Illuminate\Database\Eloquent\ModelNotFoundException;
10+
use Illuminate\Http\Request;
11+
use Illuminate\Routing\Route;
12+
use Illuminate\Support\Str;
13+
use Softonic\RestApiNestedResources\Models\MultiKeyModel;
14+
15+
class SubstituteBindings
16+
{
17+
/**
18+
* The router instance.
19+
*/
20+
protected Registrar $router;
21+
22+
/**
23+
* The IoC container instance.
24+
*/
25+
protected Container $container;
26+
27+
/**
28+
* Create a new bindings substitutor.
29+
*
30+
* @return void
31+
*/
32+
public function __construct(Registrar $router, Container $container = null)
33+
{
34+
$this->router = $router;
35+
$this->container = $container ?: new Container;
36+
}
37+
38+
/**
39+
* Handle an incoming request.
40+
*
41+
* @return mixed
42+
*/
43+
public function handle(Request $request, Closure $next)
44+
{
45+
$this->router->substituteBindings($route = $request->route());
46+
47+
$this->substituteImplicitBindings($route);
48+
49+
return $next($request);
50+
}
51+
52+
/**
53+
* Substitute the implicit Eloquent model bindings for the route.
54+
*/
55+
protected function substituteImplicitBindings(Route $route): void
56+
{
57+
$this->resolveForRoute($route);
58+
}
59+
60+
/**
61+
* Resolve the implicit route bindings for the given route.
62+
*/
63+
protected function resolveForRoute(Route $route): void
64+
{
65+
$parameters = $route->parameters();
66+
67+
foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
68+
if (!$pathParameters = static::getPathParameters($parameter->name, $parameters)) {
69+
continue;
70+
}
71+
72+
if ($pathParameters instanceof UrlRoutable) {
73+
continue;
74+
}
75+
76+
$instance = $this->container->make($parameter->getType()->getName());
77+
78+
try {
79+
$id = ($instance instanceof MultiKeyModel)
80+
? $instance::generateIdForField($instance->getKeyName(), $pathParameters)
81+
: $pathParameters[$instance->getKeyName()];
82+
$model = $instance::findOrFail($id);
83+
} catch (ModelNotFoundException $e) {
84+
throw new ModelNotFoundException(
85+
"{$e->getModel()} resource not found for " . json_encode($pathParameters, JSON_THROW_ON_ERROR)
86+
);
87+
}
88+
89+
foreach (array_keys($parameters) as $parameterName) {
90+
$route->forgetParameter($parameterName);
91+
}
92+
$route->setParameter($parameter->name, $model);
93+
}
94+
}
95+
96+
/**
97+
* Return the path parameters prepending the "id_" string to them.
98+
*/
99+
protected static function getPathParameters(string $name, array $parameters): array
100+
{
101+
$pathParameters = [];
102+
$snakeName = Str::snake($name);
103+
104+
foreach ($parameters as $parameter => $value) {
105+
$pathParameters['id_' . $parameter] = $value;
106+
107+
$snakeName = Str::after($snakeName, $parameter);
108+
109+
$snakeName = Str::after($snakeName, '_');
110+
}
111+
112+
return $pathParameters;
113+
}
114+
}

src/Http/Traits/PathParameters.php

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
<?php
2+
3+
namespace Softonic\RestApiNestedResources\Http\Traits;
4+
5+
use Illuminate\Http\Request;
6+
7+
trait PathParameters
8+
{
9+
public function getPathParameters(Request $request): array
10+
{
11+
$pathParameters = $request->route()
12+
->parameters();
13+
14+
$pathParametersKeys = array_map(
15+
fn($key) => 'id_' . $key,
16+
array_keys($pathParameters)
17+
);
18+
19+
return array_combine($pathParametersKeys, $pathParameters);
20+
}
21+
}

0 commit comments

Comments
 (0)