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

Open in your IDE?
  1. <?php
  2. /**
  3.  * @license Apache 2.0
  4.  */
  5. namespace Swagger\Annotations;
  6. use Exception;
  7. use JsonSerializable;
  8. use stdClass;
  9. use Swagger\Analyser;
  10. use Swagger\Context;
  11. use Swagger\Logger;
  12. /**
  13.  * The swagger annotation base class.
  14.  */
  15. abstract class AbstractAnnotation implements JsonSerializable
  16. {
  17.     /**
  18.      * Allows extensions to the Swagger Schema.
  19.      * The keys inside the array will be prefixed with `x-`.
  20.      * For further details see https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md#vendorExtensions.
  21.      * @var array
  22.      */
  23.     public $x;
  24.     /**
  25.      * @var Context
  26.      */
  27.     public $_context;
  28.     /**
  29.      * Annotations that couldn't be merged by mapping or postprocessing.
  30.      * @var array
  31.      */
  32.     public $_unmerged = [];
  33.     /**
  34.      * The properties which are required by [the spec](https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md)
  35.      * @var array
  36.      */
  37.     public static $_required = [];
  38.     /**
  39.      * Specify the type of the property.
  40.      * Examples:
  41.      *   'name' => 'string' // a string
  42.      *   'required' => 'boolean', // true or false
  43.      *   'tags' => '[string]', // array containing strings
  44.      *   'in' => ["query", "header", "path", "formData", "body"] // must be one on these
  45.      * @var array
  46.      */
  47.     public static $_types = [];
  48.     /**
  49.      * Declarative mapping of Annotation types to properties.
  50.      * Examples:
  51.      *   'Swagger\Annotation\Info' => 'info', // Set @SWG\Info annotation as the info property.
  52.      *   'Swagger\Annotation\Parameter' => ['parameters'],  // Append @SWG\Parameter annotations the parameters array.
  53.      *   'Swagger\Annotation\Path' => ['paths', 'path'],  // Append @SWG\Path annotations the paths array and use path as key.
  54.      * @var array
  55.      */
  56.     public static $_nested = [];
  57.     /**
  58.      * Reverse mapping of $_nested with the allowed parent annotations.
  59.      * @var string[]
  60.      */
  61.     public static $_parents = [];
  62.     /**
  63.      * List of properties are blacklisted from the JSON output.
  64.      * @var array
  65.      */
  66.     public static $_blacklist = ['_context''_unmerged'];
  67.     /**
  68.      * @param array $properties
  69.      */
  70.     public function __construct($properties)
  71.     {
  72.         if (isset($properties['_context'])) {
  73.             $this->_context $properties['_context'];
  74.             unset($properties['_context']);
  75.         } elseif (Analyser::$context) {
  76.             $this->_context Analyser::$context;
  77.         } else {
  78.             $this->_context Context::detect(1);
  79.         }
  80.         if ($this->_context->is('annotations') === false) {
  81.             $this->_context->annotations = [];
  82.         }
  83.         $this->_context->annotations[] = $this;
  84.         $nestedContext = new Context(['nested' => $this], $this->_context);
  85.         foreach ($properties as $property => $value) {
  86.             if (property_exists($this$property)) {
  87.                 $this->$property $value;
  88.                 if (is_array($value)) {
  89.                     foreach ($value as $key => $annotation) {
  90.                         if (is_object($annotation) && $annotation instanceof AbstractAnnotation) {
  91.                             $this->{$property}[$key] = $this->nested($annotation$nestedContext);
  92.                         }
  93.                     }
  94.                 }
  95.             } elseif ($property !== 'value') {
  96.                 $this->$property $value;
  97.             } elseif (is_array($value)) {
  98.                 $annotations = [];
  99.                 foreach ($value as $annotation) {
  100.                     if (is_object($annotation) && $annotation instanceof AbstractAnnotation) {
  101.                         $annotations[] = $annotation;
  102.                     } else {
  103.                         Logger::notice('Unexpected field in ' $this->identity() . ' in ' $this->_context);
  104.                     }
  105.                 }
  106.                 $this->merge($annotations);
  107.             } elseif (is_object($value)) {
  108.                 $this->merge([$value]);
  109.             } else {
  110.                 Logger::notice('Unexpected parameter in ' $this->identity());
  111.             }
  112.         }
  113.     }
  114.     public function __get($property)
  115.     {
  116.         $properties get_object_vars($this);
  117.         Logger::notice('Property "' $property '" doesn\'t exist in a ' $this->identity() . ', existing properties: "' implode('", "'array_keys($properties)) . '" in ' $this->_context);
  118.     }
  119.     public function __set($property$value)
  120.     {
  121.         $fields get_object_vars($this);
  122.         foreach (static::$_blacklist as $_property) {
  123.             unset($fields[$_property]);
  124.         }
  125.         Logger::notice('Unexpected field "' $property '" for ' $this->identity() . ', expecting "' implode('", "'array_keys($fields)) . '" in ' $this->_context);
  126.         $this->$property $value;
  127.     }
  128.     /**
  129.      * Merge given annotations to their mapped properties configured in static::$_nested.
  130.      * Annotations that couldn't be merged are added to the _unmerged array.
  131.      *
  132.      * @param AbstractAnnotation[] $annotations
  133.      * @param bool $ignore Ignore unmerged annotations
  134.      * @return AbstractAnnotation[] The unmerged annotations
  135.      */
  136.     public function merge($annotations$ignore false)
  137.     {
  138.         $unmerged = [];
  139.         $nestedContext = new Context(['nested' => $this], $this->_context);
  140.         foreach ($annotations as $annotation) {
  141.             $found false;
  142.             foreach (static::$_nested as $class => $property) {
  143.                 if ($annotation instanceof $class) {
  144.                     if (is_array($property)) { // Append to an array?
  145.                         $property $property[0];
  146.                         if ($this->$property === null) {
  147.                             $this->$property = [];
  148.                         }
  149.                         array_push($this->$property$this->nested($annotation$nestedContext));
  150.                         $found true;
  151.                     } elseif ($this->$property === null) {
  152.                         $this->$property $this->nested($annotation$nestedContext);
  153.                         $found true;
  154.                     }
  155.                     break;
  156.                 }
  157.             }
  158.             if ($found === false) {
  159.                 $unmerged[] = $annotation;
  160.             }
  161.         }
  162.         if (!$ignore) {
  163.             foreach ($unmerged as $annotation) {
  164.                 $this->_unmerged[] = $this->nested($annotation$nestedContext);
  165.             }
  166.         }
  167.         return $unmerged;
  168.     }
  169.     /**
  170.      * Merge the properties from the given object into this annotation.
  171.      * Prevents overwriting properties that are already configured.
  172.      *
  173.      * @param object $object
  174.      */
  175.     public function mergeProperties($object)
  176.     {
  177.         $defaultValues get_class_vars(get_class($this));
  178.         $currentValues get_object_vars($this);
  179.         foreach ($object as $property => $value) {
  180.             if ($property === '_context') {
  181.                 continue;
  182.             }
  183.             if ($currentValues[$property] === $defaultValues[$property]) { // Overwrite default values
  184.                 $this->$property $value;
  185.                 continue;
  186.             }
  187.             if ($property === '_unmerged') {
  188.                 $this->_unmerged array_merge($this->_unmerged$value);
  189.                 continue;
  190.             }
  191.             if ($currentValues[$property] !== $value) { // New value is not the same?
  192.                 if ($defaultValues[$property] === $value) { // but is the same as the default?
  193.                     continue; // Keep current, no notice
  194.                 }
  195.                 $identity method_exists($object'identity') ? $object->identity() : get_class($object);
  196.                 $context1 $this->_context;
  197.                 $context2 property_exists($object'_context') ? $object->_context 'unknown';
  198.                 if (is_object($this->$property) && $this->$property instanceof AbstractAnnotation) {
  199.                     $context1 $this->$property->_context;
  200.                 }
  201.                 Logger::warning('Multiple definitions for ' $identity '->' $property "\n     Using: " $context1 "\n  Skipping: " $context2);
  202.             }
  203.         }
  204.     }
  205.     public function __toString()
  206.     {
  207.         return json_encode($thisJSON_PRETTY_PRINT JSON_UNESCAPED_SLASHES);
  208.     }
  209.     public function __debugInfo()
  210.     {
  211.         $properties = [];
  212.         foreach (get_object_vars($this) as $property => $value) {
  213.             if ($value !== UNDEFINED) {
  214.                 $properties[$property] = $value;
  215.             }
  216.         }
  217.         return $properties;
  218.     }
  219.     /**
  220.      * Customize the way json_encode() renders the annotations.
  221.      * @return mixed
  222.      */
  223.     #[\ReturnTypeWillChange]
  224.     public function jsonSerialize()
  225.     {
  226.         $data = new stdClass();
  227.         // Strip undefined and null values.
  228.         $classVars get_class_vars(get_class($this));
  229.         foreach (get_object_vars($this) as $property => $value) {
  230.             if ($value !== UNDEFINED) {
  231.                 if ($classVars[$property] === UNDEFINED) { // When default is undefined, null is allowed.
  232.                     $data->$property $value;
  233.                 } elseif ($value !== null) {
  234.                     $data->$property $value;
  235.                 }
  236.             }
  237.         }
  238.         // Strip properties that are for internal (swagger-php) use.
  239.         foreach (static::$_blacklist as $property) {
  240.             unset($data->$property);
  241.         }
  242.         // Inject vendor properties.
  243.         unset($data->x);
  244.         if (is_array($this->x)) {
  245.             foreach ($this->as $property => $value) {
  246.                 $prefixed 'x-' $property;
  247.                 $data->$prefixed $value;
  248.             }
  249.         }
  250.         // Map nested keys
  251.         foreach (static::$_nested as $nested) {
  252.             if (is_string($nested) || count($nested) === 1) {
  253.                 continue;
  254.             }
  255.             $property $nested[0];
  256.             if ($this->$property === null) {
  257.                 continue;
  258.             }
  259.             $keyField $nested[1];
  260.             $object = new stdClass();
  261.             foreach ($this->$property as $key => $item) {
  262.                 if (is_numeric($key) === false && is_array($item)) {
  263.                     $object->$key $item;
  264.                 } else {
  265.                     $key $item->$keyField;
  266.                     if ($key && empty($object->$key)) {
  267.                         $object->$key $item->jsonSerialize();
  268.                         unset($object->$key->$keyField);
  269.                     }
  270.                 }
  271.             }
  272.             $data->$property $object;
  273.         }
  274.         // $ref
  275.         if (isset($data->ref)) {
  276.             $dollarRef '$ref';
  277.             $data->$dollarRef $data->ref;
  278.             unset($data->ref);
  279.         }
  280.         return $data;
  281.     }
  282.     /**
  283.      * Validate annotation tree, and log notices & warnings.
  284.      * @param array $parents The path of annotations above this annotation in the tree.
  285.      * @param array $skip (prevent stack overflow, when traversing an infinite dependency graph)
  286.      * @return boolean
  287.      * @throws Exception
  288.      */
  289.     public function validate($parents = [], $skip = [], $ref '')
  290.     {
  291.         if (in_array($this$skiptrue)) {
  292.             return true;
  293.         }
  294.         $valid true;
  295.         // Report orphaned annotations
  296.         foreach ($this->_unmerged as $annotation) {
  297.             if (!is_object($annotation)) {
  298.                 Logger::notice('Unexpected type: "' gettype($annotation) . '" in ' $this->identity() . '->_unmerged, expecting a Annotation object');
  299.                 break;
  300.             }
  301.             $class get_class($annotation);
  302.             if (isset(static::$_nested[$class])) {
  303.                 $property = static::$_nested[$class];
  304.                 Logger::notice('Only one @' str_replace('Swagger\Annotations\\''SWG\\'get_class($annotation)) . '() allowed for ' $this->identity() . " multiple found in:\n    Using: " $this->$property->_context "\n  Skipped: " $annotation->_context);
  305.             } elseif ($annotation instanceof AbstractAnnotation) {
  306.                 $message 'Unexpected ' $annotation->identity();
  307.                 if (count($class::$_parents)) {
  308.                     $shortNotations = [];
  309.                     foreach ($class::$_parents as $_class) {
  310.                         $shortNotations[] = '@' str_replace('Swagger\Annotations\\''SWG\\'$_class);
  311.                     }
  312.                     $message .= ', expected to be inside ' implode(', '$shortNotations);
  313.                 }
  314.                 Logger::notice($message ' in ' $annotation->_context);
  315.             }
  316.             $valid false;
  317.         }
  318.         // Report conflicting key
  319.         foreach (static::$_nested as $annotationClass => $nested) {
  320.             if (is_string($nested) || count($nested) === 1) {
  321.                 continue;
  322.             }
  323.             $property $nested[0];
  324.             if ($this->$property === null) {
  325.                 continue;
  326.             }
  327.             $keys = [];
  328.             $keyField $nested[1];
  329.             foreach ($this->$property as $key => $item) {
  330.                 if (is_array($item) && is_numeric($key) === false) {
  331.                     Logger::notice($this->identity() . '->' $property ' is an object literal, use nested @' str_replace('Swagger\\Annotations\\''SWG\\'$annotationClass) . '() annotation(s) in ' $this->_context);
  332.                     $keys[$key] = $item;
  333.                 } elseif (empty($item->$keyField)) {
  334.                     Logger::notice($item->identity() . ' is missing key-field: "' $keyField '" in ' $item->_context);
  335.                 } elseif (isset($keys[$item->$keyField])) {
  336.                     Logger::notice('Multiple ' $item->_identity([]) . ' with the same ' $keyField '="' $item->$keyField "\":\n  " $item->_context "\n  " $keys[$item->$keyField]->_context);
  337.                 } else {
  338.                     $keys[$item->$keyField] = $item;
  339.                 }
  340.             }
  341.         }
  342.         if (isset($this->ref)) {
  343.             if (substr($this->ref02) === '#/' && count($parents) > 0  && $parents[0] instanceof Swagger) { // Internal reference
  344.                 try {
  345.                     $parents[0]->ref($this->ref);
  346.                 } catch (Exception $exception) {
  347.                     Logger::notice($exception->getMessage().' for '.$this->identity().' in '.$this->_context);
  348.                 }
  349.             }
  350.         } else {
  351.             // Report missing required fields (when not a $ref)
  352.             foreach (static::$_required as $property) {
  353.                 if ($this->$property === null || $this->$property === UNDEFINED) {
  354.                     $message 'Missing required field "' $property '" for ' $this->identity() . ' in ' $this->_context;
  355.                     foreach (static::$_nested as $class => $nested) {
  356.                         $nestedProperty is_array($nested) ? $nested[0] : $nested;
  357.                         if ($property === $nestedProperty) {
  358.                             if ($this instanceof Swagger) {
  359.                                 $message 'Required @' str_replace('Swagger\\Annotations\\''SWG\\'$class) . '() not found';
  360.                             } elseif (is_array($nested)) {
  361.                                 $message $this->identity() . ' requires at least one @' str_replace('Swagger\\Annotations\\''SWG\\'$class) . '() in ' $this->_context;
  362.                             } else {
  363.                                 $message $this->identity() . ' requires a @' str_replace('Swagger\\Annotations\\''SWG\\'$class) . '() in ' $this->_context;
  364.                             }
  365.                             break;
  366.                         }
  367.                     }
  368.                     Logger::notice($message);
  369.                 }
  370.             }
  371.         }
  372.         // Report invalid types
  373.         foreach (static::$_types as $property => $type) {
  374.             $value $this->$property;
  375.             if ($value === null || $value === UNDEFINED) {
  376.                 continue;
  377.             }
  378.             if (is_string($type)) {
  379.                 if ($this->validateType($type$value) === false) {
  380.                     $valid false;
  381.                     Logger::notice($this->identity() . '->' $property ' is a "' gettype($value) . '", expecting a "' $type '" in ' $this->_context);
  382.                 }
  383.             } elseif (is_array($type)) { // enum?
  384.                 if (in_array($value$type) === false) {
  385.                     Logger::notice($this->identity() . '->' $property ' "' $value '" is invalid, expecting "' implode('", "'$type) . '" in ' $this->_context);
  386.                 }
  387.             } else {
  388.                 throw new Exception('Invalid ' get_class($this) . '::$_types[' $property ']');
  389.             }
  390.         }
  391.         $parents[] = $this;
  392.         return self::_validate($this$parents$skip$ref) ? $valid false;
  393.     }
  394.     /**
  395.      * Recursively validate all annotation properties.
  396.      *
  397.      * @param array|object $fields
  398.      * @param array $parents The path of annotations above this annotation in the tree.
  399.      * @param array [$skip] Array with objects which are already validated
  400.      * @return boolean
  401.      */
  402.     private static function _validate($fields$parents$skip$baseRef)
  403.     {
  404.         $valid true;
  405.         $blacklist = [];
  406.         if (is_object($fields)) {
  407.             if (in_array($fields$skiptrue)) {
  408.                 return true;
  409.             }
  410.             $skip[] = $fields;
  411.             $blacklist property_exists($fields'_blacklist') ? $fields::$_blacklist : [];
  412.         }
  413.         foreach ($fields as $field => $value) {
  414.             if ($value === null || is_scalar($value) || in_array($field$blacklist)) {
  415.                 continue;
  416.             }
  417.             $ref $baseRef !== '' $baseRef.'/'.urlencode($field) : urlencode($field);
  418.             if (is_object($value)) {
  419.                 if (method_exists($value'validate')) {
  420.                     if (!$value->validate($parents$skip$ref)) {
  421.                         $valid false;
  422.                     }
  423.                 } elseif (!self::_validate($value$parents$skip$ref)) {
  424.                     $valid false;
  425.                 }
  426.             } elseif (is_array($value) && !self::_validate($value$parents$skip$ref)) {
  427.                 $valid false;
  428.             }
  429.         }
  430.         return $valid;
  431.     }
  432.     /**
  433.      * Return a identity for easy debugging.
  434.      * Example: "@SWG\Get(path="/pets")"
  435.      * @return string
  436.      */
  437.     public function identity()
  438.     {
  439.         return $this->_identity([]);
  440.     }
  441.     /**
  442.      * Helper for generating the identity()
  443.      * @param array $properties
  444.      * @return string
  445.      */
  446.     protected function _identity($properties)
  447.     {
  448.         $fields = [];
  449.         foreach ($properties as $property) {
  450.             $value $this->$property;
  451.             if ($value !== null && $value !== UNDEFINED) {
  452.                 $fields[] = $property '=' . (is_string($value) ? '"' $value '"' $value);
  453.             }
  454.         }
  455.         return '@' str_replace('Swagger\\Annotations\\''SWG\\'get_class($this)) . '(' implode(','$fields) . ')';
  456.     }
  457.     private function validateType($type$value)
  458.     {
  459.         if (substr($type01) === '[' && substr($type, -1) === ']') { // Array of a specified type?
  460.             if ($this->validateType('array'$value) === false) {
  461.                 return false;
  462.             }
  463.             $itemType substr($type1, -1);
  464.             foreach ($value as $i => $item) {
  465.                 if ($this->validateType($itemType$item) === false) {
  466.                     return false;
  467.                 }
  468.             }
  469.             return true;
  470.         }
  471.         switch ($type) {
  472.             case 'string':
  473.                 return is_string($value);
  474.             case 'boolean':
  475.                 return is_bool($value);
  476.             case 'integer':
  477.                 return is_int($value);
  478.             case 'number':
  479.                 return is_numeric($value);
  480.             case 'array':
  481.                 if (is_array($value) === false) {
  482.                     return false;
  483.                 }
  484.                 $count 0;
  485.                 foreach ($value as $i => $item) {
  486.                     if ($count !== $i) { // not a array, but a hash/map
  487.                         return false;
  488.                     }
  489.                     $count++;
  490.                 }
  491.                 return true;
  492.             case 'scheme':
  493.                 return in_array($value, ['http''https''ws''wss']);
  494.             default:
  495.                 throw new Exception('Invalid type "' $type '"');
  496.         }
  497.     }
  498.     /**
  499.      * Wrap the context with a reference to the annotation it is nested in.
  500.      * @param AbstractAnnotation $annotation
  501.      * @param Context $nestedContext
  502.      * @return AbstractAnnotation
  503.      */
  504.     private function nested($annotation$nestedContext)
  505.     {
  506.         if (property_exists($annotation'_context') && $annotation->_context === $this->_context) {
  507.             $annotation->_context $nestedContext;
  508.         }
  509.         return $annotation;
  510.     }
  511. }