Last active
January 28, 2022 18:15
-
-
Save zanbaldwin/d6bebe6d8909312d20406c23ae6b3a7c to your computer and use it in GitHub Desktop.
(Extremely) Simple OpenAPI Specification Route Loader for Symfony
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?php declare(strict_types=1); | |
namespace App\Routing\Loader; | |
use League\JsonReference\Dereferencer; | |
use League\JsonReference\Loader\ArrayLoader; | |
use Symfony\Component\Config\FileLocatorInterface; | |
use Symfony\Component\Config\Loader\FileLoader; | |
use Symfony\Component\Config\Resource\FileResource; | |
use Symfony\Component\Routing\Route; | |
use Symfony\Component\Routing\RouteCollection; | |
use Symfony\Component\Yaml\Exception\ParseException; | |
use Symfony\Component\Yaml\Parser as YamlParser; | |
use Symfony\Component\Yaml\Yaml; | |
/** | |
* Simple OpenAPI Specification Route Loader. | |
* Optional requirement: composer require league/json-reference | |
* | |
* Usage: | |
* | |
* name_of_openapi_import: | |
* resource: '../spec/openapi.yaml' | |
* type: 'openapi' | |
* | |
* Equivalent YAML Configuration: | |
* | |
* '<OperationObject operationId>': | |
* path: '<PathsObject FieldPattern>' | |
* controller: '<OperationObject x-controller>' | |
* methods: ['<PathItemObject FieldName>'] | |
* | |
* Tested for OpenAPI Specifications: v3.0 - v3.1 | |
*/ | |
class OpenApiLoader extends FileLoader | |
{ | |
public const RESOURCE_TYPE = 'openapi'; | |
public function __construct( | |
FileLocatorInterface $locator, | |
private ?YamlParser $yamlParser = null, | |
?string $env = null, | |
private bool $dereference = false | |
) { | |
parent::__construct($locator, $env); | |
} | |
public function supports(mixed $resource, string $type = null): bool | |
{ | |
return $type === static::RESOURCE_TYPE | |
&& is_string($resource) | |
&& in_array(pathinfo($resource, \PATHINFO_EXTENSION), ['json', 'yaml', 'yml'], true); | |
} | |
public function load(mixed $resource, string $type = null): RouteCollection | |
{ | |
$file = $this->locator->locate($resource); | |
if (!stream_is_local($file) || !file_exists($file) || !is_readable($file)) { | |
throw new \InvalidArgumentException(sprintf('Resource "%s" is not a local, readable file.', $resource)); | |
} | |
try { | |
$pos = function_exists('mb_strrpos') ? mb_strrpos($file, '.') : strrpos($file, '.'); | |
$fileExtension = $pos !== false ? (function_exists('mb_substr') ? mb_substr($file, $pos + 1) : substr($file, $pos + 1)) : null; | |
$dataStructureObject = match ($fileExtension) { | |
'json' => json_decode(file_get_contents($file), false, 512, \JSON_THROW_ON_ERROR), | |
'yaml', 'yml' => ($this->yamlParser ?? new YamlParser)->parseFile($file, Yaml::PARSE_CONSTANT | Yaml::PARSE_OBJECT_FOR_MAP), | |
default => throw new \InvalidArgumentException(sprintf( | |
'Resource "%s" not supported; could not determine filetype from extension "%s".', | |
$resource, | |
$fileExtension | |
)), | |
}; | |
// Must ensure the result is an object because the dereferencer requires objects rather than associative arrays. | |
if (!is_object($dataStructureObject)) { | |
throw new \InvalidArgumentException( | |
'File contents of resource "%s" invalid; expected object map, found "%s".', | |
$resource, | |
gettype($dataStructureObject) | |
); | |
} | |
} catch (\JsonException|ParseException $e) { | |
throw new \InvalidArgumentException(sprintf('File contents of resource "%s" could not be decoded as JSON/YAML.', $resource), 0, $e); | |
} | |
$collection = new RouteCollection(); | |
$collection->addResource(new FileResource($file)); | |
// Lots of OpenAPI schemas use JSON pointers ($ref), enable dereferencing via the constructor flag. This is | |
// turned off by default because the required information (path, method, operation ID, and controller name) for | |
// this route loader is not typically deep enough to be behind references. | |
// NB: The PHP League's JSON Reference library has been abandoned for over 4 years now, only enable it if you're sure. | |
$dataStructureObject = $this->dereference ? $this->dereferenceJsonPointers($dataStructureObject, $resource) : $dataStructureObject; | |
foreach ($dataStructureObject->paths ?? [] as $path => $pathObject) { | |
foreach ($pathObject ?? [] as $method => $operationObject) { | |
if (!empty($operationObject->operationId) && !empty($operationObject->{'x-controller'})) { | |
$collection->add( | |
$operationObject->operationId, | |
new Route($path, ['_controller' => $operationObject->{'x-controller'}], [], [], null, [], strtoupper($method)) | |
); | |
} | |
} | |
} | |
return $collection; | |
} | |
protected function dereferenceJsonPointers(object $dataStructureObject, ?string $resource = null): object | |
{ | |
try { | |
$dereferencer = new Dereferencer; | |
$dereferencer->getLoaderManager()->registerLoader('string', new ArrayLoader([ | |
'schema' => $dataStructureObject, | |
])); | |
return $dereferencer->dereference('string://schema'); | |
} catch (\Throwable $e) { | |
$message = $resource !== null | |
? 'Error occurred while dereferencing resource "' . $resource . '" of JSON references.' | |
: 'Error occurred while dereferencing resource of JSON references.'; | |
throw new \InvalidArgumentException($message, 0, $e); | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment