1
0
mirror of https://github.com/guzzle/guzzle.git synced 2025-02-25 02:22:57 +01:00

Porting request visitors

This commit is contained in:
Michael Dowling 2014-02-23 12:25:37 -08:00
parent aab0e6de47
commit 10f90a0356
11 changed files with 656 additions and 152 deletions

View File

@ -44,6 +44,10 @@ class GuzzleClient implements GuzzleClientInterface
* - process: Specify if HTTP responses are parsed (defaults to true).
* Changing this setting after the client has been created will have
* no effect.
* - request_locations: Associative array of location types mapping to
* RequestLocationInterface objects.
* - response_locations: Associative array of location types mapping to
* ResponseLocationInterface objects.
*/
public function __construct(
ClientInterface $client,
@ -56,21 +60,7 @@ class GuzzleClient implements GuzzleClientInterface
$config['defaults'] = [];
}
$this->config = new Collection($config);
// Use the passed in command factory or a custom factory if provided
$this->commandFactory = isset($config['command_factory'])
? $config['command_factory']
: self::defaultCommandFactory($description);
// Add event listeners based on the configuration option
$emitter = $this->getEmitter();
if (!isset($config['validate']) || $config['validate'] === true) {
$emitter->addSubscriber(new ValidateInput());
}
$emitter->addSubscriber(new PrepareRequest());
if (!isset($config['process']) || $config['process'] === true) {
$emitter->addSubscriber(new ProcessResponse());
}
$this->processConfig();
}
public function __call($name, array $arguments)
@ -172,4 +162,34 @@ class GuzzleClient implements GuzzleClientInterface
return new Command($operation, $args, clone $client->getEmitter());
};
}
/**
* Prepares the client based on the configuration settings of the client.
*/
protected function processConfig()
{
// Use the passed in command factory or a custom factory if provided
$this->commandFactory = isset($config['command_factory'])
? $config['command_factory']
: self::defaultCommandFactory($this->description);
// Add event listeners based on the configuration option
$emitter = $this->getEmitter();
if (!isset($this->config['validate']) ||
$this->config['validate'] === true
) {
$emitter->addSubscriber(new ValidateInput());
}
$emitter->addSubscriber(new PrepareRequest(
$this->config['request_locations'] ?: []
));
if (!isset($config['process']) || $config['process'] === true) {
$emitter->addSubscriber(new ProcessResponse(
$this->config['response_locations'] ?: []
));
}
}
}

View File

@ -0,0 +1,71 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
abstract class AbstractLocation implements RequestLocationInterface
{
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {}
public function after(
RequestInterface $request,
array $context
) {}
/**
* Prepare (filter and set desired name for request item) the value for
* request.
*
* @param mixed $value
* @param Parameter $param
*
* @return array|mixed
*/
protected function prepareValue($value, Parameter $param)
{
return is_array($value)
? $this->resolveRecursively($value, $param)
: $param->filter($value);
}
/**
* Recursively prepare and filter nested values.
*
* @param array $value Value to map
* @param Parameter $param Parameter related to the current key.
*
* @return array Returns the mapped array
*/
protected function resolveRecursively(array $value, Parameter $param)
{
foreach ($value as $name => &$v) {
switch ($param->getType()) {
case 'object':
if ($subParam = $param->getProperty($name)) {
$key = $subParam->getWireName();
$value[$key] = $this->prepareValue($v, $subParam);
if ($name != $key) {
unset($value[$name]);
}
} elseif ($param->getAdditionalProperties() instanceof Parameter) {
$v = $this->prepareValue($v, $param->getAdditionalProperties());
}
break;
case 'array':
if ($items = $param->getItems()) {
$v = $this->prepareValue($v, $items);
}
break;
}
}
return $param->filter($value);
}
}

View File

@ -0,0 +1,22 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream\Stream;
/**
* Adds a body to a request
*/
class BodyLocation extends AbstractLocation
{
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
$request->setBody(Stream::factory($param->filter($value)));
}
}

View File

@ -0,0 +1,21 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
/**
* Request header location
*/
class HeaderLocation extends AbstractLocation
{
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
$request->setHeader($param->getWireName(), $param->filter($value));
}
}

