├── .github └── workflows │ └── ci.yml ├── Dsn.php ├── InvalidQueryParameterTypeException.php ├── LICENSE ├── QueryBag.php ├── README.md └── composer.json /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | pull_request: 4 | push: 5 | branches: 6 | - master 7 | jobs: 8 | tests: 9 | runs-on: ubuntu-latest 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | php: ['7.4', '8.0', '8.1', '8.2'] 14 | 15 | name: PHP ${{ matrix.php }} tests 16 | 17 | steps: 18 | - uses: actions/checkout@v2 19 | 20 | - uses: shivammathur/setup-php@v2 21 | with: 22 | php-version: ${{ matrix.php }} 23 | coverage: none 24 | 25 | - uses: "ramsey/composer-install@v1" 26 | with: 27 | composer-options: "--prefer-source" 28 | 29 | - run: vendor/bin/phpunit 30 | -------------------------------------------------------------------------------- /Dsn.php: -------------------------------------------------------------------------------- 1 | scheme = $scheme; 72 | $this->schemeProtocol = $schemeProtocol; 73 | $this->schemeExtensions = $schemeExtensions; 74 | $this->user = $user; 75 | $this->password = $password; 76 | $this->host = $host; 77 | $this->port = $port; 78 | $this->path = $path; 79 | $this->queryString = $queryString; 80 | $this->queryBag = new QueryBag($query); 81 | } 82 | 83 | public function getScheme(): string 84 | { 85 | return $this->scheme; 86 | } 87 | 88 | public function getSchemeProtocol(): string 89 | { 90 | return $this->schemeProtocol; 91 | } 92 | 93 | public function getSchemeExtensions(): array 94 | { 95 | return $this->schemeExtensions; 96 | } 97 | 98 | public function hasSchemeExtension(string $extension): bool 99 | { 100 | return in_array($extension, $this->schemeExtensions, true); 101 | } 102 | 103 | public function getUser(): ?string 104 | { 105 | return $this->user; 106 | } 107 | 108 | public function getPassword(): ?string 109 | { 110 | return $this->password; 111 | } 112 | 113 | public function getHost(): ?string 114 | { 115 | return $this->host; 116 | } 117 | 118 | public function getPort(): ?int 119 | { 120 | return $this->port; 121 | } 122 | 123 | public function getPath(): ?string 124 | { 125 | return $this->path; 126 | } 127 | 128 | public function getQueryString(): ?string 129 | { 130 | return $this->queryString; 131 | } 132 | 133 | public function getQueryBag(): QueryBag 134 | { 135 | return $this->queryBag; 136 | } 137 | 138 | public function getQuery(): array 139 | { 140 | return $this->queryBag->toArray(); 141 | } 142 | 143 | public function getString(string $name, ?string $default = null): ?string 144 | { 145 | return $this->queryBag->getString($name, $default); 146 | } 147 | 148 | public function getDecimal(string $name, ?int $default = null): ?int 149 | { 150 | return $this->queryBag->getDecimal($name, $default); 151 | } 152 | 153 | public function getOctal(string $name, ?int $default = null): ?int 154 | { 155 | return $this->queryBag->getOctal($name, $default); 156 | } 157 | 158 | public function getFloat(string $name, ?float $default = null): ?float 159 | { 160 | return $this->queryBag->getFloat($name, $default); 161 | } 162 | 163 | public function getBool(string $name, ?bool $default = null): ?bool 164 | { 165 | return $this->queryBag->getBool($name, $default); 166 | } 167 | 168 | public function getArray(string $name, array $default = []): QueryBag 169 | { 170 | return $this->queryBag->getArray($name, $default); 171 | } 172 | 173 | public function toArray() 174 | { 175 | return [ 176 | 'scheme' => $this->scheme, 177 | 'schemeProtocol' => $this->schemeProtocol, 178 | 'schemeExtensions' => $this->schemeExtensions, 179 | 'user' => $this->user, 180 | 'password' => $this->password, 181 | 'host' => $this->host, 182 | 'port' => $this->port, 183 | 'path' => $this->path, 184 | 'queryString' => $this->queryString, 185 | 'query' => $this->queryBag->toArray(), 186 | ]; 187 | } 188 | 189 | public static function parseFirst(string $dsn): ?self 190 | { 191 | return self::parse($dsn)[0]; 192 | } 193 | 194 | /** 195 | * @return Dsn[] 196 | */ 197 | public static function parse(string $dsn): array 198 | { 199 | if (!str_contains($dsn, ':')) { 200 | throw new \LogicException('The DSN is invalid. It does not have scheme separator ":".'); 201 | } 202 | 203 | list($scheme, $dsnWithoutScheme) = explode(':', $dsn, 2); 204 | 205 | $scheme = strtolower($scheme); 206 | if (false == preg_match('/^[a-z\d+-.]*$/', $scheme)) { 207 | throw new \LogicException('The DSN is invalid. Scheme contains illegal symbols.'); 208 | } 209 | 210 | $schemeParts = explode('+', $scheme); 211 | $schemeProtocol = $schemeParts[0]; 212 | 213 | unset($schemeParts[0]); 214 | $schemeExtensions = array_values($schemeParts); 215 | 216 | $user = parse_url($dsn, \PHP_URL_USER) ?: null; 217 | if (is_string($user)) { 218 | $user = rawurldecode($user); 219 | } 220 | 221 | $password = parse_url($dsn, \PHP_URL_PASS) ?: null; 222 | if (is_string($password)) { 223 | $password = rawurldecode($password); 224 | } 225 | 226 | $path = parse_url($dsn, \PHP_URL_PATH) ?: null; 227 | if ($path) { 228 | $path = rawurldecode($path); 229 | } 230 | 231 | $query = []; 232 | $queryString = parse_url($dsn, \PHP_URL_QUERY) ?: null; 233 | if (is_string($queryString)) { 234 | $query = self::httpParseQuery($queryString, '&', \PHP_QUERY_RFC3986); 235 | } 236 | $hostsPorts = ''; 237 | if (str_starts_with($dsnWithoutScheme, '//')) { 238 | $dsnWithoutScheme = substr($dsnWithoutScheme, 2); 239 | $dsnWithoutUserPassword = explode('@', $dsnWithoutScheme, 2); 240 | $dsnWithoutUserPassword = 2 === count($dsnWithoutUserPassword) ? 241 | $dsnWithoutUserPassword[1] : 242 | $dsnWithoutUserPassword[0] 243 | ; 244 | 245 | list($hostsPorts) = explode('#', $dsnWithoutUserPassword, 2); 246 | list($hostsPorts) = explode('?', $hostsPorts, 2); 247 | list($hostsPorts) = explode('/', $hostsPorts, 2); 248 | } 249 | 250 | if (empty($hostsPorts)) { 251 | return [ 252 | new self( 253 | $scheme, 254 | $schemeProtocol, 255 | $schemeExtensions, 256 | null, 257 | null, 258 | null, 259 | null, 260 | $path, 261 | $queryString, 262 | $query 263 | ), 264 | ]; 265 | } 266 | 267 | $dsns = []; 268 | $hostParts = explode(',', $hostsPorts); 269 | foreach ($hostParts as $key => $hostPart) { 270 | unset($hostParts[$key]); 271 | 272 | $parts = explode(':', $hostPart, 2); 273 | $host = $parts[0]; 274 | 275 | $port = null; 276 | if (isset($parts[1])) { 277 | $port = (int) $parts[1]; 278 | } 279 | 280 | $dsns[] = new self( 281 | $scheme, 282 | $schemeProtocol, 283 | $schemeExtensions, 284 | $user, 285 | $password, 286 | $host, 287 | $port, 288 | $path, 289 | $queryString, 290 | $query 291 | ); 292 | } 293 | 294 | return $dsns; 295 | } 296 | 297 | /** 298 | * based on http://php.net/manual/en/function.parse-str.php#119484 with some slight modifications. 299 | */ 300 | private static function httpParseQuery(string $queryString, string $argSeparator = '&', int $decType = \PHP_QUERY_RFC1738): array 301 | { 302 | $result = []; 303 | $parts = explode($argSeparator, $queryString); 304 | 305 | foreach ($parts as $part) { 306 | list($paramName, $paramValue) = explode('=', $part, 2); 307 | 308 | switch ($decType) { 309 | case \PHP_QUERY_RFC3986: 310 | $paramName = rawurldecode($paramName); 311 | $paramValue = rawurldecode($paramValue); 312 | break; 313 | case \PHP_QUERY_RFC1738: 314 | default: 315 | $paramName = urldecode($paramName); 316 | $paramValue = urldecode($paramValue); 317 | break; 318 | } 319 | 320 | if (preg_match_all('/\[([^\]]*)\]/m', $paramName, $matches)) { 321 | $paramName = substr($paramName, 0, strpos($paramName, '[')); 322 | $keys = array_merge([$paramName], $matches[1]); 323 | } else { 324 | $keys = [$paramName]; 325 | } 326 | 327 | $target = &$result; 328 | 329 | foreach ($keys as $index) { 330 | if ('' === $index) { 331 | if (is_array($target)) { 332 | $intKeys = array_filter(array_keys($target), 'is_int'); 333 | $index = count($intKeys) ? max($intKeys) + 1 : 0; 334 | } else { 335 | $target = [$target]; 336 | $index = 1; 337 | } 338 | } elseif (isset($target[$index]) && !is_array($target[$index])) { 339 | $target[$index] = [$target[$index]]; 340 | } 341 | 342 | $target = &$target[$index]; 343 | } 344 | 345 | if (is_array($target)) { 346 | $target[] = $paramValue; 347 | } else { 348 | $target = $paramValue; 349 | } 350 | } 351 | 352 | return $result; 353 | } 354 | } 355 | -------------------------------------------------------------------------------- /InvalidQueryParameterTypeException.php: -------------------------------------------------------------------------------- 1 | query = $query; 17 | } 18 | 19 | public function toArray(): array 20 | { 21 | return $this->query; 22 | } 23 | 24 | public function getString(string $name, ?string $default = null): ?string 25 | { 26 | return array_key_exists($name, $this->query) ? $this->query[$name] : $default; 27 | } 28 | 29 | public function getDecimal(string $name, ?int $default = null): ?int 30 | { 31 | $value = $this->getString($name); 32 | if (null === $value) { 33 | return $default; 34 | } 35 | 36 | if (false == preg_match('/^[\+\-]?[0-9]*$/', $value)) { 37 | throw InvalidQueryParameterTypeException::create($name, 'decimal'); 38 | } 39 | 40 | return (int) $value; 41 | } 42 | 43 | public function getOctal(string $name, ?int $default = null): ?int 44 | { 45 | $value = $this->getString($name); 46 | if (null === $value) { 47 | return $default; 48 | } 49 | 50 | if (false == preg_match('/^0[\+\-]?[0-7]*$/', $value)) { 51 | throw InvalidQueryParameterTypeException::create($name, 'octal'); 52 | } 53 | 54 | return intval($value, 8); 55 | } 56 | 57 | public function getFloat(string $name, ?float $default = null): ?float 58 | { 59 | $value = $this->getString($name); 60 | if (null === $value) { 61 | return $default; 62 | } 63 | 64 | if (false == is_numeric($value)) { 65 | throw InvalidQueryParameterTypeException::create($name, 'float'); 66 | } 67 | 68 | return (float) $value; 69 | } 70 | 71 | public function getBool(string $name, ?bool $default = null): ?bool 72 | { 73 | $value = $this->getString($name); 74 | if (null === $value) { 75 | return $default; 76 | } 77 | 78 | if (in_array($value, ['', '0', 'false'], true)) { 79 | return false; 80 | } 81 | 82 | if (in_array($value, ['1', 'true'], true)) { 83 | return true; 84 | } 85 | 86 | throw InvalidQueryParameterTypeException::create($name, 'bool'); 87 | } 88 | 89 | public function getArray(string $name, array $default = []): self 90 | { 91 | if (false == array_key_exists($name, $this->query)) { 92 | return new self($default); 93 | } 94 | 95 | $value = $this->query[$name]; 96 | 97 | if (is_array($value)) { 98 | return new self($value); 99 | } 100 | 101 | throw InvalidQueryParameterTypeException::create($name, 'array'); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |