Complex classes like Uri often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use Uri, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
18 | final class Uri implements UriInterface |
||
19 | { |
||
20 | private const SCHEMES = ['http' => 80, 'https' => 443]; |
||
21 | |||
22 | private const CHAR_UNRESERVED = 'a-zA-Z0-9_\-\.~'; |
||
23 | |||
24 | private const CHAR_SUB_DELIMS = '!\$&\'\(\)\*\+,;='; |
||
25 | |||
26 | /** @var string Uri scheme. */ |
||
27 | private $scheme = ''; |
||
28 | |||
29 | /** @var string Uri user info. */ |
||
30 | private $userInfo = ''; |
||
31 | |||
32 | /** @var string Uri host. */ |
||
33 | private $host = ''; |
||
34 | |||
35 | /** @var int|null Uri port. */ |
||
36 | private $port; |
||
37 | |||
38 | /** @var string Uri path. */ |
||
39 | private $path = ''; |
||
40 | |||
41 | /** @var string Uri query string. */ |
||
42 | private $query = ''; |
||
43 | |||
44 | /** @var string Uri fragment. */ |
||
45 | private $fragment = ''; |
||
46 | |||
47 | 150 | public function __construct(string $uri = '') |
|
48 | { |
||
49 | 150 | if ('' !== $uri) { |
|
50 | 130 | if (false === $parts = \parse_url($uri)) { |
|
51 | 4 | throw new \InvalidArgumentException("Unable to parse URI: $uri"); |
|
52 | } |
||
53 | |||
54 | // Apply parse_url parts to a URI. |
||
55 | 126 | $this->scheme = isset($parts['scheme']) ? \strtolower($parts['scheme']) : ''; |
|
56 | 126 | $this->userInfo = $parts['user'] ?? ''; |
|
57 | 126 | $this->host = isset($parts['host']) ? \strtolower($parts['host']) : ''; |
|
58 | 126 | $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; |
|
59 | 126 | $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; |
|
60 | 126 | $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; |
|
61 | 126 | $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; |
|
62 | 126 | if (isset($parts['pass'])) { |
|
63 | 5 | $this->userInfo .= ':' . $parts['pass']; |
|
64 | } |
||
65 | } |
||
66 | 146 | } |
|
67 | |||
68 | 67 | public function __toString(): string |
|
69 | { |
||
70 | 67 | return self::createUriString($this->scheme, $this->getAuthority(), $this->path, $this->query, $this->fragment); |
|
71 | } |
||
72 | |||
73 | 8 | public function getScheme(): string |
|
74 | { |
||
75 | 8 | return $this->scheme; |
|
76 | } |
||
77 | |||
78 | 73 | public function getAuthority(): string |
|
79 | { |
||
80 | 73 | if ('' === $this->host) { |
|
81 | 29 | return ''; |
|
82 | } |
||
83 | |||
84 | 46 | $authority = $this->host; |
|
85 | 46 | if ('' !== $this->userInfo) { |
|
86 | 7 | $authority = $this->userInfo . '@' . $authority; |
|
87 | } |
||
88 | |||
89 | 46 | if (null !== $this->port) { |
|
90 | 7 | $authority .= ':' . $this->port; |
|
91 | } |
||
92 | |||
93 | 46 | return $authority; |
|
94 | } |
||
95 | |||
96 | 7 | public function getUserInfo(): string |
|
97 | { |
||
98 | 7 | return $this->userInfo; |
|
99 | } |
||
100 | |||
101 | 90 | public function getHost(): string |
|
102 | { |
||
103 | 90 | return $this->host; |
|
104 | } |
||
105 | |||
106 | 42 | public function getPort(): ?int |
|
107 | { |
||
108 | 42 | return $this->port; |
|
109 | } |
||
110 | |||
111 | 23 | public function getPath(): string |
|
112 | { |
||
113 | 23 | return $this->path; |
|
114 | } |
||
115 | |||
116 | 19 | public function getQuery(): string |
|
117 | { |
||
118 | 19 | return $this->query; |
|
119 | } |
||
120 | |||
121 | 17 | public function getFragment(): string |
|
122 | { |
||
123 | 17 | return $this->fragment; |
|
124 | } |
||
125 | |||
126 | 12 | public function withScheme($scheme): self |
|
127 | { |
||
128 | 12 | if (!\is_string($scheme)) { |
|
129 | 5 | throw new \InvalidArgumentException('Scheme must be a string'); |
|
130 | } |
||
131 | |||
132 | 7 | if ($this->scheme === $scheme = \strtolower($scheme)) { |
|
133 | return $this; |
||
134 | } |
||
135 | |||
136 | 7 | $new = clone $this; |
|
137 | 7 | $new->scheme = $scheme; |
|
138 | 7 | $new->port = $new->filterPort($new->port); |
|
139 | |||
140 | 7 | return $new; |
|
141 | } |
||
142 | |||
143 | 5 | public function withUserInfo($user, $password = null): self |
|
159 | |||
160 | 7 | public function withHost($host): self |
|
161 | { |
||
162 | 7 | if (!\is_string($host)) { |
|
163 | 1 | throw new \InvalidArgumentException('Host must be a string'); |
|
164 | } |
||
165 | |||
166 | 6 | if ($this->host === $host = \strtolower($host)) { |
|
167 | return $this; |
||
168 | } |
||
169 | |||
170 | 6 | $new = clone $this; |
|
171 | 6 | $new->host = $host; |
|
172 | |||
173 | 6 | return $new; |
|
174 | } |
||
175 | |||
176 | 9 | public function withPort($port): self |
|
177 | { |
||
178 | 9 | if ($this->port === $port = $this->filterPort($port)) { |
|
179 | 1 | return $this; |
|
180 | } |
||
181 | |||
182 | 6 | $new = clone $this; |
|
183 | 6 | $new->port = $port; |
|
184 | |||
185 | 6 | return $new; |
|
186 | } |
||
187 | |||
188 | 9 | public function withPath($path): self |
|
189 | { |
||
190 | 9 | if ($this->path === $path = $this->filterPath($path)) { |
|
191 | return $this; |
||
192 | } |
||
193 | |||
194 | 8 | $new = clone $this; |
|
195 | 8 | $new->path = $path; |
|
196 | |||
197 | 8 | return $new; |
|
198 | } |
||
199 | |||
200 | 6 | public function withQuery($query): self |
|
201 | { |
||
202 | 6 | if ($this->query === $query = $this->filterQueryAndFragment($query)) { |
|
203 | return $this; |
||
204 | } |
||
205 | |||
206 | 5 | $new = clone $this; |
|
207 | 5 | $new->query = $query; |
|
208 | |||
209 | 5 | return $new; |
|
210 | } |
||
211 | |||
212 | 6 | public function withFragment($fragment): self |
|
213 | { |
||
214 | 6 | if ($this->fragment === $fragment = $this->filterQueryAndFragment($fragment)) { |
|
215 | return $this; |
||
216 | } |
||
217 | |||
218 | 5 | $new = clone $this; |
|
219 | 5 | $new->fragment = $fragment; |
|
220 | |||
221 | 5 | return $new; |
|
222 | } |
||
223 | |||
224 | /** |
||
225 | * Create a URI string from its various parts. |
||
226 | */ |
||
227 | 67 | private static function createUriString(string $scheme, string $authority, string $path, string $query, string $fragment): string |
|
265 | |||
266 | /** |
||
267 | * Is a given port non-standard for the current scheme? |
||
268 | */ |
||
269 | 12 | private static function isNonStandardPort(string $scheme, int $port): bool |
|
273 | |||
274 | 17 | private function filterPort($port): ?int |
|
275 | { |
||
276 | 17 | if (null === $port) { |
|
277 | 6 | return null; |
|
278 | } |
||
279 | |||
280 | 14 | $port = (int) $port; |
|
281 | 14 | if (1 > $port || 0xffff < $port) { |
|
282 | 2 | throw new \InvalidArgumentException(\sprintf('Invalid port: %d. Must be between 1 and 65535', $port)); |
|
283 | } |
||
284 | |||
285 | 12 | return self::isNonStandardPort($this->scheme, $port) ? $port : null; |
|
286 | } |
||
287 | |||
288 | 121 | private function filterPath($path): string |
|
289 | { |
||
290 | 121 | if (!\is_string($path)) { |
|
296 | |||
297 | 40 | private function filterQueryAndFragment($str): string |
|
305 | |||
306 | 6 | private static function rawurlencodeMatchZero(array $match): string |
|
310 | } |
||
311 |