coupon-admin/vendor/zircote/swagger-php/src/Analysis.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;
}
}