Skip to content

Commit

Permalink
Merge pull request #1 from softonic/feature/Extract-classes-to-library
Browse files Browse the repository at this point in the history
Extracted classes to library
  • Loading branch information
xaviapa authored May 3, 2022
2 parents fc93bcb + 9500fb0 commit 23468da
Show file tree
Hide file tree
Showing 11 changed files with 466 additions and 17 deletions.
2 changes: 1 addition & 1 deletion CODEOWNERS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
* @<AUTHOR>
* @xaviapa
88 changes: 78 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,38 +1,106 @@
<PACKAGE-NAME>
REST API nested resources
====================

[![Latest Version](https://img.shields.io/github/release/softonic/<PACKAGE-ID>.svg?style=flat-square)](https://github.com/softonic/<PACKAGE-ID>/releases)
[![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)
[![Software License](https://img.shields.io/badge/license-Apache%202.0-blue.svg?style=flat-square)](LICENSE.md)
[![Build Status](https://github.com/softonic/<PACKAGE-ID>/actions/workflows/build.yml/badge.svg)](https://github.com/softonic/<PACKAGE-ID>/actions/workflows/build.yml)
[![Total Downloads](https://img.shields.io/packagist/dt/softonic/<PACKAGE-ID>.svg?style=flat-square)](https://packagist.org/packages/softonic/<PACKAGE-ID>)
[![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")
[![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")
[![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)
[![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)
[![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")
[![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")

<DESCRIPTION>
Utilities to work with REST APIs with nested resources

Main features
-------------

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

Installation
-------------

You can require the last version of the package using composer
```bash
composer require softonic/<PACKAGE-ID>
composer require softonic/rest-api-nested-resources
```

### Configuration

* MultiKeyModel
```php
class UserCommentModel extends MultiKeyModel
{
/**
* Identifiers to be hashed and used in the real primary and foreign keys.
*/
protected static array $generatedIds = [
'id_user_comment' => [
'id_user',
'id_comment',
],
];
}
```

* EnsureModelExists and EnsureModelDoesNotExist
```php
class UserCommentController extends Controller
{
protected function setMiddlewares(Request $request)
{
$this->middleware(
'App\Http\Middleware\EnsureModelExists:App\Models\User,id_user',
['only' => ['store', 'update']]
);

$this->middleware(
'App\Http\Middleware\EnsureModelDoesNotExist:App\Models\UserComment,id_user,id_comment',
['only' => 'store']
);
}
}
```

* SubstituteBindings
```php
use App\Models\UserComment;

class UserCommentController extends Controller
{
public function show(UserComment $userComment)
{
...
}
}
```

* SplitPutPatchVerbs
```php
use App\Models\UserComment;

class UserCommentController extends Controller
{
use SplitPutPatchVerbs;

public function modify(UserComment $userComment, Request $request)
{
...
}

public function replace(Request $request, string $id_user, string $id_comment)
{
...
}
}
```

Testing
-------

`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/).
`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/).

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

Expand Down
12 changes: 6 additions & 6 deletions composer.json
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "softonic/<PACKAGE-ID>",
"name": "softonic/rest-api-nested-resources\n",
"type": "library",
"description" : "<DESCRIPTION>",
"description" : "Utilities to work with REST APIs with nested resources",
"keywords": [],
"license": "Apache-2.0",
"homepage": "https://github.com/softonic/<PACKAGE-ID>",
"homepage": "https://github.com/softonic/rest-api-nested-resources",
"support": {
"issues": "https://github.com/softonic/<PACKAGE-ID>/issues"
"issues": "https://github.com/softonic/rest-api-nested-resources/issues"
},
"require": {
"php": ">=8.0"
Expand All @@ -20,12 +20,12 @@
},
"autoload": {
"psr-4": {
"Softonic\\<NAMESPACE>\\": "src/"
"Softonic\\RestApiNestedResources\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Softonic\\<NAMESPACE>\\": "tests/"
"Softonic\\RestApiNestedResources\\": "tests/"
}
},
"scripts": {
Expand Down
Empty file removed src/.gitkeep
Empty file.
41 changes: 41 additions & 0 deletions src/Http/Middleware/EnsureModelDoesNotExist.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?php

namespace Softonic\RestApiNestedResources\Http\Middleware;

use Illuminate\Http\Request;
use Illuminate\Support\Arr;
use Softonic\RestApiNestedResources\Http\Traits\PathParameters;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

/**
* Middleware that checks if model does not exist.
*/
class EnsureModelDoesNotExist
{
use PathParameters;

public function handle(Request $request, callable $next, string $modelClass, ...$fieldsToCheck)
{
$parametersToCheck = $this->getParametersToCheck($request, $fieldsToCheck);

$found = (bool)($modelClass)::where($parametersToCheck)
->count();

if ($found) {
throw new ConflictHttpException(
"{$modelClass} resource already exists for " . json_encode($parametersToCheck, JSON_THROW_ON_ERROR)
);
}

return $next($request);
}

private function getParametersToCheck(Request $request, array $fieldsToCheck): array
{
$pathParameters = $this->getPathParameters($request);

$parameters = array_merge($request->all(), $pathParameters);

return Arr::only($parameters, $fieldsToCheck);
}
}
34 changes: 34 additions & 0 deletions src/Http/Middleware/EnsureModelExists.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<?php

namespace Softonic\RestApiNestedResources\Http\Middleware;

use Illuminate\Http\Request;
use Softonic\RestApiNestedResources\Http\Traits\PathParameters;
use Softonic\RestApiNestedResources\PreProcessors\EnsureModelExists as EnsureModelExistsProcessor;
use Symfony\Component\HttpKernel\Exception\ConflictHttpException;

/**
* Middleware that checks if model exists.
*/
class EnsureModelExists
{
use PathParameters;

public function __construct(private EnsureModelExistsProcessor $ensureModelExists)
{
}

/**
* @throws ConflictHttpException
*/
public function handle(Request $request, callable $next, string $modelClass, ...$fieldsToCheck)
{
$pathParameters = $this->getPathParameters($request);

$parameters = array_merge($request->all(), $pathParameters);

$this->ensureModelExists->process($modelClass, $fieldsToCheck, $parameters);

return $next($request);
}
}
114 changes: 114 additions & 0 deletions src/Http/Middleware/SubstituteBindings.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
<?php

namespace Softonic\RestApiNestedResources\Http\Middleware;

use Closure;
use Illuminate\Container\Container;
use Illuminate\Contracts\Routing\Registrar;
use Illuminate\Contracts\Routing\UrlRoutable;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Http\Request;
use Illuminate\Routing\Route;
use Illuminate\Support\Str;
use Softonic\RestApiNestedResources\Models\MultiKeyModel;

class SubstituteBindings
{
/**
* The router instance.
*/
protected Registrar $router;

/**
* The IoC container instance.
*/
protected Container $container;

/**
* Create a new bindings substitutor.
*
* @return void
*/
public function __construct(Registrar $router, Container $container = null)
{
$this->router = $router;
$this->container = $container ?: new Container;
}

/**
* Handle an incoming request.
*
* @return mixed
*/
public function handle(Request $request, Closure $next)
{
$this->router->substituteBindings($route = $request->route());

$this->substituteImplicitBindings($route);

return $next($request);
}

/**
* Substitute the implicit Eloquent model bindings for the route.
*/
protected function substituteImplicitBindings(Route $route): void
{
$this->resolveForRoute($route);
}

/**
* Resolve the implicit route bindings for the given route.
*/
protected function resolveForRoute(Route $route): void
{
$parameters = $route->parameters();

foreach ($route->signatureParameters(UrlRoutable::class) as $parameter) {
if (!$pathParameters = static::getPathParameters($parameter->name, $parameters)) {
continue;
}

if ($pathParameters instanceof UrlRoutable) {
continue;
}

$instance = $this->container->make($parameter->getType()->getName());

try {
$id = ($instance instanceof MultiKeyModel)
? $instance::generateIdForField($instance->getKeyName(), $pathParameters)
: $pathParameters[$instance->getKeyName()];
$model = $instance::findOrFail($id);
} catch (ModelNotFoundException $e) {
throw new ModelNotFoundException(
"{$e->getModel()} resource not found for " . json_encode($pathParameters, JSON_THROW_ON_ERROR)
);
}

foreach (array_keys($parameters) as $parameterName) {
$route->forgetParameter($parameterName);
}
$route->setParameter($parameter->name, $model);
}
}

/**
* Return the path parameters prepending the "id_" string to them.
*/
protected static function getPathParameters(string $name, array $parameters): array
{
$pathParameters = [];
$snakeName = Str::snake($name);

foreach ($parameters as $parameter => $value) {
$pathParameters['id_' . $parameter] = $value;

$snakeName = Str::after($snakeName, $parameter);

$snakeName = Str::after($snakeName, '_');
}

return $pathParameters;
}
}
21 changes: 21 additions & 0 deletions src/Http/Traits/PathParameters.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?php

namespace Softonic\RestApiNestedResources\Http\Traits;

use Illuminate\Http\Request;

trait PathParameters
{
public function getPathParameters(Request $request): array
{
$pathParameters = $request->route()
->parameters();

$pathParametersKeys = array_map(
fn($key) => 'id_' . $key,
array_keys($pathParameters)
);

return array_combine($pathParametersKeys, $pathParameters);
}
}
Loading

0 comments on commit 23468da

Please sign in to comment.