mirror of
https://github.com/bdnugget/magento2.git
synced 2026-11-07 09:17:35 +00:00
976 lines
32 KiB
PHP
976 lines
32 KiB
PHP
<?php
|
|
/**
|
|
* Copyright © Magento, Inc. All rights reserved.
|
|
* See COPYING.txt for license details.
|
|
*/
|
|
namespace Magento\Webapi\Model\Rest\Swagger;
|
|
|
|
use Magento\Framework\Api\SimpleDataObjectConverter;
|
|
use Magento\Framework\App\ProductMetadataInterface;
|
|
use Magento\Framework\Reflection\TypeProcessor;
|
|
use Magento\Framework\Webapi\Authorization;
|
|
use Magento\Framework\Webapi\Exception as WebapiException;
|
|
use Magento\Webapi\Controller\Rest;
|
|
use Magento\Webapi\Model\AbstractSchemaGenerator;
|
|
use Magento\Webapi\Model\Config\Converter;
|
|
use Magento\Webapi\Model\Rest\Swagger;
|
|
use Magento\Webapi\Model\Rest\SwaggerFactory;
|
|
use Magento\Webapi\Model\ServiceMetadata;
|
|
|
|
/**
|
|
* REST Swagger schema generator.
|
|
*
|
|
* Generate REST API description in a format of JSON document,
|
|
* compliant with {@link https://github.com/swagger-api/swagger-spec/blob/master/versions/2.0.md Swagger specification}
|
|
*
|
|
* @SuppressWarnings(PHPMD.CouplingBetweenObjects)
|
|
* @SuppressWarnings(PHPMD.ExcessiveClassComplexity)
|
|
*/
|
|
class Generator extends AbstractSchemaGenerator
|
|
{
|
|
/**
|
|
* Error response schema
|
|
*/
|
|
private const ERROR_SCHEMA = '#/definitions/error-response';
|
|
|
|
private const UNAUTHORIZED_DESCRIPTION = '401 Unauthorized';
|
|
|
|
protected const ARRAY_SIGNIFIER = '[0]';
|
|
|
|
/**
|
|
* Wrapper node for XML requests
|
|
*/
|
|
private const XML_SCHEMA_PARAMWRAPPER = 'request';
|
|
|
|
/**
|
|
* Swagger factory instance.
|
|
*
|
|
* @var SwaggerFactory
|
|
*/
|
|
protected SwaggerFactory $swaggerFactory;
|
|
|
|
/**
|
|
* Magento product metadata
|
|
*
|
|
* @var ProductMetadataInterface
|
|
*/
|
|
protected ProductMetadataInterface $productMetadata;
|
|
|
|
/**
|
|
* A map of Tags
|
|
*
|
|
* example:
|
|
* [
|
|
* class1Name => tag information,
|
|
* class2Name => tag information,
|
|
* ...
|
|
* ]
|
|
*
|
|
* @var array
|
|
*/
|
|
protected array $tags = [];
|
|
|
|
/**
|
|
* A map of definition
|
|
*
|
|
* example:
|
|
* [
|
|
* definitionName1 => definition,
|
|
* definitionName2 => definition,
|
|
* ...
|
|
* ]
|
|
* Note: definitionName is converted from class name
|
|
* @var array
|
|
*/
|
|
protected array $definitions = [];
|
|
|
|
/**
|
|
* List of simple parameter types not to be processed by the definitions generator
|
|
* Contains mapping to the internal swagger simple types
|
|
*
|
|
* @var string[]
|
|
*/
|
|
protected array $simpleTypeList = [
|
|
'bool' => 'boolean',
|
|
'boolean' => 'boolean',
|
|
'int' => 'integer',
|
|
'integer' => 'integer',
|
|
'double' => 'number',
|
|
'float' => 'number',
|
|
'number' => 'number',
|
|
'string' => 'string',
|
|
TypeProcessor::ANY_TYPE => 'string',
|
|
TypeProcessor::NORMALIZED_ANY_TYPE => 'string',
|
|
];
|
|
|
|
/**
|
|
* Initialize dependencies.
|
|
*
|
|
* @param \Magento\Webapi\Model\Cache\Type\Webapi $cache
|
|
* @param \Magento\Framework\Reflection\TypeProcessor $typeProcessor
|
|
* @param \Magento\Framework\Webapi\CustomAttribute\ServiceTypeListInterface $serviceTypeList
|
|
* @param \Magento\Webapi\Model\ServiceMetadata $serviceMetadata
|
|
* @param Authorization $authorization
|
|
* @param SwaggerFactory $swaggerFactory
|
|
* @param \Magento\Framework\App\ProductMetadataInterface $productMetadata
|
|
*/
|
|
public function __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
|
|
) {
|
|
$this->swaggerFactory = $swaggerFactory;
|
|
$this->productMetadata = $productMetadata;
|
|
parent::__construct(
|
|
$cache,
|
|
$typeProcessor,
|
|
$serviceTypeList,
|
|
$serviceMetadata,
|
|
$authorization
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @inheritdoc
|
|
*/
|
|
protected function generateSchema($requestedServiceMetadata, $requestScheme, $requestHost, $endpointUrl)
|
|
{
|
|
/** @var Swagger $swagger */
|
|
$swagger = $this->swaggerFactory->create();
|
|
|
|
$swagger->setInfo($this->getGeneralInfo());
|
|
|
|
$this->addCustomAttributeTypes();
|
|
$swagger->setHost($requestHost);
|
|
$swagger->setBasePath(strstr($endpointUrl, Rest::SCHEMA_PATH, true));
|
|
$swagger->setSchemes([$requestScheme]);
|
|
|
|
foreach ($requestedServiceMetadata as $serviceName => $serviceData) {
|
|
if (!isset($this->tags[$serviceName])) {
|
|
$this->tags[$serviceName] = $this->generateTagInfo($serviceName, $serviceData);
|
|
$swagger->addTag($this->tags[$serviceName]);
|
|
}
|
|
foreach ($serviceData[Converter::KEY_ROUTES] as $uri => $httpMethods) {
|
|
$uri = $this->convertPathParams($uri);
|
|
foreach ($httpMethods as $httpOperation => $httpMethodData) {
|
|
$httpOperation = strtolower($httpOperation);
|
|
$phpMethodData = $serviceData[Converter::KEY_METHODS][$httpMethodData[Converter::KEY_METHOD]];
|
|
$httpMethodData[Converter::KEY_METHOD] = $phpMethodData;
|
|
$httpMethodData['uri'] = $uri;
|
|
$httpMethodData['httpOperation'] = $httpOperation;
|
|
$swagger->addPath(
|
|
$this->convertPathParams($uri),
|
|
$httpOperation,
|
|
$this->generatePathInfo($httpOperation, $httpMethodData, $serviceName, $uri)
|
|
);
|
|
}
|
|
}
|
|
}
|
|
$swagger->setDefinitions($this->getDefinitions());
|
|
|
|
return $swagger->toSchema();
|
|
}
|
|
|
|
/**
|
|
* Get the 'Info' section data
|
|
*
|
|
* @return string[]
|
|
*/
|
|
protected function getGeneralInfo()
|
|
{
|
|
$versionParts = explode('.', $this->productMetadata->getVersion());
|
|
if (!isset($versionParts[0]) || !isset($versionParts[1])) {
|
|
return []; // Major and minor version are not set - return empty response
|
|
}
|
|
$majorMinorVersion = $versionParts[0] . '.' . $versionParts[1];
|
|
|
|
return [
|
|
'version' => $majorMinorVersion,
|
|
'title' => $this->productMetadata->getName() . ' ' . $this->productMetadata->getEdition(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* List out consumes data type
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getConsumableDatatypes()
|
|
{
|
|
return [
|
|
'application/json',
|
|
'application/xml',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* List out produces data type
|
|
*
|
|
* @return array
|
|
*/
|
|
private function getProducibleDatatypes()
|
|
{
|
|
return [
|
|
'application/json',
|
|
'application/xml',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* Generate path info based on method data
|
|
*
|
|
* @param string $methodName
|
|
* @param array $httpMethodData
|
|
* @param string $tagName
|
|
* @param string $uri
|
|
* @return array
|
|
*/
|
|
protected function generatePathInfo(string $methodName, array $httpMethodData, string $tagName, string $uri): array
|
|
{
|
|
$methodData = $httpMethodData[Converter::KEY_METHOD];
|
|
$uri = ucwords(str_replace(['/{', '}/', '{', '}'], '/', $uri), "/");
|
|
|
|
$operationId = ucfirst($methodName) . str_replace(['/', '-'], '', $uri);
|
|
$pathInfo = [
|
|
'tags' => [$tagName],
|
|
'description' => $methodData['documentation'],
|
|
'operationId' => $operationId,
|
|
'consumes' => $this->getConsumableDatatypes(),
|
|
'produces' => $this->getProducibleDatatypes(),
|
|
];
|
|
|
|
$parameters = $this->generateMethodParameters($httpMethodData, $operationId);
|
|
if ($parameters) {
|
|
$pathInfo['parameters'] = $parameters;
|
|
}
|
|
$pathInfo['responses'] = $this->generateMethodResponses($methodData);
|
|
|
|
return $pathInfo;
|
|
}
|
|
|
|
/**
|
|
* Generate response based on method data
|
|
*
|
|
* @param array $methodData
|
|
* @return array
|
|
*/
|
|
protected function generateMethodResponses($methodData)
|
|
{
|
|
$responses = [];
|
|
|
|
if (isset($methodData['interface']['out']['parameters'])
|
|
&& is_array($methodData['interface']['out']['parameters'])
|
|
) {
|
|
$parameters = $methodData['interface']['out']['parameters'];
|
|
$responses = $this->generateMethodSuccessResponse($parameters, $responses);
|
|
}
|
|
|
|
/** Handle authorization exceptions that may not be documented */
|
|
if (isset($methodData['resources'])) {
|
|
foreach ($methodData['resources'] as $resourceName) {
|
|
if ($resourceName !== 'anonymous') {
|
|
$responses[WebapiException::HTTP_UNAUTHORIZED]['description'] = self::UNAUTHORIZED_DESCRIPTION;
|
|
$responses[WebapiException::HTTP_UNAUTHORIZED]['schema']['$ref'] = self::ERROR_SCHEMA;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (isset($methodData['interface']['out']['throws'])
|
|
&& is_array($methodData['interface']['out']['throws'])
|
|
) {
|
|
foreach ($methodData['interface']['out']['throws'] as $exceptionClass) {
|
|
$responses = $this->generateMethodExceptionErrorResponses($exceptionClass, $responses);
|
|
}
|
|
}
|
|
$responses['default']['description'] = 'Unexpected error';
|
|
$responses['default']['schema']['$ref'] = self::ERROR_SCHEMA;
|
|
|
|
return $responses;
|
|
}
|
|
|
|
/**
|
|
* Generate parameters based on method data
|
|
*
|
|
* @param array $httpMethodData
|
|
* @param string $operationId
|
|
* @return array
|
|
* @SuppressWarnings(PHPMD.CyclomaticComplexity)
|
|
* @SuppressWarnings(PHPMD.NPathComplexity)
|
|
*/
|
|
private function generateMethodParameters($httpMethodData, $operationId)
|
|
{
|
|
$bodySchema = [];
|
|
$parameters = [];
|
|
|
|
$phpMethodData = $httpMethodData[Converter::KEY_METHOD];
|
|
/** Return nothing if necessary fields are not set */
|
|
if (!isset($phpMethodData['interface']['in']['parameters'])
|
|
|| !isset($httpMethodData['uri'])
|
|
|| !isset($httpMethodData['httpOperation'])
|
|
) {
|
|
return [];
|
|
}
|
|
|
|
foreach ($phpMethodData['interface']['in']['parameters'] as $parameterName => $parameterInfo) {
|
|
/** Omit forced parameters */
|
|
if (isset($httpMethodData['parameters'][$parameterName]['force'])
|
|
&& $httpMethodData['parameters'][$parameterName]['force']
|
|
) {
|
|
continue;
|
|
}
|
|
|
|
if (!isset($parameterInfo['type'])) {
|
|
return [];
|
|
}
|
|
$description = $parameterInfo['documentation'] ?? null;
|
|
|
|
/** Get location of parameter */
|
|
if (strpos($httpMethodData['uri'], (string) ('{' . $parameterName . '}')) !== false) {
|
|
$parameters[] = $this->generateMethodPathParameter($parameterName, $parameterInfo, $description);
|
|
} elseif (strtoupper($httpMethodData['httpOperation']) === 'GET') {
|
|
$parameters = $this->generateMethodQueryParameters(
|
|
$parameterName,
|
|
$parameterInfo,
|
|
$description,
|
|
$parameters
|
|
);
|
|
} else {
|
|
$bodySchema = $this->generateBodySchema(
|
|
$parameterName,
|
|
$parameterInfo,
|
|
$description,
|
|
$bodySchema
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Add all the path params that don't correspond directly the PHP parameters
|
|
*/
|
|
preg_match_all('#\\{([^\\{\\}]*)\\}#', $httpMethodData['uri'], $allPathParams);
|
|
$remainingPathParams = array_diff(
|
|
$allPathParams[1],
|
|
array_keys($phpMethodData['interface']['in']['parameters'])
|
|
);
|
|
foreach ($remainingPathParams as $pathParam) {
|
|
$parameters[] = [
|
|
'name' => $pathParam,
|
|
'in' => 'path',
|
|
'type' => 'string',
|
|
'required' => true
|
|
];
|
|
}
|
|
|
|
if ($bodySchema) {
|
|
$bodyParam = [];
|
|
$bodyParam['name'] = $operationId . 'Body';
|
|
$bodyParam['in'] = 'body';
|
|
$bodyParam['schema'] = $bodySchema;
|
|
$parameters[] = $bodyParam;
|
|
}
|
|
return $parameters;
|
|
}
|
|
|
|
/**
|
|
* Creates an array for the given query parameter
|
|
*
|
|
* @param string $name
|
|
* @param string $type
|
|
* @param string $description
|
|
* @param bool|null $required
|
|
* @return array
|
|
*/
|
|
private function createQueryParam($name, $type, $description, $required = null)
|
|
{
|
|
$param = [
|
|
'name' => $name,
|
|
'in' => 'query',
|
|
];
|
|
|
|
$param = array_merge($param, $this->getObjectSchema($type, $description));
|
|
|
|
if (isset($required)) {
|
|
$param['required'] = $required;
|
|
}
|
|
return $param;
|
|
}
|
|
|
|
/**
|
|
* Generate Tag Info for given service
|
|
*
|
|
* @param string $serviceName
|
|
* @param array $serviceData
|
|
* @return string[]
|
|
*/
|
|
protected function generateTagInfo($serviceName, $serviceData)
|
|
{
|
|
$tagInfo = [];
|
|
$tagInfo['name'] = $serviceName;
|
|
if (!empty($serviceData) && is_array($serviceData)) {
|
|
$tagInfo['description'] = $serviceData[Converter::KEY_DESCRIPTION];
|
|
}
|
|
return $tagInfo;
|
|
}
|
|
|
|
/**
|
|
* Generate definition for given type
|
|
*
|
|
* @param string $typeName
|
|
* @param string $description
|
|
* @return array
|
|
*/
|
|
protected function getObjectSchema($typeName, $description = '')
|
|
{
|
|
$simpleType = $this->getSimpleType($typeName);
|
|
if ($simpleType == false) {
|
|
$result = ['type' => 'array'];
|
|
if (!empty($description)) {
|
|
$result['description'] = $description;
|
|
}
|
|
$trimedTypeName = $typeName !== null ? rtrim($typeName, '[]') : '';
|
|
if ($simpleType = $this->getSimpleType($trimedTypeName)) {
|
|
$result['items'] = ['type' => $simpleType];
|
|
} else {
|
|
if ($typeName && strpos($typeName, '[]') !== false) {
|
|
$result['items'] = ['$ref' => $this->getDefinitionReference($trimedTypeName)];
|
|
} else {
|
|
$result = ['$ref' => $this->getDefinitionReference($trimedTypeName)];
|
|
}
|
|
if (!$this->isDefinitionExists($trimedTypeName)) {
|
|
$definitionKey = $this->toLowerCaseDashSeparated($trimedTypeName);
|
|
$this->definitions[$definitionKey] = [];
|
|
$this->definitions[$definitionKey] = $this->generateDefinition($trimedTypeName);
|
|
}
|
|
}
|
|
} else {
|
|
$result = ['type' => $simpleType];
|
|
if (!empty($description)) {
|
|
$result['description'] = $description;
|
|
}
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
/**
|
|
* Generate definition for given type
|
|
*
|
|
* @param string $typeName
|
|
* @return array
|
|
*/
|
|
protected function generateDefinition($typeName)
|
|
{
|
|
$properties = [];
|
|
$requiredProperties = [];
|
|
$typeData = $this->typeProcessor->getTypeData($typeName);
|
|
if (isset($typeData['parameters'])) {
|
|
foreach ($typeData['parameters'] as $parameterName => $parameterData) {
|
|
$properties[$parameterName] = $this->getObjectSchema(
|
|
$parameterData['type'],
|
|
$parameterData['documentation']
|
|
);
|
|
if ($parameterData['required']) {
|
|
$requiredProperties[] = $parameterName;
|
|
}
|
|
}
|
|
}
|
|
$definition = ['type' => 'object'];
|
|
if (isset($typeData['documentation'])) {
|
|
$definition['description'] = $typeData['documentation'];
|
|
}
|
|
if (!empty($properties)) {
|
|
$definition['properties'] = $properties;
|
|
}
|
|
if (!empty($requiredProperties)) {
|
|
$definition['required'] = $requiredProperties;
|
|
}
|
|
return $definition;
|
|
}
|
|
|
|
/**
|
|
* Get definitions
|
|
*
|
|
* @return array
|
|
* Todo: create interface for error response
|
|
*/
|
|
protected function getDefinitions()
|
|
{
|
|
return array_merge(
|
|
[
|
|
'error-response' => [
|
|
'type' => 'object',
|
|
'properties' => [
|
|
'message' => [
|
|
'type' => 'string',
|
|
'description' => 'Error message',
|
|
],
|
|
'errors' => [
|
|
'$ref' => '#/definitions/error-errors',
|
|
],
|
|
'code' => [
|
|
'type' => 'integer',
|
|
'description' => 'Error code',
|
|
],
|
|
'parameters' => [
|
|
'$ref' => '#/definitions/error-parameters',
|
|
],
|
|
'trace' => [
|
|
'type' => 'string',
|
|
'description' => 'Stack trace',
|
|
],
|
|
],
|
|
'required' => ['message'],
|
|
],
|
|
'error-errors' => [
|
|
'type' => 'array',
|
|
'description' => 'Errors list',
|
|
'items' => [
|
|
'$ref' => '#/definitions/error-errors-item',
|
|
],
|
|
],
|
|
'error-errors-item' => [
|
|
'type' => 'object',
|
|
'description' => 'Error details',
|
|
'properties' => [
|
|
'message' => [
|
|
'type' => 'string',
|
|
'description' => 'Error message',
|
|
],
|
|
'parameters' => [
|
|
'$ref' => '#/definitions/error-parameters',
|
|
],
|
|
],
|
|
],
|
|
'error-parameters' => [
|
|
'type' => 'array',
|
|
'description' => 'Error parameters list',
|
|
'items' => [
|
|
'$ref' => '#/definitions/error-parameters-item',
|
|
],
|
|
],
|
|
'error-parameters-item' => [
|
|
'type' => 'object',
|
|
'description' => 'Error parameters item',
|
|
'properties' => [
|
|
'resources' => [
|
|
'type' => 'string',
|
|
'description' => 'ACL resource',
|
|
],
|
|
'fieldName' => [
|
|
'type' => 'string',
|
|
'description' => 'Missing or invalid field name'
|
|
],
|
|
'fieldValue' => [
|
|
'type' => 'string',
|
|
'description' => 'Incorrect field value'
|
|
],
|
|
],
|
|
],
|
|
],
|
|
$this->snakeCaseDefinitions($this->definitions)
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Converts definitions' properties array to snake_case.
|
|
*
|
|
* @param array $definitions
|
|
* @return array
|
|
*/
|
|
private function snakeCaseDefinitions($definitions)
|
|
{
|
|
foreach ($definitions as $name => $vals) {
|
|
if (!empty($vals['properties'])) {
|
|
$definitions[$name]['properties'] = $this->convertArrayToSnakeCase($vals['properties']);
|
|
}
|
|
if (!empty($vals['required'])) {
|
|
$snakeCaseRequired = [];
|
|
foreach ($vals['required'] as $requiredProperty) {
|
|
$snakeCaseRequired[] = SimpleDataObjectConverter::camelCaseToSnakeCase($requiredProperty);
|
|
}
|
|
$definitions[$name]['required'] = $snakeCaseRequired;
|
|
}
|
|
}
|
|
return $definitions;
|
|
}
|
|
|
|
/**
|
|
* Converts associative array's key names from camelCase to snake_case, recursively.
|
|
*
|
|
* @param array $properties
|
|
* @return array
|
|
*/
|
|
private function convertArrayToSnakeCase($properties)
|
|
{
|
|
foreach ($properties as $name => $value) {
|
|
$snakeCaseName = SimpleDataObjectConverter::camelCaseToSnakeCase($name);
|
|
if (is_array($value)) {
|
|
$value = $this->convertArrayToSnakeCase($value);
|
|
}
|
|
unset($properties[$name]);
|
|
$properties[$snakeCaseName] = $value;
|
|
}
|
|
return $properties;
|
|
}
|
|
|
|
/**
|
|
* Get definition reference
|
|
*
|
|
* @param string $typeName
|
|
* @return string
|
|
*/
|
|
protected function getDefinitionReference($typeName)
|
|
{
|
|
return '#/definitions/' . $this->toLowerCaseDashSeparated($typeName);
|
|
}
|
|
|
|
/**
|
|
* Get the CamelCased type name in 'hyphen-separated-lowercase-words' format
|
|
*
|
|
* E.g. test-module5-v1-entity-all-soap-and-rest
|
|
*
|
|
* @param string $typeName
|
|
* @return string
|
|
*/
|
|
protected function toLowerCaseDashSeparated($typeName)
|
|
{
|
|
return strtolower(preg_replace('/(.)([A-Z])/', "$1-$2", $typeName));
|
|
}
|
|
|
|
/**
|
|
* Check if definition exists
|
|
*
|
|
* @param string $typeName
|
|
* @return bool
|
|
*/
|
|
protected function isDefinitionExists($typeName)
|
|
{
|
|
return isset($this->definitions[$this->toLowerCaseDashSeparated($typeName)]);
|
|
}
|
|
|
|
/**
|
|
* Create and add custom attribute types
|
|
*
|
|
* @return void
|
|
*/
|
|
protected function addCustomAttributeTypes()
|
|
{
|
|
foreach ($this->serviceTypeList->getDataTypes() as $customAttributeClass) {
|
|
$this->typeProcessor->register($customAttributeClass);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get service metadata
|
|
*
|
|
* @param string $serviceName
|
|
* @return array
|
|
*/
|
|
protected function getServiceMetadata($serviceName)
|
|
{
|
|
return $this->serviceMetadata->getRouteMetadata($serviceName);
|
|
}
|
|
|
|
/**
|
|
* Get the simple type supported by Swagger, or false if type is not simple
|
|
*
|
|
* @param string $type
|
|
* @return bool|string
|
|
*/
|
|
protected function getSimpleType($type)
|
|
{
|
|
if (array_key_exists($type, $this->simpleTypeList)) {
|
|
return $this->simpleTypeList[$type];
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Return the parameter names to describe a given parameter, mapped to the respective type
|
|
*
|
|
* Query parameters may be complex types, and multiple parameters will be listed in the schema to outline
|
|
* the structure of the type.
|
|
*
|
|
* @param string $name
|
|
* @param string $type
|
|
* @param string $description
|
|
* @param string $prefix
|
|
* @return string[]
|
|
*/
|
|
protected function getQueryParamNames($name, $type, $description, $prefix = '')
|
|
{
|
|
if ($this->typeProcessor->isTypeSimple($type)) {
|
|
// Primitive type or array of primitive types
|
|
return [
|
|
$this->handlePrimitive($name, $prefix) => [
|
|
'type' => ($type && substr($type, -2) === '[]') ? $type : $this->getSimpleType($type),
|
|
'description' => $description
|
|
]
|
|
];
|
|
}
|
|
if ($this->typeProcessor->isArrayType($type)) {
|
|
// Array of complex type
|
|
$arrayType = $type !== null ? substr($type, 0, -2) : '';
|
|
return $this->handleComplex($name, $arrayType, $prefix, true);
|
|
} else {
|
|
// Complex type
|
|
return $this->handleComplex($name, $type, $prefix, false);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Recursively generate the query param names for a complex type
|
|
*
|
|
* @param string $name
|
|
* @param string $type
|
|
* @param string $prefix
|
|
* @param bool $isArray
|
|
* @return string[]
|
|
*/
|
|
private function handleComplex($name, $type, $prefix, $isArray)
|
|
{
|
|
$typeData = $this->typeProcessor->getTypeData($type);
|
|
$parameters = $typeData['parameters'] ?? [];
|
|
$queryNames = [];
|
|
foreach ($parameters as $subParameterName => $subParameterInfo) {
|
|
$subParameterType = $subParameterInfo['type'];
|
|
$subParameterDescription = $subParameterInfo['documentation'] ?? null;
|
|
$subPrefix = $prefix
|
|
? $prefix . '[' . $name . ']'
|
|
: $name;
|
|
if ($isArray) {
|
|
$subPrefix .= self::ARRAY_SIGNIFIER;
|
|
}
|
|
$queryNames[] = $this->getQueryParamNames(
|
|
$subParameterName,
|
|
$subParameterType,
|
|
$subParameterDescription,
|
|
$subPrefix
|
|
);
|
|
}
|
|
|
|
return array_merge([], ...$queryNames);
|
|
}
|
|
|
|
/**
|
|
* Generate the query param name for a primitive type
|
|
*
|
|
* @param string $name
|
|
* @param string $prefix
|
|
* @return string
|
|
*/
|
|
private function handlePrimitive($name, $prefix)
|
|
{
|
|
return $prefix
|
|
? $prefix . '[' . $name . ']'
|
|
: $name;
|
|
}
|
|
|
|
/**
|
|
* Convert path parameters from :param to {param}
|
|
*
|
|
* @param string $uri
|
|
* @return string
|
|
*/
|
|
private function convertPathParams($uri)
|
|
{
|
|
$parts = explode('/', $uri);
|
|
$count = count($parts);
|
|
for ($i=0; $i < $count; $i++) {
|
|
if (strpos($parts[$i] ?? '', ':') === 0) {
|
|
$parts[$i] = '{' . substr($parts[$i] ?? '', 1) . '}';
|
|
}
|
|
}
|
|
return implode('/', $parts);
|
|
}
|
|
|
|
/**
|
|
* Generate method path parameter
|
|
*
|
|
* @param string $parameterName
|
|
* @param array $parameterInfo
|
|
* @param string $description
|
|
* @return string[]
|
|
*/
|
|
private function generateMethodPathParameter($parameterName, $parameterInfo, $description)
|
|
{
|
|
$param = [
|
|
'name' => $parameterName,
|
|
'in' => 'path',
|
|
'type' => $this->getSimpleType($parameterInfo['type']),
|
|
'required' => true
|
|
];
|
|
if ($description) {
|
|
$param['description'] = $description;
|
|
return $param;
|
|
}
|
|
return $param;
|
|
}
|
|
|
|
/**
|
|
* Generate method query parameters
|
|
*
|
|
* @param string $parameterName
|
|
* @param array $parameterInfo
|
|
* @param string $description
|
|
* @param array $parameters
|
|
* @return array
|
|
*/
|
|
private function generateMethodQueryParameters($parameterName, $parameterInfo, $description, $parameters)
|
|
{
|
|
$queryParams = $this->getQueryParamNames($parameterName, $parameterInfo['type'], $description);
|
|
if (count($queryParams) === 1) {
|
|
// handle simple query parameter (includes the 'required' field)
|
|
$parameters[] = $this->createQueryParam(
|
|
$parameterName,
|
|
$parameterInfo['type'],
|
|
$description,
|
|
$parameterInfo['required']
|
|
);
|
|
} else {
|
|
/**
|
|
* Complex query parameters are represented by a set of names which describes the object's fields.
|
|
*
|
|
* Omits the 'required' field.
|
|
*/
|
|
foreach ($queryParams as $name => $queryParamInfo) {
|
|
$parameters[] = $this->createQueryParam(
|
|
$name,
|
|
$queryParamInfo['type'],
|
|
$queryParamInfo['description']
|
|
);
|
|
}
|
|
}
|
|
return $parameters;
|
|
}
|
|
|
|
/**
|
|
* Generate body schema
|
|
*
|
|
* @param string $parameterName
|
|
* @param array $parameterInfo
|
|
* @param string $description
|
|
* @param array $bodySchema
|
|
* @return array
|
|
*/
|
|
private function generateBodySchema($parameterName, $parameterInfo, $description, $bodySchema)
|
|
{
|
|
$required = isset($parameterInfo['required']) ? $parameterInfo['required'] : null;
|
|
/*
|
|
* There can only be one body parameter, multiple PHP parameters are represented as different
|
|
* properties of the body.
|
|
*/
|
|
if ($required) {
|
|
$bodySchema['required'][] = $parameterName;
|
|
}
|
|
$bodySchema['properties'][$parameterName] = $this->getObjectSchema(
|
|
$parameterInfo['type'],
|
|
$description
|
|
);
|
|
$bodySchema['type'] = 'object';
|
|
|
|
/*
|
|
* Make sure we have a proper XML wrapper for request parameters for the XML format.
|
|
*/
|
|
if (!isset($bodySchema['xml']) || !is_array($bodySchema['xml'])) {
|
|
$bodySchema['xml'] = [];
|
|
}
|
|
if (!isset($bodySchema['xml']['name']) || empty($bodySchema['xml']['name'])) {
|
|
$bodySchema['xml']['name'] = self::XML_SCHEMA_PARAMWRAPPER;
|
|
}
|
|
|
|
return $bodySchema;
|
|
}
|
|
|
|
/**
|
|
* Generate method 200 response
|
|
*
|
|
* @param array $parameters
|
|
* @param array $responses
|
|
* @return array
|
|
*/
|
|
private function generateMethodSuccessResponse($parameters, $responses)
|
|
{
|
|
if (isset($parameters['result']) && is_array($parameters['result'])) {
|
|
$description = '';
|
|
if (isset($parameters['result']['documentation'])) {
|
|
$description = $parameters['result']['documentation'];
|
|
}
|
|
$schema = [];
|
|
if (isset($parameters['result']['type'])) {
|
|
$schema = $this->getObjectSchema($parameters['result']['type'], $description);
|
|
}
|
|
|
|
// Some methods may have a non-standard HTTP success code.
|
|
$specificResponseData = $parameters['result']['response_codes']['success'] ?? [];
|
|
// Default HTTP success code to 200 if nothing has been supplied.
|
|
$responseCode = $specificResponseData['code'] ?? '200';
|
|
// Default HTTP response status to 200 Success if nothing has been supplied.
|
|
$responseDescription = $specificResponseData['description'] ?? '200 Success.';
|
|
|
|
$responses[$responseCode]['description'] = $responseDescription;
|
|
if (!empty($schema)) {
|
|
$responses[$responseCode]['schema'] = $schema;
|
|
}
|
|
}
|
|
return $responses;
|
|
}
|
|
|
|
/**
|
|
* Generate method exception error responses
|
|
*
|
|
* @param array $exceptionClass
|
|
* @param array $responses
|
|
* @return array
|
|
*/
|
|
private function generateMethodExceptionErrorResponses($exceptionClass, $responses)
|
|
{
|
|
$httpCode = '500';
|
|
$description = 'Internal Server error';
|
|
if (is_subclass_of($exceptionClass, \Magento\Framework\Exception\LocalizedException::class)) {
|
|
// Map HTTP codes for LocalizedExceptions according to exception type
|
|
if (is_subclass_of($exceptionClass, \Magento\Framework\Exception\NoSuchEntityException::class)) {
|
|
$httpCode = WebapiException::HTTP_NOT_FOUND;
|
|
$description = '404 Not Found';
|
|
} elseif (is_subclass_of($exceptionClass, \Magento\Framework\Exception\AuthorizationException::class)
|
|
|| is_subclass_of($exceptionClass, \Magento\Framework\Exception\AuthenticationException::class)
|
|
) {
|
|
$httpCode = WebapiException::HTTP_UNAUTHORIZED;
|
|
$description = self::UNAUTHORIZED_DESCRIPTION;
|
|
} else {
|
|
// Input, Expired, InvalidState exceptions will fall to here
|
|
$httpCode = WebapiException::HTTP_BAD_REQUEST;
|
|
$description = '400 Bad Request';
|
|
}
|
|
}
|
|
$responses[$httpCode]['description'] = $description;
|
|
$responses[$httpCode]['schema']['$ref'] = self::ERROR_SCHEMA;
|
|
|
|
return $responses;
|
|
}
|
|
|
|
/**
|
|
* Retrieve a list of services visible to current user.
|
|
*
|
|
* @return string[]
|
|
*/
|
|
public function getListOfServices()
|
|
{
|
|
$listOfAllowedServices = [];
|
|
foreach ($this->serviceMetadata->getServicesConfig() as $serviceName => $service) {
|
|
foreach ($service[ServiceMetadata::KEY_SERVICE_METHODS] as $method) {
|
|
if ($this->authorization->isAllowed($method[ServiceMetadata::KEY_ACL_RESOURCES])) {
|
|
$listOfAllowedServices[] = $serviceName;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
return $listOfAllowedServices;
|
|
}
|
|
}
|