RestSerializer.php 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225
  1. <?php
  2. namespace Aws\Api\Serializer;
  3. use Aws\Api\MapShape;
  4. use Aws\Api\Service;
  5. use Aws\Api\Operation;
  6. use Aws\Api\Shape;
  7. use Aws\Api\StructureShape;
  8. use Aws\Api\TimestampShape;
  9. use Aws\CommandInterface;
  10. use GuzzleHttp\Psr7;
  11. use GuzzleHttp\Psr7\Uri;
  12. use GuzzleHttp\Psr7\UriResolver;
  13. use Psr\Http\Message\RequestInterface;
  14. /**
  15. * Serializes HTTP locations like header, uri, payload, etc...
  16. * @internal
  17. */
  18. abstract class RestSerializer
  19. {
  20. /** @var Service */
  21. private $api;
  22. /** @var Psr7\Uri */
  23. private $endpoint;
  24. /**
  25. * @param Service $api Service API description
  26. * @param string $endpoint Endpoint to connect to
  27. */
  28. public function __construct(Service $api, $endpoint)
  29. {
  30. $this->api = $api;
  31. $this->endpoint = Psr7\uri_for($endpoint);
  32. }
  33. /**
  34. * @param CommandInterface $command Command to serialized
  35. *
  36. * @return RequestInterface
  37. */
  38. public function __invoke(CommandInterface $command)
  39. {
  40. $operation = $this->api->getOperation($command->getName());
  41. $args = $command->toArray();
  42. $opts = $this->serialize($operation, $args);
  43. $uri = $this->buildEndpoint($operation, $args, $opts);
  44. return new Psr7\Request(
  45. $operation['http']['method'],
  46. $uri,
  47. isset($opts['headers']) ? $opts['headers'] : [],
  48. isset($opts['body']) ? $opts['body'] : null
  49. );
  50. }
  51. /**
  52. * Modifies a hash of request options for a payload body.
  53. *
  54. * @param StructureShape $member Member to serialize
  55. * @param array $value Value to serialize
  56. * @param array $opts Request options to modify.
  57. */
  58. abstract protected function payload(
  59. StructureShape $member,
  60. array $value,
  61. array &$opts
  62. );
  63. private function serialize(Operation $operation, array $args)
  64. {
  65. $opts = [];
  66. $input = $operation->getInput();
  67. // Apply the payload trait if present
  68. if ($payload = $input['payload']) {
  69. $this->applyPayload($input, $payload, $args, $opts);
  70. }
  71. foreach ($args as $name => $value) {
  72. if ($input->hasMember($name)) {
  73. $member = $input->getMember($name);
  74. $location = $member['location'];
  75. if (!$payload && !$location) {
  76. $bodyMembers[$name] = $value;
  77. } elseif ($location == 'header') {
  78. $this->applyHeader($name, $member, $value, $opts);
  79. } elseif ($location == 'querystring') {
  80. $this->applyQuery($name, $member, $value, $opts);
  81. } elseif ($location == 'headers') {
  82. $this->applyHeaderMap($name, $member, $value, $opts);
  83. }
  84. }
  85. }
  86. if (isset($bodyMembers)) {
  87. $this->payload($operation->getInput(), $bodyMembers, $opts);
  88. }
  89. return $opts;
  90. }
  91. private function applyPayload(StructureShape $input, $name, array $args, array &$opts)
  92. {
  93. if (!isset($args[$name])) {
  94. return;
  95. }
  96. $m = $input->getMember($name);
  97. if ($m['streaming'] ||
  98. ($m['type'] == 'string' || $m['type'] == 'blob')
  99. ) {
  100. // Streaming bodies or payloads that are strings are
  101. // always just a stream of data.
  102. $opts['body'] = Psr7\stream_for($args[$name]);
  103. return;
  104. }
  105. $this->payload($m, $args[$name], $opts);
  106. }
  107. private function applyHeader($name, Shape $member, $value, array &$opts)
  108. {
  109. if ($member->getType() === 'timestamp') {
  110. $timestampFormat = !empty($member['timestampFormat'])
  111. ? $member['timestampFormat']
  112. : 'rfc822';
  113. $value = TimestampShape::format($value, $timestampFormat);
  114. }
  115. if ($member['jsonvalue']) {
  116. $value = json_encode($value);
  117. if (empty($value) && JSON_ERROR_NONE !== json_last_error()) {
  118. throw new \InvalidArgumentException('Unable to encode the provided value'
  119. . ' with \'json_encode\'. ' . json_last_error_msg());
  120. }
  121. $value = base64_encode($value);
  122. }
  123. $opts['headers'][$member['locationName'] ?: $name] = $value;
  124. }
  125. /**
  126. * Note: This is currently only present in the Amazon S3 model.
  127. */
  128. private function applyHeaderMap($name, Shape $member, array $value, array &$opts)
  129. {
  130. $prefix = $member['locationName'];
  131. foreach ($value as $k => $v) {
  132. $opts['headers'][$prefix . $k] = $v;
  133. }
  134. }
  135. private function applyQuery($name, Shape $member, $value, array &$opts)
  136. {
  137. if ($member instanceof MapShape) {
  138. $opts['query'] = isset($opts['query']) && is_array($opts['query'])
  139. ? $opts['query'] + $value
  140. : $value;
  141. } elseif ($value !== null) {
  142. $type = $member->getType();
  143. if ($type === 'boolean') {
  144. $value = $value ? 'true' : 'false';
  145. } elseif ($type === 'timestamp') {
  146. $timestampFormat = !empty($member['timestampFormat'])
  147. ? $member['timestampFormat']
  148. : 'iso8601';
  149. $value = TimestampShape::format($value, $timestampFormat);
  150. }
  151. $opts['query'][$member['locationName'] ?: $name] = $value;
  152. }
  153. }
  154. private function buildEndpoint(Operation $operation, array $args, array $opts)
  155. {
  156. $varspecs = [];
  157. // Create an associative array of varspecs used in expansions
  158. foreach ($operation->getInput()->getMembers() as $name => $member) {
  159. if ($member['location'] == 'uri') {
  160. $varspecs[$member['locationName'] ?: $name] =
  161. isset($args[$name])
  162. ? $args[$name]
  163. : null;
  164. }
  165. }
  166. $relative = preg_replace_callback(
  167. '/\{([^\}]+)\}/',
  168. function (array $matches) use ($varspecs) {
  169. $isGreedy = substr($matches[1], -1, 1) == '+';
  170. $k = $isGreedy ? substr($matches[1], 0, -1) : $matches[1];
  171. if (!isset($varspecs[$k])) {
  172. return '';
  173. }
  174. if ($isGreedy) {
  175. return str_replace('%2F', '/', rawurlencode($varspecs[$k]));
  176. }
  177. return rawurlencode($varspecs[$k]);
  178. },
  179. $operation['http']['requestUri']
  180. );
  181. // Add the query string variables or appending to one if needed.
  182. if (!empty($opts['query'])) {
  183. $append = Psr7\build_query($opts['query']);
  184. $relative .= strpos($relative, '?') ? "&{$append}" : "?$append";
  185. }
  186. // If endpoint has path, remove leading '/' to preserve URI resolution.
  187. $path = $this->endpoint->getPath();
  188. if ($path && $relative[0] === '/') {
  189. $relative = substr($relative, 1);
  190. }
  191. // Expand path place holders using Amazon's slightly different URI
  192. // template syntax.
  193. return UriResolver::resolve($this->endpoint, new Uri($relative));
  194. }
  195. }