Magento 2 Documentation  2.3
Documentation for Magento 2 CMS v2.3 (December 2018)
Generator.php
Go to the documentation of this file.
1 <?php
7 
12 use Magento\Framework\Webapi\Exception as WebapiException;
17 use Magento\Webapi\Model\Rest\SwaggerFactory;
19 
30 {
34  const ERROR_SCHEMA = '#/definitions/error-response';
35 
39  const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized';
40 
42  const ARRAY_SIGNIFIER = '[0]';
43 
49  protected $swaggerFactory;
50 
56  protected $productMetadata;
57 
70  protected $tags = [];
71 
84  protected $definitions = [];
85 
92  protected $simpleTypeList = [
93  'bool' => 'boolean',
94  'boolean' => 'boolean',
95  'int' => 'integer',
96  'integer' => 'integer',
97  'double' => 'number',
98  'float' => 'number',
99  'number' => 'number',
100  'string' => 'string',
101  TypeProcessor::ANY_TYPE => 'string',
102  TypeProcessor::NORMALIZED_ANY_TYPE => 'string',
103  ];
104 
116  public function __construct(
117  \Magento\Webapi\Model\Cache\Type\Webapi $cache,
118  \Magento\Framework\Reflection\TypeProcessor $typeProcessor,
119  \Magento\Framework\Webapi\CustomAttribute\ServiceTypeListInterface $serviceTypeList,
122  SwaggerFactory $swaggerFactory,
124  ) {
125  $this->swaggerFactory = $swaggerFactory;
126  $this->productMetadata = $productMetadata;
127  parent::__construct(
128  $cache,
133  );
134  }
135 
139  protected function generateSchema($requestedServiceMetadata, $requestScheme, $requestHost, $endpointUrl)
140  {
142  $swagger = $this->swaggerFactory->create();
143 
144  $swagger->setInfo($this->getGeneralInfo());
145 
146  $this->addCustomAttributeTypes();
147  $swagger->setHost($requestHost);
148  $swagger->setBasePath(strstr($endpointUrl, Rest::SCHEMA_PATH, true));
149  $swagger->setSchemes([$requestScheme]);
150 
151  foreach ($requestedServiceMetadata as $serviceName => $serviceData) {
152  if (!isset($this->tags[$serviceName])) {
153  $this->tags[$serviceName] = $this->generateTagInfo($serviceName, $serviceData);
154  $swagger->addTag($this->tags[$serviceName]);
155  }
156  foreach ($serviceData[Converter::KEY_ROUTES] as $uri => $httpMethods) {
157  $uri = $this->convertPathParams($uri);
158  foreach ($httpMethods as $httpOperation => $httpMethodData) {
159  $httpOperation = strtolower($httpOperation);
160  $phpMethodData = $serviceData[Converter::KEY_METHODS][$httpMethodData[Converter::KEY_METHOD]];
161  $httpMethodData[Converter::KEY_METHOD] = $phpMethodData;
162  $httpMethodData['uri'] = $uri;
163  $httpMethodData['httpOperation'] = $httpOperation;
164  $swagger->addPath(
165  $this->convertPathParams($uri),
166  $httpOperation,
167  $this->generatePathInfo($httpOperation, $httpMethodData, $serviceName)
168  );
169  }
170  }
171  }
172  $swagger->setDefinitions($this->getDefinitions());
173 
174  return $swagger->toSchema();
175  }
176 
182  protected function getGeneralInfo()
183  {
184  $versionParts = explode('.', $this->productMetadata->getVersion());
185  if (!isset($versionParts[0]) || !isset($versionParts[1])) {
186  return []; // Major and minor version are not set - return empty response
187  }
188  $majorMinorVersion = $versionParts[0] . '.' . $versionParts[1];
189 
190  return [
191  'version' => $majorMinorVersion,
192  'title' => $this->productMetadata->getName() . ' ' . $this->productMetadata->getEdition(),
193  ];
194  }
195 
204  protected function generatePathInfo($methodName, $httpMethodData, $tagName)
205  {
206  $methodData = $httpMethodData[Converter::KEY_METHOD];
207 
208  $operationId = $this->typeProcessor->getOperationName($tagName, $methodData[Converter::KEY_METHOD]);
209  $operationId .= ucfirst($methodName);
210 
211  $pathInfo = [
212  'tags' => [$tagName],
213  'description' => $methodData['documentation'],
214  'operationId' => $operationId,
215  ];
216 
217  $parameters = $this->generateMethodParameters($httpMethodData, $operationId);
218  if ($parameters) {
219  $pathInfo['parameters'] = $parameters;
220  }
221  $pathInfo['responses'] = $this->generateMethodResponses($methodData);
222 
223  return $pathInfo;
224  }
225 
232  protected function generateMethodResponses($methodData)
233  {
234  $responses = [];
235 
236  if (isset($methodData['interface']['out']['parameters'])
237  && is_array($methodData['interface']['out']['parameters'])
238  ) {
239  $parameters = $methodData['interface']['out']['parameters'];
240  $responses = $this->generateMethodSuccessResponse($parameters, $responses);
241  }
242 
244  if (isset($methodData['resources'])) {
245  foreach ($methodData['resources'] as $resourceName) {
246  if ($resourceName !== 'anonymous') {
247  $responses[WebapiException::HTTP_UNAUTHORIZED]['description'] = self::UNAUTHORIZED_DESCRIPTION;
248  $responses[WebapiException::HTTP_UNAUTHORIZED]['schema']['$ref'] = self::ERROR_SCHEMA;
249  break;
250  }
251  }
252  }
253 
254  if (isset($methodData['interface']['out']['throws'])
255  && is_array($methodData['interface']['out']['throws'])
256  ) {
257  foreach ($methodData['interface']['out']['throws'] as $exceptionClass) {
258  $responses = $this->generateMethodExceptionErrorResponses($exceptionClass, $responses);
259  }
260  }
261  $responses['default']['description'] = 'Unexpected error';
262  $responses['default']['schema']['$ref'] = self::ERROR_SCHEMA;
263 
264  return $responses;
265  }
266 
276  private function generateMethodParameters($httpMethodData, $operationId)
277  {
278  $bodySchema = [];
279  $parameters = [];
280 
281  $phpMethodData = $httpMethodData[Converter::KEY_METHOD];
283  if (!isset($phpMethodData['interface']['in']['parameters'])
284  || !isset($httpMethodData['uri'])
285  || !isset($httpMethodData['httpOperation'])
286  ) {
287  return [];
288  }
289 
290  foreach ($phpMethodData['interface']['in']['parameters'] as $parameterName => $parameterInfo) {
292  if (isset($httpMethodData['parameters'][$parameterName]['force'])
293  && $httpMethodData['parameters'][$parameterName]['force']
294  ) {
295  continue;
296  }
297 
298  if (!isset($parameterInfo['type'])) {
299  return [];
300  }
301  $description = isset($parameterInfo['documentation']) ? $parameterInfo['documentation'] : null;
302 
304  if (strpos($httpMethodData['uri'], '{' . $parameterName . '}') !== false) {
305  $parameters[] = $this->generateMethodPathParameter($parameterName, $parameterInfo, $description);
306  } elseif (strtoupper($httpMethodData['httpOperation']) === 'GET') {
307  $parameters = $this->generateMethodQueryParameters(
308  $parameterName,
309  $parameterInfo,
310  $description,
311  $parameters
312  );
313  } else {
314  $bodySchema = $this->generateBodySchema(
315  $parameterName,
316  $parameterInfo,
317  $description,
318  $bodySchema
319  );
320  }
321  }
322 
326  preg_match_all('#\\{([^\\{\\}]*)\\}#', $httpMethodData['uri'], $allPathParams);
327  $remainingPathParams = array_diff(
328  $allPathParams[1],
329  array_keys($phpMethodData['interface']['in']['parameters'])
330  );
331  foreach ($remainingPathParams as $pathParam) {
332  $parameters[] = [
333  'name' => $pathParam,
334  'in' => 'path',
335  'type' => 'string',
336  'required' => true
337  ];
338  }
339 
340  if ($bodySchema) {
341  $bodyParam = [];
342  $bodyParam['name'] = $operationId . 'Body';
343  $bodyParam['in'] = 'body';
344  $bodyParam['schema'] = $bodySchema;
345  $parameters[] = $bodyParam;
346  }
347  return $parameters;
348  }
349 
359  private function createQueryParam($name, $type, $description, $required = null)
360  {
361  $param = [
362  'name' => $name,
363  'in' => 'query',
364  ];
365 
366  $param = array_merge($param, $this->getObjectSchema($type, $description));
367 
368  if (isset($required)) {
369  $param['required'] = $required;
370  }
371  return $param;
372  }
373 
381  protected function generateTagInfo($serviceName, $serviceData)
382  {
383  $tagInfo = [];
384  $tagInfo['name'] = $serviceName;
385  if (!empty($serviceData) && is_array($serviceData)) {
386  $tagInfo['description'] = $serviceData[Converter::KEY_DESCRIPTION];
387  }
388  return $tagInfo;
389  }
390 
398  protected function getObjectSchema($typeName, $description = '')
399  {
400  $simpleType = $this->getSimpleType($typeName);
401  if ($simpleType == false) {
402  $result = ['type' => 'array'];
403  if (!empty($description)) {
404  $result['description'] = $description;
405  }
406  $trimedTypeName = rtrim($typeName, '[]');
407  if ($simpleType = $this->getSimpleType($trimedTypeName)) {
408  $result['items'] = ['type' => $simpleType];
409  } else {
410  if (strpos($typeName, '[]') !== false) {
411  $result['items'] = ['$ref' => $this->getDefinitionReference($trimedTypeName)];
412  } else {
413  $result = ['$ref' => $this->getDefinitionReference($trimedTypeName)];
414  }
415  if (!$this->isDefinitionExists($trimedTypeName)) {
416  $definitionKey = $this->toLowerCaseDashSeparated($trimedTypeName);
417  $this->definitions[$definitionKey] = [];
418  $this->definitions[$definitionKey] = $this->generateDefinition($trimedTypeName);
419  }
420  }
421  } else {
422  $result = ['type' => $simpleType];
423  if (!empty($description)) {
424  $result['description'] = $description;
425  }
426  }
427  return $result;
428  }
429 
436  protected function generateDefinition($typeName)
437  {
438  $properties = [];
439  $requiredProperties = [];
440  $typeData = $this->typeProcessor->getTypeData($typeName);
441  if (isset($typeData['parameters'])) {
442  foreach ($typeData['parameters'] as $parameterName => $parameterData) {
443  $properties[$parameterName] = $this->getObjectSchema(
444  $parameterData['type'],
445  $parameterData['documentation']
446  );
447  if ($parameterData['required']) {
448  $requiredProperties[] = $parameterName;
449  }
450  }
451  }
452  $definition = ['type' => 'object'];
453  if (isset($typeData['documentation'])) {
454  $definition['description'] = $typeData['documentation'];
455  }
456  if (!empty($properties)) {
457  $definition['properties'] = $properties;
458  }
459  if (!empty($requiredProperties)) {
460  $definition['required'] = $requiredProperties;
461  }
462  return $definition;
463  }
464 
471  protected function getDefinitions()
472  {
473  return array_merge(
474  [
475  'error-response' => [
476  'type' => 'object',
477  'properties' => [
478  'message' => [
479  'type' => 'string',
480  'description' => 'Error message',
481  ],
482  'errors' => [
483  '$ref' => '#/definitions/error-errors',
484  ],
485  'code' => [
486  'type' => 'integer',
487  'description' => 'Error code',
488  ],
489  'parameters' => [
490  '$ref' => '#/definitions/error-parameters',
491  ],
492  'trace' => [
493  'type' => 'string',
494  'description' => 'Stack trace',
495  ],
496  ],
497  'required' => ['message'],
498  ],
499  'error-errors' => [
500  'type' => 'array',
501  'description' => 'Errors list',
502  'items' => [
503  '$ref' => '#/definitions/error-errors-item',
504  ],
505  ],
506  'error-errors-item' => [
507  'type' => 'object',
508  'description' => 'Error details',
509  'properties' => [
510  'message' => [
511  'type' => 'string',
512  'description' => 'Error message',
513  ],
514  'parameters' => [
515  '$ref' => '#/definitions/error-parameters',
516  ],
517  ],
518  ],
519  'error-parameters' => [
520  'type' => 'array',
521  'description' => 'Error parameters list',
522  'items' => [
523  '$ref' => '#/definitions/error-parameters-item',
524  ],
525  ],
526  'error-parameters-item' => [
527  'type' => 'object',
528  'description' => 'Error parameters item',
529  'properties' => [
530  'resources' => [
531  'type' => 'string',
532  'description' => 'ACL resource',
533  ],
534  'fieldName' => [
535  'type' => 'string',
536  'description' => 'Missing or invalid field name'
537  ],
538  'fieldValue' => [
539  'type' => 'string',
540  'description' => 'Incorrect field value'
541  ],
542  ],
543  ],
544  ],
545  $this->snakeCaseDefinitions($this->definitions)
546  );
547  }
548 
555  private function snakeCaseDefinitions($definitions)
556  {
557  foreach ($definitions as $name => $vals) {
558  if (!empty($vals['properties'])) {
559  $definitions[$name]['properties'] = $this->convertArrayToSnakeCase($vals['properties']);
560  }
561  if (!empty($vals['required'])) {
562  $snakeCaseRequired = [];
563  foreach ($vals['required'] as $requiredProperty) {
564  $snakeCaseRequired[] = SimpleDataObjectConverter::camelCaseToSnakeCase($requiredProperty);
565  }
566  $definitions[$name]['required'] = $snakeCaseRequired;
567  }
568  }
569  return $definitions;
570  }
571 
578  private function convertArrayToSnakeCase($properties)
579  {
580  foreach ($properties as $name => $value) {
581  $snakeCaseName = SimpleDataObjectConverter::camelCaseToSnakeCase($name);
582  if (is_array($value)) {
583  $value = $this->convertArrayToSnakeCase($value);
584  }
585  unset($properties[$name]);
586  $properties[$snakeCaseName] = $value;
587  }
588  return $properties;
589  }
590 
597  protected function getDefinitionReference($typeName)
598  {
599  return '#/definitions/' . $this->toLowerCaseDashSeparated($typeName);
600  }
601 
610  protected function toLowerCaseDashSeparated($typeName)
611  {
612  return strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $typeName));
613  }
614 
621  protected function isDefinitionExists($typeName)
622  {
623  return isset($this->definitions[$this->toLowerCaseDashSeparated($typeName)]);
624  }
625 
631  protected function addCustomAttributeTypes()
632  {
633  foreach ($this->serviceTypeList->getDataTypes() as $customAttributeClass) {
634  $this->typeProcessor->register($customAttributeClass);
635  }
636  }
637 
644  protected function getServiceMetadata($serviceName)
645  {
646  return $this->serviceMetadata->getRouteMetadata($serviceName);
647  }
648 
655  protected function getSimpleType($type)
656  {
657  if (array_key_exists($type, $this->simpleTypeList)) {
658  return $this->simpleTypeList[$type];
659  } else {
660  return false;
661  }
662  }
663 
676  protected function getQueryParamNames($name, $type, $description, $prefix = '')
677  {
678  if ($this->typeProcessor->isTypeSimple($type)) {
679  // Primitive type or array of primitive types
680  return [
681  $this->handlePrimitive($name, $prefix) => [
682  'type' => substr($type, -2) === '[]' ? $type : $this->getSimpleType($type),
683  'description' => $description
684  ]
685  ];
686  }
687  if ($this->typeProcessor->isArrayType($type)) {
688  // Array of complex type
689  $arrayType = substr($type, 0, -2);
690  return $this->handleComplex($name, $arrayType, $prefix, true);
691  } else {
692  // Complex type
693  return $this->handleComplex($name, $type, $prefix, false);
694  }
695  }
696 
706  private function handleComplex($name, $type, $prefix, $isArray)
707  {
708  $parameters = $this->typeProcessor->getTypeData($type)['parameters'];
709  $queryNames = [];
710  foreach ($parameters as $subParameterName => $subParameterInfo) {
711  $subParameterType = $subParameterInfo['type'];
712  $subParameterDescription = isset($subParameterInfo['documentation'])
713  ? $subParameterInfo['documentation']
714  : null;
715  $subPrefix = $prefix
716  ? $prefix . '[' . $name . ']'
717  : $name;
718  if ($isArray) {
719  $subPrefix .= self::ARRAY_SIGNIFIER;
720  }
721  $queryNames = array_merge(
722  $queryNames,
723  $this->getQueryParamNames($subParameterName, $subParameterType, $subParameterDescription, $subPrefix)
724  );
725  }
726  return $queryNames;
727  }
728 
736  private function handlePrimitive($name, $prefix)
737  {
738  return $prefix
739  ? $prefix . '[' . $name . ']'
740  : $name;
741  }
742 
749  private function convertPathParams($uri)
750  {
751  $parts = explode('/', $uri);
752  $count = count($parts);
753  for ($i=0; $i < $count; $i++) {
754  if (strpos($parts[$i], ':') === 0) {
755  $parts[$i] = '{' . substr($parts[$i], 1) . '}';
756  }
757  }
758  return implode('/', $parts);
759  }
760 
769  private function generateMethodPathParameter($parameterName, $parameterInfo, $description)
770  {
771  $param = [
772  'name' => $parameterName,
773  'in' => 'path',
774  'type' => $this->getSimpleType($parameterInfo['type']),
775  'required' => true
776  ];
777  if ($description) {
778  $param['description'] = $description;
779  return $param;
780  }
781  return $param;
782  }
783 
793  private function generateMethodQueryParameters($parameterName, $parameterInfo, $description, $parameters)
794  {
795  $queryParams = $this->getQueryParamNames($parameterName, $parameterInfo['type'], $description);
796  if (count($queryParams) === 1) {
797  // handle simple query parameter (includes the 'required' field)
798  $parameters[] = $this->createQueryParam(
799  $parameterName,
800  $parameterInfo['type'],
801  $description,
802  $parameterInfo['required']
803  );
804  } else {
810  foreach ($queryParams as $name => $queryParamInfo) {
811  $parameters[] = $this->createQueryParam(
812  $name,
813  $queryParamInfo['type'],
814  $queryParamInfo['description']
815  );
816  }
817  }
818  return $parameters;
819  }
820 
830  private function generateBodySchema($parameterName, $parameterInfo, $description, $bodySchema)
831  {
832  $required = isset($parameterInfo['required']) ? $parameterInfo['required'] : null;
833  /*
834  * There can only be one body parameter, multiple PHP parameters are represented as different
835  * properties of the body.
836  */
837  if ($required) {
838  $bodySchema['required'][] = $parameterName;
839  }
840  $bodySchema['properties'][$parameterName] = $this->getObjectSchema(
841  $parameterInfo['type'],
843  );
844  $bodySchema['type'] = 'object';
845  return $bodySchema;
846  }
847 
855  private function generateMethodSuccessResponse($parameters, $responses)
856  {
857  if (isset($parameters['result']) && is_array($parameters['result'])) {
858  $description = '';
859  if (isset($parameters['result']['documentation'])) {
860  $description = $parameters['result']['documentation'];
861  }
862  $schema = [];
863  if (isset($parameters['result']['type'])) {
864  $schema = $this->getObjectSchema($parameters['result']['type'], $description);
865  }
866 
867  // Some methods may have a non-standard HTTP success code.
868  $specificResponseData = $parameters['result']['response_codes']['success'] ?? [];
869  // Default HTTP success code to 200 if nothing has been supplied.
870  $responseCode = $specificResponseData['code'] ?? '200';
871  // Default HTTP response status to 200 Success if nothing has been supplied.
872  $responseDescription = $specificResponseData['description'] ?? '200 Success.';
873 
874  $responses[$responseCode]['description'] = $responseDescription;
875  if (!empty($schema)) {
876  $responses[$responseCode]['schema'] = $schema;
877  }
878  }
879  return $responses;
880  }
881 
889  private function generateMethodExceptionErrorResponses($exceptionClass, $responses)
890  {
891  $httpCode = '500';
892  $description = 'Internal Server error';
893  if (is_subclass_of($exceptionClass, \Magento\Framework\Exception\LocalizedException::class)) {
894  // Map HTTP codes for LocalizedExceptions according to exception type
895  if (is_subclass_of($exceptionClass, \Magento\Framework\Exception\NoSuchEntityException::class)) {
896  $httpCode = WebapiException::HTTP_NOT_FOUND;
897  $description = '404 Not Found';
898  } elseif (is_subclass_of($exceptionClass, \Magento\Framework\Exception\AuthorizationException::class)
899  || is_subclass_of($exceptionClass, \Magento\Framework\Exception\AuthenticationException::class)
900  ) {
901  $httpCode = WebapiException::HTTP_UNAUTHORIZED;
903  } else {
904  // Input, Expired, InvalidState exceptions will fall to here
905  $httpCode = WebapiException::HTTP_BAD_REQUEST;
906  $description = '400 Bad Request';
907  }
908  }
909  $responses[$httpCode]['description'] = $description;
910  $responses[$httpCode]['schema']['$ref'] = self::ERROR_SCHEMA;
911 
912  return $responses;
913  }
914 
920  public function getListOfServices()
921  {
922  $listOfAllowedServices = [];
923  foreach ($this->serviceMetadata->getServicesConfig() as $serviceName => $service) {
925  if ($this->authorization->isAllowed($method[ServiceMetadata::KEY_ACL_RESOURCES])) {
926  $listOfAllowedServices[] = $serviceName;
927  break;
928  }
929  }
930  }
931  return $listOfAllowedServices;
932  }
933 }
getQueryParamNames($name, $type, $description, $prefix='')
Definition: Generator.php:676
is_subclass_of($obj, $className)
elseif(isset( $params[ 'redirect_parent']))
Definition: iframe.phtml:17
$count
Definition: recent.phtml:13
generateSchema($requestedServiceMetadata, $requestScheme, $requestHost, $requestUri)
generatePathInfo($methodName, $httpMethodData, $tagName)
Definition: Generator.php:204
$type
Definition: item.phtml:13
$prefix
Definition: name.phtml:25
$value
Definition: gender.phtml:16
getObjectSchema($typeName, $description='')
Definition: Generator.php:398
$method
Definition: info.phtml:13
__construct(\Magento\Webapi\Model\Cache\Type\Webapi $cache, \Magento\Framework\Reflection\TypeProcessor $typeProcessor, \Magento\Framework\Webapi\CustomAttribute\ServiceTypeListInterface $serviceTypeList, \Magento\Webapi\Model\ServiceMetadata $serviceMetadata, Authorization $authorization, SwaggerFactory $swaggerFactory, ProductMetadataInterface $productMetadata)
Definition: Generator.php:116
$properties
Definition: categories.php:26
$i
Definition: gallery.phtml:31
generateTagInfo($serviceName, $serviceData)
Definition: Generator.php:381
$required
Definition: wrapper.phtml:8
if(!isset($_GET['name'])) $name
Definition: log.php:14