Skip to content

Instantly share code, notes, and snippets.

@zanbaldwin
Last active January 28, 2022 18:15
Show Gist options
  • Save zanbaldwin/d6bebe6d8909312d20406c23ae6b3a7c to your computer and use it in GitHub Desktop.
Save zanbaldwin/d6bebe6d8909312d20406c23ae6b3a7c to your computer and use it in GitHub Desktop.
(Extremely) Simple OpenAPI Specification Route Loader for Symfony
<?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