Passed
Pull Request — master (#10)
by Oleksii
13:49
created

RouteBuilder   A

Complexity

Total Complexity 25

Size/Duplication

Total Lines 170
Duplicated Lines 0 %

Test Coverage

Coverage 98.21%

Importance

Changes 0
Metric Value
wmc 25
eloc 61
dl 0
loc 170
ccs 55
cts 56
cp 0.9821
rs 10
c 0
b 0
f 0

8 Methods

Rating   Name   Duplication   Size   Complexity  
A setController() 0 5 1
A setUri() 0 5 1
A setMethods() 0 9 2
A addMethod() 0 7 2
A __construct() 0 13 1
C build() 0 67 16
A clear() 0 6 1
A setName() 0 5 1
1
<?php
2
3
declare(strict_types=1);
4
5
/*
6
 *  This file is part of the Micro framework package.
7
 *
8
 *  (c) Stanislau Komar <[email protected]>
9
 *
10
 *  For the full copyright and license information, please view the LICENSE
11
 *  file that was distributed with this source code.
12
 */
13
14
namespace Micro\Plugin\Http\Business\Route;
15
16
use Micro\Plugin\Http\Exception\RouteInvalidConfigurationException;
17
use Symfony\Component\HttpFoundation\Request;
18
19
/**
20
 * @author Stanislau Komar <[email protected]>
21
 */
22
class RouteBuilder implements RouteBuilderInterface
23
{
24
    private string|null $name;
25
26
    /**
27
     * @var array<class-string, string|null>|class-string|\Closure|null
0 ignored issues
show
Documentation Bug introduced by
The doc comment array<class-string, stri...ss-string|\Closure|null at position 2 could not be parsed: Unknown type name 'class-string' at position 2 in array<class-string, string|null>|class-string|\Closure|null.
Loading history...
28
     */
29
    private mixed $action;
30
31
    private string|null $uri;
32
33
    /**
34
     * @var array|string[]
35
     */
36
    private array $methods;
37
38
    /**
39
     * @param string[] $methodsByDefault
40
     */
41
    public function __construct(
42
        private readonly array $methodsByDefault = [
43
            Request::METHOD_GET,
44
            Request::METHOD_POST,
45
            Request::METHOD_PUT,
46
            Request::METHOD_PATCH,
47
            Request::METHOD_DELETE,
48
        ],
49
    ) {
50 24
        $this->name = null;
51 24
        $this->action = null;
52 24
        $this->uri = null;
53 24
        $this->methods = $this->methodsByDefault;
54
    }
55
56
    /**
57
     * {@inheritDoc}
58
     */
59
    public function setName(string $name): self
60
    {
61 17
        $this->name = $name;
62
63 17
        return $this;
64
    }
65
66
    /**
67
     * {@inheritDoc}
68
     */
69
    public function addMethod(string $method): self
70
    {
71 3
        if (!\in_array($method, $this->methods)) {
72 3
            $this->methods[] = mb_strtoupper($method);
73
        }
74
75 3
        return $this;
76
    }
77
78
    /**
79
     * {@inheritDoc}
80
     */
81
    public function setMethods(array $methods): self
82
    {
83 3
        $this->methods = [];
84
85 3
        foreach ($methods as $method) {
86 3
            $this->addMethod($method);
87
        }
88
89 3
        return $this;
90
    }
91
92
    /**
93
     * {@inheritDoc}
94
     */
95
    public function setController(string|array|\Closure $action): self
96
    {
97 21
        $this->action = $action;
98
99 21
        return $this;
100
    }
101
102
    /**
103
     * {@inheritDoc}
104
     */
105
    public function setUri(string $uri): self
106
    {
107 19
        $this->uri = $uri;
108
109 19
        return $this;
110
    }
111
112
    /**
113
     * TODO: Move pattern builder to separate class.
114
     *
115
     * {@inheritDoc}
116
     */
117
    public function build(): RouteInterface
118
    {
119 23
        $exceptions = [];
120
121 23
        if (!$this->uri) {
122 4
            $exceptions[] = 'Uri can not be empty.';
123
        }
124
125 23
        if ($this->name && !preg_match('/^([a-zA-Z_][a-zA-Z0-9_]*)$/', $this->name)) {
126 4
            $exceptions[] = 'The route name must match "[a-zA-Z][a-zA-Z0-9_]*".';
127
        }
128
129 23
        if (!$this->action) {
130 2
            $exceptions[] = 'The route action can not be empty.';
131
        }
132
133
        if (
134 23
            $this->action &&
135
            (
136 23
                !\is_callable($this->action) &&
137 23
                (\is_string($this->action) && (class_exists($this->action) && !$this->name))
138
            )
139
        ) {
140 3
            $exceptions[] = 'The route action should be callable. Examples: `[object, "method|<route_name>"], [Classname, "metnod|<routeName>"], Classname::method, Classname, function() {}` Current value: '.$this->action;
141
        }
142
143 23
        if (!\count($this->methods)) {
144
            $exceptions[] = 'The route should support at least one HTTP method.';
145
        }
146
147 23
        if (\count($exceptions)) {
148 10
            $exception = new RouteInvalidConfigurationException($this->name ?: $this->uri ?: 'Undefined', $exceptions);
149
150 10
            $this->clear();
151
152 10
            throw $exception;
153
        }
154
155 13
        $pattern = null;
156 13
        $parameters = null;
157
        /** @psalm-suppress PossiblyNullArgument */
158 13
        $isDynamic = preg_match_all('/\{\s*([^}\s]*)\s*}/', $this->uri, $matches);
159 13
        if ($isDynamic) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $isDynamic of type integer|null is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== null instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
160 9
            $parameters = $matches[1];
161
            /** @psalm-suppress PossiblyNullArgument */
162 9
            $pattern = '/'.addcslashes($this->uri, '/.').'$/';
0 ignored issues
show
Bug introduced by
It seems like $this->uri can also be of type null; however, parameter $string of addcslashes() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

162
            $pattern = '/'.addcslashes(/** @scrutinizer ignore-type */ $this->uri, '/.').'$/';
Loading history...
163
164 9
            foreach ($matches[0] as $replaced) {
165 9
                $pattern = str_replace($replaced, '([a-zA-Z_][a-zA-Z0-9_]*)', $pattern);
166
            }
167
        }
168
        /**
169
         * @psalm-suppress PossiblyNullArgument
170
         * @psalm-suppress MixedArgumentTypeCoercion
171
         */
172 13
        $route = new Route(
173 13
            $this->uri,
174 13
            $this->action,
175 13
            $this->methods,
176 13
            $this->name,
177 13
            $pattern,
178 13
            $parameters
179 13
        );
180
181 13
        $this->clear();
182
183 13
        return $route;
184
    }
185
186
    protected function clear(): void
187
    {
188 23
        $this->uri = null;
189 23
        $this->action = null;
190 23
        $this->methods = $this->methodsByDefault;
191 23
        $this->name = null;
192
    }
193
}
194