123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294 |
- <?php
- namespace GuzzleHttp\Command\Guzzle;
- use GuzzleHttp\Command\CommandInterface;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\BodyLocation;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\HeaderLocation;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\JsonLocation;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\ReasonPhraseLocation;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\ResponseLocationInterface;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\StatusCodeLocation;
- use GuzzleHttp\Command\Guzzle\ResponseLocation\XmlLocation;
- use GuzzleHttp\Command\Result;
- use GuzzleHttp\Command\ResultInterface;
- use Psr\Http\Message\RequestInterface;
- use Psr\Http\Message\ResponseInterface;
- /**
- * Handler used to create response models based on an HTTP response and
- * a service description.
- *
- * Response location visitors are registered with this Handler to handle
- * locations (e.g., 'xml', 'json', 'header'). All of the locations of a response
- * model that will be visited first have their ``before`` method triggered.
- * After the before method is called on every visitor that will be walked, each
- * visitor is triggered using the ``visit()`` method. After all of the visitors
- * are visited, the ``after()`` method is called on each visitor. This is the
- * place in which you should handle things like additionalProperties with
- * custom locations (i.e., this is how it is handled in the JSON visitor).
- */
- class Deserializer
- {
- /** @var ResponseLocationInterface[] $responseLocations */
- private $responseLocations;
- /** @var DescriptionInterface $description */
- private $description;
- /** @var boolean $process */
- private $process;
- /**
- * @param DescriptionInterface $description
- * @param bool $process
- * @param ResponseLocationInterface[] $responseLocations Extra response locations
- */
- public function __construct(
- DescriptionInterface $description,
- $process,
- array $responseLocations = []
- ) {
- static $defaultResponseLocations;
- if (!$defaultResponseLocations) {
- $defaultResponseLocations = [
- 'body' => new BodyLocation(),
- 'header' => new HeaderLocation(),
- 'reasonPhrase' => new ReasonPhraseLocation(),
- 'statusCode' => new StatusCodeLocation(),
- 'xml' => new XmlLocation(),
- 'json' => new JsonLocation(),
- ];
- }
- $this->responseLocations = $responseLocations + $defaultResponseLocations;
- $this->description = $description;
- $this->process = $process;
- }
- /**
- * Deserialize the response into the specified result representation
- *
- * @param ResponseInterface $response
- * @param RequestInterface|null $request
- * @param CommandInterface $command
- * @return Result|ResultInterface|void|ResponseInterface
- */
- public function __invoke(ResponseInterface $response, RequestInterface $request, CommandInterface $command)
- {
- // If the user don't want to process the result, just return the plain response here
- if ($this->process === false) {
- return $response;
- }
- $name = $command->getName();
- $operation = $this->description->getOperation($name);
- $this->handleErrorResponses($response, $request, $command, $operation);
- // Add a default Model as the result if no matching schema was found
- if (!($modelName = $operation->getResponseModel())) {
- // Not sure if this should be empty or contains the response.
- // Decided to do it how it was in the old version for now.
- return new Result();
- }
- $model = $operation->getServiceDescription()->getModel($modelName);
- if (!$model) {
- throw new \RuntimeException("Unknown model: {$modelName}");
- }
- return $this->visit($model, $response);
- }
- /**
- * Handles visit() and after() methods of the Response locations
- *
- * @param Parameter $model
- * @param ResponseInterface $response
- * @return Result|ResultInterface|void
- */
- protected function visit(Parameter $model, ResponseInterface $response)
- {
- $result = new Result();
- $context = ['visitors' => []];
- if ($model->getType() === 'object') {
- $result = $this->visitOuterObject($model, $result, $response, $context);
- } elseif ($model->getType() === 'array') {
- $result = $this->visitOuterArray($model, $result, $response, $context);
- } else {
- throw new \InvalidArgumentException('Invalid response model: ' . $model->getType());
- }
- // Call the after() method of each found visitor
- /** @var ResponseLocationInterface $visitor */
- foreach ($context['visitors'] as $visitor) {
- $result = $visitor->after($result, $response, $model);
- }
- return $result;
- }
- /**
- * Handles the before() method of Response locations
- *
- * @param string $location
- * @param Parameter $model
- * @param ResultInterface $result
- * @param ResponseInterface $response
- * @param array $context
- * @return ResultInterface
- */
- private function triggerBeforeVisitor(
- $location,
- Parameter $model,
- ResultInterface $result,
- ResponseInterface $response,
- array &$context
- ) {
- if (!isset($this->responseLocations[$location])) {
- throw new \RuntimeException("Unknown location: $location");
- }
- $context['visitors'][$location] = $this->responseLocations[$location];
- $result = $this->responseLocations[$location]->before(
- $result,
- $response,
- $model
- );
- return $result;
- }
- /**
- * Visits the outer object
- *
- * @param Parameter $model
- * @param ResultInterface $result
- * @param ResponseInterface $response
- * @param array $context
- * @return ResultInterface
- */
- private function visitOuterObject(
- Parameter $model,
- ResultInterface $result,
- ResponseInterface $response,
- array &$context
- ) {
- $parentLocation = $model->getLocation();
- // If top-level additionalProperties is a schema, then visit it
- $additional = $model->getAdditionalProperties();
- if ($additional instanceof Parameter) {
- // Use the model location if none set on additionalProperties.
- $location = $additional->getLocation() ?: $parentLocation;
- $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
- }
- // Use 'location' from all individual defined properties, but fall back
- // to the model location if no per-property location is set. Collect
- // the properties that need to be visited into an array.
- $visitProperties = [];
- foreach ($model->getProperties() as $schema) {
- $location = $schema->getLocation() ?: $parentLocation;
- if ($location) {
- $visitProperties[] = [$location, $schema];
- // Trigger the before method on each unique visitor location
- if (!isset($context['visitors'][$location])) {
- $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
- }
- }
- }
- // Actually visit each response element
- foreach ($visitProperties as $property) {
- $result = $this->responseLocations[$property[0]]->visit($result, $response, $property[1]);
- }
- return $result;
- }
- /**
- * Visits the outer array
- *
- * @param Parameter $model
- * @param ResultInterface $result
- * @param ResponseInterface $response
- * @param array $context
- * @return ResultInterface|void
- */
- private function visitOuterArray(
- Parameter $model,
- ResultInterface $result,
- ResponseInterface $response,
- array &$context
- ) {
- // Use 'location' defined on the top of the model
- if (!($location = $model->getLocation())) {
- return;
- }
- // Trigger the before method on each unique visitor location
- if (!isset($context['visitors'][$location])) {
- $result = $this->triggerBeforeVisitor($location, $model, $result, $response, $context);
- }
- // Visit each item in the response
- $result = $this->responseLocations[$location]->visit($result, $response, $model);
- return $result;
- }
- /**
- * Reads the "errorResponses" from commands, and trigger appropriate exceptions
- *
- * In order for the exception to be properly triggered, all your exceptions must be instance
- * of "GuzzleHttp\Command\Exception\CommandException". If that's not the case, your exceptions will be wrapped
- * around a CommandException
- *
- * @param ResponseInterface $response
- * @param RequestInterface $request
- * @param CommandInterface $command
- * @param Operation $operation
- */
- protected function handleErrorResponses(
- ResponseInterface $response,
- RequestInterface $request,
- CommandInterface $command,
- Operation $operation
- ) {
- $errors = $operation->getErrorResponses();
- // We iterate through each errors in service description. If the descriptor contains both a phrase and
- // status code, there must be an exact match of both. Otherwise, a match of status code is enough
- $bestException = null;
- foreach ($errors as $error) {
- $code = (int) $error['code'];
- if ($response->getStatusCode() !== $code) {
- continue;
- }
- if (isset($error['phrase']) && ! ($error['phrase'] === $response->getReasonPhrase())) {
- continue;
- }
- $bestException = $error['class'];
- // If there is an exact match of phrase + code, then we cannot find a more specialized exception in
- // the array, so we can break early instead of iterating the remaining ones
- if (isset($error['phrase'])) {
- break;
- }
- }
- if (null !== $bestException) {
- throw new $bestException($response->getReasonPhrase(), $command, null, $request, $response);
- }
- // If we reach here, no exception could be match from descriptor, and Guzzle exception will propagate if
- // option "http_errors" is set to true, which is the default setting.
- }
- }
|