vendor/zircote/swagger-php/src/Analysis.php line 423

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /**
  3.  * @license Apache 2.0
  4.  */
  5. namespace OpenApi;
  6. use OpenApi\Annotations as OA;
  7. use OpenApi\Processors\ProcessorInterface;
  8. /**
  9.  * Result of the analyser.
  10.  *
  11.  * Pretends to be an array of annotations, but also contains detected classes
  12.  * and helper functions for the processors.
  13.  */
  14. class Analysis
  15. {
  16.     /**
  17.      * @var \SplObjectStorage
  18.      */
  19.     public $annotations;
  20.     /**
  21.      * Class definitions.
  22.      *
  23.      * @var array
  24.      */
  25.     public $classes = [];
  26.     /**
  27.      * Interface definitions.
  28.      *
  29.      * @var array
  30.      */
  31.     public $interfaces = [];
  32.     /**
  33.      * Trait definitions.
  34.      *
  35.      * @var array
  36.      */
  37.     public $traits = [];
  38.     /**
  39.      * Enum definitions.
  40.      *
  41.      * @var array
  42.      */
  43.     public $enums = [];
  44.     /**
  45.      * The target OpenApi annotation.
  46.      *
  47.      * @var OA\OpenApi|null
  48.      */
  49.     public $openapi null;
  50.     /**
  51.      * @var Context|null
  52.      */
  53.     public $context null;
  54.     public function __construct(array $annotations = [], Context $context null)
  55.     {
  56.         $this->annotations = new \SplObjectStorage();
  57.         $this->context $context;
  58.         $this->addAnnotations($annotations$context);
  59.     }
  60.     public function addAnnotation(object $annotationContext $context): void
  61.     {
  62.         if ($this->annotations->contains($annotation)) {
  63.             return;
  64.         }
  65.         if ($annotation instanceof OA\OpenApi) {
  66.             $this->openapi $this->openapi ?: $annotation;
  67.         } else {
  68.             if ($context->is('annotations') === false) {
  69.                 $context->annotations = [];
  70.             }
  71.             if (in_array($annotation$context->annotationstrue) === false) {
  72.                 $context->annotations[] = $annotation;
  73.             }
  74.         }
  75.         $this->annotations->attach($annotation$context);
  76.         $blacklist property_exists($annotation'_blacklist') ? $annotation::$_blacklist : [];
  77.         foreach ($annotation as $property => $value) {
  78.             if (in_array($property$blacklist)) {
  79.                 if ($property === '_unmerged') {
  80.                     foreach ($value as $item) {
  81.                         $this->addAnnotation($item$context);
  82.                     }
  83.                 }
  84.             } elseif (is_array($value)) {
  85.                 foreach ($value as $item) {
  86.                     if ($item instanceof OA\AbstractAnnotation) {
  87.                         $this->addAnnotation($item$context);
  88.                     }
  89.                 }
  90.             } elseif ($value instanceof OA\AbstractAnnotation) {
  91.                 $this->addAnnotation($value$context);
  92.             }
  93.         }
  94.     }
  95.     public function addAnnotations(array $annotationsContext $context): void
  96.     {
  97.         foreach ($annotations as $annotation) {
  98.             $this->addAnnotation($annotation$context);
  99.         }
  100.     }
  101.     public function addClassDefinition(array $definition): void
  102.     {
  103.         $class $definition['context']->fullyQualifiedName($definition['class']);
  104.         $this->classes[$class] = $definition;
  105.     }
  106.     public function addInterfaceDefinition(array $definition): void
  107.     {
  108.         $interface $definition['context']->fullyQualifiedName($definition['interface']);
  109.         $this->interfaces[$interface] = $definition;
  110.     }
  111.     public function addTraitDefinition(array $definition): void
  112.     {
  113.         $trait $definition['context']->fullyQualifiedName($definition['trait']);
  114.         $this->traits[$trait] = $definition;
  115.     }
  116.     public function addEnumDefinition(array $definition): void
  117.     {
  118.         $enum $definition['context']->fullyQualifiedName($definition['enum']);
  119.         $this->enums[$enum] = $definition;
  120.     }
  121.     public function addAnalysis(Analysis $analysis): void
  122.     {
  123.         foreach ($analysis->annotations as $annotation) {
  124.             $this->addAnnotation($annotation$analysis->annotations[$annotation]);
  125.         }
  126.         $this->classes array_merge($this->classes$analysis->classes);
  127.         $this->interfaces array_merge($this->interfaces$analysis->interfaces);
  128.         $this->traits array_merge($this->traits$analysis->traits);
  129.         $this->enums array_merge($this->enums$analysis->enums);
  130.         if ($this->openapi === null && $analysis->openapi !== null) {
  131.             $this->openapi $analysis->openapi;
  132.         }
  133.     }
  134.     /**
  135.      * Get all subclasses of the given parent class.
  136.      *
  137.      * @param string $parent the parent class
  138.      *
  139.      * @return array map of class => definition pairs of sub-classes
  140.      */
  141.     public function getSubClasses(string $parent): array
  142.     {
  143.         $definitions = [];
  144.         foreach ($this->classes as $class => $classDefinition) {
  145.             if ($classDefinition['extends'] === $parent) {
  146.                 $definitions[$class] = $classDefinition;
  147.                 $definitions array_merge($definitions$this->getSubClasses($class));
  148.             }
  149.         }
  150.         return $definitions;
  151.     }
  152.     /**
  153.      * Get a list of all super classes for the given class.
  154.      *
  155.      * @param string $class  the class name
  156.      * @param bool   $direct flag to find only the actual class parents
  157.      *
  158.      * @return array map of class => definition pairs of parent classes
  159.      */
  160.     public function getSuperClasses(string $classbool $direct false): array
  161.     {
  162.         $classDefinition $this->classes[$class] ?? null;
  163.         if (!$classDefinition || empty($classDefinition['extends'])) {
  164.             // unknown class, or no inheritance
  165.             return [];
  166.         }
  167.         $extends $classDefinition['extends'];
  168.         $extendsDefinition $this->classes[$extends] ?? null;
  169.         if (!$extendsDefinition) {
  170.             return [];
  171.         }
  172.         $parentDetails = [$extends => $extendsDefinition];
  173.         if ($direct) {
  174.             return $parentDetails;
  175.         }
  176.         return array_merge($parentDetails$this->getSuperClasses($extends));
  177.     }
  178.     /**
  179.      * Get the list of interfaces used by the given class or by classes which it extends.
  180.      *
  181.      * @param string $class  the class name
  182.      * @param bool   $direct flag to find only the actual class interfaces
  183.      *
  184.      * @return array map of class => definition pairs of interfaces
  185.      */
  186.     public function getInterfacesOfClass(string $classbool $direct false): array
  187.     {
  188.         $classes $direct ? [] : array_keys($this->getSuperClasses($class));
  189.         // add self
  190.         $classes[] = $class;
  191.         $definitions = [];
  192.         foreach ($classes as $clazz) {
  193.             if (isset($this->classes[$clazz])) {
  194.                 $definition $this->classes[$clazz];
  195.                 if (isset($definition['implements'])) {
  196.                     foreach ($definition['implements'] as $interface) {
  197.                         if (array_key_exists($interface$this->interfaces)) {
  198.                             $definitions[$interface] = $this->interfaces[$interface];
  199.                         }
  200.                     }
  201.                 }
  202.             }
  203.         }
  204.         if (!$direct) {
  205.             // expand recursively for interfaces extending other interfaces
  206.             $collect = function ($interfaces$cb) use (&$definitions): void {
  207.                 foreach ($interfaces as $interface) {
  208.                     if (isset($this->interfaces[$interface]['extends'])) {
  209.                         $cb($this->interfaces[$interface]['extends'], $cb);
  210.                         foreach ($this->interfaces[$interface]['extends'] as $fqdn) {
  211.                             $definitions[$fqdn] = $this->interfaces[$fqdn];
  212.                         }
  213.                     }
  214.                 }
  215.             };
  216.             $collect(array_keys($definitions), $collect);
  217.         }
  218.         return $definitions;
  219.     }
  220.     /**
  221.      * Get the list of traits used by the given class/trait or by classes which it extends.
  222.      *
  223.      * @param string $source the source name
  224.      * @param bool   $direct flag to find only the actual class traits
  225.      *
  226.      * @return array map of class => definition pairs of traits
  227.      */
  228.     public function getTraitsOfClass(string $sourcebool $direct false): array
  229.     {
  230.         $sources $direct ? [] : array_keys($this->getSuperClasses($source));
  231.         // add self
  232.         $sources[] = $source;
  233.         $definitions = [];
  234.         foreach ($sources as $sourze) {
  235.             if (isset($this->classes[$sourze]) || isset($this->traits[$sourze])) {
  236.                 $definition $this->classes[$sourze] ?? $this->traits[$sourze];
  237.                 if (isset($definition['traits'])) {
  238.                     foreach ($definition['traits'] as $trait) {
  239.                         if (array_key_exists($trait$this->traits)) {
  240.                             $definitions[$trait] = $this->traits[$trait];
  241.                         }
  242.                     }
  243.                 }
  244.             }
  245.         }
  246.         if (!$direct) {
  247.             // expand recursively for traits using other traits
  248.             $collect = function ($traits$cb) use (&$definitions): void {
  249.                 foreach ($traits as $trait) {
  250.                     if (isset($this->traits[$trait]['traits'])) {
  251.                         $cb($this->traits[$trait]['traits'], $cb);
  252.                         foreach ($this->traits[$trait]['traits'] as $fqdn) {
  253.                             $definitions[$fqdn] = $this->traits[$fqdn];
  254.                         }
  255.                     }
  256.                 }
  257.             };
  258.             $collect(array_keys($definitions), $collect);
  259.         }
  260.         return $definitions;
  261.     }
  262.     /**
  263.      * @param class-string|array<class-string> $classes one or more class names
  264.      * @param bool                             $strict  in non-strict mode child classes are also detected
  265.      *
  266.      * @return OA\AbstractAnnotation[]
  267.      */
  268.     public function getAnnotationsOfType($classesbool $strict false): array
  269.     {
  270.         $unique = new \SplObjectStorage();
  271.         $annotations = [];
  272.         foreach ((array) $classes as $class) {
  273.             /** @var OA\AbstractAnnotation $annotation */
  274.             foreach ($this->annotations as $annotation) {
  275.                 if ($annotation instanceof $class && (!$strict || ($annotation->isRoot($class) && !$unique->contains($annotation)))) {
  276.                     $unique->attach($annotation);
  277.                     $annotations[] = $annotation;
  278.                 }
  279.             }
  280.         }
  281.         return $annotations;
  282.     }
  283.     /**
  284.      * @param string $fqdn the source class/interface/trait
  285.      */
  286.     public function getSchemaForSource(string $fqdn): ?OA\Schema
  287.     {
  288.         $fqdn '\\' ltrim($fqdn'\\');
  289.         foreach ([$this->classes$this->interfaces$this->traits$this->enums] as $definitions) {
  290.             if (array_key_exists($fqdn$definitions)) {
  291.                 $definition $definitions[$fqdn];
  292.                 if (is_iterable($definition['context']->annotations)) {
  293.                     foreach (array_reverse($definition['context']->annotations) as $annotation) {
  294.                         if ($annotation instanceof OA\Schema && $annotation->isRoot(OA\Schema::class) && !$annotation->_context->is('generated')) {
  295.                             return $annotation;
  296.                         }
  297.                     }
  298.                 }
  299.             }
  300.         }
  301.         return null;
  302.     }
  303.     public function getContext(object $annotation): ?Context
  304.     {
  305.         if ($annotation instanceof OA\AbstractAnnotation) {
  306.             return $annotation->_context;
  307.         }
  308.         if ($this->annotations->contains($annotation) === false) {
  309.             throw new \Exception('Annotation not found');
  310.         }
  311.         $context $this->annotations[$annotation];
  312.         if ($context instanceof Context) {
  313.             return $context;
  314.         }
  315.         // Weird, did you use the addAnnotation/addAnnotations methods?
  316.         throw new \Exception('Annotation has no context');
  317.     }
  318.     /**
  319.      * Build an analysis with only the annotations that are merged into the OpenAPI annotation.
  320.      */
  321.     public function merged(): Analysis
  322.     {
  323.         if ($this->openapi === null) {
  324.             throw new \Exception('No openapi target set. Run the MergeIntoOpenApi processor');
  325.         }
  326.         $unmerged $this->openapi->_unmerged;
  327.         $this->openapi->_unmerged = [];
  328.         $analysis = new Analysis([$this->openapi], $this->context);
  329.         $this->openapi->_unmerged $unmerged;
  330.         return $analysis;
  331.     }
  332.     /**
  333.      * Analysis with only the annotations that not merged.
  334.      */
  335.     public function unmerged(): Analysis
  336.     {
  337.         return $this->split()->unmerged;
  338.     }
  339.     /**
  340.      * Split the annotation into two analysis.
  341.      * One with annotations that are merged and one with annotations that are not merged.
  342.      *
  343.      * @return object {merged: Analysis, unmerged: Analysis}
  344.      */
  345.     public function split()
  346.     {
  347.         $result = new \stdClass();
  348.         $result->merged $this->merged();
  349.         $result->unmerged = new Analysis([], $this->context);
  350.         foreach ($this->annotations as $annotation) {
  351.             if ($result->merged->annotations->contains($annotation) === false) {
  352.                 $result->unmerged->annotations->attach($annotation$this->annotations[$annotation]);
  353.             }
  354.         }
  355.         return $result;
  356.     }
  357.     /**
  358.      * Apply the processor(s).
  359.      *
  360.      * @param callable|ProcessorInterface|array<ProcessorInterface|callable> $processors One or more processors
  361.      */
  362.     public function process($processors null): void
  363.     {
  364.         if (is_array($processors) === false && is_callable($processors) || $processors instanceof ProcessorInterface) {
  365.             $processors = [$processors];
  366.         }
  367.         foreach ($processors as $processor) {
  368.             $processor($this);
  369.         }
  370.     }
  371.     public function validate(): bool
  372.     {
  373.         if ($this->openapi !== null) {
  374.             return $this->openapi->validate();
  375.         }
  376.         $this->context->logger->warning('No openapi target set. Run the MergeIntoOpenApi processor before validate()');
  377.         return false;
  378.     }
  379. }