vendor/zircote/swagger-php/src/Annotations/AbstractAnnotation.php line 100

Open in your IDE?
  1. <?php declare(strict_types=1);
  2. /**
  3.  * @license Apache 2.0
  4.  */
  5. namespace OpenApi\Annotations;
  6. use OpenApi\Analyser;
  7. use OpenApi\Context;
  8. use OpenApi\Generator;
  9. use OpenApi\Util;
  10. use Symfony\Component\Yaml\Yaml;
  11. /**
  12.  * The openapi annotation base class.
  13.  */
  14. abstract class AbstractAnnotation implements \JsonSerializable
  15. {
  16.     /**
  17.      * While the OpenAPI Specification tries to accommodate most use cases, additional data can be added to extend the specification at certain points.
  18.      * For further details see https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md#specificationExtensions
  19.      * The keys inside the array will be prefixed with `x-`.
  20.      *
  21.      * @var array
  22.      */
  23.     public $x Generator::UNDEFINED;
  24.     /**
  25.      * Arbitrary attachables for this annotation.
  26.      * These will be ignored but can be used for custom processing.
  27.      */
  28.     public $attachables Generator::UNDEFINED;
  29.     /**
  30.      * @var Context
  31.      */
  32.     public $_context;
  33.     /**
  34.      * Annotations that couldn't be merged by mapping or postprocessing.
  35.      *
  36.      * @var array
  37.      */
  38.     public $_unmerged = [];
  39.     /**
  40.      * The properties which are required by [the spec](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.0.0.md).
  41.      *
  42.      * @var array
  43.      */
  44.     public static $_required = [];
  45.     /**
  46.      * Specify the type of the property.
  47.      * Examples:
  48.      *   'name' => 'string' // a string
  49.      *   'required' => 'boolean', // true or false
  50.      *   'tags' => '[string]', // array containing strings
  51.      *   'in' => ["query", "header", "path", "formData", "body"] // must be one on these
  52.      *   'oneOf' => [Schema::class] // array of schema objects.
  53.      *
  54.      * @var array
  55.      */
  56.     public static $_types = [];
  57.     /**
  58.      * Declarative mapping of Annotation types to properties.
  59.      * Examples:
  60.      *   Info::clas => 'info', // Set @OA\Info annotation as the info property.
  61.      *   Parameter::clas => ['parameters'],  // Append @OA\Parameter annotations the parameters array.
  62.      *   PathItem::clas => ['paths', 'path'],  // Append @OA\PathItem annotations the paths array and use path as key.
  63.      *
  64.      * @var array
  65.      */
  66.     public static $_nested = [];
  67.     /**
  68.      * Reverse mapping of $_nested with the allowed parent annotations.
  69.      *
  70.      * @var string[]
  71.      */
  72.     public static $_parents = [];
  73.     /**
  74.      * List of properties are blacklisted from the JSON output.
  75.      *
  76.      * @var array
  77.      */
  78.     public static $_blacklist = ['_context''_unmerged''attachables'];
  79.     public function __construct(array $properties)
  80.     {
  81.         if (isset($properties['_context'])) {
  82.             $this->_context $properties['_context'];
  83.             unset($properties['_context']);
  84.         } elseif (Analyser::$context) {
  85.             $this->_context Analyser::$context;
  86.         } else {
  87.             $this->_context Context::detect(1);
  88.         }
  89.         if ($this->_context->is('annotations') === false) {
  90.             $this->_context->annotations = [];
  91.         }
  92.         $this->_context->annotations[] = $this;
  93.         $nestedContext = new Context(['nested' => $this], $this->_context);
  94.         foreach ($properties as $property => $value) {
  95.             if (property_exists($this$property)) {
  96.                 $this->$property $value;
  97.                 if (is_array($value)) {
  98.                     foreach ($value as $key => $annotation) {
  99.                         if (is_object($annotation) && $annotation instanceof AbstractAnnotation) {
  100.                             $this->$property[$key] = $this->nested($annotation$nestedContext);
  101.                         }
  102.                     }
  103.                 }
  104.             } elseif ($property !== 'value') {
  105.                 $this->$property $value;
  106.             } elseif (is_array($value)) {
  107.                 $annotations = [];
  108.                 foreach ($value as $annotation) {
  109.                     if (is_object($annotation) && $annotation instanceof AbstractAnnotation) {
  110.                         $annotations[] = $annotation;
  111.                     } else {
  112.                         $this->_context->logger->warning('Unexpected field in ' $this->identity() . ' in ' $this->_context);
  113.                     }
  114.                 }
  115.                 $this->merge($annotations);
  116.             } elseif (is_object($value)) {
  117.                 $this->merge([$value]);
  118.             } else {
  119.                 $this->_context->logger->warning('Unexpected parameter in ' $this->identity());
  120.             }
  121.         }
  122.     }
  123.     public function __get($property)
  124.     {
  125.         $properties get_object_vars($this);
  126.         $this->_context->logger->warning('Property "' $property '" doesn\'t exist in a ' $this->identity() . ', existing properties: "' implode('", "'array_keys($properties)) . '" in ' $this->_context);
  127.     }
  128.     public function __set($property$value)
  129.     {
  130.         $fields get_object_vars($this);
  131.         foreach (static::$_blacklist as $_property) {
  132.             unset($fields[$_property]);
  133.         }
  134.         $this->_context->logger->warning('Unexpected field "' $property '" for ' $this->identity() . ', expecting "' implode('", "'array_keys($fields)) . '" in ' $this->_context);
  135.         $this->$property $value;
  136.     }
  137.     /**
  138.      * Merge given annotations to their mapped properties configured in static::$_nested.
  139.      *
  140.      * Annotations that couldn't be merged are added to the _unmerged array.
  141.      *
  142.      * @param AbstractAnnotation[] $annotations
  143.      * @param bool                 $ignore      Ignore unmerged annotations
  144.      *
  145.      * @return AbstractAnnotation[] The unmerged annotations
  146.      */
  147.     public function merge(array $annotationsbool $ignore false): array
  148.     {
  149.         $unmerged = [];
  150.         $nestedContext = new Context(['nested' => $this], $this->_context);
  151.         foreach ($annotations as $annotation) {
  152.             $mapped false;
  153.             if ($details = static::matchNested(get_class($annotation))) {
  154.                 $property $details->value;
  155.                 if (is_array($property)) {
  156.                     $property $property[0];
  157.                     if ($this->$property === Generator::UNDEFINED) {
  158.                         $this->$property = [];
  159.                     }
  160.                     $this->$property[] = $this->nested($annotation$nestedContext);
  161.                     $mapped true;
  162.                 } elseif ($this->$property === Generator::UNDEFINED) {
  163.                     // ignore duplicate nested if only one expected
  164.                     $this->$property $this->nested($annotation$nestedContext);
  165.                     $mapped true;
  166.                 }
  167.             }
  168.             if (!$mapped) {
  169.                 $unmerged[] = $annotation;
  170.             }
  171.         }
  172.         if (!$ignore) {
  173.             foreach ($unmerged as $annotation) {
  174.                 $this->_unmerged[] = $this->nested($annotation$nestedContext);
  175.             }
  176.         }
  177.         return $unmerged;
  178.     }
  179.     /**
  180.      * Merge the properties from the given object into this annotation.
  181.      * Prevents overwriting properties that are already configured.
  182.      *
  183.      * @param object $object
  184.      */
  185.     public function mergeProperties($object): void
  186.     {
  187.         $defaultValues get_class_vars(get_class($this));
  188.         $currentValues get_object_vars($this);
  189.         foreach ($object as $property => $value) {
  190.             if ($property === '_context') {
  191.                 continue;
  192.             }
  193.             if ($currentValues[$property] === $defaultValues[$property]) { // Overwrite default values
  194.                 $this->$property $value;
  195.                 continue;
  196.             }
  197.             if ($property === '_unmerged') {
  198.                 $this->_unmerged array_merge($this->_unmerged$value);
  199.                 continue;
  200.             }
  201.             if ($currentValues[$property] !== $value) { // New value is not the same?
  202.                 if ($defaultValues[$property] === $value) { // but is the same as the default?
  203.                     continue; // Keep current, no notice
  204.                 }
  205.                 $identity method_exists($object'identity') ? $object->identity() : get_class($object);
  206.                 $context1 $this->_context;
  207.                 $context2 property_exists($object'_context') ? $object->_context 'unknown';
  208.                 if (is_object($this->$property) && $this->{$property} instanceof AbstractAnnotation) {
  209.                     $context1 $this->$property->_context;
  210.                 }
  211.                 $this->_context->logger->error('Multiple definitions for ' $identity '->' $property "\n     Using: " $context1 "\n  Skipping: " $context2);
  212.             }
  213.         }
  214.     }
  215.     /**
  216.      * Generate the documentation in YAML format.
  217.      */
  218.     public function toYaml($flags null): string
  219.     {
  220.         if ($flags === null) {
  221.             $flags Yaml::DUMP_OBJECT_AS_MAP Yaml::DUMP_EMPTY_ARRAY_AS_SEQUENCE;
  222.         }
  223.         return Yaml::dump(json_decode($this->toJson(JSON_INVALID_UTF8_IGNORE)), 102$flags);
  224.     }
  225.     /**
  226.      * Generate the documentation in YAML format.
  227.      */
  228.     public function toJson($flags null): string
  229.     {
  230.         if ($flags === null) {
  231.             $flags JSON_PRETTY_PRINT JSON_UNESCAPED_SLASHES JSON_UNESCAPED_UNICODE JSON_INVALID_UTF8_IGNORE;
  232.         }
  233.         return json_encode($this$flags);
  234.     }
  235.     public function __debugInfo()
  236.     {
  237.         $properties = [];
  238.         foreach (get_object_vars($this) as $property => $value) {
  239.             if ($value !== Generator::UNDEFINED) {
  240.                 $properties[$property] = $value;
  241.             }
  242.         }
  243.         return $properties;
  244.     }
  245.     /**
  246.      * Customize the way json_encode() renders the annotations.
  247.      *
  248.      * @return mixed
  249.      */
  250.     #[\ReturnTypeWillChange]
  251.     public function jsonSerialize()
  252.     {
  253.         $data = new \stdClass();
  254.         // Strip undefined values.
  255.         foreach (get_object_vars($this) as $property => $value) {
  256.             if ($value !== Generator::UNDEFINED) {
  257.                 $data->$property $value;
  258.             }
  259.         }
  260.         // Strip properties that are for internal (swagger-php) use.
  261.         foreach (static::$_blacklist as $property) {
  262.             unset($data->$property);
  263.         }
  264.         // Correct empty array to empty objects.
  265.         foreach (static::$_types as $property => $type) {
  266.             if ($type === 'object' && is_array($data->$property) && empty($data->$property)) {
  267.                 $data->$property = new \stdClass();
  268.             }
  269.         }
  270.         // Inject vendor properties.
  271.         unset($data->x);
  272.         if (is_array($this->x)) {
  273.             foreach ($this->as $property => $value) {
  274.                 $prefixed 'x-' $property;
  275.                 $data->$prefixed $value;
  276.             }
  277.         }
  278.         // Map nested keys
  279.         foreach (static::$_nested as $nested) {
  280.             if (is_string($nested) || count($nested) === 1) {
  281.                 continue;
  282.             }
  283.             $property $nested[0];
  284.             if ($this->$property === Generator::UNDEFINED) {
  285.                 continue;
  286.             }
  287.             $keyField $nested[1];
  288.             $object = new \stdClass();
  289.             foreach ($this->$property as $key => $item) {
  290.                 if (is_numeric($key) === false && is_array($item)) {
  291.                     $object->$key $item;
  292.                 } else {
  293.                     $key $item->$keyField;
  294.                     if ($key !== Generator::UNDEFINED && empty($object->$key)) {
  295.                         if ($item instanceof \JsonSerializable) {
  296.                             $object->$key $item->jsonSerialize();
  297.                         } else {
  298.                             $object->$key $item;
  299.                         }
  300.                         unset($object->$key->$keyField);
  301.                     }
  302.                 }
  303.             }
  304.             $data->$property $object;
  305.         }
  306.         // $ref
  307.         if (isset($data->ref)) {
  308.             // OAS 3.0 does not allow $ref to have siblings: http://spec.openapis.org/oas/v3.0.3#fixed-fields-18
  309.             $data = (object) ['$ref' => $data->ref];
  310.         }
  311.         return $data;
  312.     }
  313.     /**
  314.      * Validate annotation tree, and log notices & warnings.
  315.      *
  316.      * @param array $parents the path of annotations above this annotation in the tree
  317.      * @param array $skip    (prevent stack overflow, when traversing an infinite dependency graph)
  318.      */
  319.     public function validate(array $parents = [], array $skip = [], string $ref ''): bool
  320.     {
  321.         if (in_array($this$skiptrue)) {
  322.             return true;
  323.         }
  324.         $valid true;
  325.         // Report orphaned annotations
  326.         foreach ($this->_unmerged as $annotation) {
  327.             if (!is_object($annotation)) {
  328.                 $this->_context->logger->warning('Unexpected type: "' gettype($annotation) . '" in ' $this->identity() . '->_unmerged, expecting a Annotation object');
  329.                 break;
  330.             }
  331.             $class get_class($annotation);
  332.             if ($details = static::matchNested($class)) {
  333.                 $property $details->value;
  334.                 if (is_array($property)) {
  335.                     $this->_context->logger->warning('Only one ' Util::shorten(get_class($annotation)) . '() allowed for ' $this->identity() . ' multiple found, skipped: ' $annotation->_context);
  336.                 } else {
  337.                     $this->_context->logger->warning('Only one ' Util::shorten(get_class($annotation)) . '() allowed for ' $this->identity() . " multiple found in:\n    Using: " $this->$property->_context "\n  Skipped: " $annotation->_context);
  338.                 }
  339.             } elseif ($annotation instanceof AbstractAnnotation) {
  340.                 $message 'Unexpected ' $annotation->identity();
  341.                 if ($class::$_parents) {
  342.                     $message .= ', expected to be inside ' implode(', 'Util::shorten($class::$_parents));
  343.                 }
  344.                 $this->_context->logger->warning($message ' in ' $annotation->_context);
  345.             }
  346.             $valid false;
  347.         }
  348.         // Report conflicting key
  349.         foreach (static::$_nested as $annotationClass => $nested) {
  350.             if (is_string($nested) || count($nested) === 1) {
  351.                 continue;
  352.             }
  353.             $property $nested[0];
  354.             if ($this->$property === Generator::UNDEFINED) {
  355.                 continue;
  356.             }
  357.             $keys = [];
  358.             $keyField $nested[1];
  359.             foreach ($this->$property as $key => $item) {
  360.                 if (is_array($item) && is_numeric($key) === false) {
  361.                     $this->_context->logger->warning($this->identity() . '->' $property ' is an object literal, use nested ' Util::shorten($annotationClass) . '() annotation(s) in ' $this->_context);
  362.                     $keys[$key] = $item;
  363.                 } elseif ($item->$keyField === Generator::UNDEFINED) {
  364.                     $this->_context->logger->error($item->identity() . ' is missing key-field: "' $keyField '" in ' $item->_context);
  365.                 } elseif (isset($keys[$item->$keyField])) {
  366.                     $this->_context->logger->error('Multiple ' $item->_identity([]) . ' with the same ' $keyField '="' $item->$keyField "\":\n  " $item->_context "\n  " $keys[$item->$keyField]->_context);
  367.                 } else {
  368.                     $keys[$item->$keyField] = $item;
  369.                 }
  370.             }
  371.         }
  372.         if (property_exists($this'ref') && $this->ref !== Generator::UNDEFINED && $this->ref !== null) {
  373.             if (substr($this->ref02) === '#/' && count($parents) > && $parents[0] instanceof OpenApi) {
  374.                 // Internal reference
  375.                 try {
  376.                     $parents[0]->ref($this->ref);
  377.                 } catch (\Exception $e) {
  378.                     $this->_context->logger->warning($e->getMessage() . ' for ' $this->identity() . ' in ' $this->_context, ['exception' => $e]);
  379.                 }
  380.             }
  381.         } else {
  382.             // Report missing required fields (when not a $ref)
  383.             foreach (static::$_required as $property) {
  384.                 if ($this->$property === Generator::UNDEFINED) {
  385.                     $message 'Missing required field "' $property '" for ' $this->identity() . ' in ' $this->_context;
  386.                     foreach (static::$_nested as $class => $nested) {
  387.                         $nestedProperty is_array($nested) ? $nested[0] : $nested;
  388.                         if ($property === $nestedProperty) {
  389.                             if ($this instanceof OpenApi) {
  390.                                 $message 'Required ' Util::shorten($class) . '() not found';
  391.                             } elseif (is_array($nested)) {
  392.                                 $message $this->identity() . ' requires at least one ' Util::shorten($class) . '() in ' $this->_context;
  393.                             } else {
  394.                                 $message $this->identity() . ' requires a ' Util::shorten($class) . '() in ' $this->_context;
  395.                             }
  396.                             break;
  397.                         }
  398.                     }
  399.                     $this->_context->logger->warning($message);
  400.                 }
  401.             }
  402.         }
  403.         // Report invalid types
  404.         foreach (static::$_types as $property => $type) {
  405.             $value $this->$property;
  406.             if ($value === Generator::UNDEFINED || $value === null) {
  407.                 continue;
  408.             }
  409.             if (is_string($type)) {
  410.                 if ($this->validateType($type$value) === false) {
  411.                     $valid false;
  412.                     $this->_context->logger->warning($this->identity() . '->' $property ' is a "' gettype($value) . '", expecting a "' $type '" in ' $this->_context);
  413.                 }
  414.             } elseif (is_array($type)) { // enum?
  415.                 if (in_array($value$type) === false) {
  416.                     $this->_context->logger->warning($this->identity() . '->' $property ' "' $value '" is invalid, expecting "' implode('", "'$type) . '" in ' $this->_context);
  417.                 }
  418.             } else {
  419.                 throw new \Exception('Invalid ' get_class($this) . '::$_types[' $property ']');
  420.             }
  421.         }
  422.         $parents[] = $this;
  423.         return self::_validate($this$parents$skip$ref) ? $valid false;
  424.     }
  425.     /**
  426.      * Recursively validate all annotation properties.
  427.      *
  428.      * @param array|object $fields
  429.      * @param array        $parents the path of annotations above this annotation in the tree
  430.      * @param array        $skip    List of objects already validated
  431.      */
  432.     private static function _validate($fields, array $parents, array $skipstring $baseRef): bool
  433.     {
  434.         $valid true;
  435.         $blacklist = [];
  436.         if (is_object($fields)) {
  437.             if (in_array($fields$skiptrue)) {
  438.                 return true;
  439.             }
  440.             $skip[] = $fields;
  441.             $blacklist property_exists($fields'_blacklist') ? $fields::$_blacklist : [];
  442.         }
  443.         foreach ($fields as $field => $value) {
  444.             if ($value === null || is_scalar($value) || in_array($field$blacklist)) {
  445.                 continue;
  446.             }
  447.             $ref $baseRef !== '' $baseRef '/' urlencode((string) $field) : urlencode((string) $field);
  448.             if (is_object($value)) {
  449.                 if (method_exists($value'validate')) {
  450.                     if (!$value->validate($parents$skip$ref)) {
  451.                         $valid false;
  452.                     }
  453.                 } elseif (!self::_validate($value$parents$skip$ref)) {
  454.                     $valid false;
  455.                 }
  456.             } elseif (is_array($value) && !self::_validate($value$parents$skip$ref)) {
  457.                 $valid false;
  458.             }
  459.         }
  460.         return $valid;
  461.     }
  462.     /**
  463.      * Return a identity for easy debugging.
  464.      * Example: "@OA\Get(path="/pets")".
  465.      */
  466.     public function identity(): string
  467.     {
  468.         $class get_class($this);
  469.         $properties = [];
  470.         foreach (static::$_parents as $parent) {
  471.             foreach ($parent::$_nested as $annotationClass => $entry) {
  472.                 if ($annotationClass === $class && is_array($entry) && $this->{$entry[1]} !== Generator::UNDEFINED) {
  473.                     $properties[] = $entry[1];
  474.                     break 2;
  475.                 }
  476.             }
  477.         }
  478.         return $this->_identity($properties);
  479.     }
  480.     /**
  481.      * An annotation is a root if it is the top-level / outermost annotation in a PHP docblock.
  482.      */
  483.     public function isRoot(): bool
  484.     {
  485.         if (!$this->_context) {
  486.             return true;
  487.         }
  488.         $count count($this->_context->annotations);
  489.         return $count && $this->_context->annotations[$count 1] === $this;
  490.     }
  491.     /**
  492.      * Find matching nested details.
  493.      *
  494.      * @param string $class the class to match
  495.      *
  496.      * @return null|object key/value object or `null`
  497.      */
  498.     public static function matchNested(string $class)
  499.     {
  500.         if (array_key_exists($class, static::$_nested)) {
  501.             return (object) ['key' => $class'value' => static::$_nested[$class]];
  502.         }
  503.         $parent $class;
  504.         // only consider the immediate OpenApi parent
  505.         while (!== strpos($parent'OpenApi\\Annotations\\') && $parent get_parent_class($parent)) {
  506.             if ($kvp = static::matchNested($parent)) {
  507.                 return $kvp;
  508.             }
  509.         }
  510.         return null;
  511.     }
  512.     /**
  513.      * Helper for generating the identity().
  514.      */
  515.     protected function _identity(array $properties): string
  516.     {
  517.         $fields = [];
  518.         foreach ($properties as $property) {
  519.             $value $this->$property;
  520.             if ($value !== null && $value !== Generator::UNDEFINED) {
  521.                 $fields[] = $property '=' . (is_string($value) ? '"' $value '"' $value);
  522.             }
  523.         }
  524.         return Util::shorten(get_class($this)) . '(' implode(','$fields) . ')';
  525.     }
  526.     /**
  527.      * Validates the matching of the property value to a annotation type.
  528.      *
  529.      * @param string $type  The annotations property type
  530.      * @param mixed  $value The property value
  531.      */
  532.     private function validateType(string $type$value): bool
  533.     {
  534.         if (substr($type01) === '[' && substr($type, -1) === ']') { // Array of a specified type?
  535.             if ($this->validateType('array'$value) === false) {
  536.                 return false;
  537.             }
  538.             $itemType substr($type1, -1);
  539.             foreach ($value as $i => $item) {
  540.                 if ($this->validateType($itemType$item) === false) {
  541.                     return false;
  542.                 }
  543.             }
  544.             return true;
  545.         }
  546.         if (is_subclass_of($typeAbstractAnnotation::class)) {
  547.             $type 'object';
  548.         }
  549.         return $this->validateDefaultTypes($type$value);
  550.     }
  551.     /**
  552.      * Validates default Open Api types.
  553.      *
  554.      * @param string $type  The property type
  555.      * @param mixed  $value The value to validate
  556.      */
  557.     private function validateDefaultTypes(string $type$value): bool
  558.     {
  559.         switch ($type) {
  560.             case 'string':
  561.                 return is_string($value);
  562.             case 'boolean':
  563.                 return is_bool($value);
  564.             case 'integer':
  565.                 return is_int($value);
  566.             case 'number':
  567.                 return is_numeric($value);
  568.             case 'object':
  569.                 return is_object($value);
  570.             case 'array':
  571.                 return $this->validateArrayType($value);
  572.             case 'scheme':
  573.                 return in_array($value, ['http''https''ws''wss'], true);
  574.             default:
  575.                 throw new \Exception('Invalid type "' $type '"');
  576.         }
  577.     }
  578.     /**
  579.      * Validate array type.
  580.      */
  581.     private function validateArrayType($value): bool
  582.     {
  583.         if (is_array($value) === false) {
  584.             return false;
  585.         }
  586.         $count 0;
  587.         foreach ($value as $i => $item) {
  588.             //not a array, but a hash/map
  589.             if ($count !== $i) {
  590.                 return false;
  591.             }
  592.             $count++;
  593.         }
  594.         return true;
  595.     }
  596.     /**
  597.      * Wrap the context with a reference to the annotation it is nested in.
  598.      *
  599.      * @param AbstractAnnotation $annotation
  600.      *
  601.      * @return AbstractAnnotation
  602.      */
  603.     private function nested($annotationContext $nestedContext)
  604.     {
  605.         if (property_exists($annotation'_context') && $annotation->_context === $this->_context) {
  606.             $annotation->_context $nestedContext;
  607.         }
  608.         return $annotation;
  609.     }
  610. }