View File

@ -0,0 +1,59 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream\Stream;
/**
* Creates a JSON document
*/
class JsonLocation extends AbstractLocation
{
/** @var bool Whether or not to add a Content-Type header when JSON is found */
protected $jsonContentType;
/** @var \SplObjectStorage Data object for persisting JSON data */
protected $data;
/**
* @param string $contentType Content-Type header to add to the request if
* JSON is added to the body. Pass an empty string to omit.
*/
public function __construct($contentType = 'application/json')
{
$this->jsonContentType = $contentType;
$this->data = new \SplObjectStorage();
}
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
$json = isset($this->data[$context['command']])
? $this->data[$context['command']]
: [];
$json[$param->getWireName()] = $this->prepareValue($value, $param);
$this->data[$context['command']] = $json;
}
public function after(
RequestInterface $request,
array $context
) {
if (!isset($this->data[$context['command']])) {
return;
}
// Don't overwrite the Content-Type if one is set
if ($this->jsonContentType && !$request->hasHeader('Content-Type')) {
$request->setHeader('Content-Type', $this->jsonContentType);
}
$request->setBody(Stream::factory(json_encode($this->data[$context['command']])));
unset($this->data[$context['command']]);
}
}

View File

@ -0,0 +1,30 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Post\PostBodyInterface;
/**
* Adds POST fields to a request
*/
class PostFieldLocation extends AbstractLocation
{
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
$body = $request->getBody();
if (!($body instanceof PostBodyInterface)) {
throw new \RuntimeException('Must be a POST body interface');
}
$body->setField(
$param->getWireName(),
$this->prepValue($value, $param)
);
}
}

View File

@ -0,0 +1,34 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Post\PostBodyInterface;
use GuzzleHttp\Post\PostFileInterface;
use GuzzleHttp\Post\PostFile;
/**
* Adds POST files to a request
*/
class PostFileLocation extends AbstractLocation
{
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
$body = $request->getBody();
if (!($body instanceof PostBodyInterface)) {
throw new \RuntimeException('Must be a POST body interface');
}
$value = $param->filter($value);
if (!($value instanceof PostFileInterface)) {
$value = new PostFile($param->getWireName(), $value);
}
$body->addFile($value);
}
}

View File

@ -0,0 +1,24 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
/**
* Adds query string values to requests
*/
class QueryLocation extends AbstractLocation
{
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
$request->setHeader(
$param->getWireName(),
$this->prepValue($value, $param)
);
}
}

View File

@ -0,0 +1,40 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Message\RequestInterface;
/**
* Handles locations specified in a service description
*/
interface RequestLocationInterface
{
/**
* Visits a location for each top-level parameter
*
* @param RequestInterface $request Request being modified
* @param Parameter $param Parameter being visited
* @param mixed $value Associated value
* @param array $context Associative array containing a client
* and command key.
*/
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
);
/**
* Called when all of the parameters of a command have been visited.
*
* @param RequestInterface $request Request being modified
* @param array $context Associative array containing a client
* and command key.
*/
public function after(
RequestInterface $request,
array $context
);
}

View File

