Completed
Push — master ( c6b316...f71d6f )
by BENOIT
01:20
created

QueryString::getRenderer()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 1
eloc 2
nc 1
nop 0
dl 0
loc 4
rs 10
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 QueryStringRendererInterface
17
     */
18
    private $renderer;
19
20
    /**
21
     * QueryString constructor.
22
     * @param array|null                       $params
23
     * @param QueryStringRendererInterface|null $renderer
24
     * @throws \InvalidArgumentException
25
     */
26
    protected function __construct(?array $params = [], QueryStringRendererInterface $renderer = null)
27
    {
28
        $params = $params ?? [];
29
        foreach ($params as $key => $value) {
30
            $this->params[(string) $key] = $value;
31
        }
32
        $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...
33
    }
34
35
    /**
36
     * @param array $params
37
     * @param QueryStringRendererInterface|null $renderer
38
     * @return QueryString
39
     */
40
    private static function createFromParams(array $params, QueryStringRendererInterface $renderer = null): self
41
    {
42
        return new self($params, $renderer);
43
    }
44
45
    /**
46
     * @param \Psr\Http\Message\UriInterface $uri
47
     * @param QueryStringRendererInterface|null $renderer
48
     * @return QueryString
49
     * @throws \TypeError
50
     */
51
    private static function createFromUri($uri, QueryStringRendererInterface $renderer = null): self
52
    {
53
        if (!is_a($uri, 'Psr\Http\Message\UriInterface')) {
54
            throw new \TypeError(
55
                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...
56
                    'Argument 1 passed to %s() must implement interface Psr\Http\Message\UriInterface, %s given, called',
57
                    __METHOD__,
58
                    is_object($uri) ? get_class($uri) : gettype($uri)
59
                )
60
            );
61
        }
62
        $qs = $uri->getQuery();
63
        $params = [];
64
        parse_str($qs, $params);
65
        return new self($params, $renderer);
66
    }
67
68
    /**
69
     * @param string $qs
70
     * @param QueryStringRendererInterface|null $renderer
71
     * @return QueryString
72
     */
73
    private static function createFromString(string $qs, QueryStringRendererInterface $renderer = null): self
74
    {
75
        $params = [];
76
        parse_str($qs, $params);
77
        return new self($params, $renderer);
78
    }
79
80
    /**
81
     * @param QueryStringRendererInterface|null $renderer
82
     * @return QueryString
83
     * @throws \RuntimeException
84
     */
85
    public static function createFromCurrentLocation(QueryStringRendererInterface $renderer = null): self
86
    {
87
        if (!isset($_SERVER['REQUEST_URI'])) {
88
            throw new \RuntimeException('$_SERVER[\'REQUEST_URI\'] has not been set.');
89
        }
90
        return self::createFromString($_SERVER['REQUEST_URI'], $renderer);
91
    }
92
93
    /**
94
     * @return QueryString
95
     * @throws \RuntimeException
96
     */
97
    public function withCurrentLocation(): self
98
    {
99
        return self::createFromCurrentLocation($this->renderer);
100
    }
101
102
    /**
103
     * @param          $input
104
     * @return QueryString
105
     * @throws \InvalidArgumentException
106
     */
107
    public static function factory($input = null, QueryStringRendererInterface $renderer = null): self
108
    {
109
        if (is_array($input)) {
110
            return self::createFromParams($input, $renderer);
111
        } elseif (is_a($input, 'Psr\Http\Message\UriInterface')) {
112
            return self::createFromUri($input, $renderer);
113
        } elseif (is_string($input)) {
114
            return self::createFromString($input, $renderer);
115
        } elseif (null === $input) {
116
            return self::createFromParams([], $renderer);
117
        }
118
        throw new \InvalidArgumentException(sprintf('Expected array, string or Psr\Http\Message\UriInterface, got %s', is_object($input) ? get_class($input) : gettype($input)));
119
    }
120
121
    /**
122
     * @return array
123
     */
