welcome back to dyb-tech

This commit is contained in:
Daniel Guzman
2024-05-18 02:28:01 +02:00
parent 9513cdba09
commit 9f30bc98c7
6149 changed files with 668407 additions and 0 deletions
@@ -0,0 +1,96 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
use OpenApi\Processors\Concerns\DocblockTrait;
class AugmentParameters implements ProcessorInterface
{
use DocblockTrait;
protected $augmentOperationParameters;
public function __construct(bool $augmentOperationParameters = true)
{
$this->augmentOperationParameters = $augmentOperationParameters;
}
public function isAugmentOperationParameters(): bool
{
return $this->augmentOperationParameters;
}
/**
* If set to <code>true</code> try to find operation parameter descriptions in the operation docblock.
*
* @param bool $augmentOperationParameters
*/
public function setAugmentOperationParameters(bool $augmentOperationParameters): void
{
$this->augmentOperationParameters = $augmentOperationParameters;
}
public function __invoke(Analysis $analysis)
{
$this->augmentSharedParameters($analysis);
if ($this->augmentOperationParameters) {
$this->augmentOperationParameters($analysis);
}
}
/**
* Use the parameter->name as key field (parameter->parameter) when used as reusable component
* (openapi->components->parameters).
*/
protected function augmentSharedParameters(Analysis $analysis): void
{
if (!Generator::isDefault($analysis->openapi->components) && !Generator::isDefault($analysis->openapi->components->parameters)) {
$keys = [];
$parametersWithoutKey = [];
foreach ($analysis->openapi->components->parameters as $parameter) {
if (!Generator::isDefault($parameter->parameter)) {
$keys[$parameter->parameter] = $parameter;
} else {
$parametersWithoutKey[] = $parameter;
}
}
foreach ($parametersWithoutKey as $parameter) {
if (!Generator::isDefault($parameter->name) && empty($keys[$parameter->name])) {
$parameter->parameter = $parameter->name;
$keys[$parameter->parameter] = $parameter;
}
}
}
}
protected function augmentOperationParameters(Analysis $analysis): void
{
/** @var OA\Operation[] $operations */
$operations = $analysis->getAnnotationsOfType(OA\Operation::class);
foreach ($operations as $operation) {
if (!Generator::isDefault($operation->parameters)) {
$tags = [];
$this->extractContent($operation->_context->comment, $tags);
if (array_key_exists('param', $tags)) {
foreach ($tags['param'] as $name => $details) {
foreach ($operation->parameters as $parameter) {
if ($parameter->name == $name) {
if (Generator::isDefault($parameter->description) && $details['description']) {
$parameter->description = $details['description'];
}
}
}
}
}
}
}
}
}
@@ -0,0 +1,190 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Use the property context to extract useful information and inject that into the annotation.
*/
class AugmentProperties implements ProcessorInterface
{
use Concerns\DocblockTrait;
use Concerns\RefTrait;
use Concerns\TypesTrait;
public function __invoke(Analysis $analysis)
{
$refs = [];
if (!Generator::isDefault($analysis->openapi->components) && !Generator::isDefault($analysis->openapi->components->schemas)) {
foreach ($analysis->openapi->components->schemas as $schema) {
if (!Generator::isDefault($schema->schema)) {
$refKey = $this->toRefKey($schema->_context, $schema->_context->class);
$refs[$refKey] = OA\Components::ref($schema);
}
}
}
/** @var OA\Property[] $properties */
$properties = $analysis->getAnnotationsOfType(OA\Property::class);
foreach ($properties as $property) {
$context = $property->_context;
if (Generator::isDefault($property->property)) {
$property->property = $context->property;
}
if (!Generator::isDefault($property->ref)) {
continue;
}
$typeAndDescription = $this->extractVarTypeAndDescription((string) $context->comment);
if (Generator::isDefault($property->type)) {
$this->augmentType($analysis, $property, $context, $refs, $typeAndDescription['type']);
} else {
if (!is_array($property->type)) {
$this->mapNativeType($property, $property->type);
}
}
if (Generator::isDefault($property->description) && $typeAndDescription['description']) {
$property->description = trim($typeAndDescription['description']);
}
if (Generator::isDefault($property->description) && $this->isRoot($property)) {
$property->description = $this->extractContent($context->comment);
}
if (Generator::isDefault($property->example) && ($example = $this->extractExampleDescription((string) $context->comment))) {
$property->example = $example;
}
if (Generator::isDefault($property->deprecated) && ($deprecated = $this->isDeprecated($context->comment))) {
$property->deprecated = $deprecated;
}
}
}
protected function augmentType(Analysis $analysis, OA\Property $property, Context $context, array $refs, ?string $varType): void
{
// docblock typehints
if ($varType) {
$allTypes = strtolower(trim($varType));
if ($this->isNullable($allTypes) && Generator::isDefault($property->nullable)) {
$property->nullable = true;
}
$allTypes = $this->stripNull($allTypes);
preg_match('/^([^\[]+)(.*$)/', $allTypes, $typeMatches);
$type = $typeMatches[1];
// finalise property type/ref
if (!$this->mapNativeType($property, $type)) {
$refKey = $this->toRefKey($context, $type);
if (Generator::isDefault($property->ref) && array_key_exists($refKey, $refs)) {
$property->ref = $refs[$refKey];
}
}
// ok, so we possibly have a type or ref
if (!Generator::isDefault($property->ref) && $typeMatches[2] === '' && !Generator::isDefault($property->nullable) && $property->nullable) {
$refKey = $this->toRefKey($context, $type);
$property->oneOf = [
$schema = new OA\Schema([
'ref' => $refs[$refKey],
'_context' => new Context(['generated' => true], $property->_context),
]),
];
$analysis->addAnnotation($schema, $schema->_context);
$property->nullable = true;
} elseif ($typeMatches[2] === '[]') {
if (Generator::isDefault($property->items)) {
$property->items = $items = new OA\Items(
[
'type' => $property->type,
'_context' => new Context(['generated' => true], $context),
]
);
$analysis->addAnnotation($items, $items->_context);
if (!Generator::isDefault($property->ref)) {
$property->items->ref = $property->ref;
$property->ref = Generator::UNDEFINED;
}
$property->type = 'array';
}
}
}
// native typehints
if ($context->type && !Generator::isDefault($context->type)) {
if ($context->nullable === true) {
$property->nullable = true;
}
$type = strtolower($context->type);
if (!$this->mapNativeType($property, $type)) {
$refKey = $this->toRefKey($context, $type);
if (Generator::isDefault($property->ref) && array_key_exists($refKey, $refs)) {
$this->applyRef($analysis, $property, $refs[$refKey]);
} else {
if (is_string($context->type) && $typeSchema = $analysis->getSchemaForSource($context->type)) {
if (Generator::isDefault($property->format)) {
$property->ref = OA\Components::ref($typeSchema);
$property->type = Generator::UNDEFINED;
}
}
}
}
}
if (!Generator::isDefault($property->const) && Generator::isDefault($property->type)) {
if (!$this->mapNativeType($property, gettype($property->const))) {
$property->type = Generator::UNDEFINED;
}
}
}
protected function isNullable(string $typeDescription): bool
{
return in_array('null', explode('|', strtolower($typeDescription)));
}
protected function stripNull(string $typeDescription): string
{
if (strpos($typeDescription, '|') === false) {
return $typeDescription;
}
$types = [];
foreach (explode('|', $typeDescription) as $type) {
if (strtolower($type) === 'null') {
continue;
}
$types[] = $type;
}
return implode('|', $types);
}
protected function applyRef(Analysis $analysis, OA\Property $property, string $ref): void
{
if ($property->nullable === true) {
$property->oneOf = [
$schema = new OA\Schema([
'ref' => $ref,
'_context' => new Context(['generated' => true], $property->_context),
]),
];
$analysis->addAnnotation($schema, $schema->_context);
} else {
$property->ref = $ref;
}
}
}
@@ -0,0 +1,100 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
class AugmentRefs implements ProcessorInterface
{
use Concerns\RefTrait;
public function __invoke(Analysis $analysis)
{
$this->resolveAllOfRefs($analysis);
$this->resolveFQCNRefs($analysis);
$this->removeDuplicateRefs($analysis);
}
/**
* Update refs broken due to `allOf` augmenting.
*/
protected function resolveAllOfRefs(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class);
// ref rewriting
$updatedRefs = [];
foreach ($schemas as $schema) {
if (!Generator::isDefault($schema->allOf)) {
// do we have to keep track of properties refs that need updating?
foreach ($schema->allOf as $ii => $allOfSchema) {
if (!Generator::isDefault($allOfSchema->properties)) {
$updatedRefs[OA\Components::ref($schema->schema . '/properties', false)] = OA\Components::ref($schema->schema . '/allOf/' . $ii . '/properties', false);
break;
}
}
}
}
if ($updatedRefs) {
foreach ($analysis->annotations as $annotation) {
if (property_exists($annotation, 'ref') && !Generator::isDefault($annotation->ref) && $annotation->ref !== null) {
foreach ($updatedRefs as $origRef => $updatedRef) {
if (0 === strpos($annotation->ref, $origRef)) {
$annotation->ref = str_replace($origRef, $updatedRef, $annotation->ref);
}
}
}
}
}
}
protected function resolveFQCNRefs(Analysis $analysis): void
{
/** @var OA\AbstractAnnotation[] $annotations */
$annotations = $analysis->getAnnotationsOfType([OA\Examples::class, OA\Header::class, OA\Link::class, OA\Parameter::class, OA\PathItem::class, OA\RequestBody::class, OA\Response::class, OA\Schema::class, OA\SecurityScheme::class]);
foreach ($annotations as $annotation) {
if (property_exists($annotation, 'ref') && !Generator::isDefault($annotation->ref) && is_string($annotation->ref) && !$this->isRef($annotation->ref)) {
// check if we have a schema for this
if ($refSchema = $analysis->getSchemaForSource($annotation->ref)) {
$annotation->ref = OA\Components::ref($refSchema);
}
}
}
}
protected function removeDuplicateRefs(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class);
foreach ($schemas as $schema) {
if (!Generator::isDefault($schema->allOf)) {
$refs = [];
$dupes = [];
foreach ($schema->allOf as $ii => $allOfSchema) {
if (!Generator::isDefault($allOfSchema->ref)) {
if (in_array($allOfSchema->ref, $refs)) {
$dupes[] = $allOfSchema->ref;
$analysis->annotations->detach($allOfSchema);
unset($schema->allOf[$ii]);
continue;
}
$refs[] = $allOfSchema->ref;
}
}
if ($dupes) {
$schema->allOf = array_values($schema->allOf);
}
}
}
}
}
@@ -0,0 +1,146 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Use the Schema context to extract useful information and inject that into the annotation.
*
* Merges properties.
*/
class AugmentSchemas implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class);
$this->augmentSchema($schemas);
$this->mergeUnmergedProperties($analysis);
$this->augmentType($analysis, $schemas);
$this->mergeAllOf($analysis, $schemas);
}
/**
* @param array<OA\Schema> $schemas
*/
protected function augmentSchema(array $schemas): void
{
foreach ($schemas as $schema) {
if (!$schema->isRoot(OA\Schema::class)) {
continue;
}
if (Generator::isDefault($schema->schema)) {
if ($schema->_context->is('class')) {
$schema->schema = $schema->_context->class;
} elseif ($schema->_context->is('interface')) {
$schema->schema = $schema->_context->interface;
} elseif ($schema->_context->is('trait')) {
$schema->schema = $schema->_context->trait;
} elseif ($schema->_context->is('enum')) {
$schema->schema = $schema->_context->enum;
}
}
}
}
/**
* Merge unmerged @OA\Property annotations into the @OA\Schema of the class.
*/
protected function mergeUnmergedProperties(Analysis $analysis): void
{
// Merge unmerged @OA\Property annotations into the @OA\Schema of the class
$unmergedProperties = $analysis->unmerged()->getAnnotationsOfType(OA\Property::class);
foreach ($unmergedProperties as $property) {
if ($property->_context->nested) {
continue;
}
$schemaContext = $property->_context->with('class')
?: $property->_context->with('interface')
?: $property->_context->with('trait')
?: $property->_context->with('enum');
if ($schemaContext->annotations) {
foreach ($schemaContext->annotations as $annotation) {
if ($annotation instanceof OA\Schema) {
if ($annotation->_context->nested) {
// we shouldn't merge property into nested schemas
continue;
}
$annotation->merge([$property], true);
break;
}
}
}
}
}
/**
* Set schema type based on various properties.
*
* @param array<OA\Schema> $schemas
*/
protected function augmentType(Analysis $analysis, array $schemas): void
{
foreach ($schemas as $schema) {
if (Generator::isDefault($schema->type)) {
if (is_array($schema->properties) && count($schema->properties) > 0) {
$schema->type = 'object';
} elseif (is_array($schema->additionalProperties) && count($schema->additionalProperties) > 0) {
$schema->type = 'object';
} elseif (is_array($schema->patternProperties) && count($schema->patternProperties) > 0) {
$schema->type = 'object';
} elseif (is_array($schema->propertyNames) && count($schema->propertyNames) > 0) {
$schema->type = 'object';
}
} else {
if (is_string($schema->type) && $typeSchema = $analysis->getSchemaForSource($schema->type)) {
if (Generator::isDefault($schema->format)) {
$schema->ref = OA\Components::ref($typeSchema);
$schema->type = Generator::UNDEFINED;
}
}
}
}
}
/**
* Merge schema properties into `allOf` if both exist.
*
* @param array<OA\Schema> $schemas
*/
protected function mergeAllOf(Analysis $analysis, array $schemas): void
{
foreach ($schemas as $schema) {
if (!Generator::isDefault($schema->properties) && !Generator::isDefault($schema->allOf)) {
$allOfPropertiesSchema = null;
foreach ($schema->allOf as $allOfSchema) {
if (!Generator::isDefault($allOfSchema->properties)) {
$allOfPropertiesSchema = $allOfSchema;
break;
}
}
if (!$allOfPropertiesSchema) {
$allOfPropertiesSchema = new OA\Schema([
'properties' => [],
'type' => 'object',
'_context' => new Context(['generated' => true], $schema->_context),
]);
$analysis->addAnnotation($allOfPropertiesSchema, $allOfPropertiesSchema->_context);
$schema->allOf[] = $allOfPropertiesSchema;
}
$allOfPropertiesSchema->properties = array_merge($allOfPropertiesSchema->properties, $schema->properties);
$schema->properties = Generator::UNDEFINED;
}
}
}
}
@@ -0,0 +1,60 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Build the openapi->paths using the detected `@OA\PathItem` and `@OA\Operation` (`@OA\Get`, `@OA\Post`, etc).
*/
class BuildPaths implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
$paths = [];
// Merge @OA\PathItems with the same path.
if (!Generator::isDefault($analysis->openapi->paths)) {
foreach ($analysis->openapi->paths as $annotation) {
if (empty($annotation->path)) {
$annotation->_context->logger->warning($annotation->identity() . ' is missing required property "path" in ' . $annotation->_context);
} elseif (isset($paths[$annotation->path])) {
$paths[$annotation->path]->mergeProperties($annotation);
$analysis->annotations->detach($annotation);
} else {
$paths[$annotation->path] = $annotation;
}
}
}
/** @var OA\Operation[] $operations */
$operations = $analysis->unmerged()->getAnnotationsOfType(OA\Operation::class);
// Merge @OA\Operations into existing @OA\PathItems or create a new one.
foreach ($operations as $operation) {
if ($operation->path) {
if (empty($paths[$operation->path])) {
$paths[$operation->path] = $pathItem = new OA\PathItem(
[
'path' => $operation->path,
'_context' => new Context(['generated' => true], $operation->_context),
]
);
$analysis->addAnnotation($pathItem, $pathItem->_context);
}
if ($paths[$operation->path]->merge([$operation])) {
$operation->_context->logger->warning('Unable to merge ' . $operation->identity() . ' in ' . $operation->_context);
}
}
}
if ($paths) {
$analysis->openapi->paths = array_values($paths);
}
}
}
@@ -0,0 +1,35 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
class CleanUnmerged implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
$split = $analysis->split();
$merged = $split->merged->annotations;
$unmerged = $split->unmerged->annotations;
/** @var OA\AbstractAnnotation $annotation */
foreach ($analysis->annotations as $annotation) {
if (property_exists($annotation, '_unmerged')) {
foreach ($annotation->_unmerged as $i => $item) {
if ($merged->contains($item)) {
unset($annotation->_unmerged[$i]); // Property was merged
}
}
}
}
$analysis->openapi->_unmerged = [];
foreach ($unmerged as $annotation) {
$analysis->openapi->_unmerged[] = $annotation;
}
}
}
@@ -0,0 +1,96 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
class CleanUnusedComponents implements ProcessorInterface
{
use Concerns\CollectorTrait;
public function __invoke(Analysis $analysis)
{
if (Generator::isDefault($analysis->openapi->components)) {
return;
}
$analysis->annotations = $this->collect($analysis->annotations);
// allow multiple runs to catch nested dependencies
for ($ii = 0; $ii < 10; ++$ii) {
if (!$this->cleanup($analysis)) {
break;
}
}
}
protected function cleanup(Analysis $analysis): bool
{
$usedRefs = [];
foreach ($analysis->annotations as $annotation) {
if (property_exists($annotation, 'ref') && !Generator::isDefault($annotation->ref) && $annotation->ref !== null) {
$usedRefs[$annotation->ref] = $annotation->ref;
}
foreach (['allOf', 'anyOf', 'oneOf'] as $sub) {
if (property_exists($annotation, $sub) && !Generator::isDefault($annotation->{$sub})) {
foreach ($annotation->{$sub} as $subElem) {
if (is_object($subElem) && property_exists($subElem, 'ref') && !Generator::isDefault($subElem->ref) && $subElem->ref !== null) {
$usedRefs[$subElem->ref] = $subElem->ref;
}
}
}
}
if ($annotation instanceof OA\OpenApi || $annotation instanceof OA\Operation) {
if (!Generator::isDefault($annotation->security)) {
foreach ($annotation->security as $security) {
foreach (array_keys($security) as $securityName) {
$ref = OA\Components::COMPONENTS_PREFIX . 'securitySchemes/' . $securityName;
$usedRefs[$ref] = $ref;
}
}
}
}
}
$unusedRefs = [];
foreach (OA\Components::$_nested as $nested) {
if (2 == count($nested)) {
// $nested[1] is the name of the property that holds the component name
[$componentType, $nameProperty] = $nested;
if (!Generator::isDefault($analysis->openapi->components->{$componentType})) {
foreach ($analysis->openapi->components->{$componentType} as $component) {
$ref = OA\Components::ref($component);
if (!in_array($ref, $usedRefs)) {
$unusedRefs[$ref] = [$ref, $nameProperty];
}
}
}
}
}
// remove unused
foreach ($unusedRefs as $refDetails) {
[$ref, $nameProperty] = $refDetails;
[$hash, $components, $componentType, $name] = explode('/', $ref);
foreach ($analysis->openapi->components->{$componentType} as $ii => $component) {
if ($component->{$nameProperty} == $name) {
$annotation = $analysis->openapi->components->{$componentType}[$ii];
foreach ($this->collect([$annotation]) as $unused) {
$analysis->annotations->detach($unused);
}
unset($analysis->openapi->components->{$componentType}[$ii]);
}
}
}
return 0 != count($unusedRefs);
}
}
@@ -0,0 +1,67 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors\Concerns;
use OpenApi\Annotations as OA;
trait CollectorTrait
{
/**
* Collects a complete list of all nested/referenced annotations.
*/
public function collect(iterable $annotations): \SplObjectStorage
{
$storage = new \SplObjectStorage();
foreach ($annotations as $annotation) {
if ($annotation instanceof OA\AbstractAnnotation) {
$storage->addAll($this->traverse($annotation));
}
}
return $storage;
}
public function traverse(OA\AbstractAnnotation $annotation): \SplObjectStorage
{
$storage = new \SplObjectStorage();
if ($storage->contains($annotation)) {
return $storage;
}
$storage->attach($annotation);
foreach (array_merge($annotation::$_nested, ['allOf', 'anyOf', 'oneOf', 'callbacks']) as $properties) {
foreach ((array) $properties as $property) {
if (isset($annotation->{$property})) {
$storage->addAll($this->traverseNested($annotation->{$property}));
}
}
}
return $storage;
}
/**
* @param string|array|OA\AbstractAnnotation $nested
*/
protected function traverseNested($nested): \SplObjectStorage
{
$storage = new \SplObjectStorage();
if (is_array($nested)) {
foreach ($nested as $value) {
$storage->addAll($this->traverseNested($value));
}
} elseif ($nested instanceof OA\AbstractAnnotation) {
$storage->addAll($this->traverse($nested));
}
return $storage;
}
}
@@ -0,0 +1,210 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors\Concerns;
use OpenApi\Annotations as OA;
use OpenApi\Attributes as OAT;
use OpenApi\Generator;
trait DocblockTrait
{
/**
* An annotation is a root if it is the top-level / outermost annotation in a PHP docblock.
*/
public function isRoot(OA\AbstractAnnotation $annotation): bool
{
if (!$annotation->_context) {
return true;
}
if (1 == count($annotation->_context->annotations)) {
return true;
}
/** @var array<class-string,bool> $matchPriorityMap */
$matchPriorityMap = [
OA\OpenApi::class,
OA\Operation::class => false,
OA\Property::class => false,
OA\Parameter::class => false,
OA\Response::class => false,
OA\Schema::class => true,
OAT\Schema::class => true,
];
// try to find best root match
foreach ($matchPriorityMap as $className => $strict) {
foreach ($annotation->_context->annotations as $contextAnnotation) {
if ($strict) {
if ($className === get_class($contextAnnotation)) {
return $annotation === $contextAnnotation;
}
} else {
if ($contextAnnotation instanceof $className) {
return $annotation === $contextAnnotation;
}
}
}
}
return false;
}
protected function handleTag(string $line, ?array &$tags = null): void
{
if (null === $tags) {
return;
}
// split of tag name
$token = preg_split("@[\s+ ]@u", $line, 2);
if (2 == count($token)) {
$tag = substr($token[0], 1);
$tail = $token[1];
if (!array_key_exists($tag, $tags)) {
$tags[$tag] = [];
}
if (false !== ($dpos = strpos($tail, '$'))) {
$type = trim(substr($tail, 0, $dpos));
$token = preg_split("@[\s+ ]@u", substr($tail, $dpos), 2);
$name = trim(substr($token[0], 1));
$description = 2 == count($token) ? trim($token[1]) : null;
$tags[$tag][$name] = [
'type' => $type,
'description' => $description,
];
}
}
}
/**
* The text contents of the phpdoc comment (excl. tags).
*/
public function extractContent(?string $docblock, ?array &$tags = null): string
{
if (Generator::isDefault($docblock)) {
return Generator::UNDEFINED;
}
$comment = preg_split('/(\n|\r\n)/', (string) $docblock);
$comment[0] = preg_replace('/[ \t]*\\/\*\*/', '', $comment[0]); // strip '/**'
$i = count($comment) - 1;
$comment[$i] = preg_replace('/\*\/[ \t]*$/', '', $comment[$i]); // strip '*/'
$lines = [];
$append = false;
$skip = false;
foreach ($comment as $line) {
$line = ltrim($line, "\t *");
if (substr($line, 0, 1) === '@') {
$this->handleTag($line, $tags);
$skip = true;
}
if ($skip) {
continue;
}
if ($append) {
$i = count($lines) - 1;
$lines[$i] = substr($lines[$i], 0, -1) . $line;
} else {
$lines[] = $line;
}
$append = (substr($line, -1) === '\\');
}
$description = trim(implode("\n", $lines));
if ($description === '') {
return Generator::UNDEFINED;
}
return $description;
}
/**
* A short piece of text, usually one line, providing the basic function of the associated element.
*/
public function extractSummary(?string $docblock): string
{
if (!$content = $this->extractContent($docblock)) {
return Generator::UNDEFINED;
}
$lines = preg_split('/(\n|\r\n)/', $content);
$summary = '';
foreach ($lines as $line) {
$summary .= $line . "\n";
if ($line === '' || substr($line, -1) === '.') {
return trim($summary);
}
}
$summary = trim($summary);
if ($summary === '') {
return Generator::UNDEFINED;
}
return $summary;
}
/**
* An optional longer piece of text providing more details on the associated elements function.
*
* This is very useful when working with a complex element.
*/
public function extractDescription(?string $docblock): string
{
$summary = $this->extractSummary($docblock);
if (!$summary) {
return Generator::UNDEFINED;
}
$description = '';
if (false !== ($substr = substr($this->extractContent($docblock), strlen($summary)))) {
$description = trim($substr);
}
return $description ?: Generator::UNDEFINED;
}
/**
* Extract property type and description from a `@var` dockblock line.
*
* @return array<string, string> extracted `type` and `description`; values default to `null`
*/
public function extractVarTypeAndDescription(?string $docblock): array
{
$comment = str_replace("\r\n", "\n", (string) $docblock);
$comment = preg_replace('/\*\/[ \t]*$/', '', $comment); // strip '*/'
preg_match('/@var\s+(?<type>[^\s]+)([ \t])?(?<description>.+)?$/im', $comment, $matches);
return array_merge(
['type' => null, 'description' => null],
array_filter($matches, function ($key) {
return in_array($key, ['type', 'description']);
}, ARRAY_FILTER_USE_KEY)
);
}
// ------------------------------------------------------------------------
/**
* Extract example text from a `@example` dockblock line.
*/
public function extractExampleDescription(?string $docblock): ?string
{
preg_match('/@example\s+([ \t])?(?<example>.+)?$/im', $docblock, $matches);
return isset($matches['example']) ? $matches['example'] : null;
}
/**
* Returns true if the `\@deprecated` tag is present, false otherwise.
*/
public function isDeprecated(?string $docblock): bool
{
return 1 === preg_match('/@deprecated\s+([ \t])?(?<deprecated>.+)?$/im', (string) $docblock);
}
}
@@ -0,0 +1,65 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors\Concerns;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Steps:
* 1. Determine direct parent / interfaces / traits
* 2. With each:
* - traverse up inheritance tree
* - inherit from first with schema; all other with scheme can be ignored
* - merge from all without schema
* => update all $ref that might reference a property merged.
*/
trait MergePropertiesTrait
{
protected function inheritFrom(Analysis $analysis, OA\Schema $schema, OA\Schema $from, string $refPath, Context $context): void
{
if (Generator::isDefault($schema->allOf)) {
$schema->allOf = [];
}
// merging other properties into allOf is done in the AugmentSchemas processor
$schema->allOf[] = $refSchema = new OA\Schema([
'ref' => OA\Components::ref($refPath),
'_context' => new Context(['generated' => true], $context),
]);
$analysis->addAnnotation($refSchema, $refSchema->_context);
}
protected function mergeProperties(OA\Schema $schema, array $from, array &$existing): void
{
foreach ($from['properties'] as $context) {
if (is_iterable($context->annotations)) {
foreach ($context->annotations as $annotation) {
if ($annotation instanceof OA\Property && !in_array($annotation->_context->property, $existing, true)) {
$existing[] = $annotation->_context->property;
$schema->merge([$annotation], true);
}
}
}
}
}
protected function mergeMethods(OA\Schema $schema, array $from, array &$existing): void
{
foreach ($from['methods'] as $context) {
if (is_iterable($context->annotations)) {
foreach ($context->annotations as $annotation) {
if ($annotation instanceof OA\Property && !in_array($annotation->_context->property, $existing, true)) {
$existing[] = $annotation->_context->property;
$schema->merge([$annotation], true);
}
}
}
}
}
}
@@ -0,0 +1,24 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors\Concerns;
use OpenApi\Context;
trait RefTrait
{
protected function toRefKey(Context $context, ?string $name): string
{
$fqn = strtolower($context->fullyQualifiedName($name));
return ltrim($fqn, '\\');
}
protected function isRef(?string $ref): bool
{
return $ref && 0 === strpos($ref, '#/');
}
}
@@ -0,0 +1,61 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors\Concerns;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
trait TypesTrait
{
protected static $NATIVE_TYPE_MAP = [
'array' => 'array',
'byte' => ['string', 'byte'],
'boolean' => 'boolean',
'bool' => 'boolean',
'int' => 'integer',
'integer' => 'integer',
'long' => ['integer', 'long'],
'float' => ['number', 'float'],
'double' => ['number', 'double'],
'string' => 'string',
'date' => ['string', 'date'],
'datetime' => ['string', 'date-time'],
'\\datetime' => ['string', 'date-time'],
'datetimeimmutable' => ['string', 'date-time'],
'\\datetimeimmutable' => ['string', 'date-time'],
'datetimeinterface' => ['string', 'date-time'],
'\\datetimeinterface' => ['string', 'date-time'],
'number' => 'number',
'object' => 'object',
];
public function mapNativeType(OA\Schema $schema, string $type): bool
{
if (!array_key_exists($type, self::$NATIVE_TYPE_MAP)) {
return false;
}
$type = self::$NATIVE_TYPE_MAP[$type];
if (is_array($type)) {
if (Generator::isDefault($schema->format)) {
$schema->format = $type[1];
}
$type = $type[0];
}
$schema->type = $type;
return true;
}
public function native2spec(string $type): string
{
$mapped = array_key_exists($type, self::$NATIVE_TYPE_MAP) ? self::$NATIVE_TYPE_MAP[$type] : $type;
return is_array($mapped) ? $mapped[0] : $mapped;
}
}
@@ -0,0 +1,94 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
/**
* Checks if the annotation has a summary and/or description property
* and uses the text in the comment block (above the annotations) as summary and/or description.
*
* Use `null`, for example: `@Annotation(description=null)`, if you don't want the annotation to have a description.
*/
class DocBlockDescriptions implements ProcessorInterface
{
use Concerns\DocblockTrait;
public function __invoke(Analysis $analysis)
{
/** @var OA\AbstractAnnotation $annotation */
foreach ($analysis->annotations as $annotation) {
if (property_exists($annotation, '_context') === false) {
// only annotations with context
continue;
}
if (!$this->isRoot($annotation)) {
// only top-level annotations
continue;
}
$hasSummary = property_exists($annotation, 'summary');
$hasDescription = property_exists($annotation, 'description');
if (!$hasSummary && !$hasDescription) {
continue;
}
if ($hasSummary && $hasDescription) {
$this->summaryAndDescription($annotation);
} elseif ($hasDescription) {
$this->description($annotation);
}
}
}
/**
* @param OA\Operation|OA\Property|OA\Parameter|OA\Schema $annotation
*/
protected function description(OA\AbstractAnnotation $annotation): void
{
if (!Generator::isDefault($annotation->description)) {
if ($annotation->description === null) {
$annotation->description = Generator::UNDEFINED;
}
return;
}
$annotation->description = $this->extractContent($annotation->_context->comment);
}
/**
* @param OA\Operation|OA\Property|OA\Parameter|OA\Schema $annotation
*/
protected function summaryAndDescription(OA\AbstractAnnotation $annotation): void
{
$ignoreSummary = !Generator::isDefault($annotation->summary);
$ignoreDescription = !Generator::isDefault($annotation->description);
if ($annotation->summary === null) {
$ignoreSummary = true;
$annotation->summary = Generator::UNDEFINED;
}
if ($annotation->description === null) {
$annotation->description = Generator::UNDEFINED;
$ignoreDescription = true;
}
if ($ignoreSummary && $ignoreDescription) {
return;
}
if ($ignoreSummary) {
$annotation->description = $this->extractContent($annotation->_context->comment);
} elseif ($ignoreDescription) {
$annotation->summary = $this->extractContent($annotation->_context->comment);
} else {
$annotation->summary = $this->extractSummary($annotation->_context->comment);
$annotation->description = $this->extractDescription($annotation->_context->comment);
}
}
}
@@ -0,0 +1,49 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
/**
* Iterate over the chain of ancestors of a schema and:
* - if the ancestor has a schema
* => inherit from the ancestor if it has a schema (allOf) and stop.
* - else
* => merge ancestor properties into the schema.
*/
class ExpandClasses implements ProcessorInterface
{
use Concerns\MergePropertiesTrait;
public function __invoke(Analysis $analysis)
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true);
foreach ($schemas as $schema) {
if ($schema->_context->is('class')) {
$ancestors = $analysis->getSuperClasses($schema->_context->fullyQualifiedName($schema->_context->class));
$existing = [];
foreach ($ancestors as $ancestor) {
$ancestorSchema = $analysis->getSchemaForSource($ancestor['context']->fullyQualifiedName($ancestor['class']));
if ($ancestorSchema) {
$refPath = !Generator::isDefault($ancestorSchema->schema) ? $ancestorSchema->schema : $ancestor['class'];
$this->inheritFrom($analysis, $schema, $ancestorSchema, $refPath, $ancestor['context']);
// one ancestor is enough
break;
} else {
$this->mergeMethods($schema, $ancestor, $existing);
$this->mergeProperties($schema, $ancestor, $existing);
}
}
}
}
}
}
@@ -0,0 +1,112 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
/**
* Expands PHP enums.
*
* Determines `schema`, `enum` and `type`.
*/
class ExpandEnums implements ProcessorInterface
{
use Concerns\TypesTrait;
public function __invoke(Analysis $analysis)
{
if (!class_exists('\\ReflectionEnum')) {
return;
}
$this->expandContextEnum($analysis);
$this->expandSchemaEnum($analysis);
}
protected function expandContextEnum(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true);
foreach ($schemas as $schema) {
if ($schema->_context->is('enum')) {
$re = new \ReflectionEnum($schema->_context->fullyQualifiedName($schema->_context->enum));
$schema->schema = !Generator::isDefault($schema->schema) ? $schema->schema : $re->getShortName();
$schemaType = $schema->type;
$enumType = null;
if ($re->isBacked()) {
$backingType = $re->getBackingType();
if ($backingType instanceof \ReflectionNamedType) {
$enumType = $backingType->getName();
}
}
// no (or invalid) schema type means name
$useName = Generator::isDefault($schemaType) || ($enumType && $this->native2spec($enumType) != $schemaType);
$schema->enum = array_map(function ($case) use ($useName) {
return ($useName || !($case instanceof \ReflectionEnumBackedCase)) ? $case->name : $case->getBackingValue();
}, $re->getCases());
$schema->type = $useName ? 'string' : $enumType;
$this->mapNativeType($schema, $schemaType);
}
}
}
protected function expandSchemaEnum(Analysis $analysis): void
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType([OA\Schema::class, OA\ServerVariable::class]);
foreach ($schemas as $schema) {
if (Generator::isDefault($schema->enum)) {
continue;
}
if (is_string($schema->enum)) {
// might be enum class-string
if (is_a($schema->enum, \UnitEnum::class, true)) {
$cases = $schema->enum::cases();
} else {
throw new \InvalidArgumentException("Unexpected enum value, requires specifying the Enum class string: $schema->enum");
}
} else {
// might be an array of \UnitEnum::class, string, int, etc...
assert(is_array($schema->enum));
$cases = [];
// transform each Enum cases into UnitEnum
foreach ($schema->enum as $enum) {
if (is_string($enum) && function_exists('enum_exists') && enum_exists($enum)) {
foreach ($enum::cases() as $case) {
$cases[] = $case;
}
} else {
$cases[] = $enum;
}
}
}
$enums = [];
foreach ($cases as $enum) {
if (is_a($enum, \UnitEnum::class)) {
$enums[] = $enum->value ?? $enum->name;
} else {
$enums[] = $enum;
}
}
$schema->enum = $enums;
}
}
}
@@ -0,0 +1,55 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
/**
* Look at all (direct) interfaces for a schema and:
* - merge interfaces annotations/methods into the schema if the interface does not have a schema itself
* - inherit from the interface if it has a schema (allOf).
*/
class ExpandInterfaces
{
use Concerns\MergePropertiesTrait;
public function __invoke(Analysis $analysis)
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true);
foreach ($schemas as $schema) {
if ($schema->_context->is('class')) {
$className = $schema->_context->fullyQualifiedName($schema->_context->class);
$interfaces = $analysis->getInterfacesOfClass($className, true);
if (class_exists($className) && ($parent = get_parent_class($className)) && ($inherited = array_keys(class_implements($parent)))) {
// strip interfaces we inherit from ancestor
foreach (array_keys($interfaces) as $interface) {
if (in_array(ltrim($interface, '\\'), $inherited)) {
unset($interfaces[$interface]);
}
}
}
$existing = [];
foreach ($interfaces as $interface) {
$interfaceName = $interface['context']->fullyQualifiedName($interface['interface']);
$interfaceSchema = $analysis->getSchemaForSource($interfaceName);
if ($interfaceSchema) {
$refPath = !Generator::isDefault($interfaceSchema->schema) ? $interfaceSchema->schema : $interface['interface'];
$this->inheritFrom($analysis, $schema, $interfaceSchema, $refPath, $interface['context']);
} else {
$this->mergeMethods($schema, $interface, $existing);
}
}
}
}
}
}
@@ -0,0 +1,80 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
/**
* Look at all (direct) traits for a schema and:
* - merge trait annotations/methods/properties into the schema if the trait does not have a schema itself
* - inherit from the trait if it has a schema (allOf).
*/
class ExpandTraits implements ProcessorInterface
{
use Concerns\MergePropertiesTrait;
public function __invoke(Analysis $analysis)
{
/** @var OA\Schema[] $schemas */
$schemas = $analysis->getAnnotationsOfType(OA\Schema::class, true);
// do regular trait inheritance / merge
foreach ($schemas as $schema) {
if ($schema->_context->is('trait')) {
$traits = $analysis->getTraitsOfClass($schema->_context->fullyQualifiedName($schema->_context->trait), true);
$existing = [];
foreach ($traits as $trait) {
$traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait']));
if ($traitSchema) {
$refPath = !Generator::isDefault($traitSchema->schema) ? $traitSchema->schema : $trait['trait'];
$this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']);
} else {
$this->mergeMethods($schema, $trait, $existing);
$this->mergeProperties($schema, $trait, $existing);
}
}
}
}
foreach ($schemas as $schema) {
if ($schema->_context->is('class') && !$schema->_context->is('generated')) {
// look at class traits
$traits = $analysis->getTraitsOfClass($schema->_context->fullyQualifiedName($schema->_context->class), true);
$existing = [];
foreach ($traits as $trait) {
$traitSchema = $analysis->getSchemaForSource($trait['context']->fullyQualifiedName($trait['trait']));
if ($traitSchema) {
$refPath = !Generator::isDefault($traitSchema->schema) ? $traitSchema->schema : $trait['trait'];
$this->inheritFrom($analysis, $schema, $traitSchema, $refPath, $trait['context']);
} else {
$this->mergeMethods($schema, $trait, $existing);
$this->mergeProperties($schema, $trait, $existing);
}
}
// also merge ancestor traits of non schema parents
$ancestors = $analysis->getSuperClasses($schema->_context->fullyQualifiedName($schema->_context->class));
$existing = [];
foreach ($ancestors as $ancestor) {
$ancestorSchema = $analysis->getSchemaForSource($ancestor['context']->fullyQualifiedName($ancestor['class']));
if ($ancestorSchema) {
// stop here as we inherit everything above
break;
} else {
$traits = $analysis->getTraitsOfClass($schema->_context->fullyQualifiedName($ancestor['class']), true);
foreach ($traits as $trait) {
$this->mergeMethods($schema, $trait, $existing);
$this->mergeProperties($schema, $trait, $existing);
}
}
}
}
}
}
}
@@ -0,0 +1,37 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Merge reusable annotation into @OA\Schemas.
*/
class MergeIntoComponents implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
$components = $analysis->openapi->components;
if (Generator::isDefault($components)) {
$components = new OA\Components(['_context' => new Context(['generated' => true], $analysis->context)]);
}
/** @var OA\AbstractAnnotation $annotation */
foreach ($analysis->annotations as $annotation) {
if ($annotation instanceof OA\AbstractAnnotation
&& in_array(OA\Components::class, $annotation::$_parents)
&& false === $annotation->_context->is('nested')) {
// A top level annotation.
$components->merge([$annotation], true);
$analysis->openapi->components = $components;
}
}
}
}
@@ -0,0 +1,59 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Merge all @OA\OpenApi annotations into one.
*/
class MergeIntoOpenApi implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
// Auto-create the OpenApi annotation.
if (!$analysis->openapi) {
$context = new Context([], $analysis->context);
$analysis->addAnnotation(new OA\OpenApi(['_context' => $context]), $context);
}
$openapi = $analysis->openapi;
$openapi->_analysis = $analysis;
// Merge annotations into the target openapi
$merge = [];
/** @var OA\AbstractAnnotation $annotation */
foreach ($analysis->annotations as $annotation) {
if ($annotation === $openapi) {
continue;
}
if ($annotation instanceof OA\OpenApi) {
$paths = $annotation->paths;
unset($annotation->paths);
$openapi->mergeProperties($annotation);
if (!Generator::isDefault($paths)) {
foreach ($paths as $path) {
if (Generator::isDefault($openapi->paths)) {
$openapi->paths = [];
}
$openapi->paths[] = $path;
}
}
} elseif (
$annotation instanceof OA\AbstractAnnotation
&& in_array(OA\OpenApi::class, $annotation::$_parents)
&& property_exists($annotation, '_context')
&& false === $annotation->_context->is('nested')) {
// A top level annotation.
$merge[] = $annotation;
}
}
$openapi->merge($merge, true);
}
}
@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Split JsonContent into Schema and MediaType.
*/
class MergeJsonContent implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
/** @var OA\JsonContent[] $annotations */
$annotations = $analysis->getAnnotationsOfType(OA\JsonContent::class);
foreach ($annotations as $jsonContent) {
$parent = $jsonContent->_context->nested;
if (!($parent instanceof OA\Response) && !($parent instanceof OA\RequestBody) && !($parent instanceof OA\Parameter)) {
if ($parent) {
$jsonContent->_context->logger->warning('Unexpected ' . $jsonContent->identity() . ' in ' . $parent->identity() . ' in ' . $parent->_context);
} else {
$jsonContent->_context->logger->warning('Unexpected ' . $jsonContent->identity() . ' must be nested');
}
continue;
}
if (Generator::isDefault($parent->content)) {
$parent->content = [];
}
$parent->content['application/json'] = $mediaType = new OA\MediaType([
'schema' => $jsonContent,
'example' => $jsonContent->example,
'examples' => $jsonContent->examples,
'_context' => new Context(['generated' => true], $jsonContent->_context),
]);
$analysis->addAnnotation($mediaType, $mediaType->_context);
if (!$parent instanceof OA\Parameter) {
$parent->content['application/json']->mediaType = 'application/json';
}
$jsonContent->example = Generator::UNDEFINED;
$jsonContent->examples = Generator::UNDEFINED;
$index = array_search($jsonContent, $parent->_unmerged, true);
if ($index !== false) {
array_splice($parent->_unmerged, $index, 1);
}
}
}
}
@@ -0,0 +1,56 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Context;
use OpenApi\Generator;
/**
* Split XmlContent into Schema and MediaType.
*/
class MergeXmlContent implements ProcessorInterface
{
public function __invoke(Analysis $analysis)
{
/** @var OA\XmlContent[] $annotations */
$annotations = $analysis->getAnnotationsOfType(OA\XmlContent::class);
foreach ($annotations as $xmlContent) {
$parent = $xmlContent->_context->nested;
if (!($parent instanceof OA\Response) && !($parent instanceof OA\RequestBody) && !($parent instanceof OA\Parameter)) {
if ($parent) {
$xmlContent->_context->logger->warning('Unexpected ' . $xmlContent->identity() . ' in ' . $parent->identity() . ' in ' . $parent->_context);
} else {
$xmlContent->_context->logger->warning('Unexpected ' . $xmlContent->identity() . ' must be nested');
}
continue;
}
if (Generator::isDefault($parent->content)) {
$parent->content = [];
}
$parent->content['application/xml'] = $mediaType = new OA\MediaType([
'schema' => $xmlContent,
'example' => $xmlContent->example,
'examples' => $xmlContent->examples,
'_context' => new Context(['generated' => true], $xmlContent->_context),
]);
$analysis->addAnnotation($mediaType, $mediaType->_context);
if (!$parent instanceof OA\Parameter) {
$parent->content['application/xml']->mediaType = 'application/xml';
}
$xmlContent->example = Generator::UNDEFINED;
$xmlContent->examples = Generator::UNDEFINED;
$index = array_search($xmlContent, $parent->_unmerged, true);
if ($index !== false) {
array_splice($parent->_unmerged, $index, 1);
}
}
}
}
@@ -0,0 +1,78 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
use OpenApi\Analysis;
use OpenApi\Annotations as OA;
use OpenApi\Generator;
/**
* Generate the OperationId based on the context of the OpenApi annotation.
*/
class OperationId implements ProcessorInterface
{
protected $hash;
public function __construct(bool $hash = true)
{
$this->hash = $hash;
}
public function isHash(): bool
{
return $this->hash;
}
/**
* If set to <code>true</code> generate ids (md5) instead of clear text operation ids.
*
* @param bool $hash
*/
public function setHash(bool $hash): OperationId
{
$this->hash = $hash;
return $this;
}
public function __invoke(Analysis $analysis)
{
$allOperations = $analysis->getAnnotationsOfType(OA\Operation::class);
/** @var OA\Operation $operation */
foreach ($allOperations as $operation) {
if (null === $operation->operationId) {
$operation->operationId = Generator::UNDEFINED;
}
if (!Generator::isDefault($operation->operationId)) {
continue;
}
$context = $operation->_context;
if ($context) {
$source = $context->class ?? $context->interface ?? $context->trait;
$operationId = null;
if ($source) {
$method = $context->method ? ('::' . $context->method) : '';
if ($context->namespace) {
$operationId = $context->namespace . '\\' . $source . $method;
} else {
$operationId = $source . $method;
}
} elseif ($context->method) {
$operationId = $context->method;
}
if ($operationId) {
$operationId = strtoupper($operation->method) . '::' . $operation->path . '::' . $operationId;
$operation->operationId = $this->hash ? md5($operationId) : $operationId;
}
}
}
}
}
@@ -0,0 +1,11 @@
<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi\Processors;
interface ProcessorInterface
{
}