<?php declare(strict_types=1);
/**
* @license Apache 2.0
*/
namespace OpenApi;
use OpenApi\Annotations as OA;
use OpenApi\Processors\ProcessorInterface;
/**
* 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 = [];
/**
* Interface definitions.
*
* @var array
*/
public $interfaces = [];
/**
* Trait definitions.
*
* @var array
*/
public $traits = [];
/**
* Enum definitions.
*
* @var array
*/
public $enums = [];
/**
* The target OpenApi annotation.
*
* @var OA\OpenApi|null
*/
public $openapi = null;
/**
* @var Context|null
*/
public $context = null;
public function __construct(array $annotations = [], Context $context = null)
{
$this->annotations = new \SplObjectStorage();
$this->context = $context;
$this->addAnnotations($annotations, $context);
}
public function addAnnotation(object $annotation, Context $context): void
{
if ($this->annotations->contains($annotation)) {
return;
}
if ($annotation instanceof OA\OpenApi) {
$this->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);
}
}
} elseif (is_array($value)) {
foreach ($value as $item) {
if ($item instanceof OA\AbstractAnnotation) {
$this->addAnnotation($item, $context);
}
}
} elseif ($value instanceof OA\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 addEnumDefinition(array $definition): void
{
$enum = $definition['context']->fullyQualifiedName($definition['enum']);
$this->enums[$enum] = $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);
$this->enums = array_merge($this->enums, $analysis->enums);
if ($this->openapi === null && $analysis->openapi !== null) {
$this->openapi = $analysis->openapi;
}
}
/**
* Get all subclasses 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
* @param bool $direct flag to find only the actual class parents
*
* @return array map of class => definition pairs of parent classes
*/
public function getSuperClasses(string $class, bool $direct = false): array
{
$classDefinition = $this->classes[$class] ?? null;
if (!$classDefinition || empty($classDefinition['extends'])) {
// unknown class, or no inheritance
return [];
}
$extends = $classDefinition['extends'];
$extendsDefinition = $this->classes[$extends] ?? null;
if (!$extendsDefinition) {
return [];
}
$parentDetails = [$extends => $extendsDefinition];
if ($direct) {
return $parentDetails;
}
return array_merge($parentDetails, $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): void {
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 = $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 traits
$collect = function ($traits, $cb) use (&$definitions): void {
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 class-string|array<class-string> $classes one or more class names
* @param bool $strict in non-strict mode child classes are also detected
*
* @return OA\AbstractAnnotation[]
*/
public function getAnnotationsOfType($classes, bool $strict = false): array
{
$unique = new \SplObjectStorage();
$annotations = [];
foreach ((array) $classes as $class) {
/** @var OA\AbstractAnnotation $annotation */
foreach ($this->annotations as $annotation) {
if ($annotation instanceof $class && (!$strict || ($annotation->isRoot($class) && !$unique->contains($annotation)))) {
$unique->attach($annotation);
$annotations[] = $annotation;
}
}
}
return $annotations;
}
/**
* @param string $fqdn the source class/interface/trait
*/
public function getSchemaForSource(string $fqdn): ?OA\Schema
{
$fqdn = '\\' . ltrim($fqdn, '\\');
foreach ([$this->classes, $this->interfaces, $this->traits, $this->enums] as $definitions) {
if (array_key_exists($fqdn, $definitions)) {
$definition = $definitions[$fqdn];
if (is_iterable($definition['context']->annotations)) {
foreach (array_reverse($definition['context']->annotations) as $annotation) {
if ($annotation instanceof OA\Schema && $annotation->isRoot(OA\Schema::class) && !$annotation->_context->is('generated')) {
return $annotation;
}
}
}
}
}
return null;
}
public function getContext(object $annotation): ?Context
{
if ($annotation instanceof OA\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 callable|ProcessorInterface|array<ProcessorInterface|callable> $processors One or more processors
*/
public function process($processors = null): void
{
if (is_array($processors) === false && is_callable($processors) || $processors instanceof ProcessorInterface) {
$processors = [$processors];
}
foreach ($processors as $processor) {
$processor($this);
}
}
public function validate(): bool
{
if ($this->openapi !== null) {
return $this->openapi->validate();
}
$this->context->logger->warning('No openapi target set. Run the MergeIntoOpenApi processor before validate()');
return false;
}
}