@ -0,0 +1,247 @@
<?php
namespace GuzzleHttp\Service\Guzzle\RequestLocation;
use GuzzleHttp\Service\Guzzle\Description\Operation;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Service\Guzzle\GuzzleCommandInterface;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream\Stream;
/**
* Creates an XML document
*/
class XmlLocation extends AbstractLocation
{
/** @var \SplObjectStorage Data object for persisting XML data */
protected $data;
/** @var \XMLWriter XML writer resource */
protected $writer;
/** @var bool Content-Type header added when XML is found */
protected $contentType;
/**
* @param string $contentType Set to a non-empty string to add a
* Content-Type header to a request if any XML content is added to the
* body. Pass an empty string to disable the addition of the header.
*/
public function __construct($contentType = 'application/xml')
{
$this->contentType = $contentType;
$this->data = new \SplObjectStorage();
}
public function visit(
RequestInterface $request,
Parameter $param,
$value,
array $context
) {
/* @var GuzzleCommandInterface $command */
$command = $context['command'];
$xml = isset($this->data[$command])
? $this->data[$command]
: $this->createRootElement($command->getOperation());
$this->addXml($xml, $param, $value);
$this->data[$command] = $xml;
}
public function after(
RequestInterface $request,
array $context
) {
$xml = null;
/* @var GuzzleCommandInterface $command */
$command = $context['command'];
// If data was found that needs to be serialized, then do so
if (isset($this->data[$command])) {
$xml = $this->finishDocument($this->writer);
unset($this->data[$command]);
} else {
// Check if XML should always be sent for the command
$operation = $command->getOperation();
if ($operation->getData('xmlAllowEmpty')) {
$writer = $this->createRootElement($operation);
$xml = $this->finishDocument($writer);
}
}
if ($xml) {
$request->setBody(Stream::factory($xml));
// Don't overwrite the Content-Type if one is set
if ($this->contentType && !$request->hasHeader('Content-Type')) {
$request->setHeader('Content-Type', $this->contentType);
}
}
}
/**
* Create the root XML element to use with a request
*
* @param Operation $operation Operation object
*
* @return \XMLWriter
*/
protected function createRootElement(Operation $operation)
{
static $defaultRoot = array('name' => 'Request');
// If no root element was specified, then just wrap the XML in 'Request'
$root = $operation->getData('xmlRoot') ?: $defaultRoot;
// Allow the XML declaration to be customized with xmlEncoding
$encoding = $operation->getData('xmlEncoding');
$writer = $this->startDocument($encoding);
$writer->startElement($root['name']);
// Create the wrapping element with no namespaces if no namespaces were present
if (!empty($root['namespaces'])) {
// Create the wrapping element with an array of one or more namespaces
foreach ((array) $root['namespaces'] as $prefix => $uri) {
$nsLabel = 'xmlns';
if (!is_numeric($prefix)) {
$nsLabel .= ':'.$prefix;
}
$writer->writeAttribute($nsLabel, $uri);
}
}
return $writer;
}
/**
* Recursively build the XML body
*
* @param \XMLWriter $writer XML to modify
* @param Parameter $param API Parameter
* @param mixed $value Value to add
*/
protected function addXml(\XMLWriter $writer, Parameter $param, $value)
{
if ($value === null) {
return;
}
$value = $param->filter($value);
$type = $param->getType();
$name = $param->getWireName();
$prefix = null;
$namespace = $param->getData('xmlNamespace');
if (false !== strpos($name, ':')) {
list($prefix, $name) = explode(':', $name, 2);
}
if ($type == 'object' || $type == 'array') {
if (!$param->getData('xmlFlattened')) {
$writer->startElementNS(null, $name, $namespace);
}
if ($param->getType() == 'array') {
$this->addXmlArray($writer, $param, $value);
} elseif ($param->getType() == 'object') {
$this->addXmlObject($writer, $param, $value);
}
if (!$param->getData('xmlFlattened')) {
$writer->endElement();
}
return;
}
if ($param->getData('xmlAttribute')) {
$this->writeAttribute($writer, $prefix, $name, $namespace, $value);
} else {
$this->writeElement($writer, $prefix, $name, $namespace, $value);
}
}
/**
* Write an attribute with namespace if used
*
* @param \XMLWriter $writer XMLWriter instance
* @param string $prefix Namespace prefix if any
* @param string $name Attribute name
* @param string $namespace The uri of the namespace
* @param string $value The attribute content
*/
protected function writeAttribute($writer, $prefix, $name, $namespace, $value)
{
if (empty($namespace)) {
$writer->writeAttribute($name, $value);
} else {
$writer->writeAttributeNS($prefix, $name, $namespace, $value);
}
}
/**
* Write an element with namespace if used
*
* @param \XMLWriter $writer XML writer resource
* @param string $prefix Namespace prefix if any
* @param string $name Element name
* @param string $namespace The uri of the namespace
* @param string $value The element content
*/
protected function writeElement(\XMLWriter $writer, $prefix, $name, $namespace, $value)
{
$writer->startElementNS($prefix, $name, $namespace);
if (strpbrk($value, '<>&')) {
$writer->writeCData($value);
} else {
$writer->writeRaw($value);
}
$writer->endElement();
}
/**
* Create a new xml writer and start a document
*
* @param string $encoding document encoding
*
* @return \XMLWriter the writer resource
*/
protected function startDocument($encoding)
{
$this->writer = new \XMLWriter();
$this->writer->openMemory();
$this->writer->startDocument('1.0', $encoding);
return $this->writer;
}
/**
* End the document and return the output
*
* @param \XMLWriter $writer
*
* @return \string the writer resource
*/
protected function finishDocument($writer)
{
$writer->endDocument();
return $writer->outputMemory();
}
/**
* Add an array to the XML
*/
protected function addXmlArray(\XMLWriter $writer, Parameter $param, &$value)
{
if ($items = $param->getItems()) {
foreach ($value as $v) {
$this->addXml($writer, $items, $v);
}
}
}
/**
* Add an object to the XML
*/
protected function addXmlObject(\XMLWriter $writer, Parameter $param, &$value)
{
foreach ($value as $name => $v) {
if ($property = $param->getProperty($name)) {
$this->addXml($writer, $property, $v);
}
}
}
}

