diff --git a/src/Message/Post/PostBody.php b/src/Message/Post/PostBody.php index 47b8235d..d587365f 100644 --- a/src/Message/Post/PostBody.php +++ b/src/Message/Post/PostBody.php @@ -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; diff --git a/src/Query.php b/src/Query.php index b5fd763d..09a910c9 100644 --- a/src/Query.php +++ b/src/Query.php @@ -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; + } } diff --git a/src/QueryAggregator/AbstractAggregator.php b/src/QueryAggregator/AbstractAggregator.php deleted file mode 100644 index 2344ef14..00000000 --- a/src/QueryAggregator/AbstractAggregator.php +++ /dev/null @@ -1,40 +0,0 @@ -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); -} diff --git a/src/QueryAggregator/DuplicateAggregator.php b/src/QueryAggregator/DuplicateAggregator.php deleted file mode 100644 index 022cca1d..00000000 --- a/src/QueryAggregator/DuplicateAggregator.php +++ /dev/null @@ -1,15 +0,0 @@ -numericIndices = $numericIndices; - } - - protected function createPrefixKey($key, $prefix) - { - return !$this->numericIndices && is_int($key) ? "{$prefix}[]" : "{$prefix}[{$key}]"; - } -} diff --git a/src/QueryAggregator/QueryAggregatorInterface.php b/src/QueryAggregator/QueryAggregatorInterface.php deleted file mode 100644 index 2b9b5d2e..00000000 --- a/src/QueryAggregator/QueryAggregatorInterface.php +++ /dev/null @@ -1,18 +0,0 @@ -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); } diff --git a/tests/QueryAggregator/DuplicateAggregatorTest.php b/tests/QueryAggregator/DuplicateAggregatorTest.php deleted file mode 100644 index 0a4a6ad3..00000000 --- a/tests/QueryAggregator/DuplicateAggregatorTest.php +++ /dev/null @@ -1,44 +0,0 @@ - [ - '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); - } -} diff --git a/tests/QueryAggregator/PhpAggregatorTest.php b/tests/QueryAggregator/PhpAggregatorTest.php deleted file mode 100644 index 0481f61b..00000000 --- a/tests/QueryAggregator/PhpAggregatorTest.php +++ /dev/null @@ -1,45 +0,0 @@ - [ - '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); - } -} diff --git a/tests/QueryTest.php b/tests/QueryTest.php index 841db488..a5ca87b1 100644 --- a/tests/QueryTest.php +++ b/tests/QueryTest.php @@ -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); + } }