Passed
Push — master ( 1b37b7...612acf )
by Tobias
01:49
created

DsnParser   A

Complexity

Total Complexity 23

Size/Duplication

Total Lines 143
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 1
Metric Value
wmc 23
eloc 56
c 1
b 0
f 1
dl 0
loc 143
rs 10

6 Methods

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