welcome back to dyb-tech
This commit is contained in:
@@ -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 element’s 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
|
||||
{
|
||||
}
|
||||
Reference in New Issue
Block a user