ServiceMetadata.php 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. <?php
  2. /**
  3. * Copyright © Magento, Inc. All rights reserved.
  4. * See COPYING.txt for license details.
  5. */
  6. declare(strict_types=1);
  7. namespace Magento\WebapiAsync\Plugin;
  8. use Magento\Webapi\Model\Config\Converter as WebapiConverter;
  9. use Magento\AsynchronousOperations\Api\Data\AsyncResponseInterface;
  10. use Magento\WebapiAsync\Controller\Rest\AsynchronousSchemaRequestProcessor;
  11. use Magento\WebapiAsync\Model\ServiceConfig\Converter;
  12. class ServiceMetadata
  13. {
  14. /**
  15. * @var \Magento\Webapi\Model\Config
  16. */
  17. private $webapiConfig;
  18. /**
  19. * @var \Magento\WebapiAsync\Model\ServiceConfig
  20. */
  21. private $serviceConfig;
  22. /**
  23. * @var AsynchronousSchemaRequestProcessor
  24. */
  25. private $asynchronousSchemaRequestProcessor;
  26. /**
  27. * @var \Magento\Framework\Webapi\Rest\Request
  28. */
  29. private $request;
  30. /**
  31. * @var \Magento\Framework\Reflection\TypeProcessor
  32. */
  33. private $typeProcessor;
  34. /**
  35. * @var array
  36. */
  37. private $responseDefinitionReplacement;
  38. /**
  39. * @var array
  40. */
  41. private $synchronousOnlyHttpMethods = [
  42. 'GET'
  43. ];
  44. /**
  45. * ServiceMetadata constructor.
  46. *
  47. * @param \Magento\Webapi\Model\Config $webapiConfig
  48. * @param \Magento\WebapiAsync\Model\ServiceConfig $serviceConfig
  49. * @param AsynchronousSchemaRequestProcessor $asynchronousSchemaRequestProcessor
  50. */
  51. public function __construct(
  52. \Magento\Webapi\Model\Config $webapiConfig,
  53. \Magento\WebapiAsync\Model\ServiceConfig $serviceConfig,
  54. \Magento\Framework\Webapi\Rest\Request $request,
  55. AsynchronousSchemaRequestProcessor $asynchronousSchemaRequestProcessor,
  56. \Magento\Framework\Reflection\TypeProcessor $typeProcessor
  57. ) {
  58. $this->webapiConfig = $webapiConfig;
  59. $this->serviceConfig = $serviceConfig;
  60. $this->request = $request;
  61. $this->asynchronousSchemaRequestProcessor = $asynchronousSchemaRequestProcessor;
  62. $this->typeProcessor = $typeProcessor;
  63. }
  64. /**
  65. * @param \Magento\Webapi\Model\ServiceMetadata $subject
  66. * @param array $result
  67. * @return array
  68. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  69. */
  70. public function afterGetServicesConfig(\Magento\Webapi\Model\ServiceMetadata $subject, array $result)
  71. {
  72. if ($this->asynchronousSchemaRequestProcessor->canProcess($this->request)) {
  73. $synchronousOnlyServiceMethods = $this->getSynchronousOnlyServiceMethods($subject);
  74. // Replace all results with the async response schema
  75. foreach ($result as $serviceName => $serviceData) {
  76. // Check all of the methods on the service
  77. foreach ($serviceData[WebapiConverter::KEY_METHODS] as $methodName => $methodData) {
  78. // Exclude service methods that are marked as synchronous only
  79. if ($this->isServiceMethodSynchronousOnly(
  80. $serviceName,
  81. $methodName,
  82. $synchronousOnlyServiceMethods
  83. )) {
  84. $this->removeServiceMethodDefinition($result, $serviceName, $methodName);
  85. } else {
  86. $this->replaceResponseDefinition($result, $serviceName, $methodName);
  87. }
  88. }
  89. }
  90. }
  91. return $result;
  92. }
  93. /**
  94. * @param $serviceName
  95. * @param $methodName
  96. * @param array $synchronousOnlyServiceMethods
  97. * @return bool
  98. */
  99. private function isServiceMethodSynchronousOnly($serviceName, $methodName, array $synchronousOnlyServiceMethods)
  100. {
  101. return isset($synchronousOnlyServiceMethods[$serviceName][$methodName]);
  102. }
  103. /**
  104. * @param string $serviceName
  105. * @return array
  106. */
  107. private function getServiceVersions(string $serviceName)
  108. {
  109. $services = $this->webapiConfig->getServices();
  110. return array_keys($services[WebapiConverter::KEY_SERVICES][$serviceName]);
  111. }
  112. /**
  113. * Get a list of all service methods that cannot be executed asynchronously.
  114. *
  115. * @param \Magento\Webapi\Model\ServiceMetadata $serviceMetadata
  116. * @return array
  117. */
  118. private function getSynchronousOnlyServiceMethods(\Magento\Webapi\Model\ServiceMetadata $serviceMetadata)
  119. {
  120. $synchronousOnlyServiceMethods = [];
  121. $services = $this->serviceConfig->getServices()[Converter::KEY_SERVICES] ?? [];
  122. foreach ($services as $service => $serviceData) {
  123. if (!isset($serviceData[Converter::KEY_METHODS])) {
  124. continue;
  125. }
  126. foreach ($serviceData[Converter::KEY_METHODS] as $method => $methodData) {
  127. if ($this->isMethodDataSynchronousOnly($methodData)) {
  128. $this->appendSynchronousOnlyServiceMethodsWithInterface(
  129. $serviceMetadata,
  130. $synchronousOnlyServiceMethods,
  131. $service,
  132. $method
  133. );
  134. }
  135. }
  136. }
  137. return array_merge_recursive(
  138. $synchronousOnlyServiceMethods,
  139. $this->getSynchronousOnlyRoutesAsServiceMethods($serviceMetadata)
  140. );
  141. }
  142. /**
  143. * Get service methods associated with routes that can't be processed as asynchronous.
  144. *
  145. * @param \Magento\Webapi\Model\ServiceMetadata $serviceMetadata
  146. * @return array
  147. * @SuppressWarnings(PHPMD.UnusedLocalVariable)
  148. */
  149. private function getSynchronousOnlyRoutesAsServiceMethods(
  150. \Magento\Webapi\Model\ServiceMetadata $serviceMetadata
  151. ) {
  152. $synchronousOnlyServiceMethods = [];
  153. $serviceRoutes = $this->webapiConfig->getServices()[\Magento\Webapi\Model\Config\Converter::KEY_ROUTES];
  154. foreach ($serviceRoutes as $serviceRoutePath => $serviceRouteMethods) {
  155. foreach ($serviceRouteMethods as $serviceRouteMethod => $serviceRouteMethodData) {
  156. // Check if the HTTP method associated with the route is not able to be async.
  157. if (in_array(strtoupper($serviceRouteMethod), $this->synchronousOnlyHttpMethods)) {
  158. $this->appendSynchronousOnlyServiceMethodsWithInterface(
  159. $serviceMetadata,
  160. $synchronousOnlyServiceMethods,
  161. $serviceRouteMethodData[WebapiConverter::KEY_SERVICE][WebapiConverter::KEY_SERVICE_CLASS],
  162. $serviceRouteMethodData[WebapiConverter::KEY_SERVICE][WebapiConverter::KEY_SERVICE_METHOD]
  163. );
  164. }
  165. }
  166. }
  167. return $synchronousOnlyServiceMethods;
  168. }
  169. /**
  170. * @param \Magento\Webapi\Model\ServiceMetadata $serviceMetadata
  171. * @param array $synchronousOnlyServiceMethods
  172. * @param $serviceInterface
  173. * @param $serviceMethod
  174. */
  175. private function appendSynchronousOnlyServiceMethodsWithInterface(
  176. \Magento\Webapi\Model\ServiceMetadata $serviceMetadata,
  177. array &$synchronousOnlyServiceMethods,
  178. $serviceInterface,
  179. $serviceMethod
  180. ) {
  181. foreach ($this->getServiceVersions($serviceInterface) as $serviceVersion) {
  182. $serviceName = $serviceMetadata->getServiceName($serviceInterface, $serviceVersion);
  183. if (!array_key_exists($serviceName, $synchronousOnlyServiceMethods)) {
  184. $synchronousOnlyServiceMethods[$serviceName] = [];
  185. }
  186. $synchronousOnlyServiceMethods[$serviceName][$serviceMethod] = true;
  187. }
  188. }
  189. /**
  190. * @param array $result
  191. * @param $serviceName
  192. * @param $methodName
  193. */
  194. private function removeServiceMethodDefinition(array &$result, $serviceName, $methodName)
  195. {
  196. unset($result[$serviceName][WebapiConverter::KEY_METHODS][$methodName]);
  197. // Remove the service altogether if there is no methods left.
  198. if (count($result[$serviceName][WebapiConverter::KEY_METHODS]) === 0) {
  199. unset($result[$serviceName]);
  200. }
  201. }
  202. /**
  203. * @param array $result
  204. * @param $serviceName
  205. * @param $methodName
  206. */
  207. private function replaceResponseDefinition(array &$result, $serviceName, $methodName)
  208. {
  209. if (isset($result[$serviceName][WebapiConverter::KEY_METHODS][$methodName]['interface']['out'])) {
  210. $replacement = $this->getResponseDefinitionReplacement();
  211. $result[$serviceName][WebapiConverter::KEY_METHODS][$methodName]['interface']['out'] = $replacement;
  212. }
  213. }
  214. /**
  215. * Check if a method on the given service is defined as synchronous only using XML.
  216. *
  217. * @param array $methodData
  218. * @return bool
  219. */
  220. private function isMethodDataSynchronousOnly(array $methodData)
  221. {
  222. if (!isset($methodData[Converter::KEY_SYNCHRONOUS_INVOCATION_ONLY])) {
  223. return false;
  224. }
  225. return $methodData[Converter::KEY_SYNCHRONOUS_INVOCATION_ONLY];
  226. }
  227. /**
  228. * @return array
  229. */
  230. private function getResponseDefinitionReplacement()
  231. {
  232. if ($this->responseDefinitionReplacement === null) {
  233. $this->responseDefinitionReplacement = [
  234. 'parameters' => [
  235. 'result' => [
  236. 'type' => $this->typeProcessor->register(AsyncResponseInterface::class),
  237. 'documentation' => 'Returns response information for the asynchronous request.',
  238. 'required' => true,
  239. 'response_codes' => [
  240. 'success' => [
  241. 'code' => '202',
  242. 'description' => '202 Accepted.'
  243. ]
  244. ]
  245. ]
  246. ]
  247. ];
  248. }
  249. return $this->responseDefinitionReplacement;
  250. }
  251. }