1
0
mirror of https://github.com/guzzle/guzzle.git synced 2025-02-12 11:30:49 +01:00

Moving query aggregators to callables

This commit is contained in:
Michael Dowling 2014-02-16 23:20:17 -08:00
parent 2b779dccb3
commit ce56db078b
10 changed files with 165 additions and 219 deletions

View File

@ -5,8 +5,6 @@ namespace GuzzleHttp\Message\Post;
use GuzzleHttp\Message\RequestInterface;
use GuzzleHttp\Stream\Stream;
use GuzzleHttp\Stream\StreamInterface;
use GuzzleHttp\QueryAggregator\PhpAggregator;
use GuzzleHttp\QueryAggregator\QueryAggregatorInterface;
use GuzzleHttp\Query;
/**
@ -17,7 +15,7 @@ class PostBody implements PostBodyInterface
/** @var StreamInterface */
private $body;
/** @var QueryAggregatorInterface */
/** @var callable */
private $aggregator;
private $fields = [];
@ -57,11 +55,15 @@ class PostBody implements PostBodyInterface
}
/**
* Set the aggregation strategy that will be used to turn multi-valued fields into a string
* Set the aggregation strategy that will be used to turn multi-valued
* fields into a string.
*
* @param QueryAggregatorInterface $aggregator
* The aggregation function accepts a deeply nested array of query string
* values and returns a flattened associative array of key value pairs.
*
* @param callable $aggregator
*/
final public function setAggregator(QueryAggregatorInterface $aggregator)
final public function setAggregator(callable $aggregator)
{
$this->aggregator = $aggregator;
}
@ -224,12 +226,12 @@ class PostBody implements PostBodyInterface
/**
* Get the aggregator used to join multi-valued field parameters
*
* @return QueryAggregatorInterface
* @return callable
*/
final protected function getAggregator()
{
if (!$this->aggregator) {
$this->aggregator = new PhpAggregator();
$this->aggregator = Query::phpAggregator();
}
return $this->aggregator;

View File

@ -2,10 +2,6 @@
namespace GuzzleHttp;
use GuzzleHttp\QueryAggregator\QueryAggregatorInterface;
use GuzzleHttp\QueryAggregator\DuplicateAggregator;
use GuzzleHttp\QueryAggregator\PhpAggregator;
/**
* Manages query string variables and can aggregate them into a string
*/
@ -17,7 +13,7 @@ class Query extends Collection
/** @var bool URL encode fields and values */
private $encoding = self::RFC3986;
/** @var QueryAggregatorInterface */
/** @var callable */
private $aggregator;
/**
@ -60,7 +56,7 @@ class Query extends Collection
// Use the duplicate aggregator if duplicates were found and not using PHP style arrays
if ($foundDuplicates && !$foundPhpStyle) {
$q->setAggregator(new DuplicateAggregator());
$q->setAggregator(self::duplicateAggregator());
}
return $q;
@ -77,12 +73,20 @@ class Query extends Collection
return '';
}
// The default aggregator is statically cached
static $defaultAggregator;
if (!$this->aggregator) {
$this->aggregator = new PhpAggregator();
if (!$defaultAggregator) {
$defaultAggregator = self::phpAggregator();
}
$this->aggregator = $defaultAggregator;
}
$result = '';
foreach ($this->aggregator->aggregate($this->data) as $key => $values) {
$aggregator = $this->aggregator;
foreach ($aggregator($this->data) as $key => $values) {
foreach ($values as $value) {
if ($result) {
$result .= '&';
@ -110,13 +114,20 @@ class Query extends Collection
}
/**
* Controls how multi-valued query string parameters are aggregated into a string
* Controls how multi-valued query string parameters are aggregated into a
* string.
*
* @param QueryAggregatorInterface $aggregator Converts an array of query string variables into a string
* $query->setAggregator($query::duplicateAggregator());
*
* @param callable $aggregator Callable used to converts a deeply nested
* array of query string variables into a flattened array of key value
* pairs. The callable accepts an array of query data and returns a
* flattened array of key value pairs where each value is an array of
* strings.
*
* @return self
*/
public function setAggregator(QueryAggregatorInterface $aggregator)
public function setAggregator(callable $aggregator)
{
$this->aggregator = $aggregator;
@ -141,4 +152,74 @@ class Query extends Collection
return $this;
}
/**
* Query string aggregator that does not aggregate nested query string
* values and allows duplicates in the resulting array.
*
* Example: http://test.com?q=1&q=2
*
* @return callable
*/
public static function duplicateAggregator()
{
return function (array $data) {
return self::walkQuery($data, '', function ($key, $prefix) {
return is_int($key) ? $prefix : "{$prefix}[{$key}]";
});
};
}
/**
* Aggregates nested query string variables using the same techinque as
* ``http_build_query()``.
*
* @param bool $numericIndices Pass false to not include numeric indices
* when multi-values query string parameters are present.
*
* @return callable
*/
public static function phpAggregator($numericIndices = true)
{
return function (array $data) use ($numericIndices) {
return self::walkQuery(
$data,
'',
function ($key, $prefix) use ($numericIndices) {
return !$numericIndices && is_int($key)
? "{$prefix}[]"
: "{$prefix}[{$key}]";
}
);
};
}
/**
* Easily create query aggregation functions by providing a key prefix
* function to this query string array walker.
*
* @param array $query Query string to walk
* @param string $keyPrefix Key prefix (start with '')
* @param callable $prefixer Function used to create a key prefix
*
* @return array
*/
public static function walkQuery(array $query, $keyPrefix, callable $prefixer)
{
$result = [];
foreach ($query as $key => $value) {
if ($keyPrefix) {
$key = $prefixer($key, $keyPrefix);
}
if (is_array($value)) {
$result += self::walkQuery($value, $key, $prefixer);
} elseif (isset($result[$key])) {
$result[$key][] = $value;
} else {
$result[$key] = array($value);
}
}
return $result;
}
}

