DsnParser   A
last analyzed

Complexity

Total Complexity 27

Size/Duplication

Total Lines 163
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 27
eloc 64
c 1
b 0
f 1
dl 0
loc 163
rs 10

8 Methods

Rating   Name   Duplication   Size   Complexity  
A getQuery() 0 8 2
A parseFunc() 0 23 5
A parsePath() 0 8 2
A parseArguments() 0 9 2
A parseUrl() 0 8 2
A explodeUrl() 0 8 2
B getDsn() 0 42 9
A parse() 0 10 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Nyholm\Dsn;
6
7
use Nyholm\Dsn\Configuration\Dsn;
8
use Nyholm\Dsn\Configuration\DsnFunction;
9
use Nyholm\Dsn\Configuration\Path;
10
use Nyholm\Dsn\Configuration\Url;
11
use Nyholm\Dsn\Exception\DsnTypeNotSupported;
12
use Nyholm\Dsn\Exception\FunctionsNotAllowedException;
13
use Nyholm\Dsn\Exception\SyntaxException;
14
15
/**
16
 * A factory class to parse a string and create a DsnFunction.
17
 *
18
 * @author Tobias Nyholm <[email protected]>
19
 */
20
class DsnParser
21
{
22
    private const FUNCTION_REGEX = '#^([a-zA-Z0-9\+-]+):?\((.*)\)(?:\?(.*))?$#';
23
    private const ARGUMENTS_REGEX = '#([^\s,]+\([^)]+\)(?:\?[^\s,]*)?|[^\s,]+)#';
24
    private const UNRESERVED = 'a-zA-Z0-9-\._~';
25
    private const SUB_DELIMS = '!\$&\'\(\}\*\+,;=';
26
27
    /**
28
     * Parse A DSN thay may contain functions. If no function is present in the
29
     * string, then a "dsn()" function will be added.
30
     *
31
     * @throws SyntaxException
32
     */
33
    public static function parseFunc(string $dsn): DsnFunction
34
    {
35
        // Detect a function or add default function
36
        $parameters = [];
37
        if (1 === preg_match(self::FUNCTION_REGEX, $dsn, $matches)) {
38
            $functionName = $matches[1];
39
            $arguments = $matches[2];
40
            parse_str($matches[3] ?? '', $parameters);
41
        } else {
42
            $functionName = 'dsn';
43
            $arguments = $dsn;
44
        }
45
46
        if (empty($arguments)) {
47
            throw new SyntaxException($dsn, 'dsn' === $functionName ? 'The DSN is empty' : 'A function must have arguments, an empty string was provided.');
48
        }
49
50
        // explode arguments and respect function parentheses
51
        if (preg_match_all(self::ARGUMENTS_REGEX, $arguments, $matches)) {
52
            $arguments = $matches[1];
53
        }
54
55
        return new DsnFunction($functionName, array_map(\Closure::fromCallable([self::class, 'parseArguments']), $arguments), $parameters);
56
    }
57
58
    /**
59
     * Parse a DSN without functions.
60
     *
61
     * @throws FunctionsNotAllowedException if the DSN contains a function
62
     * @throws SyntaxException
63
     */
64
    public static function parse(string $dsn): Dsn
65
    {
66
        if (1 === preg_match(self::FUNCTION_REGEX, $dsn, $matches)) {
67
            if ('dsn' === $matches[1]) {
68
                return self::parse($matches[2]);
69
            }
70
            throw new FunctionsNotAllowedException($dsn);
71
        }
72
73
        return self::getDsn($dsn);
74
    }
75
76
    public static function parseUrl(string $dsn): Url
77
    {
78
        $dsn = self::parse($dsn);
79
        if (!$dsn instanceof Url) {
80
            throw DsnTypeNotSupported::onlyUrl($dsn);
81
        }
82
83
        return $dsn;
84
    }
85
86
    public static function parsePath(string $dsn): Path
87
    {
88
        $dsn = self::parse($dsn);
89
        if (!$dsn instanceof Path) {
90
            throw DsnTypeNotSupported::onlyPath($dsn);
91
        }
92
93
        return $dsn;
94
    }
95
96
    /**
97
     * @return DsnFunction|Dsn
98
     */
99
    private static function parseArguments(string $dsn)
100
    {
101
        // Detect a function exists
102
        if (1 === preg_match(self::FUNCTION_REGEX, $dsn)) {
103
            return self::parseFunc($dsn);
104
        }
105
106
        // Assert: $dsn does not contain any functions.
107
        return self::getDsn($dsn);
108
    }
109
110
    /**
111
     * @throws SyntaxException
112
     */
113
    private static function getDsn(string $dsn): Dsn
114
    {
115
        // Find the scheme if it exists and trim the double slash.
116
        if (!preg_match('#^(?:(?<alt>['.self::UNRESERVED.self::SUB_DELIMS.'%]+:[0-9]+(?:[/?].*)?)|(?<scheme>[a-zA-Z0-9\+-\.]+):(?://)?(?<dsn>.*))$#', $dsn, $matches)) {
117
            throw new SyntaxException($dsn, 'A DSN must contain a scheme [a-zA-Z0-9\+-\.]+ and a colon.');
118
        }
119
        $scheme = null;
120
        $dsn = $matches['alt'];
121
        if (!empty($matches['scheme'])) {
122
            $scheme = $matches['scheme'];
123
            $dsn = $matches['dsn'];
124
        }
125
126
        if ('' === $dsn) {
127
            return new Dsn($scheme);
128
        }
129
130
        // Parse user info
131
        if (!preg_match('#^(?:(['.self::UNRESERVED.self::SUB_DELIMS.'%]+)?(?::(['.self::UNRESERVED.self::SUB_DELIMS.'%]*))?@)?([^\s@]+)$#', $dsn, $matches)) {
132
            throw new SyntaxException($dsn, 'The provided DSN is not valid. Maybe you need to url-encode the user/password?');
133
        }
134
135
        $authentication = [
136
            'user' => empty($matches[1]) ? null : urldecode($matches[1]),
137
            'password' => empty($matches[2]) ? null : urldecode($matches[2]),
138
        ];
139
140
        if ('?' === $matches[3][0]) {
141
            $parts = self::explodeUrl('http://localhost'.$matches[3], $dsn);
142
143
            return new Dsn($scheme, self::getQuery($parts));
144
        }
145
146
        if ('/' === $matches[3][0]) {
147
            $parts = self::explodeUrl($matches[3], $dsn);
148
149
            return new Path($scheme, $parts['path'], self::getQuery($parts), $authentication);
150
        }
151
152
        $parts = self::explodeUrl('http://'.$matches[3], $dsn);
153
154
        return new Url($scheme, $parts['host'], $parts['port'] ?? null, $parts['path'] ?? null, self::getQuery($parts), $authentication);
155
    }
156
157
    /**
158
     * Parse URL and throw exception if the URL is not valid.
159
     *
160
     * @throws SyntaxException
161
     */
162
    private static function explodeUrl(string $url, string $dsn): array
163
    {
164
        $url = parse_url($url);
165
        if (false === $url) {
166
            throw new SyntaxException($dsn, 'The provided DSN is not valid.');
167
        }
168
169
        return $url;
170
    }
171
172
    /**
173
     * Parse query params into an array.
174
     */
175
    private static function getQuery(array $parts): array
176
    {
177
        $query = [];
178
        if (isset($parts['query'])) {
179
            parse_str($parts['query'], $query);
180
        }
181
182
        return $query;
183
    }
184
}
185