Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
97.89% covered (success)
97.89%
93 / 95
77.78% covered (warning)
77.78%
7 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
UriFactory
97.89% covered (success)
97.89%
93 / 95
77.78% covered (warning)
77.78%
7 / 9
49
0.00% covered (danger)
0.00%
0 / 1
 __construct
n/a
0 / 0
n/a
0 / 0
1
 getQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hasQuery
0.00% covered (danger)
0.00%
0 / 1
0.00% covered (danger)
0.00%
0 / 1
2
 clean
100.00% covered (success)
100.00%
5 / 5
100.00% covered (success)
100.00%
1 / 1
4
 setQuery
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
3
 setupUriBuilder
100.00% covered (success)
100.00%
20 / 20
100.00% covered (success)
100.00%
1 / 1
4
 clear
100.00% covered (success)
100.00%
4 / 4
100.00% covered (success)
100.00%
1 / 1
2
 clearLike
100.00% covered (success)
100.00%
6 / 6
100.00% covered (success)
100.00%
1 / 1
3
 unique
100.00% covered (success)
100.00%
37 / 37
100.00% covered (success)
100.00%
1 / 1
26
 build
94.12% covered (success)
94.12%
16 / 17
0.00% covered (danger)
0.00%
0 / 1
4.00
1<?php
2/**
3 * Jingga
4 *
5 * PHP Version 8.1
6 *
7 * @package   phpOMS\Uri
8 * @copyright Dennis Eichhorn
9 * @license   OMS License 2.0
10 * @version   1.0.0
11 * @link      https://jingga.app
12 */
13declare(strict_types=1);
14
15namespace phpOMS\Uri;
16
17/**
18 * UriFactory class.
19 *
20 * Used in order to create a uri
21 *
22 * @package phpOMS\Uri
23 * @license OMS License 2.0
24 * @link    https://jingga.app
25 * @since   1.0.0
26 */
27final class UriFactory
28{
29    /**
30     * Dynamic query elements.
31     *
32     * @var string[]
33     * @since 1.0.0
34     */
35    private static array $uri = [];
36
37    /**
38     * Function for parsing urls
39     *
40     * @var null|\Closure
41     * @since 1.0.0
42     */
43    private static ?\Closure $replaceFunction = null;
44
45    /**
46     * Constructor.
47     *
48     * @since 1.0.0
49     * @codeCoverageIgnore
50     */
51    private function __construct()
52    {
53    }
54
55    /**
56     * Set global query replacements.
57     *
58     * @param string $key Replacement key
59     *
60     * @return null|string
61     *
62     * @since 1.0.0
63     */
64    public static function getQuery(string $key) : ?string
65    {
66        return self::$uri[$key] ?? null;
67    }
68
69    /**
70     * Has query replacement.
71     *
72     * @param string $key Replacement key
73     *
74     * @return bool
75     *
76     * @since 1.0.0
77     */
78    public static function hasQuery(string $key) : bool
79    {
80        return isset(self::$uri[$key]);
81    }
82
83    /**
84     * Cleanup
85     *
86     * @param string $identifier Identifier for cleaning up (e.g. * = everything, / = only path, ? = only query parameters, # only fragment etc.)
87     *
88     * @return void
89     *
90     * @since 1.0.0
91     */
92    public static function clean(string $identifier = '?') : void
93    {
94        if ($identifier === '*') {
95            self::$uri = [];
96        } else {
97            foreach (self::$uri as $key => $_) {
98                if (\str_starts_with($key, $identifier)) {
99                    unset(self::$uri[$key]);
100                }
101            }
102        }
103    }
104
105    /**
106     * Set global query replacements.
107     *
108     * @param string $key       Replacement key
109     * @param string $value     Replacement value
110     * @param bool   $overwrite Overwrite if already exists
111     *
112     * @return bool
113     *
114     * @since 1.0.0
115     */
116    public static function setQuery(string $key, string $value, bool $overwrite = false) : bool
117    {
118        if ($overwrite || !isset(self::$uri[$key])) {
119            self::$uri[$key] = $value;
120
121            return true;
122        }
123
124        return false;
125    }
126
127    /**
128     * Setup uri builder based on current request
129     *
130     * @param UriInterface $uri Uri
131     *
132     * @return void
133     *
134     * @since 1.0.0
135     */
136    public static function setupUriBuilder(UriInterface $uri) : void
137    {
138        self::setQuery('/scheme', $uri->scheme);
139        self::setQuery('/host', $uri->host);
140        self::setQuery('/port', (string) $uri->port);
141        self::setQuery('/tld', \rtrim($uri->getBase(), '/'));
142        self::setQuery('/rootPath', $uri->getRootPath());
143        self::setQuery('?', '?' . $uri->getQuery());
144        self::setQuery('%', $uri->__toString());
145        self::setQuery('#', $uri->fragment);
146        self::setQuery('/', $uri->getPath());
147        self::setQuery(':user', $uri->user);
148        self::setQuery(':pass', $uri->pass);
149
150        $data = $uri->getPathElements();
151        foreach ($data as $key => $value) {
152            self::setQuery('/' . $key, $value);
153        }
154
155        $data = $uri->getQueryArray();
156        foreach ($data as $key => $value) {
157            self::setQuery('?' . $key, $value, true);
158        }
159
160        $data = $uri->fragments;
161        foreach ($data as $key => $value) {
162            self::setQuery('#' . $key, $value, true);
163        }
164    }
165
166    /**
167     * Clear uri component
168     *
169     * @param string $key Uri component key
170     *
171     * @return bool
172     *
173     * @since 1.0.0
174     */
175    public static function clear(string $key) : bool
176    {
177        if (isset(self::$uri[$key])) {
178            unset(self::$uri[$key]);
179
180            return true;
181        }
182
183        return false;
184    }
185
186    /**
187     * Clear uri components that follow a certain pattern
188     *
189     * @param string $pattern Uri key pattern to remove
190     *
191     * @return bool
192     *
193     * @since 1.0.0
194     */
195    public static function clearLike(string $pattern) : bool
196    {
197        $success = false;
198
199        foreach (self::$uri as $key => $value) {
200            if (((bool) \preg_match('~^' . $pattern . '$~', $key))) {
201                unset(self::$uri[$key]);
202                $success = true;
203            }
204        }
205
206        return $success;
207    }
208
209    /**
210     * Simplify url
211     *
212     * While adding, and removing elements to a uri it can have multiple parameters or empty parameters which need to be cleaned up
213     *
214     * @param string $url Url to simplify
215     *
216     * @return string
217     *
218     * @since 1.0.0
219     */
220    private static function unique(string $url) : string
221    {
222        // handle edge cases / normalization
223        /*
224        $url = \str_replace(
225                ['=%', '=#', '=?'],
226                ['=%25', '=%23', '=%3F'],
227                $url
228            );
229        */
230
231        if (\stripos($url, '?') === false && ($pos = \stripos($url, '&')) !== false) {
232            $url = \substr_replace($url, '?', $pos, 1);
233        }
234
235        /** @var array $urlStructure */
236        $urlStructure = \parse_url($url);
237        if ($urlStructure === false) {
238            return $url; // @codeCoverageIgnore
239        }
240
241        if (isset($urlStructure['query'])) {
242            $len = \strlen($urlStructure['query']);
243            for ($i = 0; $i < $len; ++$i) {
244                if ($urlStructure['query'][$i] === '?') {
245                    $urlStructure['query'] = \substr_replace($urlStructure['query'], '&', $i, 1);
246                } elseif ($urlStructure['query'][$i] === '\\') {
247                    ++$i;
248                }
249            }
250
251            \parse_str($urlStructure['query'], $urlStructure['query']);
252
253            foreach ($urlStructure['query'] as $para => $query) {
254                if (($query === '' && \stripos($url, $para . '=') !== false) || $query === '---') {
255                    unset($urlStructure['query'][$para]);
256                }
257            }
258        }
259
260        $escaped =
261            (isset($urlStructure['scheme']) && !empty($urlStructure['scheme'])
262                ? $urlStructure['scheme'] . '://' : '')
263            . (isset($urlStructure['username'])
264                ? $urlStructure['username'] . ':' : '')
265            . (isset($urlStructure['password'])
266                ? $urlStructure['password'] . '@' : '')
267            . (isset($urlStructure['host']) && !empty($urlStructure['host'])
268                ? $urlStructure['host'] : '')
269            . (isset($urlStructure['port']) && !empty($urlStructure['port'])
270                ? ':' . $urlStructure['port'] : '')
271            . (isset($urlStructure['path']) && !empty($urlStructure['path'])
272                ? $urlStructure['path'] : '')
273            . (isset($urlStructure['query']) && !empty($urlStructure['query'])
274                ? '?' . \rtrim(\str_replace('=&', '&', \http_build_query($urlStructure['query'])), '=') : '')
275            . (isset($urlStructure['fragment']) && !empty($urlStructure['fragment'])
276                ? '#' . \str_replace('\#', '#', $urlStructure['fragment']) : '');
277
278        return \str_replace(
279                ['%5C%7B', '%5C%7D', '%5C%3F', '%5C%23'],
280                ['{', '}', '?', '#'],
281                $escaped
282            );
283    }
284
285    /**
286     * Build uri.
287     *
288     * # = DOM id
289     * . = DOM class
290     * / = Current path
291     * ? = Current query
292     * @ =
293     * $ = Other data
294     *
295     * @param string                $uri     Path data
296     * @param array<string, string> $toMatch Optional special replacements
297     *
298     * @return string
299     *
300     * @throws \Exception
301     *
302     * @since 1.0.0
303     */
304    public static function build(string $uri, array $toMatch = []) : string
305    {
306        if (\stripos($uri, '{') === false) {
307            return $uri;
308        }
309
310        if (self::$replaceFunction === null) {
311            self::$replaceFunction = static function ($match) use ($toMatch) : string {
312                $match = \substr($match[0], 1, \strlen($match[0]) - 2);
313
314                return (string) ($toMatch[$match]
315                    ?? (self::$uri[$match] ?? (
316                        ($match[0] ?? '') === '?'
317                            ? '---' // only do this for query parameters
318                            : ''
319                        )
320                    ));
321            };
322        }
323
324        $parsed = \preg_replace_callback(
325            '(\{[\/#\?%@\.\$][a-zA-Z0-9_\-]*\})',
326            self::$replaceFunction,
327            $uri
328        );
329
330        return self::unique($parsed ?? '');
331    }
332}