Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
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 |
||
| 17 | class Uri implements UriInterface |
||
| 18 | { |
||
| 19 | private static $schemes = [ |
||
| 20 | 'http' => 80, |
||
| 21 | 'https' => 443, |
||
| 22 | ]; |
||
| 23 | |||
| 24 | private static $charUnreserved = 'a-zA-Z0-9_\-\.~'; |
||
| 25 | |||
| 26 | private static $charSubDelims = '!\$&\'\(\)\*\+,;='; |
||
| 27 | |||
| 28 | /** @var string Uri scheme. */ |
||
| 29 | private $scheme = ''; |
||
| 30 | |||
| 31 | /** @var string Uri user info. */ |
||
| 32 | private $userInfo = ''; |
||
| 33 | |||
| 34 | /** @var string Uri host. */ |
||
| 35 | private $host = ''; |
||
| 36 | |||
| 37 | /** @var int|null Uri port. */ |
||
| 38 | private $port; |
||
| 39 | |||
| 40 | /** @var string Uri path. */ |
||
| 41 | private $path = ''; |
||
| 42 | |||
| 43 | /** @var string Uri query string. */ |
||
| 44 | private $query = ''; |
||
| 45 | |||
| 46 | /** @var string Uri fragment. */ |
||
| 47 | private $fragment = ''; |
||
| 48 | |||
| 49 | /** |
||
| 50 | * @param string $uri |
||
| 51 | */ |
||
| 52 | 123 | public function __construct($uri = '') |
|
| 53 | { |
||
| 54 | 123 | if ('' != $uri) { |
|
| 55 | 82 | $parts = parse_url($uri); |
|
| 56 | 82 | if (false === $parts) { |
|
| 57 | 4 | throw new \InvalidArgumentException("Unable to parse URI: $uri"); |
|
| 58 | } |
||
| 59 | |||
| 60 | 78 | $this->applyParts($parts); |
|
| 61 | } |
||
| 62 | 119 | } |
|
| 63 | |||
| 64 | 63 | public function __toString(): string |
|
| 65 | { |
||
| 66 | 63 | return self::createUriString( |
|
| 67 | 63 | $this->scheme, |
|
| 68 | 63 | $this->getAuthority(), |
|
| 69 | 63 | $this->path, |
|
| 70 | 63 | $this->query, |
|
| 71 | 63 | $this->fragment |
|
| 72 | ); |
||
| 73 | } |
||
| 74 | |||
| 75 | 30 | public function getScheme(): string |
|
| 76 | { |
||
| 77 | 30 | return $this->scheme; |
|
| 78 | } |
||
| 79 | |||
| 80 | 69 | public function getAuthority(): string |
|
| 81 | { |
||
| 82 | 69 | if ('' == $this->host) { |
|
| 83 | 29 | return ''; |
|
| 84 | } |
||
| 85 | |||
| 86 | 42 | $authority = $this->host; |
|
| 87 | 42 | if ('' != $this->userInfo) { |
|
| 88 | 5 | $authority = $this->userInfo.'@'.$authority; |
|
| 89 | } |
||
| 90 | |||
| 91 | 42 | if (null !== $this->port) { |
|
| 92 | 5 | $authority .= ':'.$this->port; |
|
| 93 | } |
||
| 94 | |||
| 95 | 42 | return $authority; |
|
| 96 | } |
||
| 97 | |||
| 98 | 7 | public function getUserInfo(): string |
|
| 99 | { |
||
| 100 | 7 | return $this->userInfo; |
|
| 101 | } |
||
| 102 | |||
| 103 | 69 | public function getHost(): string |
|
| 104 | { |
||
| 105 | 69 | return $this->host; |
|
| 106 | } |
||
| 107 | |||
| 108 | 42 | public function getPort() |
|
| 109 | { |
||
| 110 | 42 | return $this->port; |
|
| 111 | } |
||
| 112 | |||
| 113 | 17 | public function getPath(): string |
|
| 114 | { |
||
| 115 | 17 | return $this->path; |
|
| 116 | } |
||
| 117 | |||
| 118 | 14 | public function getQuery(): string |
|
| 119 | { |
||
| 120 | 14 | return $this->query; |
|
| 121 | } |
||
| 122 | |||
| 123 | 13 | public function getFragment(): string |
|
| 124 | { |
||
| 125 | 13 | return $this->fragment; |
|
| 126 | } |
||
| 127 | |||
| 128 | 29 | public function withScheme($scheme): self |
|
| 129 | { |
||
| 130 | 29 | $scheme = $this->filterScheme($scheme); |
|
| 131 | |||
| 132 | 28 | if ($this->scheme === $scheme) { |
|
| 133 | return $this; |
||
| 134 | } |
||
| 135 | |||
| 136 | 28 | $new = clone $this; |
|
| 137 | 28 | $new->scheme = $scheme; |
|
| 138 | 28 | $new->port = $new->filterPort($new->port); |
|
| 139 | |||
| 140 | 28 | return $new; |
|
| 141 | } |
||
| 142 | |||
| 143 | 4 | public function withUserInfo($user, $password = null): self |
|
| 144 | { |
||
| 145 | 4 | $info = $user; |
|
| 146 | 4 | if ('' != $password) { |
|
| 147 | 4 | $info .= ':'.$password; |
|
| 148 | } |
||
| 149 | |||
| 150 | 4 | if ($this->userInfo === $info) { |
|
| 151 | return $this; |
||
| 152 | } |
||
| 153 | |||
| 154 | 4 | $new = clone $this; |
|
| 155 | 4 | $new->userInfo = $info; |
|
| 156 | |||
| 157 | 4 | return $new; |
|
| 158 | } |
||
| 159 | |||
| 160 | 13 | public function withHost($host): self |
|
| 161 | { |
||
| 162 | 13 | $host = $this->filterHost($host); |
|
| 163 | |||
| 164 | 12 | if ($this->host === $host) { |
|
| 165 | return $this; |
||
| 166 | } |
||
| 167 | |||
| 168 | 12 | $new = clone $this; |
|
| 169 | 12 | $new->host = $host; |
|
| 170 | |||
| 171 | 12 | return $new; |
|
| 172 | } |
||
| 173 | |||
| 174 | 8 | public function withPort($port): self |
|
| 175 | { |
||
| 176 | 8 | $port = $this->filterPort($port); |
|
| 177 | |||
| 178 | 6 | if ($this->port === $port) { |
|
| 179 | 1 | return $this; |
|
| 180 | } |
||
| 181 | |||
| 182 | 5 | $new = clone $this; |
|
| 183 | 5 | $new->port = $port; |
|
| 184 | |||
| 185 | 5 | return $new; |
|
| 186 | } |
||
| 187 | |||
| 188 | 15 | public function withPath($path): self |
|
| 189 | { |
||
| 190 | 15 | $path = $this->filterPath($path); |
|
| 191 | |||
| 192 | 14 | if ($this->path === $path) { |
|
| 193 | return $this; |
||
| 194 | } |
||
| 195 | |||
| 196 | 14 | $new = clone $this; |
|
| 197 | 14 | $new->path = $path; |
|
| 198 | |||
| 199 | 14 | return $new; |
|
| 200 | } |
||
| 201 | |||
| 202 | 12 | public function withQuery($query): self |
|
| 203 | { |
||
| 204 | 12 | $query = $this->filterQueryAndFragment($query); |
|
| 205 | |||
| 206 | 11 | if ($this->query === $query) { |
|
| 207 | return $this; |
||
| 208 | } |
||
| 209 | |||
| 210 | 11 | $new = clone $this; |
|
| 211 | 11 | $new->query = $query; |
|
| 212 | |||
| 213 | 11 | return $new; |
|
| 214 | } |
||
| 215 | |||
| 216 | 5 | public function withFragment($fragment): self |
|
| 217 | { |
||
| 218 | 5 | $fragment = $this->filterQueryAndFragment($fragment); |
|
| 219 | |||
| 220 | 4 | if ($this->fragment === $fragment) { |
|
| 221 | return $this; |
||
| 222 | } |
||
| 223 | |||
| 224 | 4 | $new = clone $this; |
|
| 225 | 4 | $new->fragment = $fragment; |
|
| 226 | |||
| 227 | 4 | return $new; |
|
| 228 | } |
||
| 229 | |||
| 230 | /** |
||
| 231 | * Apply parse_url parts to a URI. |
||
| 232 | * |
||
| 233 | * @param array $parts Array of parse_url parts to apply |
||
| 234 | */ |
||
| 235 | 78 | private function applyParts(array $parts) |
|
| 236 | { |
||
| 237 | 78 | $this->scheme = isset($parts['scheme']) ? $this->filterScheme($parts['scheme']) : ''; |
|
| 238 | 78 | $this->userInfo = isset($parts['user']) ? $parts['user'] : ''; |
|
| 239 | 78 | $this->host = isset($parts['host']) ? $this->filterHost($parts['host']) : ''; |
|
| 240 | 78 | $this->port = isset($parts['port']) ? $this->filterPort($parts['port']) : null; |
|
| 241 | 78 | $this->path = isset($parts['path']) ? $this->filterPath($parts['path']) : ''; |
|
| 242 | 78 | $this->query = isset($parts['query']) ? $this->filterQueryAndFragment($parts['query']) : ''; |
|
| 243 | 78 | $this->fragment = isset($parts['fragment']) ? $this->filterQueryAndFragment($parts['fragment']) : ''; |
|
| 244 | 78 | if (isset($parts['pass'])) { |
|
| 245 | 4 | $this->userInfo .= ':'.$parts['pass']; |
|
| 246 | } |
||
| 247 | 78 | } |
|
| 248 | |||
| 249 | /** |
||
| 250 | * Create a URI string from its various parts. |
||
| 251 | * |
||
| 252 | * @param string $scheme |
||
| 253 | * @param string $authority |
||
| 254 | * @param string $path |
||
| 255 | * @param string $query |
||
| 256 | * @param string $fragment |
||
| 257 | * |
||
| 258 | * @return string |
||
| 259 | */ |
||
| 260 | 63 | private static function createUriString($scheme, $authority, $path, $query, $fragment): string |
|
| 261 | { |
||
| 262 | 63 | $uri = ''; |
|
| 263 | 63 | if ('' != $scheme) { |
|
| 264 | 38 | $uri .= $scheme.':'; |
|
| 265 | } |
||
| 266 | |||
| 267 | 63 | if ('' != $authority) { |
|
| 268 | 38 | $uri .= '//'.$authority; |
|
| 269 | } |
||
| 270 | |||
| 271 | 63 | if ('' != $path) { |
|
| 272 | 51 | if ('/' !== $path[0]) { |
|
| 273 | 7 | if ('' != $authority) { |
|
| 274 | // If the path is rootless and an authority is present, the path MUST be prefixed by "/" |
||
| 275 | 7 | $path = '/'.$path; |
|
| 276 | } |
||
| 277 | 44 | } elseif (isset($path[1]) && '/' === $path[1]) { |
|
| 278 | 1 | if ('' == $authority) { |
|
| 279 | // If the path is starting with more than one "/" and no authority is present, the |
||
| 280 | // starting slashes MUST be reduced to one. |
||
| 281 | 1 | $path = '/'.ltrim($path, '/'); |
|
| 282 | } |
||
| 283 | } |
||
| 284 | |||
| 285 | 51 | $uri .= $path; |
|
| 286 | } |
||
| 287 | |||
| 288 | 63 | if ('' != $query) { |
|
| 289 | 33 | $uri .= '?'.$query; |
|
| 290 | } |
||
| 291 | |||
| 292 | 63 | if ('' != $fragment) { |
|
| 293 | 14 | $uri .= '#'.$fragment; |
|
| 294 | } |
||
| 295 | |||
| 296 | 63 | return $uri; |
|
| 297 | } |
||
| 298 | |||
| 299 | /** |
||
| 300 | * Is a given port non-standard for the current scheme? |
||
| 301 | * |
||
| 302 | * @param string $scheme |
||
| 303 | * @param int $port |
||
| 304 | * |
||
| 305 | * @return bool |
||
| 306 | */ |
||
| 307 | 11 | private static function isNonStandardPort($scheme, $port): bool |
|
| 311 | |||
| 312 | /** |
||
| 313 | * @param string $scheme |
||
| 314 | * |
||
| 315 | * @throws \InvalidArgumentException If the scheme is invalid |
||
| 316 | * |
||
| 317 | * @return string |
||
| 318 | */ |
||
| 319 | 70 | private function filterScheme($scheme): string |
|
| 327 | |||
| 328 | /** |
||
| 329 | * @param string $host |
||
| 330 | * |
||
| 331 | * @throws \InvalidArgumentException If the host is invalid |
||
| 332 | * |
||
| 333 | * @return string |
||
| 334 | */ |
||
| 335 | 57 | private function filterHost($host): string |
|
| 343 | |||
| 344 | /** |
||
| 345 | * @param int|null $port |
||
| 346 | * |
||
| 347 | * @throws \InvalidArgumentException If the port is invalid |
||
| 348 | * |
||
| 349 | * @return int|null |
||
| 350 | */ |
||
| 351 | 38 | private function filterPort($port) |
|
| 364 | |||
| 365 | /** |
||
| 366 | * Filters the path of a URI. |
||
| 367 | * |
||
| 368 | * @param string $path |
||
| 369 | * |
||
| 370 | * @throws \InvalidArgumentException If the path is invalid |
||
| 371 | * |
||
| 372 | * @return string |
||
| 373 | */ |
||
| 374 | 80 | View Code Duplication | private function filterPath($path): string |
|
1 ignored issue
–
show
|
|||
| 375 | { |
||
| 376 | 80 | if (!is_string($path)) { |
|
| 377 | 1 | throw new \InvalidArgumentException('Path must be a string'); |
|
| 378 | } |
||
| 379 | |||
| 380 | 79 | return preg_replace_callback( |
|
| 381 | 79 | '/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/]++|%(?![A-Fa-f0-9]{2}))/', |
|
| 382 | 79 | [$this, 'rawurlencodeMatchZero'], |
|
| 383 | 79 | $path |
|
| 384 | ); |
||
| 385 | } |
||
| 386 | |||
| 387 | /** |
||
| 388 | * Filters the query string or fragment of a URI. |
||
| 389 | * |
||
| 390 | * @param string $str |
||
| 391 | * |
||
| 392 | * @throws \InvalidArgumentException If the query or fragment is invalid |
||
| 393 | * |
||
| 394 | * @return string |
||
| 395 | */ |
||
| 396 | 39 | View Code Duplication | private function filterQueryAndFragment($str): string |
|
1 ignored issue
–
show
|
|||
| 397 | { |
||
| 398 | 39 | if (!is_string($str)) { |
|
| 399 | 2 | throw new \InvalidArgumentException('Query and fragment must be a string'); |
|
| 400 | } |
||
| 401 | |||
| 402 | 37 | return preg_replace_callback( |
|
| 403 | 37 | '/(?:[^'.self::$charUnreserved.self::$charSubDelims.'%:@\/\?]++|%(?![A-Fa-f0-9]{2}))/', |
|
| 404 | 37 | [$this, 'rawurlencodeMatchZero'], |
|
| 405 | 37 | $str |
|
| 406 | ); |
||
| 407 | } |
||
| 408 | |||
| 409 | 6 | private function rawurlencodeMatchZero(array $match): string |
|
| 413 | } |
||
| 414 |
Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.
You can also find more detailed suggestions in the “Code” section of your repository.