View File

@ -1,40 +0,0 @@
<?php
namespace GuzzleHttp\QueryAggregator;
abstract class AbstractAggregator implements QueryAggregatorInterface
{
public function aggregate(array $query)
{
return $this->walkQuery($query, '');
}
protected function walkQuery(array $query, $keyPrefix)
{
$result = [];
foreach ($query as $key => $value) {
if ($keyPrefix) {
$key = $this->createPrefixKey($key, $keyPrefix);
}
if (is_array($value)) {
$result += $this->walkQuery($value, $key);
} elseif (isset($result[$key])) {
$result[$key][] = $value;
} else {
$result[$key] = array($value);
}
}
return $result;
}
/**
* Computes a key for a key and prefix
*
* @param string $key
* @param string $keyPrefix
*
* @return string
*/
abstract protected function createPrefixKey($key, $keyPrefix);
}

View File

@ -1,15 +0,0 @@
<?php
namespace GuzzleHttp\QueryAggregator;
/**
* Aggregates nested query string variables using PHP style arrays, but does not
* combine duplicate values of the same name under array keys.
*/
class DuplicateAggregator extends AbstractAggregator
{
protected function createPrefixKey($key, $keyPrefix)
{
return is_int($key) ? $keyPrefix : "{$keyPrefix}[{$key}]";
}
}

View File

@ -1,24 +0,0 @@
<?php
namespace GuzzleHttp\QueryAggregator;
/**
* Aggregates nested query string variables using PHP style []
*/
class PhpAggregator extends AbstractAggregator
{
private $numericIndices;
/**
* @param bool $numericIndices Set to false to disable numeric indices (e.g. foo[] = bar vs foo[0] = bar)
*/
public function __construct($numericIndices = true)
{
$this->numericIndices = $numericIndices;
}
protected function createPrefixKey($key, $prefix)
{
return !$this->numericIndices && is_int($key) ? "{$prefix}[]" : "{$prefix}[{$key}]";
}
}

View File

@ -1,18 +0,0 @@
<?php
namespace GuzzleHttp\QueryAggregator;
/**
* Interface used for aggregating multi-value query string parameters into a flattened array
*/
interface QueryAggregatorInterface
{
/**
* Aggregate a query string array into a flattened array
*
* @param array $query Query string parameters
*
* @return array
*/
public function aggregate(array $query);
}

View File

@ -5,7 +5,7 @@ namespace GuzzleHttp\Tests\Message\Post;
use GuzzleHttp\Message\Post\PostBody;
use GuzzleHttp\Message\Request;
use GuzzleHttp\Message\Post\PostFile;
use GuzzleHttp\QueryAggregator\DuplicateAggregator;
use GuzzleHttp\Query;
/**
* @covers GuzzleHttp\Message\Post\PostBody
@ -112,7 +112,7 @@ class PostBodyTest extends \PHPUnit_Framework_TestCase
$this->assertEquals('foo%5B0%5D=baz&foo%5B1%5D=bar', (string) $b);
$b = new PostBody();
$b->setField('foo', ['baz', 'bar']);
$agg = new DuplicateAggregator();
$agg = Query::duplicateAggregator();
$b->setAggregator($agg);
$this->assertEquals('foo=baz&foo=bar', (string) $b);
}

View File

@ -1,44 +0,0 @@
<?php
namespace GuzzleHttp\Tests\QueryAggregator;
use GuzzleHttp\QueryAggregator\DuplicateAggregator;
/**
* @covers \GuzzleHttp\QueryAggregator\DuplicateAggregator
* @covers \GuzzleHttp\QueryAggregator\AbstractAggregator
*/
class DuplicateAggregatorTest extends \PHPUnit_Framework_TestCase
{
private $encodeData = [
't' => [
'v1' => ['a', '1'],
'v2' => 'b',
'v3' => ['v4' => 'c', 'v5' => 'd']
]
];
public function testEncodes()
{
$agg = new DuplicateAggregator();
$result = $agg->aggregate($this->encodeData);
$this->assertEquals(array(
't[v1]' => ['a', '1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
public function testEncodesNoNumericIndices()
{
$agg = new DuplicateAggregator(false);
$result = $agg->aggregate($this->encodeData);
$this->assertEquals(array(
't[v1]' => ['a', '1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
}

View File

@ -1,45 +0,0 @@
<?php
namespace GuzzleHttp\Tests\QueryAggregator;
use GuzzleHttp\QueryAggregator\PhpAggregator;
/**
* @covers \GuzzleHttp\QueryAggregator\PhpAggregator
* @covers \GuzzleHttp\QueryAggregator\AbstractAggregator
*/
class PhpAggregatorTest extends \PHPUnit_Framework_TestCase
{
private $encodeData = [
't' => [
'v1' => ['a', '1'],
'v2' => 'b',
'v3' => ['v4' => 'c', 'v5' => 'd']
]
];
public function testEncodes()
{
$agg = new PhpAggregator();
$result = $agg->aggregate($this->encodeData);
$this->assertEquals(array(
't[v1][0]' => ['a'],
't[v1][1]' => ['1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
public function testEncodesNoNumericIndices()
{
$agg = new PhpAggregator(false);
$result = $agg->aggregate($this->encodeData);
$this->assertEquals(array(
't[v1][]' => ['a', '1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
}

View File

@ -3,7 +3,6 @@
namespace GuzzleHttp\Tests;
use GuzzleHttp\Query;
use GuzzleHttp\QueryAggregator\DuplicateAggregator;
class QueryTest extends \PHPUnit_Framework_TestCase
{
@ -50,17 +49,10 @@ class QueryTest extends \PHPUnit_Framework_TestCase
public function testCanSetAggregator()
{
$agg = $this->getMockBuilder('GuzzleHttp\QueryAggregator\QueryAggregatorInterface')
->setMethods('aggregate')
->getMockForAbstractClass();
$q = new Query(['foo' => ['bar', 'baz']]);
$q->setAggregator($agg);
$agg->expects($this->once())
->method('aggregate')
->will($this->returnValue(['foo' => ['barANDbaz']]));
$q->setAggregator(function (array $data) {
return ['foo' => ['barANDbaz']];
});
$this->assertEquals('foo=barANDbaz', (string) $q);
}
@ -71,7 +63,7 @@ class QueryTest extends \PHPUnit_Framework_TestCase
$q->add('facet', 'width');
$q->add('facet.field', 'foo');
// Use the duplicate aggregator
$q->setAggregator(new DuplicateAggregator());
$q->setAggregator($q::duplicateAggregator());
$this->assertEquals('facet=size&facet=width&facet.field=foo', (string) $q);
}
@ -173,4 +165,61 @@ class QueryTest extends \PHPUnit_Framework_TestCase
{
$this->assertEquals('', (string) Query::fromString(''));
}
private $encodeData = [
't' => [
'v1' => ['a', '1'],
'v2' => 'b',
'v3' => ['v4' => 'c', 'v5' => 'd']
]
];
public function testEncodesDuplicateAggregator()
{
$agg = Query::duplicateAggregator();
$result = $agg($this->encodeData);
$this->assertEquals(array(
't[v1]' => ['a', '1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
public function testDuplicateEncodesNoNumericIndices()
{
$agg = Query::duplicateAggregator();
$result = $agg($this->encodeData);
$this->assertEquals(array(
't[v1]' => ['a', '1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
public function testEncodesPhpAggregator()
{
$agg = Query::phpAggregator();
$result = $agg($this->encodeData);
$this->assertEquals(array(
't[v1][0]' => ['a'],
't[v1][1]' => ['1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
public function testPhpEncodesNoNumericIndices()
{
$agg = Query::phpAggregator(false);
$result = $agg($this->encodeData);
$this->assertEquals(array(
't[v1][]' => ['a', '1'],
't[v2]' => ['b'],
't[v3][v4]' => ['c'],
't[v3][v5]' => ['d'],
), $result);
}
}