507 lines
16 KiB
PHP
507 lines
16 KiB
PHP
<?php declare(strict_types=1);
|
|
|
|
/**
|
|
* @license Apache 2.0
|
|
*/
|
|
|
|
namespace OpenApi;
|
|
|
|
use OpenApi\Annotations\AbstractAnnotation;
|
|
use OpenApi\Annotations\OpenApi;
|
|
use OpenApi\Annotations\Schema;
|
|
use OpenApi\Processors\AugmentParameters;
|
|
use OpenApi\Processors\AugmentProperties;
|
|
use OpenApi\Processors\AugmentSchemas;
|
|
use OpenApi\Processors\BuildPaths;
|
|
use OpenApi\Processors\CleanUnmerged;
|
|
use OpenApi\Processors\DocBlockDescriptions;
|
|
use OpenApi\Processors\ExpandInterfaces;
|
|
use OpenApi\Processors\ExpandTraits;
|
|
use OpenApi\Processors\InheritProperties;
|
|
use OpenApi\Processors\MergeIntoComponents;
|
|
use OpenApi\Processors\MergeIntoOpenApi;
|
|
use OpenApi\Processors\MergeJsonContent;
|
|
use OpenApi\Processors\MergeXmlContent;
|
|
use OpenApi\Processors\OperationId;
|
|
|
|
/**
|
|
* Result of the analyser.
|
|
*
|
|
* Pretends to be an array of annotations, but also contains detected classes
|
|
* and helper functions for the processors.
|
|
*/
|
|
class Analysis
|
|
{
|
|
/**
|
|
* @var \SplObjectStorage
|
|
*/
|
|
public $annotations;
|
|
|
|
/**
|
|
* Class definitions.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $classes = [];
|
|
|
|
/**
|
|
* Trait definitions.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $traits = [];
|
|
|
|
/**
|
|
* Interface definitions.
|
|
*
|
|
* @var array
|
|
*/
|
|
public $interfaces = [];
|
|
|
|
/**
|
|
* The target OpenApi annotation.
|
|
*
|
|
* @var OpenApi
|
|
*/
|
|
public $openapi;
|
|
|
|
/**
|
|
* @var Context
|
|
*/
|
|
protected $context;
|
|
|
|
/**
|
|
* Registry for the post-processing operations.
|
|
*
|
|
* @var callable[]
|
|
*/
|
|
private static $processors;
|
|
|
|
public function __construct(array $annotations = [], ?Context $context = null)
|
|
{
|
|
$this->annotations = new \SplObjectStorage();
|
|
if (count($annotations) !== 0) {
|
|
if ($context === null) {
|
|
$context = Context::detect(1);
|
|
}
|
|
$this->addAnnotations($annotations, $context);
|
|
}
|
|
$this->context = $context;
|
|
}
|
|
|
|
public function addAnnotation($annotation, ?Context $context): void
|
|
{
|
|
if ($this->annotations->contains($annotation)) {
|
|
return;
|
|
}
|
|
if ($annotation instanceof AbstractAnnotation) {
|
|
$context = $annotation->_context;
|
|
if ($this->openapi === null && $annotation instanceof OpenApi) {
|
|
$this->openapi = $annotation;
|
|
}
|
|
} else {
|
|
if ($context->is('annotations') === false) {
|
|
$context->annotations = [];
|
|
}
|
|
if (in_array($annotation, $context->annotations, true) === false) {
|
|
$context->annotations[] = $annotation;
|
|
}
|
|
}
|
|
$this->annotations->attach($annotation, $context);
|
|
$blacklist = property_exists($annotation, '_blacklist') ? $annotation::$_blacklist : [];
|
|
foreach ($annotation as $property => $value) {
|
|
if (in_array($property, $blacklist)) {
|
|
if ($property === '_unmerged') {
|
|
foreach ($value as $item) {
|
|
$this->addAnnotation($item, $context);
|
|
}
|
|
}
|
|
continue;
|
|
} elseif (is_array($value)) {
|
|
foreach ($value as $item) {
|
|
if ($item instanceof AbstractAnnotation) {
|
|
$this->addAnnotation($item, $context);
|
|
}
|
|
}
|
|
} elseif ($value instanceof AbstractAnnotation) {
|
|
$this->addAnnotation($value, $context);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function addAnnotations(array $annotations, Context $context): void
|
|
{
|
|
foreach ($annotations as $annotation) {
|
|
$this->addAnnotation($annotation, $context);
|
|
}
|
|
}
|
|
|
|
public function addClassDefinition(array $definition): void
|
|
{
|
|
$class = $definition['context']->fullyQualifiedName($definition['class']);
|
|
$this->classes[$class] = $definition;
|
|
}
|
|
|
|
public function addInterfaceDefinition(array $definition): void
|
|
{
|
|
$interface = $definition['context']->fullyQualifiedName($definition['interface']);
|
|
$this->interfaces[$interface] = $definition;
|
|
}
|
|
|
|
public function addTraitDefinition(array $definition): void
|
|
{
|
|
$trait = $definition['context']->fullyQualifiedName($definition['trait']);
|
|
$this->traits[$trait] = $definition;
|
|
}
|
|
|
|
public function addAnalysis(Analysis $analysis): void
|
|
{
|
|
foreach ($analysis->annotations as $annotation) {
|
|
$this->addAnnotation($annotation, $analysis->annotations[$annotation]);
|
|
}
|
|
$this->classes = array_merge($this->classes, $analysis->classes);
|
|
$this->interfaces = array_merge($this->interfaces, $analysis->interfaces);
|
|
$this->traits = array_merge($this->traits, $analysis->traits);
|
|
if ($this->openapi === null && $analysis->openapi !== null) {
|
|
$this->openapi = $analysis->openapi;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get all sub classes of the given parent class.
|
|
*
|
|
* @param string $parent the parent class
|
|
*
|
|
* @return array map of class => definition pairs of sub-classes
|
|
*/
|
|
public function getSubClasses(string $parent): array
|
|
{
|
|
$definitions = [];
|
|
foreach ($this->classes as $class => $classDefinition) {
|
|
if ($classDefinition['extends'] === $parent) {
|
|
$definitions[$class] = $classDefinition;
|
|
$definitions = array_merge($definitions, $this->getSubClasses($class));
|
|
}
|
|
}
|
|
|
|
return $definitions;
|
|
}
|
|
|
|
/**
|
|
* Get a list of all super classes for the given class.
|
|
*
|
|
* @param string $class the class name
|
|
*
|
|
* @return array map of class => definition pairs of parent classes
|
|
*/
|
|
public function getSuperClasses(string $class): array
|
|
{
|
|
$classDefinition = isset($this->classes[$class]) ? $this->classes[$class] : null;
|
|
if (!$classDefinition || empty($classDefinition['extends'])) {
|
|
// unknown class, or no inheritance
|
|
return [];
|
|
}
|
|
|
|
$extends = $classDefinition['extends'];
|
|
$extendsDefinition = isset($this->classes[$extends]) ? $this->classes[$extends] : null;
|
|
if (!$extendsDefinition) {
|
|
return [];
|
|
}
|
|
|
|
return array_merge([$extends => $extendsDefinition], $this->getSuperClasses($extends));
|
|
}
|
|
|
|
/**
|
|
* Get the list of interfaces used by the given class or by classes which it extends.
|
|
*
|
|
* @param string $class the class name
|
|
* @param bool $direct flag to find only the actual class interfaces
|
|
*
|
|
* @return array map of class => definition pairs of interfaces
|
|
*/
|
|
public function getInterfacesOfClass(string $class, bool $direct = false): array
|
|
{
|
|
$classes = $direct ? [] : array_keys($this->getSuperClasses($class));
|
|
// add self
|
|
$classes[] = $class;
|
|
|
|
$definitions = [];
|
|
foreach ($classes as $clazz) {
|
|
if (isset($this->classes[$clazz])) {
|
|
$definition = $this->classes[$clazz];
|
|
if (isset($definition['implements'])) {
|
|
foreach ($definition['implements'] as $interface) {
|
|
if (array_key_exists($interface, $this->interfaces)) {
|
|
$definitions[$interface] = $this->interfaces[$interface];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$direct) {
|
|
// expand recursively for interfaces extending other interfaces
|
|
$collect = function ($interfaces, $cb) use (&$definitions) {
|
|
foreach ($interfaces as $interface) {
|
|
if (isset($this->interfaces[$interface]['extends'])) {
|
|
$cb($this->interfaces[$interface]['extends'], $cb);
|
|
foreach ($this->interfaces[$interface]['extends'] as $fqdn) {
|
|
$definitions[$fqdn] = $this->interfaces[$fqdn];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
$collect(array_keys($definitions), $collect);
|
|
}
|
|
|
|
return $definitions;
|
|
}
|
|
|
|
/**
|
|
* Get the list of traits used by the given class/trait or by classes which it extends.
|
|
*
|
|
* @param string $source the source name
|
|
* @param bool $direct flag to find only the actual class traits
|
|
*
|
|
* @return array map of class => definition pairs of traits
|
|
*/
|
|
public function getTraitsOfClass(string $source, bool $direct = false): array
|
|
{
|
|
$sources = $direct ? [] : array_keys($this->getSuperClasses($source));
|
|
// add self
|
|
$sources[] = $source;
|
|
|
|
$definitions = [];
|
|
foreach ($sources as $sourze) {
|
|
if (isset($this->classes[$sourze]) || isset($this->traits[$sourze])) {
|
|
$definition = isset($this->classes[$sourze]) ? $this->classes[$sourze] : $this->traits[$sourze];
|
|
if (isset($definition['traits'])) {
|
|
foreach ($definition['traits'] as $trait) {
|
|
if (array_key_exists($trait, $this->traits)) {
|
|
$definitions[$trait] = $this->traits[$trait];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!$direct) {
|
|
// expand recursively for traits using other tratis
|
|
$collect = function ($traits, $cb) use (&$definitions) {
|
|
foreach ($traits as $trait) {
|
|
if (isset($this->traits[$trait]['traits'])) {
|
|
$cb($this->traits[$trait]['traits'], $cb);
|
|
foreach ($this->traits[$trait]['traits'] as $fqdn) {
|
|
$definitions[$fqdn] = $this->traits[$fqdn];
|
|
}
|
|
}
|
|
}
|
|
};
|
|
$collect(array_keys($definitions), $collect);
|
|
}
|
|
|
|
return $definitions;
|
|
}
|
|
|
|
/**
|
|
* @param bool $strict in non-strict mode child classes are also detected
|
|
*
|
|
* @return AbstractAnnotation[]
|
|
*/
|
|
public function getAnnotationsOfType(string $class, bool $strict = false): array
|
|
{
|
|
$annotations = [];
|
|
if ($strict) {
|
|
foreach ($this->annotations as $annotation) {
|
|
if (get_class($annotation) === $class) {
|
|
$annotations[] = $annotation;
|
|
}
|
|
}
|
|
} else {
|
|
foreach ($this->annotations as $annotation) {
|
|
if ($annotation instanceof $class) {
|
|
$annotations[] = $annotation;
|
|
}
|
|
}
|
|
}
|
|
|
|
return $annotations;
|
|
}
|
|
|
|
/**
|
|
* @param string $fqdn the source class/interface/trait
|
|
*/
|
|
public function getSchemaForSource(string $fqdn): ?Schema
|
|
{
|
|
$sourceDefinitions = [
|
|
$this->classes,
|
|
$this->interfaces,
|
|
$this->traits,
|
|
];
|
|
|
|
foreach ($sourceDefinitions as $definitions) {
|
|
if (array_key_exists($fqdn, $definitions)) {
|
|
$definition = $definitions[$fqdn];
|
|
if (is_iterable($definition['context']->annotations)) {
|
|
foreach ($definition['context']->annotations as $annotation) {
|
|
if (get_class($annotation) === Schema::class) {
|
|
return $annotation;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param object $annotation
|
|
*
|
|
* @return \OpenApi\Context
|
|
*/
|
|
public function getContext($annotation): Context
|
|
{
|
|
if ($annotation instanceof AbstractAnnotation) {
|
|
return $annotation->_context;
|
|
}
|
|
if ($this->annotations->contains($annotation) === false) {
|
|
throw new \Exception('Annotation not found');
|
|
}
|
|
$context = $this->annotations[$annotation];
|
|
if ($context instanceof Context) {
|
|
return $context;
|
|
}
|
|
// Weird, did you use the addAnnotation/addAnnotations methods?
|
|
throw new \Exception('Annotation has no context');
|
|
}
|
|
|
|
/**
|
|
* Build an analysis with only the annotations that are merged into the OpenAPI annotation.
|
|
*/
|
|
public function merged(): Analysis
|
|
{
|
|
if ($this->openapi === null) {
|
|
throw new \Exception('No openapi target set. Run the MergeIntoOpenApi processor');
|
|
}
|
|
$unmerged = $this->openapi->_unmerged;
|
|
$this->openapi->_unmerged = [];
|
|
$analysis = new Analysis([$this->openapi], $this->context);
|
|
$this->openapi->_unmerged = $unmerged;
|
|
|
|
return $analysis;
|
|
}
|
|
|
|
/**
|
|
* Analysis with only the annotations that not merged.
|
|
*/
|
|
public function unmerged(): Analysis
|
|
{
|
|
return $this->split()->unmerged;
|
|
}
|
|
|
|
/**
|
|
* Split the annotation into two analysis.
|
|
* One with annotations that are merged and one with annotations that are not merged.
|
|
*
|
|
* @return object {merged: Analysis, unmerged: Analysis}
|
|
*/
|
|
public function split()
|
|
{
|
|
$result = new \stdClass();
|
|
$result->merged = $this->merged();
|
|
$result->unmerged = new Analysis([], $this->context);
|
|
foreach ($this->annotations as $annotation) {
|
|
if ($result->merged->annotations->contains($annotation) === false) {
|
|
$result->unmerged->annotations->attach($annotation, $this->annotations[$annotation]);
|
|
}
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Apply the processor(s).
|
|
*
|
|
* @param \Closure|\Closure[] $processors One or more processors
|
|
*/
|
|
public function process($processors = null): void
|
|
{
|
|
if ($processors === null) {
|
|
// Use the default and registered processors.
|
|
$processors = self::processors();
|
|
}
|
|
if (is_array($processors) === false && is_callable($processors)) {
|
|
$processors = [$processors];
|
|
}
|
|
foreach ($processors as $processor) {
|
|
$processor($this);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get direct access to the processors array.
|
|
*
|
|
* @return array reference
|
|
*/
|
|
public static function &processors()
|
|
{
|
|
if (!self::$processors) {
|
|
// Add default processors.
|
|
self::$processors = [
|
|
new DocBlockDescriptions(),
|
|
new MergeIntoOpenApi(),
|
|
new MergeIntoComponents(),
|
|
new ExpandInterfaces(),
|
|
new ExpandTraits(),
|
|
new AugmentSchemas(),
|
|
new AugmentProperties(),
|
|
new BuildPaths(),
|
|
new InheritProperties(),
|
|
new AugmentParameters(),
|
|
new MergeJsonContent(),
|
|
new MergeXmlContent(),
|
|
new OperationId(),
|
|
new CleanUnmerged(),
|
|
];
|
|
}
|
|
|
|
return self::$processors;
|
|
}
|
|
|
|
/**
|
|
* Register a processor.
|
|
*
|
|
* @param \Closure $processor
|
|
*/
|
|
public static function registerProcessor($processor): void
|
|
{
|
|
array_push(self::processors(), $processor);
|
|
}
|
|
|
|
/**
|
|
* Unregister a processor.
|
|
*
|
|
* @param \Closure $processor
|
|
*/
|
|
public static function unregisterProcessor($processor): void
|
|
{
|
|
$processors = &self::processors();
|
|
$key = array_search($processor, $processors, true);
|
|
if ($key === false) {
|
|
throw new \Exception('Given processor was not registered');
|
|
}
|
|
unset($processors[$key]);
|
|
}
|
|
|
|
public function validate(): bool
|
|
{
|
|
if ($this->openapi !== null) {
|
|
return $this->openapi->validate();
|
|
}
|
|
Logger::notice('No openapi target set. Run the MergeIntoOpenApi processor before validate()');
|
|
|
|
return false;
|
|
}
|
|
}
|