Skip to content

Instantly share code, notes, and snippets.

@canvural
Last active December 14, 2021 17:14
Show Gist options
  • Save canvural/0fb5f444b633f8a13fccc5d7e8d0c5d9 to your computer and use it in GitHub Desktop.
Save canvural/0fb5f444b633f8a13fccc5d7e8d0c5d9 to your computer and use it in GitHub Desktop.
Laravel middleware to validate the incoming request against OpenAPI spec
<?php
declare(strict_types=1);
namespace App\Http\Middleware;
use Closure;
use Exception;
use Illuminate\Http\Request;
use Illuminate\Validation\ValidationException;
use League\OpenAPIValidation\PSR7\Exception\ValidationFailed;
use League\OpenAPIValidation\PSR7\OperationAddress;
use League\OpenAPIValidation\PSR7\ValidatorBuilder;
use League\OpenAPIValidation\Schema\Exception\SchemaMismatch;
use Nyholm\Psr7\Factory\Psr17Factory;
use Psr\Http\Message\ServerRequestInterface;
use Symfony\Bridge\PsrHttpMessage\Factory\PsrHttpFactory;
use function base_path;
use function implode;
use function str_replace;
use function strtolower;
use function sprintf;
class ValidateOpenAPISpec
{
/**
* @return mixed
*
* @throws ValidationException
* @throws Exception
*/
public function handle(Request $request, Closure $next)
{
$psr7Request = $this->getPsr7Request($request);
$validator = (new ValidatorBuilder())
->fromYamlFile(base_path('.docs/spec.yaml'))
->getRoutedRequestValidator();
$address = new OperationAddress(sprintf('/%s', $request->route()->uri), strtolower($psr7Request->getMethod()));
try {
$validator->validate($address, $psr7Request);
} catch (ValidationFailed $exception) {
throw $this->convertToValidationException($exception);
}
return $next($request);
}
private function getPsr7Request(Request $request) : ServerRequestInterface
{
$psr17Factory = new Psr17Factory();
return (new PsrHttpFactory($psr17Factory, $psr17Factory, $psr17Factory, $psr17Factory))
->createRequest($request);
}
private function convertToValidationException(ValidationFailed $exception) : ValidationException
{
$ex = $exception->getPrevious();
if ($ex === null) {
return ValidationException::withMessages([$exception->getMessage()]);
}
if ($ex instanceof SchemaMismatch) {
if ($ex->dataBreadCrumb() === null) {
return ValidationException::withMessages([$exception->getMessage()]);
}
$field = implode('.', $ex->dataBreadCrumb()->buildChain());
return ValidationException::withMessages([
$field => str_replace(['Keyword validation failed: '], '', $ex->getMessage()),
]);
}
return ValidationException::withMessages([$ex->getMessage()]);
}
}
@NicolasCharpentier
Copy link

NicolasCharpentier commented Feb 20, 2021

@canvural beware, there is a bug.

League\OpenAPIValidation\PSR7 project expects to have in the OperationAddress the url in this form /user/{id}, not this form /user/12. Request::getUri()::getPath() returns the latter.

I understood that by encoutering a \LogicException wrapping a RequiredParameterMissing and debugging all the way up :)

So instead I don't bother passing the uri, and I just give the request to the library to let it handle the matching.

It goes like :

        $validator = (new ValidatorBuilder())
            ->fromJson($specification)
            //->getRoutedRequestValidator();
            ->getRequestValidator();

[...]

        $validator->validate($psrRequest);

@canvural
Copy link
Author

@NicolasCharpentier you are right! I actually fixed this in my project code but didn't update the gist. I'm using $request->route()->uri and this gives me the correct path 👍

@canvural
Copy link
Author

Updated the gist also!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment