Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
9 / 9
CRAP
100.00% covered (success)
100.00%
1 / 1
QueryString
100.00% covered (success)
100.00%
29 / 29
100.00% covered (success)
100.00%
9 / 9
16
100.00% covered (success)
100.00%
1 / 1
 getQuerystringParamNameTester
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setQuerystringParamNameTester
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setParams
100.00% covered (success)
100.00%
2 / 2
100.00% covered (success)
100.00%
1 / 1
2
 setParam
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
4
 getParams
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 setQueryEncoding
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
2
 getQueryEncoding
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 parse
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
3
 render
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
1
1<?php
2/**
3 * @author: Doug Wilbourne (dougwilbourne@gmail.com)
4 * @version 1.0
5 */
6
7namespace pvc\http\url;
8
9use pvc\http\err\InvalidQueryEncodingException;
10use pvc\http\err\InvalidQuerystringException;
11use pvc\http\err\InvalidQuerystringParamNameException;
12use pvc\http\err\InvalidQuerystringSeparatorException;
13use pvc\interfaces\http\QueryStringInterface;
14use pvc\interfaces\validator\ValTesterInterface;
15
16/**
17 * Class QueryString
18 *
19 * The purpose of the class is to provide an easy way to manipulate querystring parameters and values without
20 * having to do any string manipulation.  Once the parameter names and corresponding values are configured, this
21 * object outputs an actual querystring.  You can build a querystring from scratch with this object.  You can
22 * also hydrate this object with an existing querystring using the ParserQueryString object in the pvc Parser
23 * library.
24 */
25class QueryString implements QueryStringInterface
26{
27    /**
28     * @var non-empty-string
29     */
30    protected string $separator = '&';
31
32    /**
33     * @var array<string, string>
34     *
35     * param name => value pairs.  Because http_build_query's first argument is an array (or an object), and because
36     * it uses the keys to the array to generate parameter names, there is no getting around the fact that the
37     * parameter names must all be unique.  So although a querystring like '?a=4&a=5' is not illegal (and who knows
38     * why you would ever want to do it), you can't generate such a thing using http_build_query, which is what the
39     * render method below uses to generate the querystring.
40     */
41    protected array $params = [];
42
43    /**
44     * @var int
45     * this is the default for http_build_query
46     */
47    protected int $queryEncoding = PHP_QUERY_RFC1738;
48
49    /**
50     * @var ValTesterInterface<string>
51     *
52     * The http_build_query function has a parameter called 'numeric prefix', which will prepend a string (which
53     * must start with a letter) to a numeric array index in order to create a query parameter name.  This class
54     * takes a slightly different approach by testing each proposed parameter name before using it. So you can be as
55     * restrictive or as lax as you would like in creating parameter names, as long as the parameter names are strings.
56     * But in theory, no testing is really required: everything gets url encoded before being transmitted anyway.......
57     */
58    protected ValTesterInterface $querystringParamNameTester;
59
60    /**
61     * getQuerystringParamNameTester
62     * @return ValTesterInterface<string>|null
63     */
64    public function getQuerystringParamNameTester(): ?ValTesterInterface
65    {
66        return $this->querystringParamNameTester ?? null;
67    }
68
69    /**
70     * setQuerystringParamNameTester
71     * @param ValTesterInterface<string> $querystringParamNameTester
72     */
73    public function setQuerystringParamNameTester(ValTesterInterface $querystringParamNameTester): void
74    {
75        $this->querystringParamNameTester = $querystringParamNameTester;
76    }
77
78    /**
79     * setParams
80     * @param array<string, string> $params
81     * @throws InvalidQuerystringException
82     * @throws InvalidQuerystringParamNameException
83     */
84    public function setParams(array $params) : void
85    {
86        foreach ($params as $key => $value) {
87            $this->setParam($key, $value);
88        }
89    }
90
91    /**
92     * addParam
93     * @param string $varName
94     * @param string $value
95     * @throws InvalidQuerystringException
96     * @throws InvalidQuerystringParamNameException
97     *
98     * will overwrite duplicate parameter names
99     */
100    public function setParam(string $varName, string $value): void
101    {
102        if ($varName === '') {
103            throw new InvalidQuerystringException();
104        }
105        $nameTester = $this->getQuerystringParamNameTester();
106        if ($nameTester && !$nameTester->testValue($varName)) {
107            throw new InvalidQuerystringParamNameException();
108        }
109        $this->params[$varName] = $value;
110    }
111
112    /**
113     * getParams
114     * @return array<string, string>
115     */
116    public function getParams(): array
117    {
118        return $this->params;
119    }
120
121    /**
122     * setQueryEncoding
123     * @param int $encoding
124     * @throws InvalidQueryEncodingException
125     */
126    public function setQueryEncoding(int $encoding): void
127    {
128        if (!in_array($encoding, [PHP_QUERY_RFC1738, PHP_QUERY_RFC3986])) {
129            throw new InvalidQueryEncodingException();
130        }
131        $this->queryEncoding = $encoding;
132    }
133
134    /**
135     * getQueryEncoding
136     * @return int
137     */
138    public function getQueryEncoding(): int
139    {
140        return $this->queryEncoding;
141    }
142
143    /**
144     * parse
145     * parses a querystring
146     *
147     * @param string  $queryString
148     *
149     * @return void
150     *
151     */
152    public function parse(string $queryString): void
153    {
154        $params = [];
155        $queryString = trim($queryString, '?');
156
157        $paramStrings = explode($this->separator, $queryString);
158
159        foreach ($paramStrings as $paramString) {
160            $array = explode('=', $paramString);
161
162            /**
163             * cannot have a string like 'a=1=2'.  Need 0 or 1 equals signs.  Zero equals signs is a parameter with no
164             * value attached
165             */
166            if (count($array) > 2) {
167                throw new InvalidQuerystringException();
168            }
169
170            $paramName = $array[0];
171            $paramValue = $array[1] ?? '';
172
173            /**
174             * if the parameter name is duplicated in the querystring, this results in the last value being used
175             */
176            $params[$paramName] = $paramValue;
177        }
178
179        $this->setParams($params);
180    }
181
182
183    /**
184     * render
185     * @return string
186     *
187     * the numeric prefix parameter will never be used because the querystringParameterTester ensures the parameter
188     * name starts with a letter.
189     *
190     * The method does not prepend the querystring with a '?'.  The '?' is a delimiter in the URL, not really part of
191     * the querystring per se.  The '?' is inserted when the URL is rendered.
192     *
193     *  urlencode / urldecode translate the percent-encoded bits as well as plus signs.  rawurlencode
194     *  and rawurldecode do not translate plus signs, and are designed to be compliant with RFC 3986, which specifies
195     *  the syntaxes for URI's, URN's and URL's.
196     */
197    public function render(): string
198    {
199        $numericPrefix = '';
200        $argSeparator = $this->separator;
201        return http_build_query($this->getParams(), $numericPrefix, $argSeparator, $this->queryEncoding);
202    }
203}