View File

@ -3,15 +3,19 @@
namespace GuzzleHttp\Service\Guzzle\Subscriber;
use GuzzleHttp\Event\SubscriberInterface;
use GuzzleHttp\Message\RequestInterface as Request;
use GuzzleHttp\Post\PostBodyInterface;
use GuzzleHttp\Post\PostFile;
use GuzzleHttp\Post\PostFileInterface;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Service\Guzzle\GuzzleClientInterface;
use GuzzleHttp\Service\Guzzle\GuzzleCommandInterface;
use GuzzleHttp\Service\Guzzle\RequestLocation\BodyLocation;
use GuzzleHttp\Service\Guzzle\RequestLocation\HeaderLocation;
use GuzzleHttp\Service\Guzzle\RequestLocation\JsonLocation;
use GuzzleHttp\Service\Guzzle\RequestLocation\PostFieldLocation;
use GuzzleHttp\Service\Guzzle\RequestLocation\PostFileLocation;
use GuzzleHttp\Service\Guzzle\RequestLocation\QueryLocation;
use GuzzleHttp\Service\Guzzle\RequestLocation\XmlLocation;
use GuzzleHttp\Service\PrepareEvent;
use GuzzleHttp\Service\Guzzle\Description\Parameter;
use GuzzleHttp\Service\Guzzle\RequestLocation\RequestLocationInterface;
/**
* Subscriber used to create HTTP requests for commands based on a service
@ -19,11 +23,35 @@ use GuzzleHttp\Service\Guzzle\Description\Parameter;
*/
class PrepareRequest implements SubscriberInterface
{
/** @var RequestLocationInterface[] */
private $requestLocations;
public static function getSubscribedEvents()
{
return ['prepare' => ['onPrepare']];
}
/**
* @param RequestLocationInterface[] $requestLocations Extra request locations
*/
public function __construct(array $requestLocations = [])
{
static $defaultRequestLocations;
if (!$defaultRequestLocations) {
$defaultRequestLocations = [
'body' => new BodyLocation(),
'query' => new QueryLocation(),
'header' => new HeaderLocation(),
'json' => new JsonLocation(),
'xml' => new XmlLocation(),
'postField' => new PostFieldLocation(),
'postFile' => new PostFileLocation()
];
}
$this->requestLocations = $requestLocations + $defaultRequestLocations;
}
public function onPrepare(PrepareEvent $event)
{
/* @var GuzzleCommandInterface $command */
@ -35,13 +63,52 @@ class PrepareRequest implements SubscriberInterface
$event->setRequest($request);
}
/**
* Prepares a request for sending using location visitors
*
* @param GuzzleCommandInterface $command Command to prepare
* @param GuzzleClientInterface $client Client that owns the command
* @param RequestInterface $request Request being created
* @throws \RuntimeException If a location cannot be handled
*/
protected function prepareRequest(
GuzzleCommandInterface $command,
GuzzleClientInterface $client,
RequestInterface $request
) {
$visitedLocations = [];
$context = ['client' => $client, 'command' => $command];
foreach ($command->getOperation()->getParams() as $name => $param) {
/* @var Parameter $param */
$location = $param->getLocation();
// Skip parameters that have not been set or are URI location
if ($location == 'uri' || !$command->hasParam($name)) {
continue;
}
if (!isset($this->requestLocations[$location])) {
throw new \RuntimeException("No location registered for $location");
}
$visitedLocations[$location] = true;
$this->requestLocations[$location]->visit(
$request,
$param,
$command[$name],
$context
);
}
foreach (array_keys($visitedLocations) as $location) {
$this->requestLocations[$location]->after($request, $context);
}
}
/**
* Create a request for the command and operation
*
* @param GuzzleCommandInterface $command Command being executed
* @param GuzzleClientInterface $client Client used to execute the command
*
* @return Request
* @return RequestInterface
* @throws \RuntimeException
*/
protected function createRequest(
@ -91,135 +158,4 @@ class PrepareRequest implements SubscriberInterface
$command['request_options'] ?: []
);
}
protected function prepareRequest(
GuzzleCommandInterface $command,
GuzzleClientInterface $client,
Request $request
) {
static $methods;
if (!$methods) {
$methods = array_flip(get_class_methods(__CLASS__));
}
$context = ['command' => $command, 'client' => $client];
foreach ($command->getOperation()->getParams() as $name => $value) {
/* @var Parameter $value */
// Skip parameters that have not been set or are URI location
if (!$command->hasParam($name) || $value->getLocation() == 'uri') {
continue;
}
$method = 'visit_' . $value->getLocation();
if (isset($methods[$method])) {
$this->{$method}($request, $value, $command[$name], $context);
} else {
// @todo: Handle more complicated or custom locations somehow
}
}
}
/**
* Adds a header to the request.
*/
protected function visit_header(Request $request, Parameter $param, $value)
{
$request->setHeader($param->getWireName(), $param->filter($value));
}
/**
* Adds a query string value to the request.
*/
protected function visit_query(Request $request, Parameter $param, $value)
{
$request->setHeader($param->getWireName(), $this->prepValue($value, $param));
}
/**
* Adds a body to the request.
*/
protected function visit_body(Request $request, Parameter $param, $value)
{
$request->setBody(Stream::factory($param->filter($value)));
}
/**
* Adds a POST field to the request.
*/
protected function visit_postField(Request $request, Parameter $param, $value)
{
$body = $request->getBody();
if (!($body instanceof PostBodyInterface)) {
throw new \RuntimeException('Must be a POST body interface');
}
$body->setField($param->getWireName(), $this->prepValue($value, $param));
}
/**
* Adds a POST file to the request.
*/
protected function visit_postFile(Request $request, Parameter $param, $value)
{
$body = $request->getBody();
if (!($body instanceof PostBodyInterface)) {
throw new \RuntimeException('Must be a POST body interface');
}
$value = $param->filter($value);
if (!($value instanceof PostFileInterface)) {
$value = new PostFile($param->getWireName(), $value);
}
$body->addFile($value);
}
/**
* Prepare (filter and set desired name for request item) the value for
* request.
*
* @param mixed $value
* @param Parameter $param
*
* @return array|mixed
*/
protected function prepValue($value, Parameter $param)
{
return is_array($value)
? $this->resolveRecursively($value, $param)
: $param->filter($value);
}
/**
* Recursively prepare and filter nested values.
*
* @param array $value Value to map
* @param Parameter $param Parameter related to the current key.
*
* @return array Returns the mapped array
*/
protected function resolveRecursively(array $value, Parameter $param)
{
foreach ($value as $name => &$v) {
switch ($param->getType()) {
case 'object':
if ($subParam = $param->getProperty($name)) {
$key = $subParam->getWireName();
$value[$key] = $this->prepValue($v, $subParam);
if ($name != $key) {
unset($value[$name]);
}
} elseif ($param->getAdditionalProperties() instanceof Parameter) {
$v = $this->prepValue($v, $param->getAdditionalProperties());
}
break;
case 'array':
if ($items = $param->getItems()) {
$v = $this->prepValue($v, $items);
}
break;
}
}
return $param->filter($value);
}
}