NelmioApiDocBundle/OpenApiPhp/ModelRegister.php
Filip Benčo 78664ef9ec
OpenApi 3 Support (#1623)
* Initial pass for OA3 upgrade

* Fix Util Tests

* Fix first batch of Unit Tests. Up to Model

* Another batch of fixed tests

* Update annotations

* Convert Model & Property Describers

* Update tests, Fix RouteDescribers, FIx additional bugs

* Another batch of updates

* Another batch of fixed Functional Tests

* Fix FunctionalTest tests

* Fix Bazinga Tests

* FIx FOS Rest

* Fix JMS TEsts & describers

* Fix all Tests

* Fix few stuff from own CR

* CS Fixes

* CS Fixes 2

* CS Fixes 3

* CS Fixes 4

* Remove collection bug

* Updates after first CRs

* CS

* Drop support for SF3

* Update the docs

* Add an upgrade guide

* misc doc fixes

* Configurable media types

* Code Style Fixes

* Don't use ::$ref for @Response and @RequestBody

* Fix upgrading guide

* Fix OA case

Co-authored-by: Filip Benčo <filip.benco@websupport.sk>
Co-authored-by: Guilhem Niot <guilhem.niot@gmail.com>
Co-authored-by: Mantis Development <mantis@users.noreply.github.com>
2020-05-28 13:19:11 +02:00

172 lines
6.0 KiB
PHP

<?php
/*
* This file is part of the NelmioApiDocBundle package.
*
* (c) Nelmio
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Nelmio\ApiDocBundle\OpenApiPhp;
use Nelmio\ApiDocBundle\Annotation\Model as ModelAnnotation;
use Nelmio\ApiDocBundle\Model\Model;
use Nelmio\ApiDocBundle\Model\ModelRegistry;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use Symfony\Component\PropertyInfo\Type;
/**
* Resolves the path in SwaggerPhp annotation when needed.
*
* @internal
*/
final class ModelRegister
{
/** @var ModelRegistry */
private $modelRegistry;
/** @var string[] */
private $mediaTypes;
public function __construct(ModelRegistry $modelRegistry, array $mediaTypes)
{
$this->modelRegistry = $modelRegistry;
$this->mediaTypes = $mediaTypes;
}
public function __invoke(Analysis $analysis, array $parentGroups = null)
{
foreach ($analysis->annotations as $annotation) {
// @Model using the ref field
if ($annotation instanceof OA\Schema && $annotation->ref instanceof ModelAnnotation) {
$model = $annotation->ref;
$annotation->ref = $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options));
// It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
continue;
}
// Misusage of ::$ref
if (($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) && $annotation->ref instanceof ModelAnnotation) {
throw new \InvalidArgumentException(sprintf('Using @Model inside @%s::$ref is not allowed. You should use ::$ref with @Property, @Parameter, @Schema, @Items but within @Response or @RequestBody you should put @Model directly at the root of the annotation : `@Response(..., @Model(...))`.', get_class($annotation)));
}
// Implicit usages
// We don't use $ref for @Responses, @RequestBody and @Parameter to respect semantics
// We don't replace these objects with the @Model found (we inject it in a subfield) whereas we do for @Schemas
$model = $this->getModel($annotation); // We check whether there is a @Model annotation nested
if (null === $model) {
continue;
}
if ($annotation instanceof OA\Response || $annotation instanceof OA\RequestBody) {
$properties = [
'_context' => Util::createContext(['nested' => $annotation], $annotation->_context),
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
];
foreach ($this->mediaTypes as $mediaType) {
$this->createContentForMediaType($mediaType, $properties, $annotation, $analysis);
}
$this->detach($model, $annotation, $analysis);
continue;
}
if (!$annotation instanceof OA\Parameter) {
throw new \InvalidArgumentException(sprintf("@Model annotation can't be nested with an annotation of type @%s.", get_class($annotation)));
}
if ($annotation->schema instanceof OA\Schema && 'array' === $annotation->schema->type) {
$annotationClass = OA\Items::class;
} else {
$annotationClass = OA\Schema::class;
}
if (!is_string($model->type)) {
// Ignore invalid annotations, they are validated later
continue;
}
$annotation->merge([new $annotationClass([
'ref' => $this->modelRegistry->register(new Model($this->createType($model->type), $this->getGroups($model, $parentGroups), $model->options)),
])]);
// It is no longer an unmerged annotation
$this->detach($model, $annotation, $analysis);
}
}
private function getGroups(ModelAnnotation $model, array $parentGroups = null)
{
if (null === $model->groups) {
return $parentGroups;
}
return array_merge($parentGroups ?? [], $model->groups);
}
private function detach(ModelAnnotation $model, OA\AbstractAnnotation $annotation, Analysis $analysis)
{
foreach ($annotation->_unmerged as $key => $unmerged) {
if ($unmerged === $model) {
unset($annotation->_unmerged[$key]);
break;
}
}
$analysis->annotations->detach($model);
}
private function createType(string $type): Type
{
if ('[]' === substr($type, -2)) {
return new Type(Type::BUILTIN_TYPE_ARRAY, false, null, true, null, $this->createType(substr($type, 0, -2)));
}
return new Type(Type::BUILTIN_TYPE_OBJECT, false, $type);
}
private function getModel(OA\AbstractAnnotation $annotation): ?ModelAnnotation
{
foreach ($annotation->_unmerged as $unmerged) {
if ($unmerged instanceof ModelAnnotation) {
return $unmerged;
}
}
return null;
}
private function createContentForMediaType(
string $type,
array $properties,
OA\AbstractAnnotation $annotation,
Analysis $analysis
) {
switch ($type) {
case 'json':
$modelAnnotation = new OA\JsonContent($properties);
break;
case 'xml':
$modelAnnotation = new OA\XmlContent($properties);
break;
default:
throw new \InvalidArgumentException(sprintf("@Model annotation is not compatible with the media type '%s'. It must be one of 'json' or 'xml'.", $this->mediaType));
}
$annotation->merge([$modelAnnotation]);
$analysis->addAnnotation($modelAnnotation, null);
}
}