124
    public function getParams(): ?array
125
    {
126
        return $this->params;
127
    }
128
129
    /**
130
     * @param string $key
131
     * @param array  ...$deepKeys
132
     * @return mixed|null
133
     */
134
    public function getParam(string $key, ...$deepKeys)
135
    {
136
        $param = $this->params[$key] ?? null;
137
        foreach ($deepKeys as $key) {
138
            if (!isset($param[$key])) {
139
                return null;
140
            }
141
            $param = $param[$key];
142
        }
143
        return $param;
144
    }
145
146
    /**
147
     * @param string $key
148
     * @return bool
149
     */
150
    public function hasParam(string $key, ...$deepKeys): bool
151
    {
152
        return [] === $deepKeys ? array_key_exists($key, $this->params) : null !== $this->getParam($key, ...$deepKeys);
153
    }
154
155
    /**
156
     * @param string $key
157
     * @param        $value
158
     * @return QueryString
159
     */
160
    public function withParam(string $key, $value): self
161
    {
162
        $clone = clone $this;
163
        $clone->params[$key] = $value;
164
        return $clone;
165
    }
166
167
    /**
168
     * @param array $params
169
     * @return QueryString
170
     */
171
    public function withParams(array $params): self
172
    {
173
        $clone = clone $this;
174
        $clone->params = [];
175
        foreach ($params as $key => $value) {
176
            $clone->params[(string) $key] = $value;
177
        }
178
        return $clone;
179
    }
180
181
    /**
182
     * @param string $key
183
     * @param array  ...$deepKeys
184
     * @return QueryString
185
     */
186
    public function withoutParam(string $key, ...$deepKeys): self
187
    {
188
        $clone = clone $this;
189
190
        // $key does not exist
191
        if (!isset($clone->params[$key])) {
192
            return $clone;
193
        }
194
195
        // $key exists and there are no $deepKeys
196
        if ([] === $deepKeys) {
197
            unset($clone->params[$key]);
198
            return $clone;
199
        }
200
201
        // Deepkeys
202
        $clone->params[$key] = $this->removeFromPath($clone->params[$key], ...$deepKeys);
203
        return $clone;
204
    }
205
206
    /**
207
     * @return QueryStringRendererInterface
208
     */
209
    public function getRenderer(): QueryStringRendererInterface
210
    {
211
        return $this->renderer;
212
    }
213
214
    /**
215
     * @param QueryStringRendererInterface $renderer
216
     * @return QueryString
217
     */
218
    public function withRenderer(QueryStringRendererInterface $renderer): self
219
    {
220
        $clone = clone $this;
221
        $clone->renderer = $renderer;
222
        return $clone;
223
    }
224
225
    /**
226
     * @return string
227
     */
228
    public function __toString(): string
229
    {
230
        return $this->renderer->render($this);
231
    }
232
233
    /**
234
     * @param array $array
235
     * @return bool
236
     */
237
    private function isAnIndexedArray(array $array): bool
238
    {
239
        $keys = array_keys($array);
240
        return $keys === array_filter($keys, 'is_int');
241
    }
242
243
    /**
244
     * @param array $params
245
     * @param array ...$keys
246
     * @return array
247
     */
248
    private function removeFromPath(array $params, ...$keys): array
249
    {
250
        $nbKeys = count($keys);
251
        $lastIndex = $nbKeys - 1;
252
        $cursor = &$params;
253
254
        foreach ($keys as $k => $key) {
255
            if (!isset($cursor[$key])) {
256
                return $params; // End here if not found
257
            }
258
259
            if ($k === $lastIndex) {
260
                unset($cursor[$key]);
261
                if (is_array($cursor) && $this->isAnIndexedArray($cursor)) {
262
                    $cursor = array_values($cursor);
263
                }
264
                break;
265
            }
266
267
            $cursor = &$cursor[$key];
268
        }
269
270
        return $params;
271
    }
272
}
273