Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
7 / 7
CRAP
100.00% covered (success)
100.00%
1 / 1
Url
100.00% covered (success)
100.00%
56 / 56
100.00% covered (success)
100.00%
7 / 7
35
100.00% covered (success)
100.00%
1 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 testUrl
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
2
 getQuery
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 hydrateFromArray
100.00% covered (success)
100.00%
25 / 25
100.00% covered (success)
100.00%
1 / 1
10
 parse
100.00% covered (success)
100.00%
3 / 3
100.00% covered (success)
100.00%
1 / 1
3
 getUrlParts
100.00% covered (success)
100.00%
11 / 11
100.00% covered (success)
100.00%
1 / 1
9
 render
100.00% covered (success)
100.00%
14 / 14
100.00% covered (success)
100.00%
1 / 1
9
1<?php
2
3/**
4 * @author Doug Wilbourne (dougwilbourne@gmail.com)
5 */
6
7namespace pvc\http\url;
8
9use pvc\http\err\InvalidUrlException;
10use pvc\interfaces\http\QueryStringInterface;
11use pvc\interfaces\http\UrlInterface;
12use pvc\interfaces\validator\ValTesterInterface;
13
14/**
15 * Class Url
16 *
17 * The purpose of the class is to make it easy to manipulate the various parts of a url without having to resort
18 * to string manipulation.
19 *
20 * There is no validation done when setting the values of the individual components.  But, by default the render
21 * method will validate the url before returning the generated url and will throw an exception if it is not valid.
22 * This behavior is configurable.
23 *
24 * You can create a url from scratch with this object.  You can also start with an existing url and hydrate this
25 * object using the ParserUrl class found in the pvc Parser library.  And you can even hydrate this object
26 * directly from an array which is produced by php's parse_url method.  Just be aware that the parse_url verb
27 * will mangle pieces of a url when it finds characters it does not like.  The ParserUrl class validates a url
28 * before parsing and automatically hydrates the Url object for you.
29 *
30 * @phpstan-type UrlShape array{scheme?:string, host?:string, port?:int<0, 65535>, user?:string, pass?:string, path?:string, query?:string, fragment?:string}
31 */
32class Url implements UrlInterface
33{
34    /**
35     * @var ?string
36     * protocol e.g. http, https, ftp, etc.
37     */
38    public ?string $scheme = null;
39
40    public ?string $host = null;
41
42    /**
43     * @var int<0, 65535>|null
44     */
45    public ?int $port = null;
46
47    public ?string $user = null;
48
49    public ?string $pass = null;
50
51    public ?string $path = null;
52
53    public ?string $fragment = null;
54
55    /**
56     * @param QueryStringInterface $queryString
57     * @param ?ValTesterInterface<string> $urlTester
58     *
59     * if $urlTester is not supplied, incoming url strings to be parsed and
60     * outgoing url strings that have been rendered will not be checked for
61     * validity
62     */
63    public function __construct(
64        protected QueryStringInterface $queryString,
65        protected ?ValTesterInterface         $urlTester = null,
66    )
67    {
68    }
69
70    protected function testUrl(string $url): bool
71    {
72        return null === $this->urlTester || $this->urlTester->testValue($url);
73    }
74
75    /**
76     * getQueryString
77     * @return QueryStringInterface
78     */
79    public function getQuery(): QueryStringInterface
80    {
81        return $this->queryString;
82    }
83
84    /**
85     * @param UrlShape $urlParts
86     * @return void
87     */
88    public function hydrateFromArray(array $urlParts): void
89    {
90        foreach ($urlParts as $partName => $part) {
91            switch ($partName) {
92                case 'scheme':
93                    $this->scheme = $part;
94                    break;
95                case 'host':
96                    $this->host = $part;
97                    break;
98                case 'port':
99                    $this->port = $part;
100                    break;
101                case 'user':
102                    $this->user = $part;
103                    break;
104                case 'pass':
105                    $this->pass = $part;
106                    break;
107                case 'path':
108                    $this->path = $part;
109                    break;
110                case 'query':
111                    $this->getQuery()->parse($part);
112                    break;
113                case 'fragment':
114                    $this->fragment = $part;
115                    break;
116            }
117        }
118    }
119
120    /**
121     * parse
122     *
123     * @param string  $url
124     *
125     * @return void
126     */
127    public function parse(string $url): void
128    {
129        /**
130         * parse_url will happily mangle the results of urls which are not well-formed,
131         * so we optionally validate the url first
132         */
133        if (!$this->testUrl($url) || (false === ($urlParts = parse_url($url)))) {
134            throw new InvalidUrlException($url);
135        }
136
137        $this->hydrateFromArray($urlParts);
138    }
139
140    /**
141     * getUrlParts
142     * @return UrlShape
143     */
144    public function getUrlParts(): array
145    {
146        $result = [];
147
148        if ($this->scheme) $result['scheme'] = $this->scheme;
149        if ($this->host) $result['host'] = $this->host;
150        if ($this->port) $result['port'] = $this->port;
151        if ($this->user) $result['user'] = $this->user;
152        if ($this->pass) $result['pass'] = $this->pass;
153        if ($this->path) $result['path'] = $this->path;
154
155        $query = $this->queryString->render();
156        if ($query) $result['query'] = $query;
157
158        if ($this->fragment) $result['fragment'] = $this->fragment;
159        return $result;
160
161    }
162
163    /**
164     * generateURLString
165     * @return string
166     * @throws InvalidUrlException
167     *
168     */
169    public function render(): string
170    {
171        $urlString = '';
172        $urlString .= $this->scheme ? $this->scheme . '://' : '';
173        $urlString .= $this->user;
174
175        /**
176         * user is separated from password by a colon.  Does it make sense to output a password if there is no user?
177         * For now, this outputs a password even if there is no user.
178         */
179        $urlString .= $this->pass ? ':' . $this->pass : '';
180
181        /**
182         * separate userid / password from path with a '@'
183         */
184        $urlString .= ($this->user || $this->pass) ? '@' : '';
185
186        $urlString .= $this->host;
187        $urlString .= $this->port ? ':' . $this->port : '';
188        $urlString .= $this->path;
189
190        $query = $this->getQuery()->render();
191        $urlString .= $query ? '?' . $query : '';
192
193        $urlString .= $this->fragment ? '#'.$this->fragment : '';
194
195        if (!$this->testUrl($urlString)) {
196            throw new InvalidUrlException($urlString);
197        }
198
199        return $urlString;
200    }
201}