Completed
Push — master ( 0918de...c6b316 )
by BENOIT
01:36
created

QueryString::withRenderer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 6
Code Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 4
nc 1
nop 1
dl 0
loc 6
rs 9.4285
c 0
b 0
f 0
1
<?php
2
3
namespace BenTools\QueryString;
4
5
use BenTools\QueryString\Renderer\NativeRenderer;
6
use BenTools\QueryString\Renderer\QueryStringRendererInterface;
7
8
final class QueryString
9
{
10
    /**
11
     * @var array
12
     */
13
    private $params = [];
14
15
    /**
16
     * @var string
17
     */
18
    private $separator;
19
20
    /**
21
     * @var QueryStringRendererInterface
22
     */
23
    private $renderer;
24
25
    /**
26
     * QueryString constructor.
27
     * @param array|null                       $params
28
     * @param QueryStringRendererInterface|null $renderer
29
     * @throws \InvalidArgumentException
30
     */
31
    protected function __construct(?array $params = [], QueryStringRendererInterface $renderer = null)
32
    {
33
        $params = $params ?? [];
34
        foreach ($params as $key => $value) {
35
            $this->params[(string) $key] = $value;
36
        }
37
        $this->renderer = $renderer ?? NativeRenderer::rfc3986();
0 ignored issues
show
Documentation Bug introduced by
It seems like $renderer ?? \BenTools\Q...tiveRenderer::rfc3986() can also be of type object<self>. However, the property $renderer is declared as type object<BenTools\QueryStr...tringRendererInterface>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
38
    }
39
40
    /**
41
     * @param array $params
42
     * @param QueryStringRendererInterface|null $renderer
43
     * @return QueryString
44
     */
45
    private static function createFromParams(array $params, QueryStringRendererInterface $renderer = null): self
46
    {
47
        return new self($params, $renderer);
48
    }
49
50
    /**
51
     * @param \Psr\Http\Message\UriInterface $uri
52
     * @param QueryStringRendererInterface|null $renderer
53
     * @return QueryString
54
     * @throws \TypeError
55
     */
56
    private static function createFromUri($uri, QueryStringRendererInterface $renderer = null): self
57
    {
58
        if (!is_a($uri, 'Psr\Http\Message\UriInterface')) {
59
            throw new \TypeError(
60
                sprintf(
0 ignored issues
show
Unused Code introduced by
The call to TypeError::__construct() has too many arguments starting with sprintf('Argument 1 pass...($uri) : gettype($uri)).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
61
                    'Argument 1 passed to %s() must implement interface Psr\Http\Message\UriInterface, %s given, called',
62
                    __METHOD__,
63
                    is_object($uri) ? get_class($uri) : gettype($uri)
64
                )
65
            );
66
        }
67
        $qs = $uri->getQuery();
68
        $params = [];
69
        parse_str($qs, $params);
70
        return new self($params, $renderer);
71
    }
72
73
    /**
74
     * @param string $qs
75
     * @param QueryStringRendererInterface|null $renderer
76
     * @return QueryString
77
     */
78
    private static function createFromString(string $qs, QueryStringRendererInterface $renderer = null): self
79
    {
80
        $params = [];
81
        parse_str($qs, $params);
82
        return new self($params, $renderer);
83
    }
84
85
    /**
86
     * @param QueryStringRendererInterface|null $renderer
87
     * @return QueryString
88
     * @throws \RuntimeException
89
     */
90
    public static function createFromCurrentLocation(QueryStringRendererInterface $renderer = null): self
91
    {
92
        if (!isset($_SERVER['REQUEST_URI'])) {
93
            throw new \RuntimeException('$_SERVER[\'REQUEST_URI\'] has not been set.');
94
        }
95
        return self::createFromString($_SERVER['REQUEST_URI'], $renderer);
96
    }
97
98
    /**
99
     * @return QueryString
100
     * @throws \RuntimeException
101
     */
102
    public function withCurrentLocation(): self
103
    {
104
        return self::createFromCurrentLocation($this->renderer);
105
    }
106
107
    /**
108
     * @param          $input
109
     * @return QueryString
110
     * @throws \InvalidArgumentException
111
     */
112
    public static function factory($input = null, QueryStringRendererInterface $renderer = null): self
113
    {
114
        if (is_array($input)) {
115
            return self::createFromParams($input, $renderer);
116
        } elseif (is_a($input, 'Psr\Http\Message\UriInterface')) {
117
            return self::createFromUri($input, $renderer);
118
        } elseif (is_string($input)) {
119
            return self::createFromString($input, $renderer);
120
        } elseif (null === $input) {
121
            return self::createFromParams([], $renderer);
122
        }
123
        throw new \InvalidArgumentException(sprintf('Expected array, string or Psr\Http\Message\UriInterface, got %s', is_object($input) ? get_class($input) : gettype($input)));
124
    }
125
126
    /**
127
     * @return array
128
     */
129
    public function getParams(): ?array
130
    {
131
        return $this->params;
132
    }
133
134
    /**
135
     * @param string $key
136
     * @param array  ...$deepKeys
137
     * @return mixed|null
138
     */
139
    public function getParam(string $key, ...$deepKeys)
140
    {
141
        $param = $this->params[$key] ?? null;
142
        foreach ($deepKeys as $key) {
143
            if (!isset($param[$key])) {
144
                return null;
145
            }
146
            $param = $param[$key];
147
        }
148
        return $param;
149
    }
150
151
    /**
152
     * @param string $key
153
     * @return bool
154
     */
155
    public function hasParam(string $key, ...$deepKeys): bool
156
    {
157
        return [] === $deepKeys ? array_key_exists($key, $this->params) : null !== $this->getParam($key, ...$deepKeys);
158
    }
159
160
    /**
161
     * @param string $key
162
     * @param        $value
163
     * @return QueryString
164
     */
165
    public function withParam(string $key, $value): self
166
    {
167
        $clone = clone $this;
168
        $clone->params[$key] = $value;
169
        return $clone;
170
    }
171
172
    /**
173
     * @param array $params
174
     * @return QueryString
175
     */
176
    public function withParams(array $params): self
177
    {
178
        $clone = clone $this;
179
        $clone->params = [];
180
        foreach ($params as $key => $value) {
181
            $clone->params[(string) $key] = $value;
182
        }
183
        return $clone;
184
    }
185
186
    /**
187
     * @param string $key
188
     * @param array  ...$deepKeys
189
     * @return QueryString
190
     */
191
    public function withoutParam(string $key, ...$deepKeys): self
192
    {
193
        $clone = clone $this;
194
195
        // $key does not exist
196
        if (!isset($clone->params[$key])) {
197
            return $clone;
198
        }
199
200
        // $key exists and there are no $deepKeys
201
        if ([] === $deepKeys) {
202
            unset($clone->params[$key]);
203
            return $clone;
204
        }
205
206
        // Deepkeys
207
        $clone->params[$key] = $this->removeFromPath($clone->params[$key], ...$deepKeys);
208
        return $clone;
209
    }
210
211
    /**
212
     * @return null|string
213
     */
214
    public function getSeparator(): ?string
215
    {
216
        return $this->separator;
217
    }
218
219
    /**
220
     * @param null|string $separator
221
     * @return QueryString
222
     */
223
    public function withSeparator(?string $separator): self
224
    {
225
        $clone = clone $this;
226
        $clone->separator = $separator;
227
        return $clone;
228
    }
229
230
    /**
231
     * @param QueryStringRendererInterface $renderer
232
     * @return QueryString
233
     */
234
    public function withRenderer(QueryStringRendererInterface $renderer): self
235
    {
236
        $clone = clone $this;
237
        $clone->renderer = $renderer;
238
        return $clone;
239
    }
240
241
    /**
242
     * @return string
243
     */
244
    public function __toString(): string
245
    {
246
        return $this->renderer->render($this);
247
    }
248
249
    /**
250
     * @param array $array
251
     * @return bool
252
     */
253
    private function isAnIndexedArray(array $array): bool
254
    {
255
        $keys = array_keys($array);
256
        return $keys === array_filter($keys, 'is_int');
257
    }
258
259
    /**
260
     * @param array $params
261
     * @param array ...$keys
262
     * @return array
263
     */
264
    private function removeFromPath(array $params, ...$keys): array
265
    {
266
        $nbKeys = count($keys);
267
        $lastIndex = $nbKeys - 1;
268
        $cursor = &$params;
269
270
        foreach ($keys as $k => $key) {
271
            if (!isset($cursor[$key])) {
272
                return $params; // End here if not found
273
            }
274
275
            if ($k === $lastIndex) {
276
                unset($cursor[$key]);
277
                if (is_array($cursor) && $this->isAnIndexedArray($cursor)) {
278
                    $cursor = array_values($cursor);
279
                }
280
                break;
281
            }
282
283
            $cursor = &$cursor[$key];
284
        }
285
286
        return $params;
287
    }
288
}
289