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; } }