├── .github
└── workflows
│ └── ci-workflow.yml
├── .gitignore
├── .scrutinizer.yml
├── LICENSE
├── README.md
├── composer.json
├── doc
├── Instantiation.md
├── ManipulateParameters.md
└── RenderAsString.md
├── phpunit.xml.dist
├── src
├── Pairs.php
├── Parser
│ ├── FlatParser.php
│ ├── NativeParser.php
│ └── QueryStringParserInterface.php
├── QueryString.php
├── Renderer
│ ├── ArrayValuesNormalizerRenderer.php
│ ├── FlatRenderer.php
│ ├── NativeRenderer.php
│ ├── QueryStringRendererInterface.php
│ └── QueryStringRendererTrait.php
└── functions.php
└── tests
├── ArrayValuesNormalizerRendererTest.php
├── FlatParserTest.php
├── FlatRendererTest.php
├── NativeParserTest.php
├── NativeRendererTest.php
├── PairsTest.php
└── QueryStringTest.php
/.github/workflows/ci-workflow.yml:
--------------------------------------------------------------------------------
1 | name: CI Workflow
2 |
3 | on:
4 | push:
5 | branches: [ master ]
6 | pull_request:
7 | branches: [ master ]
8 |
9 | jobs:
10 | code-style:
11 | name: Code Style & Static Analysis
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Checkout
15 | uses: actions/checkout@v4
16 |
17 | - name: Setup PHP
18 | uses: shivammathur/setup-php@v2
19 | with:
20 | php-version: 8.4
21 | extensions: mbstring, pcntl
22 |
23 | - name: Validate composer.json and composer.lock
24 | run: composer validate
25 |
26 | - name: Install dependencies
27 | run: composer install --prefer-dist --no-progress
28 |
29 | - name: Check code style
30 | run: vendor/bin/phpcs --standard=psr2 -n src/
31 |
32 |
33 | tests:
34 | name: Test Suite
35 | runs-on: ubuntu-latest
36 | strategy:
37 | max-parallel: 10
38 | matrix:
39 | php:
40 | - 7.1
41 | - 7.2
42 | - 7.3
43 | - 7.4
44 | - 8.0
45 | - 8.1
46 | - 8.2
47 | - 8.3
48 | - 8.4
49 |
50 | steps:
51 | - name: Checkout
52 | uses: actions/checkout@v4
53 |
54 | - name: Setup PHP
55 | uses: shivammathur/setup-php@v2
56 | with:
57 | php-version: ${{ matrix.php }}
58 | extensions: mbstring, curl, zip
59 | coverage: xdebug
60 |
61 | - name: Install dependencies
62 | run: composer install --prefer-dist --no-progress
63 |
64 | - name: Run tests
65 | run: vendor/bin/phpunit --coverage-clover=coverage.xml
66 |
67 | - name: Upload coverage to Codecov
68 | uses: codecov/codecov-action@v1
69 | with:
70 | token: ${{ secrets.CODECOV_TOKEN }}
71 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | vendor
2 | composer.lock
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | filter:
2 | excluded_paths: [tests/*]
3 | checks:
4 | php:
5 | code_rating: true
6 | remove_extra_empty_lines: true
7 | remove_php_closing_tag: true
8 | remove_trailing_whitespace: true
9 | fix_use_statements:
10 | remove_unused: true
11 | preserve_multiple: false
12 | preserve_blanklines: true
13 | order_alphabetically: true
14 | fix_php_opening_tag: true
15 | fix_linefeed: true
16 | fix_line_ending: true
17 | fix_identation_4spaces: true
18 | fix_doc_comments: true
19 | tools:
20 | php_analyzer: true
21 | php_code_coverage: false
22 | php_code_sniffer:
23 | config:
24 | standard: PSR2
25 | filter:
26 | paths: ['src']
27 | php_loc:
28 | enabled: true
29 | excluded_dirs: [vendor, tests]
30 | php_cpd:
31 | enabled: true
32 | excluded_dirs: [vendor, tests]
33 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2017 Benoit POLASZEK
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in
13 | all copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://packagist.org/packages/bentools/querystring)
2 | [](https://packagist.org/packages/bentools/querystring)
3 | [](https://github.com/bpolaszek/querystring/actions/workflows/ci-workflow.yml)
4 | [](https://codecov.io/gh/bpolaszek/querystring)
5 | [](https://scrutinizer-ci.com/g/bpolaszek/querystring)
6 | [](https://packagist.org/packages/bentools/querystring)
7 |
8 | # QueryString
9 |
10 | A lightweight, object-oriented, Query String manipulation library.
11 |
12 | ## Why?
13 |
14 | Because I needed an intuitive way to add or remove parameters from a query string, in any project.
15 |
16 | Oh, and, I also wanted that `['foos' => ['foo', 'bar']]` resolved to `foos[]=foo&foos[]=bar` instead of `foos[0]=foo&foos[1]=bar`, unlike many libraries do.
17 |
18 | Thanks to object-oriented design, you can define the way query strings are [parsed](doc/Instantiation.md) and [rendered](doc/RenderAsString.md#change-renderer).
19 |
20 | ## Usage
21 |
22 | Simple as that:
23 | ```php
24 | require_once __DIR__ . '/vendor/autoload.php';
25 |
26 | use function BenTools\QueryString\query_string;
27 |
28 | $qs = query_string(
29 | 'foo=bar&baz=bat'
30 | );
31 | $qs = $qs->withParam('foo', 'foofoo')
32 | ->withoutParam('baz')
33 | ->withParam('ho', 'hi');
34 |
35 | print_r($qs->getParams());
36 | /* Array
37 | (
38 | [foo] => foofoo
39 | [ho] => hi
40 | ) */
41 |
42 | print $qs; // foo=foofoo&ho=hi
43 | ```
44 |
45 | ## Documentation
46 |
47 | [Instantiation / Parsing](doc/Instantiation.md)
48 |
49 | [Manipulate parameters](doc/ManipulateParameters.md)
50 |
51 | [Render as string](doc/RenderAsString.md)
52 |
53 | ## Installation
54 | PHP 7.1+ is required.
55 | > composer require bentools/querystring:^1.0
56 |
57 | ## Tests
58 | > ./vendor/bin/phpunit
59 |
60 | ## License
61 | MIT
62 |
63 | ## See also
64 |
65 | [bentools/uri-factory](https://github.com/bpolaszek/uri-factory) - A PSR-7 `UriInterface` factory based on your own dependencies.
66 |
67 | [bentools/pager](https://github.com/bpolaszek/bentools-pager) - A simple, object oriented Pager.
68 |
69 | [bentools/where](https://github.com/bpolaszek/where) - A framework-agnostic fluent, immutable, SQL query builder.
70 |
71 | [bentools/picker](https://github.com/bpolaszek/picker) - Pick a random item from an array, with weight management.
72 |
73 | [bentools/psr7-request-matcher](https://github.com/bpolaszek/psr7-request-matcher) - A PSR-7 request matcher interface.
74 |
75 | [bentools/cartesian-product](https://github.com/bpolaszek/cartesian-product) - Generate all possible combinations from a multidimensionnal array.
76 |
77 | [bentools/string-combinations](https://github.com/bpolaszek/string-combinations) - A string combinations generator.
78 |
79 | [bentools/flatten-iterator](https://github.com/bpolaszek/flatten-iterator) - An iterator that flattens multiple iterators or arrays.
80 |
--------------------------------------------------------------------------------
/composer.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "bentools/querystring",
3 | "description": "Query String manipulation library. PHP 7.1+. No dependency, immutable, PSR-7 compliant.",
4 | "keywords": ["url", "uri", "querystring", "query string", "psr-7", "psr7"],
5 | "authors": [
6 | {
7 | "name": "Beno!t POLASZEK",
8 | "email": "bpolaszek@gmail.com",
9 | "homepage": "http://www.polaszek.fr",
10 | "role": "Developer"
11 | }
12 | ],
13 | "type": "library",
14 | "license": "MIT",
15 | "require": {
16 | "php": ">=7.1"
17 | },
18 | "require-dev": {
19 | "league/uri": "^5.0",
20 | "phpunit/phpunit": "^7.5|^8.0|^9.0",
21 | "squizlabs/php_codesniffer": "^3.0",
22 | "symfony/var-dumper": "@stable"
23 | },
24 | "autoload": {
25 | "psr-4": {
26 | "BenTools\\QueryString\\": "src"
27 | },
28 | "files": [
29 | "src/functions.php"
30 | ]
31 | },
32 | "autoload-dev": {
33 | "psr-4": {
34 | "BenTools\\QueryString\\Tests\\": "tests"
35 | },
36 | "files": [
37 | "vendor/symfony/var-dumper/Resources/functions/dump.php"
38 | ]
39 | },
40 | "config": {
41 | "sort-packages": true
42 | },
43 | "extra": {
44 | "branch-alias": {
45 | "dev-master": "1.0.x-dev"
46 | }
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/doc/Instantiation.md:
--------------------------------------------------------------------------------
1 | # Instantiation
2 |
3 | You can create a `QueryString` object from a PSR-7 `UriInterface` object, a string or an array of values.
4 |
5 | ```php
6 | require_once __DIR__ . '/vendor/autoload.php';
7 |
8 | use function BenTools\QueryString\query_string;
9 |
10 | // Instanciate from an existing Psr\Http\Message\UriInterface object
11 | $qs = query_string($uri);
12 |
13 | // Instanciate from a string
14 | $qs = query_string('foo=bar&baz=bat');
15 | $qs = query_string('?foo=bar&baz=bat'); // This works too
16 |
17 | // Instanciate from an array
18 | $qs = query_string(['foo' => 'bar', 'baz' => 'bat']);
19 |
20 | // Or create an empty object to get started
21 | $qs = query_string();
22 | ```
23 |
24 | If you don't like shortcut functions, use the class' factory:
25 | ```php
26 | use BenTools\QueryString\QueryString;
27 | $qs = QueryString::factory('foo=bar&baz=bat'); // Same argument requirements
28 | ```
29 |
30 | ## Instantiate from current location
31 |
32 | It will read `$_SERVER['QUERY_STRING']`.
33 |
34 | ```php
35 | use function BenTools\QueryString\query_string;
36 | $qs = query_string()->withCurrentLocation();
37 | ```
38 | or:
39 | ```php
40 | use BenTools\QueryString\QueryString;
41 | $qs = QueryString::createFromCurrentLocation();
42 | ```
43 |
44 | Of course this will throw a `RuntimeException` when trying to run this from `cli` :smile:
45 |
46 | ## Flat query parser
47 |
48 | By default, PHP considers that `?foo=bar&foo=baz` has a single parameter `foo` with value `baz`.
49 |
50 | If you need to parse such query strings, you can use the `FlatParser`: multiple keys will be detected and converted to arrays.
51 |
52 | ```php
53 | use BenTools\QueryString\Parser\FlatParser;
54 | use function BenTools\QueryString\query_string;
55 |
56 | $qs = query_string('foo=bar&foo=baz&bar=foo', new FlatParser());
57 | var_dump($qs->getParam('foo')); // array('bar', 'baz')
58 | var_dump($qs->getParam('bar')); // string 'foo'
59 | ```
60 |
61 | ## Create your own parser
62 |
63 | [bentools/querystring](https://github.com/bpolaszek/querystring) relies on PHP's `parse_str()` function to create an array of parameters from your query string.
64 |
65 | This may not fit your needs, because sometimes you have to decode some query strings like this:
66 |
67 | > status=foo&status=bar
68 |
69 | In this case, `parse_str()` will not consider `status` as an array. You can implement your own `QueryStringParserInterface` to get over this:
70 |
71 | ```php
72 | use BenTools\QueryString\Parser\QueryStringParserInterface;
73 | use function BenTools\QueryString\query_string;
74 | use function BenTools\QueryString\pairs;
75 |
76 | $myParser = new class implements QueryStringParserInterface
77 | {
78 |
79 | public function parse(string $queryString): array
80 | {
81 | $params = [];
82 |
83 | foreach (pairs($queryString) as $key => $value) {
84 | if (isset($params[$key])) {
85 | $params[$key] = (array) $params[$key];
86 | $params[$key][] = $value;
87 | } else {
88 | $params[$key] = $value;
89 | }
90 | }
91 |
92 | return $params;
93 | }
94 |
95 | };
96 |
97 | $qs = query_string('foo=bar&foo=baz&baz=bat', $myParser);
98 | print_r($qs->getParams());
99 | /* Array
100 | (
101 | [foo] => Array
102 | (
103 | [0] => bar
104 | [1] => baz
105 | )
106 |
107 | [baz] => bat
108 | ) */
109 |
110 | // You can also set it as the default parser:
111 | QueryString::setDefaultParser($myParser);
112 |
113 | // Or restore the native one
114 | QueryString::restoreDefaultParser();
115 | ```
116 |
117 |
118 | [Next](ManipulateParameters.md) - Manipulate parameters
119 |
--------------------------------------------------------------------------------
/doc/ManipulateParameters.md:
--------------------------------------------------------------------------------
1 | # Manipulate parameters
2 |
3 | **Retrieve all parameters**
4 | ```php
5 | use function BenTools\QueryString\query_string;
6 |
7 | $qs = query_string(
8 | 'foo=bar&baz=bat'
9 | );
10 |
11 | print_r($qs->getParams());
12 | /* Array
13 | (
14 | [foo] => bar
15 | [baz] => bat
16 | ) */
17 | ```
18 |
19 | **Retrieve specific parameter**
20 | ```php
21 | print_r($qs->getParam('foo')); // bar
22 | ```
23 |
24 | **Add / replace parameter**
25 | ```php
26 | $qs = $qs->withParam('foo', 'foofoo');
27 | print_r($qs->getParams());
28 | /* Array
29 | (
30 | [foo] => foofoo
31 | [baz] => bat
32 | ) */
33 | print($qs); // foo=foofoo&baz=bat
34 | ```
35 |
36 | **Remove parameter**
37 | ```php
38 | $qs = $qs->withoutParam('baz');
39 | print($qs); // foo=foofoo
40 | ```
41 |
42 | **Create from a complex, nested array**
43 | ```php
44 | $qs = query_string([
45 | 'yummy' => [
46 | 'fruits' => [
47 | 'strawberries',
48 | 'apples',
49 | 'raspberries',
50 | ],
51 | ]
52 | ]);
53 | ```
54 | **Retrieve a parameter at a specific path**
55 | ```php
56 | print($qs->getParam('yummy', 'fruits', 2)); // raspberries
57 | ```
58 |
59 |
60 | **Remove a parameter at a specific path**
61 |
62 | _Example: remove "apples", resolved at `$params['yummy']['fruits'][1]`_
63 |
64 | ```php
65 | $qs = $qs->withoutParam('yummy', 'fruits', 1);
66 | print_r($qs->getParams());
67 | /* Array
68 | (
69 | [yummy] => Array
70 | (
71 | [fruits] => Array
72 | (
73 | [0] => strawberries
74 | [1] => raspberries // Yep, this indexed array has been reordered.
75 | )
76 |
77 | )
78 |
79 | )*/
80 | ```
81 |
82 | ## Retrieve key / value pairs
83 |
84 | This can be useful if you want to generate hidden input fields based on current query string.
85 |
86 | ```php
87 | use function BenTools\QueryString\query_string;
88 | use function BenTools\QueryString\withoutNumericIndices;
89 |
90 | $qs = query_string(
91 | 'f[status][]=pending&f[status][]=reopened&f[status][]=waiting for changes',
92 | withoutNumericIndices()
93 | );
94 |
95 | foreach ($qs->getPairs() as $key => $value) {
96 | printf(
97 | '' . PHP_EOL,
98 | urldecode($key),
99 | htmlentities($value)
100 | );
101 | }
102 | ```
103 |
104 | Output:
105 | ```
106 |
107 |
108 |
109 | ```
110 |
111 | More conveniently, you can `urldecode` keys and values if needed:
112 | ```php
113 | foreach ($qs->getPairs($decodeKeys = true, $decodeValues = true) as $key => $value) {
114 | printf(
115 | '' . PHP_EOL,
116 | $key,
117 | $value
118 | );
119 | }
120 | ```
121 |
122 | Output:
123 | ```
124 |
125 |
126 |
127 | ```
128 |
129 |
130 | [Previous](Instantiation.md) - Instantiation
131 |
132 | [Next](RenderAsString.md) - Render as string
133 |
--------------------------------------------------------------------------------
/doc/RenderAsString.md:
--------------------------------------------------------------------------------
1 | # Render as string
2 |
3 | ```php
4 | use function BenTools\QueryString\query_string;
5 |
6 | $qs = query_string([
7 | 'yummy' => [
8 | 'fruits' => [
9 | 'strawberries',
10 | 'raspberries',
11 | ],
12 | ]
13 | ]);
14 |
15 | print(urldecode((string) $qs)); // yummy[fruits][0]=strawberries&yummy[fruits][1]=raspberries
16 | ```
17 |
18 | Note that the leading question mark will never be included.
19 |
20 | ## Change renderer
21 |
22 | ### Remove numeric indices:
23 | This renderer will render indexed arrays as `foo[]=bar&foo[]=baz` instead of `foo[0]=bar&foo[1]=baz`.
24 |
25 | ```php
26 | use function BenTools\QueryString\withoutNumericIndices;
27 |
28 | $qs = $qs->withRenderer(withoutNumericIndices());
29 | print(urldecode((string) $qs));
30 | ```
31 |
32 | ### Flat renderer
33 | This renderer will render indexed arrays as `foo=bar&foo=baz` instead of `foo[0]=bar&foo[1]=baz`.
34 |
35 | ```php
36 | use function BenTools\QueryString\flat;
37 |
38 | $qs = $qs->withRenderer(flat());
39 | print(urldecode((string) $qs));
40 | ```
41 |
42 | ### Global setting
43 |
44 | You can define a default renderer on a global scope for future QueryString objects:
45 |
46 | ```php
47 | use BenTools\QueryString\QueryString;
48 | use function BenTools\QueryString\withoutNumericIndices;
49 |
50 | QueryString::setDefaultRenderer(withoutNumericIndices());
51 | ```
52 |
53 | You can also create your own rendering logic by implementing `BenTools\QueryString\Renderer\QueryStringRendererInterface`.
54 |
55 |
56 | ## Change encoding
57 |
58 | This library renders query strings with [RFC 3986](http://www.rfc-base.org/txt/rfc-3986.txt) by default, but you can change it if needed.
59 | ```php
60 | $qs = query_string('param=foo bar');
61 | print((string) $qs); // param=foo%20bar
62 |
63 | $qs = $qs->withRenderer(
64 | $qs->getRenderer()->withEncoding(PHP_QUERY_RFC1738)
65 | );
66 | print((string) $qs); // param=foo+bar
67 | ```
68 |
69 | ## Change separator
70 |
71 | ```php
72 | $qs = query_string('foo=bar&baz=bat');
73 | $qs = $qs->withRenderer(
74 | $qs->getRenderer()->withSeparator(';')
75 | );
76 | print((string) $qs); // foo=bar;baz=bat
77 | ```
78 |
79 |
80 | ## PSR-7 manipulation
81 | Example:
82 |
83 | ```php
84 | use function BenTools\QueryString\query_string;
85 |
86 | /**
87 | * @var \Psr\Http\Message\MessageInterface $uri
88 | */
89 | print((string) $uri); // http://www.example.net/
90 |
91 | $uri = $uri->withQuery(
92 | (string) query_string($uri)->withParam('foo', 'bar')
93 | );
94 |
95 | print((string) $uri); // http://www.example.net/?foo=bar
96 | ```
97 |
98 | [Previous](ManipulateParameters.md) - Manipulate parameters
99 |
--------------------------------------------------------------------------------
/phpunit.xml.dist:
--------------------------------------------------------------------------------
1 |
2 |
18 |
19 | tests
20 |
21 |
22 |
23 |
24 | src
25 |
26 |
27 |
28 |
--------------------------------------------------------------------------------
/src/Pairs.php:
--------------------------------------------------------------------------------
1 | queryString = $queryString;
41 | $this->decodeKeys = $decodeKeys;
42 | $this->decodeValues = $decodeValues;
43 | $this->separator = $separator;
44 | }
45 |
46 | /**
47 | * @param string $queryString
48 | * @return Pairs
49 | */
50 | public function withQueryString(string $queryString): self
51 | {
52 | $clone = clone $this;
53 | $clone->queryString = $queryString;
54 | return $clone;
55 | }
56 |
57 | /**
58 | * @param bool $decodeKeys
59 | * @return Pairs
60 | */
61 | public function withDecodeKeys(bool $decodeKeys): self
62 | {
63 | $clone = clone $this;
64 | $clone->decodeKeys = $decodeKeys;
65 | return $clone;
66 | }
67 |
68 | /**
69 | * @param bool $decodeValues
70 | * @return Pairs
71 | */
72 | public function withDecodeValues(bool $decodeValues): self
73 | {
74 | $clone = clone $this;
75 | $clone->decodeValues = $decodeValues;
76 | return $clone;
77 | }
78 |
79 | /**
80 | * @param null|string $separator
81 | * @return Pairs
82 | */
83 | public function withSeparator(?string $separator): self
84 | {
85 | $clone = clone $this;
86 | $clone->separator = $separator;
87 | return $clone;
88 | }
89 |
90 | /**
91 | * @return Traversable
92 | */
93 | public function getIterator(): Traversable
94 | {
95 | if ('' === trim($this->queryString)) {
96 | return;
97 | }
98 |
99 | $separator = $this->separator ?? ini_get('arg_separator.input');
100 |
101 | if ('' === $separator) {
102 | throw new \RuntimeException("A separator cannot be blank.");
103 | }
104 |
105 | $pairs = explode($separator, $this->queryString);
106 |
107 | foreach ($pairs as $pair) {
108 | $keyValue = explode('=', $pair);
109 | $key = $keyValue[0];
110 | $value = $keyValue[1] ?? null;
111 |
112 | if (true === $this->decodeKeys) {
113 | $key = urldecode($key);
114 | }
115 |
116 | if (true === $this->decodeValues) {
117 | $value = urldecode($value);
118 | }
119 |
120 | yield $key => $value;
121 | }
122 | }
123 |
124 | /**
125 | * @return string
126 | */
127 | public function __toString(): string
128 | {
129 | return (string) $this->queryString;
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/src/Parser/FlatParser.php:
--------------------------------------------------------------------------------
1 | separator = $separator;
19 | }
20 |
21 | /**
22 | * @return array
23 | */
24 | public function parse(string $queryString): array
25 | {
26 | $params = [];
27 | $pairs = explode($this->separator, $queryString);
28 | foreach ($pairs as $pair) {
29 | if (!isset($params[$pair]) && false === strpos($pair, '=')) {
30 | $key = urldecode($pair);
31 | $params[$key] = null;
32 | continue;
33 | }
34 | [$key, $value] = explode('=', $pair);
35 | $key = urldecode($key);
36 | $value = urldecode($value);
37 | if (!isset($params[$key])) {
38 | $params[$key] = $value;
39 | } elseif (!is_array($params[$key])) {
40 | $params[$key] = [$params[$key], $value];
41 | } else {
42 | $params[$key][] = $value;
43 | }
44 | }
45 |
46 | return $params;
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/Parser/NativeParser.php:
--------------------------------------------------------------------------------
1 | $value) {
42 | $this->params[(string) $key] = $value;
43 | }
44 | $this->renderer = self::getDefaultRenderer();
45 | }
46 |
47 | /**
48 | * @param array $params
49 | * @return QueryString
50 | */
51 | private static function createFromParams(array $params): self
52 | {
53 | return new self($params);
54 | }
55 |
56 | /**
57 | * @param \Psr\Http\Message\UriInterface $uri
58 | * @param QueryStringParserInterface $queryStringParser
59 | * @return QueryString
60 | */
61 | private static function createFromUri($uri, QueryStringParserInterface $queryStringParser): self
62 | {
63 | return self::createFromString($uri->getQuery(), $queryStringParser);
64 | }
65 |
66 | /**
67 | * @param string $string
68 | * @param QueryStringParserInterface $queryStringParser
69 | * @return QueryString
70 | */
71 | private static function createFromString(string $string, QueryStringParserInterface $queryStringParser): self
72 | {
73 | return new self($queryStringParser->parse($string));
74 | }
75 |
76 | /**
77 | * @param QueryStringParserInterface|null $queryStringParser
78 | * @return QueryString
79 | * @throws \RuntimeException
80 | */
81 | public static function createFromCurrentLocation(?QueryStringParserInterface $queryStringParser = null): self
82 | {
83 | if (!isset($_SERVER['REQUEST_URI'])) {
84 | throw new \RuntimeException('$_SERVER[\'REQUEST_URI\'] has not been set.');
85 | }
86 | return self::createFromString($_SERVER['REQUEST_URI'], $queryStringParser ?? self::getDefaultParser());
87 | }
88 |
89 | /**
90 | * @return QueryString
91 | * @throws \RuntimeException
92 | */
93 | public function withCurrentLocation(): self
94 | {
95 | return self::createFromCurrentLocation();
96 | }
97 |
98 | /**
99 | * @param $input
100 | * @param QueryStringParserInterface|null $queryStringParser
101 | * @return QueryString
102 | * @throws \InvalidArgumentException
103 | * @throws \TypeError
104 | */
105 | public static function factory($input = null, ?QueryStringParserInterface $queryStringParser = null): self
106 | {
107 | if (is_array($input)) {
108 | return self::createFromParams($input);
109 | } elseif (null === $input) {
110 | return self::createFromParams([]);
111 | } elseif (is_a($input, 'Psr\Http\Message\UriInterface')) {
112 | return self::createFromUri($input, $queryStringParser ?? self::getDefaultParser());
113 | } elseif (is_string($input)) {
114 | return self::createFromString($input, $queryStringParser ?? self::getDefaultParser());
115 | }
116 | throw new \InvalidArgumentException(sprintf('Expected array, string or Psr\Http\Message\UriInterface, got %s', is_object($input) ? get_class($input) : gettype($input)));
117 | }
118 |
119 | /**
120 | * @return array
121 | */
122 | public function getParams(): ?array
123 | {
124 | return $this->params;
125 | }
126 |
127 | /**
128 | * @param string $key
129 | * @param array ...$deepKeys
130 | * @return mixed|null
131 | */
132 | public function getParam(string $key, ...$deepKeys)
133 | {
134 | $param = $this->params[$key] ?? null;
135 | foreach ($deepKeys as $key) {
136 | if (!isset($param[$key])) {
137 | return null;
138 | }
139 | $param = $param[$key];
140 | }
141 | return $param;
142 | }
143 |
144 | /**
145 | * @param string $key
146 | * @return bool
147 | */
148 | public function hasParam(string $key, ...$deepKeys): bool
149 | {
150 | return [] === $deepKeys ? array_key_exists($key, $this->params) : null !== $this->getParam($key, ...$deepKeys);
151 | }
152 |
153 | /**
154 | * Yield key => value pairs.
155 | *
156 | * @param bool $decodeKeys
157 | * @param bool $decodeValues
158 | * @return Traversable
159 | */
160 | public function getPairs(bool $decodeKeys = false, bool $decodeValues = false): Traversable
161 | {
162 | return new Pairs((string) $this, $decodeKeys, $decodeValues, $this->getRenderer()->getSeparator());
163 | }
164 |
165 | /**
166 | * @param string $key
167 | * @param $value
168 | * @return QueryString
169 | */
170 | public function withParam(string $key, $value): self
171 | {
172 | $clone = clone $this;
173 | $clone->params[$key] = $value;
174 | return $clone;
175 | }
176 |
177 | /**
178 | * @param array $params
179 | * @return QueryString
180 | */
181 | public function withParams(array $params, bool $append = false): self
182 | {
183 | $clone = clone $this;
184 | if (!$append) {
185 | $clone->params = [];
186 | }
187 | foreach ($params as $key => $value) {
188 | $clone->params[(string) $key] = $value;
189 | }
190 | return $clone;
191 | }
192 |
193 | /**
194 | * @param string $key
195 | * @param array ...$deepKeys
196 | * @return QueryString
197 | */
198 | public function withoutParam(string $key, ...$deepKeys): self
199 | {
200 | $clone = clone $this;
201 |
202 | // $key does not exist
203 | if (!isset($clone->params[$key])) {
204 | return $clone;
205 | }
206 |
207 | // $key exists and there are no $deepKeys
208 | if ([] === $deepKeys) {
209 | unset($clone->params[$key]);
210 | return $clone;
211 | }
212 |
213 | // Deepkeys
214 | $clone->params[$key] = $this->removeFromPath($clone->params[$key], ...$deepKeys);
215 | return $clone;
216 | }
217 |
218 | /**
219 | * @return QueryStringRendererInterface
220 | */
221 | public function getRenderer(): QueryStringRendererInterface
222 | {
223 | return $this->renderer;
224 | }
225 |
226 | /**
227 | * @param QueryStringRendererInterface $renderer
228 | * @return QueryString
229 | */
230 | public function withRenderer(QueryStringRendererInterface $renderer): self
231 | {
232 | $clone = clone $this;
233 | $clone->renderer = $renderer;
234 | return $clone;
235 | }
236 |
237 | /**
238 | * @return string
239 | */
240 | public function __toString(): string
241 | {
242 | return $this->renderer->render($this);
243 | }
244 |
245 | /**
246 | * @param array $array
247 | * @return bool
248 | */
249 | private function isAnIndexedArray(array $array): bool
250 | {
251 | $keys = array_keys($array);
252 | return $keys === array_filter($keys, 'is_int');
253 | }
254 |
255 | /**
256 | * @param array $params
257 | * @param array ...$keys
258 | * @return array
259 | */
260 | private function removeFromPath(array $params, ...$keys): array
261 | {
262 | $nbKeys = count($keys);
263 | $lastIndex = $nbKeys - 1;
264 | $cursor = &$params;
265 |
266 | foreach ($keys as $k => $key) {
267 | if (!isset($cursor[$key])) {
268 | return $params; // End here if not found
269 | }
270 |
271 | if ($k === $lastIndex) {
272 | unset($cursor[$key]);
273 | if (is_array($cursor) && $this->isAnIndexedArray($cursor)) {
274 | $cursor = array_values($cursor);
275 | }
276 | break;
277 | }
278 |
279 | $cursor = &$cursor[$key];
280 | }
281 |
282 | return $params;
283 | }
284 |
285 | /**
286 | * Returns the default renderer.
287 | *
288 | * @return QueryStringRendererInterface
289 | */
290 | public static function getDefaultRenderer(): QueryStringRendererInterface
291 | {
292 | if (!isset(self::$defaultRenderer)) {
293 | self::restoreDefaultRenderer();
294 | }
295 | return self::$defaultRenderer;
296 | }
297 |
298 | /**
299 | * Changes default renderer.
300 | *
301 | * @param QueryStringRendererInterface $defaultRenderer
302 | */
303 | public static function setDefaultRenderer(QueryStringRendererInterface $defaultRenderer): void
304 | {
305 | self::$defaultRenderer = $defaultRenderer;
306 | }
307 |
308 | /**
309 | * Restores the default renderer.
310 | */
311 | public static function restoreDefaultRenderer(): void
312 | {
313 | self::$defaultRenderer = NativeRenderer::factory();
314 | }
315 |
316 | /**
317 | * Returns the default parser.
318 | *
319 | * @return QueryStringParserInterface
320 | */
321 | public static function getDefaultParser(): QueryStringParserInterface
322 | {
323 | if (!isset(self::$defaultParser)) {
324 | self::restoreDefaultParser();
325 | }
326 | return self::$defaultParser;
327 | }
328 |
329 | /**
330 | * Changes default parser.
331 | *
332 | * @param QueryStringParserInterface $defaultParser
333 | */
334 | public static function setDefaultParser(QueryStringParserInterface $defaultParser): void
335 | {
336 | self::$defaultParser = $defaultParser;
337 | }
338 |
339 | /**
340 | * Restores the default parser.
341 | */
342 | public static function restoreDefaultParser(): void
343 | {
344 | self::$defaultParser = new NativeParser();
345 | }
346 | }
347 |
--------------------------------------------------------------------------------
/src/Renderer/ArrayValuesNormalizerRenderer.php:
--------------------------------------------------------------------------------
1 | renderer = $renderer;
22 | }
23 |
24 | public static function factory(?QueryStringRendererInterface $renderer = null)
25 | {
26 | return new self($renderer ?? NativeRenderer::factory());
27 | }
28 |
29 | /**
30 | * @inheritDoc
31 | */
32 | public function render(QueryString $queryString): string
33 | {
34 | $separator = $this->getSeparator() ?? ini_get('arg_separator.output');
35 | $input = $this->renderer->render($queryString);
36 | $output = '';
37 |
38 | $iterator = new IteratorIterator(new Pairs($input, false, false, $separator));
39 | $iterator->rewind();
40 | while (true === $iterator->valid()) {
41 | $key = $iterator->key();
42 | $value = $iterator->current();
43 | $iterator->next();
44 | $output .= sprintf(
45 | '%s=%s',
46 | preg_replace('/\%5B\d+\%5D/', '%5B%5D', $key),
47 | $value
48 | );
49 | if (false !== $iterator->valid()) {
50 | $output .= $separator;
51 | }
52 | }
53 |
54 | return $output;
55 | }
56 |
57 | /**
58 | * @inheritDoc
59 | */
60 | public function getEncoding(): int
61 | {
62 | return $this->renderer->getEncoding();
63 | }
64 |
65 | /**
66 | * @inheritDoc
67 | */
68 | public function withEncoding(int $encoding): QueryStringRendererInterface
69 | {
70 | return new self($this->renderer->withEncoding($encoding));
71 | }
72 |
73 | /**
74 | * @inheritDoc
75 | */
76 | public function getSeparator(): ?string
77 | {
78 | return $this->renderer->getSeparator();
79 | }
80 |
81 | /**
82 | * @inheritDoc
83 | */
84 | public function withSeparator(?string $separator): QueryStringRendererInterface
85 | {
86 | return new self($this->renderer->withSeparator($separator));
87 | }
88 | }
89 |
--------------------------------------------------------------------------------
/src/Renderer/FlatRenderer.php:
--------------------------------------------------------------------------------
1 | renderer = $renderer;
17 | }
18 |
19 | public static function factory(?QueryStringRendererInterface $renderer = null)
20 | {
21 | return new self($renderer ?? NativeRenderer::factory());
22 | }
23 |
24 | /**
25 | * @inheritDoc
26 | */
27 | public function render(QueryString $queryString): string
28 | {
29 | $separator = $this->getSeparator() ?? ini_get('arg_separator.output');
30 | $parts = [[]];
31 |
32 | foreach ($queryString->getParams() as $key => $value) {
33 | $parts[] = $this->getParts($key, $value);
34 | }
35 |
36 | return \implode($separator, \array_merge([], ...$parts));
37 | }
38 |
39 | /**
40 | * @inheritDoc
41 | */
42 | public function getEncoding(): int
43 | {
44 | return $this->renderer->getEncoding();
45 | }
46 |
47 | /**
48 | * @inheritDoc
49 | */
50 | public function withEncoding(int $encoding): QueryStringRendererInterface
51 | {
52 | return new self($this->renderer->withEncoding($encoding));
53 | }
54 |
55 | /**
56 | * @inheritDoc
57 | */
58 | public function getSeparator(): ?string
59 | {
60 | return $this->renderer->getSeparator();
61 | }
62 |
63 | /**
64 | * @inheritDoc
65 | */
66 | public function withSeparator(?string $separator): QueryStringRendererInterface
67 | {
68 | return new self($this->renderer->withSeparator($separator));
69 | }
70 |
71 | private function getParts($key, $value): array
72 | {
73 | if (\is_iterable($value)) {
74 | $parts = [[]];
75 | foreach ($value as $sub) {
76 | $parts[] = $this->getParts($key, $sub);
77 | }
78 |
79 | return \array_merge([], ...$parts);
80 | }
81 |
82 | $encode = \PHP_QUERY_RFC1738 === $this->getEncoding() ? '\\urlencode' : '\\rawurlencode';
83 |
84 | return [$key . '=' . \call_user_func($encode, $value)];
85 | }
86 | }
87 |
--------------------------------------------------------------------------------
/src/Renderer/NativeRenderer.php:
--------------------------------------------------------------------------------
1 | encoding = $encoding;
18 | }
19 |
20 | /**
21 | * @param int $encoding
22 | * @return NativeRenderer
23 | * @throws \InvalidArgumentException
24 | */
25 | public static function factory(int $encoding = self::DEFAULT_ENCODING): self
26 | {
27 | self::validateEncoding($encoding);
28 |
29 | return new self($encoding);
30 | }
31 |
32 |
33 | /**
34 | * @inheritDoc
35 | */
36 | public function render(QueryString $queryString): string
37 | {
38 | return http_build_query(
39 | $queryString->getParams(),
40 | '',
41 | $this->separator ?? ini_get('arg_separator.output'),
42 | $this->encoding
43 | );
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/src/Renderer/QueryStringRendererInterface.php:
--------------------------------------------------------------------------------
1 | encoding;
23 | }
24 |
25 | /**
26 | * @param int $encoding
27 | * @return QueryStringRendererInterface
28 | * @throws \InvalidArgumentException
29 | */
30 | public function withEncoding(int $encoding): QueryStringRendererInterface
31 | {
32 | self::validateEncoding($encoding);
33 |
34 | $clone = clone $this;
35 | $clone->encoding = $encoding;
36 | return $clone;
37 | }
38 |
39 | /**
40 | * @return null|string
41 | */
42 | public function getSeparator(): ?string
43 | {
44 | return $this->separator;
45 | }
46 |
47 | /**
48 | * @param null|string $separator
49 | * @return QueryStringRendererInterface
50 | */
51 | public function withSeparator(?string $separator): QueryStringRendererInterface
52 | {
53 | $clone = clone $this;
54 | $clone->separator = $separator;
55 |
56 | return $clone;
57 | }
58 |
59 | /**
60 | * @param int $encoding
61 | * @throws \InvalidArgumentException
62 | */
63 | protected static function validateEncoding(int $encoding): void
64 | {
65 | if (!in_array($encoding, [PHP_QUERY_RFC1738, PHP_QUERY_RFC3986])) {
66 | throw new \InvalidArgumentException("Invalid encoding.");
67 | }
68 | }
69 | }
70 |
--------------------------------------------------------------------------------
/src/functions.php:
--------------------------------------------------------------------------------
1 | 'bar',
20 | 'sort' => [
21 | 'bar' => 'desc',
22 | 'foo' => 'asc',
23 | ],
24 | 'filters' => [
25 | 'foo' => [
26 | 'bar',
27 | 'baz',
28 | ],
29 | 'bar' => [
30 | 'foo bar',
31 | ],
32 | ],
33 | ];
34 |
35 | $qs = query_string($data);
36 | $renderer = withoutNumericIndices();
37 | $this->assertInstanceOf(ArrayValuesNormalizerRenderer::class, $renderer);
38 | $this->assertEquals('foo=bar&sort%5Bbar%5D=desc&sort%5Bfoo%5D=asc&filters%5Bfoo%5D%5B%5D=bar&filters%5Bfoo%5D%5B%5D=baz&filters%5Bbar%5D%5B%5D=foo%20bar', $renderer->render($qs));
39 | $this->assertEquals('foo=bar&sort[bar]=desc&sort[foo]=asc&filters[foo][]=bar&filters[foo][]=baz&filters[bar][]=foo bar', urldecode($qs->withRenderer($renderer)));
40 | $this->assertEquals('foo=bar&sort%5Bbar%5D=desc&sort%5Bfoo%5D=asc&filters%5Bfoo%5D%5B%5D=bar&filters%5Bfoo%5D%5B%5D=baz&filters%5Bbar%5D%5B%5D=foo+bar', $renderer->withEncoding(PHP_QUERY_RFC1738)->render($qs));
41 | $this->assertEquals('foo=bar&sort[bar]=desc&sort[foo]=asc&filters[foo][]=bar&filters[foo][]=baz&filters[bar][]=foo bar', urldecode($qs->withRenderer($renderer->withEncoding(PHP_QUERY_RFC1738))));
42 | }
43 |
44 | public function testRendererOnlyAffectsKeys(): void
45 | {
46 | $data = [
47 | 'foo' => [
48 | 'bar baz[0]'
49 | ]
50 | ];
51 | $qs = query_string($data);
52 | $this->assertEquals('foo%5B%5D=bar%20baz%5B0%5D', (string) $qs->withRenderer(withoutNumericIndices()));
53 | }
54 |
55 | public function testChangeEncoding(): void
56 | {
57 | $renderer = ArrayValuesNormalizerRenderer::factory();
58 | $this->assertNotSame($renderer->withEncoding($renderer->getEncoding()), $renderer);
59 |
60 | $this->assertEquals(QueryStringRendererInterface::DEFAULT_ENCODING, $renderer->getEncoding());
61 | $renderer = $renderer->withEncoding(PHP_QUERY_RFC1738);
62 | $this->assertEquals(PHP_QUERY_RFC1738, $renderer->getEncoding());
63 | }
64 |
65 | public function testChangeSeparator(): void
66 | {
67 |
68 | ini_set('arg_separator.output', '~');
69 |
70 | $qs = query_string(['foo' => 'bar', 'bar' => 'baz']);
71 | $renderer = ArrayValuesNormalizerRenderer::factory();
72 | $this->assertNull($renderer->getSeparator());
73 | $this->assertEquals('foo=bar~bar=baz', $renderer->render($qs));
74 | $this->assertNotSame($renderer->withSeparator($renderer->getSeparator()), $renderer);
75 |
76 | $renderer = $renderer->withSeparator('|');
77 | $this->assertEquals('|', $renderer->getSeparator());
78 | $this->assertEquals('foo=bar|bar=baz', $renderer->render($qs));
79 |
80 | $renderer = $renderer->withSeparator(null); // Reset to default
81 | $this->assertEquals('foo=bar~bar=baz', $renderer->render($qs));
82 |
83 | ini_set('arg_separator.output', $this->defaultSeparator);
84 | }
85 |
86 | public function testBlankSeparator(): void
87 | {
88 | $this->expectException(\RuntimeException::class);
89 | $qs = query_string(['foo' => 'bar', 'bar' => 'baz']);
90 | $renderer = ArrayValuesNormalizerRenderer::factory();
91 | $renderer = $renderer->withSeparator(''); // Blank separator
92 | $renderer->render($qs);
93 | }
94 |
95 | public function setUp(): void
96 | {
97 | $this->defaultSeparator = ini_get('arg_separator.output');
98 | }
99 |
100 | public function tearDown(): void
101 | {
102 | ini_set('arg_separator.output', $this->defaultSeparator);
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/tests/FlatParserTest.php:
--------------------------------------------------------------------------------
1 | ['foo', 'bar', 'foo bar'],
19 | 'foo' => 'bar',
20 | 'hi' => null,
21 | ];
22 | $this->assertEquals($expected, $qs->getParams());
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/tests/FlatRendererTest.php:
--------------------------------------------------------------------------------
1 | 'bar',
15 | 'foos' => [
16 | 'bar',
17 | 'foo bar',
18 | ],
19 | 'fruits' => [
20 | 'banana' => 'yellow',
21 | 'strawberry' => 'red',
22 | ],
23 | ];
24 |
25 | $qs = query_string($data);
26 | $renderer = flat();
27 |
28 | $this->assertEquals('foo=bar&foos=bar&foos=foo%20bar&fruits=yellow&fruits=red', (string) $qs->withRenderer(
29 | $renderer
30 | ));
31 |
32 | $this->assertEquals('foo=bar&foos=bar&foos=foo+bar&fruits=yellow&fruits=red', (string) $qs->withRenderer(
33 | $renderer->withEncoding(PHP_QUERY_RFC1738)
34 | ));
35 |
36 | $this->assertEquals('foo=bar;foos=bar;foos=foo+bar;fruits=yellow;fruits=red', (string) $qs->withRenderer(
37 | $renderer->withEncoding(PHP_QUERY_RFC1738)->withSeparator(';')
38 | ));
39 | }
40 |
41 | }
42 |
--------------------------------------------------------------------------------
/tests/NativeParserTest.php:
--------------------------------------------------------------------------------
1 | assertEquals(['foo' => 'bar', 'baz' => 'bat'], $parser->parse('foo=bar&baz=bat'));
15 | $this->assertEquals(['foo' => 'bar', 'baz' => 'bat'], $parser->parse('?foo=bar&baz=bat'));
16 | $this->assertEquals(['foo' => 'baz'], $parser->parse('foo=bar&foo=baz'));
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/tests/NativeRendererTest.php:
--------------------------------------------------------------------------------
1 | 'bar',
18 | 'sort' => [
19 | 'bar' => 'desc',
20 | 'foo' => 'asc',
21 | ],
22 | 'filters' => [
23 | 'foo' => [
24 | 'bar',
25 | 'baz',
26 | ],
27 | 'bar' => [
28 | 'foo bar',
29 | ],
30 | ],
31 | ];
32 |
33 | $qs = query_string($data);
34 | $renderer = NativeRenderer::factory();
35 | $this->assertInstanceOf(NativeRenderer::class, $renderer);
36 | $this->assertEquals('foo=bar&sort%5Bbar%5D=desc&sort%5Bfoo%5D=asc&filters%5Bfoo%5D%5B0%5D=bar&filters%5Bfoo%5D%5B1%5D=baz&filters%5Bbar%5D%5B0%5D=foo%20bar', $renderer->render($qs));
37 | $this->assertEquals('foo=bar&sort[bar]=desc&sort[foo]=asc&filters[foo][0]=bar&filters[foo][1]=baz&filters[bar][0]=foo bar', urldecode($renderer->render($qs)));
38 | $qs = $qs->withRenderer($qs->getRenderer()->withEncoding(PHP_QUERY_RFC1738));
39 | $this->assertEquals('foo=bar&sort%5Bbar%5D=desc&sort%5Bfoo%5D=asc&filters%5Bfoo%5D%5B0%5D=bar&filters%5Bfoo%5D%5B1%5D=baz&filters%5Bbar%5D%5B0%5D=foo+bar', $renderer->withEncoding(PHP_QUERY_RFC1738)->render($qs));
40 | $this->assertEquals('foo=bar&sort[bar]=desc&sort[foo]=asc&filters[foo][0]=bar&filters[foo][1]=baz&filters[bar][0]=foo bar', urldecode($qs->withRenderer($renderer->withEncoding(PHP_QUERY_RFC1738))));
41 | }
42 |
43 | public function testFactoryFails(): void
44 | {
45 | $this->expectException(\InvalidArgumentException::class);
46 | NativeRenderer::factory(1000);
47 | }
48 |
49 | public function testChangeEncodingFails(): void
50 | {
51 | $this->expectException(\InvalidArgumentException::class);
52 | $renderer = NativeRenderer::factory();
53 | $renderer->withEncoding(1000);
54 | }
55 |
56 | public function testChangeEncoding(): void
57 | {
58 | $renderer = NativeRenderer::factory();
59 | $this->assertNotSame($renderer->withEncoding($renderer->getEncoding()), $renderer);
60 |
61 | $this->assertEquals(QueryStringRendererInterface::DEFAULT_ENCODING, $renderer->getEncoding());
62 | $renderer = $renderer->withEncoding(PHP_QUERY_RFC1738);
63 | $this->assertEquals(PHP_QUERY_RFC1738, $renderer->getEncoding());
64 | }
65 |
66 | public function testChangeSeparator(): void
67 | {
68 |
69 | ini_set('arg_separator.output', '~');
70 |
71 | $qs = query_string(['foo' => 'bar', 'bar' => 'baz']);
72 | $renderer = NativeRenderer::factory();
73 | $this->assertNull($renderer->getSeparator());
74 | $this->assertEquals('foo=bar~bar=baz', $renderer->render($qs));
75 | $this->assertNotSame($renderer->withSeparator($renderer->getSeparator()), $renderer);
76 |
77 | $renderer = $renderer->withSeparator('|');
78 | $this->assertEquals('|', $renderer->getSeparator());
79 | $this->assertEquals('foo=bar|bar=baz', $renderer->render($qs));
80 |
81 | $renderer = $renderer->withSeparator(null); // Reset to default
82 | $this->assertEquals('foo=bar~bar=baz', $renderer->render($qs));
83 |
84 | $renderer = $renderer->withSeparator(''); // Blank separator
85 | $this->assertEquals('foo=barbar=baz', $renderer->render($qs));
86 |
87 | ini_set('arg_separator.output', $this->defaultSeparator);
88 | }
89 |
90 | public function setUp(): void
91 | {
92 | $this->defaultSeparator = ini_get('arg_separator.output');
93 | }
94 |
95 | public function tearDown(): void
96 | {
97 | ini_set('arg_separator.output', $this->defaultSeparator);
98 | }
99 | }
100 |
--------------------------------------------------------------------------------
/tests/PairsTest.php:
--------------------------------------------------------------------------------
1 | rewind();
18 | $this->assertEquals('foo%5Bbar%5D', $pairs->key());
19 | $this->assertEquals('baz%20bat', $pairs->current());
20 | }
21 |
22 | public function testPairsWithKeyDecoding(): void
23 | {
24 | $qs = (string) query_string('foo[bar]=baz bat');
25 | $pairs = new IteratorIterator(new Pairs($qs, true));
26 | $pairs->rewind();
27 | $this->assertEquals('foo[bar]', $pairs->key());
28 | $this->assertEquals('baz%20bat', $pairs->current());
29 |
30 | $qs = (string) query_string('foo[bar]=baz bat');
31 | $pairs = new IteratorIterator((new Pairs($qs))->withDecodeKeys(true));
32 | $pairs->rewind();
33 | $this->assertEquals('foo[bar]', $pairs->key());
34 | $this->assertEquals('baz%20bat', $pairs->current());
35 | }
36 |
37 | public function testPairsWithValueDecoding(): void
38 | {
39 | $qs = (string) query_string('foo[bar]=baz bat');
40 | $pairs = new IteratorIterator(new Pairs($qs, false, true));
41 | $pairs->rewind();
42 | $this->assertEquals('foo%5Bbar%5D', $pairs->key());
43 | $this->assertEquals('baz bat', $pairs->current());
44 |
45 | $qs = (string) query_string('foo[bar]=baz bat');
46 | $pairs = new IteratorIterator((new Pairs($qs))->withDecodeValues(true));
47 | $pairs->rewind();
48 | $this->assertEquals('foo%5Bbar%5D', $pairs->key());
49 | $this->assertEquals('baz bat', $pairs->current());
50 | }
51 |
52 | public function testPairsWithDifferentSeparator(): void
53 | {
54 | $qs = 'foo=bar;baz=bat';
55 | $this->assertEquals(['foo' => 'bar', 'baz' => 'bat'], iterator_to_array(new Pairs($qs, false, false, ';')));
56 | $qs = 'foo=bar;baz=bat';
57 | $this->assertEquals(['foo' => 'bar', 'baz' => 'bat'], iterator_to_array((new Pairs($qs))->withSeparator(';')));
58 | }
59 |
60 | public function testPairsWithMissingValues(): void
61 | {
62 | $qs = 'foo=&baz';
63 | $this->assertEquals(['foo' => '', 'baz' => null], iterator_to_array(new Pairs($qs, false, false)));
64 | }
65 |
66 | public function testPairsOnEmptyQueryString(): void
67 | {
68 | $qs = ' ';
69 | $this->assertEquals([], iterator_to_array(new Pairs($qs)));
70 | }
71 | }
72 |
--------------------------------------------------------------------------------
/tests/QueryStringTest.php:
--------------------------------------------------------------------------------
1 | 'bar', 'baz' => 'bat'];
23 | $qs = query_string($array);
24 | $this->assertInstanceOf(QueryString::class, $qs);
25 | $this->assertEquals('foo=bar&baz=bat', (string) $qs);
26 | $this->assertEquals($array, $qs->getParams());
27 |
28 |
29 | $uri = Http::createFromString('http://www.example.net?foo=bar&baz=bat');
30 | $qs = query_string($uri);
31 | $this->assertInstanceOf(QueryString::class, $qs);
32 | $this->assertEquals('foo=bar&baz=bat', (string) $qs);
33 |
34 |
35 | $qs = query_string('foo=bar&baz=bat');
36 | $this->assertInstanceOf(QueryString::class, $qs);
37 | $this->assertEquals('foo=bar&baz=bat', (string) $qs);
38 |
39 | $qs = query_string();
40 | $this->assertInstanceOf(QueryString::class, $qs);
41 | $this->assertEquals('', (string) $qs);
42 | $this->assertEquals([], $qs->getParams());
43 | }
44 |
45 | public function testFactoryFailsWithInvalidArgument(): void
46 | {
47 | $this->expectException(\InvalidArgumentException::class);
48 | query_string(new \stdClass());
49 | }
50 |
51 |
52 | public function testCurrentLocationFactory(): void
53 | {
54 | $_SERVER['REQUEST_URI'] = 'foo=bar&baz=bat';
55 |
56 | $qs = QueryString::createFromCurrentLocation();
57 | $this->assertEquals('foo=bar&baz=bat', (string) $qs);
58 |
59 | $qs = query_string('bat=bab')->withCurrentLocation();
60 | $this->assertEquals('foo=bar&baz=bat', (string) $qs);
61 |
62 | unset($_SERVER['REQUEST_URI']);
63 | }
64 |
65 | /**
66 | * In CLI mode, $_SERVER['REQUEST_URI'] should not be set.
67 | */
68 | public function testCurrentLocationFactoryFailsWhenNotSet(): void
69 | {
70 | $this->expectException(\RuntimeException::class);
71 | QueryString::createFromCurrentLocation();
72 | }
73 |
74 | public function testRenderer(): void
75 | {
76 | $data = [
77 | 'foo' => 'bar',
78 | 'sort' => [
79 | 'bar' => 'desc',
80 | 'foo' => 'asc',
81 | ],
82 | 'filters' => [
83 | 'foo' => [
84 | 'bar',
85 | 'baz',
86 | ],
87 | 'bar' => [
88 | 'foo bar',
89 | ],
90 | ],
91 | ];
92 |
93 | $qs = query_string($data);
94 | $renderer = NativeRenderer::factory();
95 | $this->assertEquals((string) $qs, $renderer->render($qs));
96 | $renderer = NativeRenderer::factory(PHP_QUERY_RFC3986);
97 | $this->assertEquals((string) $qs, $renderer->render($qs));
98 | }
99 |
100 | public function testChangeRenderer(): void
101 | {
102 | $data = [
103 | 'foo' => 'bar',
104 | 'sort' => [
105 | 'bar' => 'desc',
106 | 'foo' => 'asc',
107 | ],
108 | 'filters' => [
109 | 'foo' => [
110 | 'bar',
111 | 'baz',
112 | ],
113 | 'bar' => [
114 | 'foo bar',
115 | ],
116 | ],
117 | ];
118 | $qs = query_string($data);
119 | $renderer = ArrayValuesNormalizerRenderer::factory();
120 | $qs = $qs->withRenderer($renderer);
121 | $this->assertEquals((string) $qs, $renderer->render($qs));
122 | }
123 |
124 | public function testDefaultRenderer(): void
125 | {
126 | $renderer = QueryString::getDefaultRenderer();
127 | $this->assertEquals(NativeRenderer::class, get_class(QueryString::getDefaultRenderer()));
128 | $this->assertSame($renderer, query_string()->getRenderer());
129 |
130 | $renderer = ArrayValuesNormalizerRenderer::factory();
131 | QueryString::setDefaultRenderer($renderer);
132 | $this->assertEquals(ArrayValuesNormalizerRenderer::class, get_class(QueryString::getDefaultRenderer()));
133 | $this->assertSame($renderer, query_string()->getRenderer());
134 |
135 | QueryString::restoreDefaultRenderer();
136 | $this->assertEquals(NativeRenderer::class, get_class(QueryString::getDefaultRenderer()));
137 | $this->assertSame(QueryString::getDefaultRenderer(), query_string()->getRenderer());
138 | }
139 |
140 | public function testAddParam(): void
141 | {
142 | $qs = query_string(['foo' => 'bar']);
143 | $qs = $qs->withParam('bar', 'baz');
144 | $this->assertEquals(['foo' => 'bar', 'bar' => 'baz'], $qs->getParams());
145 | }
146 |
147 | public function testReplaceParams(): void
148 | {
149 | $qs = query_string(['foo' => 'bar']);
150 | $qs = $qs->withParams(['bar' => 'baz']);
151 | $this->assertEquals(['bar' => 'baz'], $qs->getParams());
152 | }
153 |
154 | public function testAppendParams(): void
155 | {
156 | $qs = query_string(['foo' => 'bar']);
157 | $qs = $qs->withParams(['bar' => 'baz'], true);
158 | $this->assertEquals(['foo' => 'bar', 'bar' => 'baz'], $qs->getParams());
159 | }
160 |
161 | public function testSimpleGetParam(): void
162 | {
163 | $qs = query_string(['foo' => 'bar']);
164 | $this->assertTrue($qs->hasParam('foo'));
165 | $this->assertEquals('bar', $qs->getParam('foo'));
166 | $this->assertFalse($qs->hasParam('bar'));
167 | $this->assertNull($qs->getParam('bar'));
168 | }
169 |
170 | public function testGetComplexGetParam(): void
171 | {
172 | $data = [
173 | 'filters' => [
174 | 'foo' => [
175 | 'bar',
176 | 'baz',
177 | ],
178 | ],
179 | ];
180 | $qs = query_string($data);
181 |
182 | // 1st level
183 | $this->assertEquals($data['filters'], $qs->getParam('filters'));
184 | $this->assertNull($qs->getParam('filters', 'bar'));
185 |
186 | // 2nd level
187 | $this->assertTrue($qs->hasParam('filters', 'foo'));
188 | $this->assertEquals($data['filters']['foo'], $qs->getParam('filters', 'foo'));
189 | $this->assertFalse($qs->hasParam('filters', 'bar'));
190 | $this->assertNull($qs->getParam('filters', 'bar'));
191 |
192 | // 3rd level
193 | $this->assertTrue($qs->hasParam('filters', 'foo', 0));
194 | $this->assertEquals($data['filters']['foo'][0], $qs->getParam('filters', 'foo', 0));
195 | $this->assertTrue($qs->hasParam('filters', 'foo', 1));
196 | $this->assertEquals($data['filters']['foo'][1], $qs->getParam('filters', 'foo', 1));
197 | $this->assertFalse($qs->hasParam('filters', 'foo', 2));
198 | $this->assertNull($qs->getParam('filters', 'foo', 2));
199 | $this->assertFalse($qs->hasParam('filters', 'bar', 0));
200 | $this->assertNull($qs->getParam('filters', 'bar', 0));
201 | }
202 |
203 | public function testSimpleWithoutParam(): void
204 | {
205 | $qs = query_string(['foo' => 'bar', 'bar' => 'baz']);
206 | $qs = $qs->withoutParam('bar');
207 | $this->assertEquals(['foo' => 'bar'], $qs->getParams());
208 | }
209 |
210 | public function testComplexWithoutParam(): void
211 | {
212 | $data = [
213 | 'filters' => [
214 | 'foo' => [
215 | 'bar',
216 | 'baz',
217 | ],
218 | 'bar' => [
219 | 'bar' => 'foo',
220 | 'foo' => 'bar',
221 | ],
222 | ],
223 | ];
224 |
225 | // Try to remove unexisting params
226 | $qs = query_string($data);
227 | $qs = $qs->withoutParam('filters', 'dummy');
228 | $this->assertEquals($data, $qs->getParams());
229 | $qs = $qs->withoutParam('filters', 'dummy', 'dummy');
230 | $this->assertEquals($data, $qs->getParams());
231 | $qs = $qs->withoutParam('filters', 'foo', 'dummy');
232 | $this->assertEquals($data, $qs->getParams());
233 |
234 | // Try to remove 2nd level key
235 | $qs2 = $qs->withoutParam('filters', 'bar');
236 | $this->assertEquals([
237 | 'filters' => [
238 | 'foo' => [
239 | 'bar',
240 | 'baz',
241 | ],
242 | ]
243 | ], $qs2->getParams());
244 |
245 | // Try to remove 3rd level key
246 | $qs2 = $qs->withoutParam('filters', 'foo', 0);
247 | $this->assertEquals([
248 | 'filters' => [
249 | 'foo' => [
250 | 'baz',
251 | ],
252 | 'bar' => [
253 | 'bar' => 'foo',
254 | 'foo' => 'bar',
255 | ],
256 | ]
257 | ], $qs2->getParams());
258 |
259 | $qs2 = $qs->withoutParam('filters', 'foo', 1);
260 | $this->assertEquals([
261 | 'filters' => [
262 | 'foo' => [
263 | 'bar',
264 | ],
265 | 'bar' => [
266 | 'bar' => 'foo',
267 | 'foo' => 'bar',
268 | ],
269 | ]
270 | ], $qs2->getParams());
271 |
272 | $qs2 = $qs->withoutParam('filters', 'bar', 'bar');
273 | $this->assertEquals([
274 | 'filters' => [
275 | 'foo' => [
276 | 'bar',
277 | 'baz',
278 | ],
279 | 'bar' => [
280 | 'foo' => 'bar',
281 | ],
282 | ]
283 | ], $qs2->getParams());
284 |
285 | $qs2 = $qs->withoutParam('filters', 'bar', 'foo');
286 | $this->assertEquals([
287 | 'filters' => [
288 | 'foo' => [
289 | 'bar',
290 | 'baz',
291 | ],
292 | 'bar' => [
293 | 'bar' => 'foo',
294 | ],
295 | ]
296 | ], $qs2->getParams());
297 | }
298 |
299 | public function testGetPairs(): void
300 | {
301 | $qs = query_string('a=b&c=d&e[]=f&e[]=g&h[foo]=bar&h[bar][]=baz&h[bar][]=bat&boo')->withRenderer(withoutNumericIndices());
302 | $pairs = new IteratorIterator($qs->getPairs());
303 | $pairs->rewind();
304 |
305 | $this->assertEquals('a', $pairs->key());
306 | $this->assertEquals('b', $pairs->current());
307 |
308 | $pairs->next();
309 |
310 | $this->assertEquals('c', $pairs->key());
311 | $this->assertEquals('d', $pairs->current());
312 |
313 | $pairs->next();
314 |
315 | $this->assertEquals('e%5B%5D', $pairs->key());
316 | $this->assertEquals('f', $pairs->current());
317 |
318 | $pairs->next();
319 |
320 | $this->assertEquals('e%5B%5D', $pairs->key());
321 | $this->assertEquals('g', $pairs->current());
322 |
323 | $pairs->next();
324 |
325 | $this->assertEquals('h%5Bfoo%5D', $pairs->key());
326 | $this->assertEquals('bar', $pairs->current());
327 |
328 | $pairs->next();
329 |
330 | $this->assertEquals('h%5Bbar%5D%5B%5D', $pairs->key());
331 | $this->assertEquals('baz', $pairs->current());
332 |
333 | $pairs->next();
334 |
335 | $this->assertEquals('h%5Bbar%5D%5B%5D', $pairs->key());
336 | $this->assertEquals('bat', $pairs->current());
337 |
338 | $pairs->next();
339 |
340 | $this->assertEquals('boo', $pairs->key());
341 | $this->assertEquals('', $pairs->current());
342 |
343 |
344 | $pairs->next();
345 | $this->assertNull($pairs->current());
346 | }
347 |
348 | public function testPairsWithKeyDecoding(): void
349 | {
350 | $qs = query_string('foo[bar]=baz bat');
351 | $pairs = new IteratorIterator($qs->getPairs(true));
352 | $pairs->rewind();
353 | $this->assertEquals('foo[bar]', $pairs->key());
354 | $this->assertEquals('baz%20bat', $pairs->current());
355 | }
356 |
357 | public function testPairsWithValueDecoding(): void
358 | {
359 | $qs = query_string('foo[bar]=baz bat');
360 | $pairs = new IteratorIterator($qs->getPairs(false, true));
361 | $pairs->rewind();
362 | $this->assertEquals('foo%5Bbar%5D', $pairs->key());
363 | $this->assertEquals('baz bat', $pairs->current());
364 | }
365 |
366 | public function testChangeEncoding(): void
367 | {
368 | $qs = query_string(['foo' => 'foo bar']);
369 | $this->assertEquals('foo=foo%20bar', (string) $qs);
370 | $qs = $qs->withRenderer(
371 | $qs->getRenderer()->withEncoding(PHP_QUERY_RFC1738)
372 | );
373 | $this->assertEquals('foo=foo+bar', (string) $qs);
374 | $qs = $qs->withRenderer(
375 | $qs->getRenderer()->withEncoding(PHP_QUERY_RFC3986)
376 | );
377 | $this->assertEquals('foo=foo%20bar', (string) $qs);
378 | }
379 |
380 | public function testImmutability(): void
381 | {
382 | $qs = query_string([]);
383 | $qs2 = $qs->withParam('bar', 'baz');
384 | $this->assertNotSame($qs, $qs2);
385 |
386 | $qs = query_string(['foo' => 'bar']);
387 | $qs2 = $qs->withoutParam('dummy');
388 | $this->assertNotSame($qs, $qs2);
389 |
390 | $qs = query_string(['foo' => 'bar']);
391 | $qs2 = $qs->withParams(['bar' => 'baz']);
392 | $this->assertNotSame($qs, $qs2);
393 |
394 | $qs = query_string([]);
395 | $qs2 = $qs->withRenderer(new class implements QueryStringRendererInterface {
396 |
397 | use QueryStringRendererTrait;
398 |
399 | public function render(QueryString $queryString): string
400 | {
401 | return '';
402 | }
403 |
404 | });
405 | $this->assertNotSame($qs, $qs2);
406 | }
407 |
408 | public function testAnotherParser(): void
409 | {
410 | $dummyParser = new class implements QueryStringParserInterface
411 | {
412 | public function parse(string $queryString): array
413 | {
414 | return ['ho' => 'hi'];
415 | }
416 |
417 | };
418 | $qs = query_string('foo=bar', $dummyParser);
419 | $this->assertEquals(['ho' => 'hi'], $qs->getParams());
420 | }
421 |
422 | public function testChangeDefaultParser(): void
423 | {
424 | $dummyParser = new class implements QueryStringParserInterface
425 | {
426 | public function parse(string $queryString): array
427 | {
428 | return ['ho' => 'hi'];
429 | }
430 |
431 | };
432 | QueryString::setDefaultParser($dummyParser);
433 | $qs = query_string('foo=bar');
434 | $this->assertEquals(['ho' => 'hi'], $qs->getParams());
435 |
436 | QueryString::restoreDefaultParser();
437 | $qs = query_string('foo=bar');
438 | $this->assertEquals(['foo' => 'bar'], $qs->getParams());
439 | }
440 | }
441 |
--------------------------------------------------------------------------------