Passed
Push — new-features ( 9c9516...88ac40 )
by Bogdan
03:04
created

ExtractorDefinitionBuilder   A

Complexity

Total Complexity 24

Size/Duplication

Total Lines 182
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 97.73%

Importance

Changes 0
Metric Value
wmc 24
lcom 1
cbo 3
dl 0
loc 182
ccs 43
cts 44
cp 0.9773
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A build() 0 21 4
C buildExtractor() 0 28 7
A buildVariableDefinition() 0 19 4
A buildArrayDefinition() 0 14 4
A getDepth() 0 8 3
A hasGroups() 0 4 2
1
<?php declare(strict_types=1);
2
3
/*
4
 * This file is part of the pinepain/js-sandbox PHP library.
5
 *
6
 * Copyright (c) 2016-2017 Bogdan Padalko <[email protected]>
7
 *
8
 * Licensed under the MIT license: http://opensource.org/licenses/MIT
9
 *
10
 * For the full copyright and license information, please view the
11
 * LICENSE file that was distributed with this source or visit
12
 * http://opensource.org/licenses/MIT
13
 */
14
15
16
namespace Pinepain\JsSandbox\Extractors;
17
18
19
use Pinepain\JsSandbox\Extractors\Definition\ExtractorDefinitionInterface;
20
use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinition;
21
use Pinepain\JsSandbox\Extractors\Definition\PlainExtractorDefinitionInterface;
22
use Pinepain\JsSandbox\Extractors\Definition\VariableExtractorDefinition;
23
24
25
class ExtractorDefinitionBuilder implements ExtractorDefinitionBuilderInterface
26
{
27
    /**
28
     * @var string
29
     */
30
    protected $type_regexp = '/
31
    ^
32
    (
33
        (?<name>
34
            [-_\w]*
35
        )
36
        (?:
37
            \s*
38
            (?<group>
39
                \(
40
                \s*
41
                (?<param>
42
                    (?-4)*
43
                    |
44
                    [\w\\\\]+
45
                )
46
                \s*
47
                \)
48
            )
49
        )?
50
        (?:
51
            \s*
52
            (?<arr>(?:\s*\[\s*\]\s*)+)
53
        )?
54
        (?:
55
            \s*
56
            \|
57
            \s*
58
            (?<alt>(?-6))
59
        )?
60
    )
61
    $
62
    /xi';
63
64
    protected $type_regexp2 = '/
65
    ^
66
        (?:((\w+\b(?:\(.*\))?(?:\s*\[\s*\])?)(?:\s*\|\s*(?-1))*))
67
        |
68
        (?:\(\s*(?-2)\s*\)(?:\s*\[\s*\])?)
69
        |
70
        (\[\s*\])
71
    $
72
    /xi';
73
74
    /**
75
     * {@inheritdoc}
76
     */
77 19
    public function build(string $definition): ExtractorDefinitionInterface
78
    {
79 19
        $definition = trim($definition);
80
81 19
        if (!$definition) {
82 1
            throw new ExtractorDefinitionBuilderException('Definition must be non-empty string');
83
        }
84
85
        try {
86 18
            if (preg_match($this->type_regexp, $definition, $matches)) {
87 17
                $extractor = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches));
88
89 16
                return $extractor;
90
            }
91 2
        } catch (ExtractorDefinitionBuilderException $e) {
92
            // We don't care about what specific issue we hit inside,
93
            // for API user it means that the definition is invalid
94
        }
95
96 3
        throw new ExtractorDefinitionBuilderException("Unable to parse definition: '{$definition}'");
97
    }
98
99
    /**
100
     * @param string $name
101
     * @param null|string $param
102
     * @param null|string $alt_definitions
103
     * @param int $depth
104
     * @param bool $groups
105
     *
106
     * @return ExtractorDefinitionInterface
107
     * @throws ExtractorDefinitionBuilderException
108
     */
109 17
    protected function buildExtractor(string $name, string $param, string $alt_definitions, int $depth, bool $groups): ExtractorDefinitionInterface
110
    {
111 17
        $next = null;
112
113 17
        if ('' !== $param && preg_match($this->type_regexp, $param, $matches)) {
114 9
            $next = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches));
115
        }
116
117 17
        if ($name) {
118 13
            $definition = new PlainExtractorDefinition($name, $next);
119
        } else {
120 10
            $definition = $next;
121
        }
122
123 17
        if ($depth > 0) {
124 6
            $definition = $this->buildArrayDefinition($definition, $depth, $groups);
125
        }
126
127 16
        if (!$definition) {
128 1
            throw new ExtractorDefinitionBuilderException('Empty group is not allowed');
129
        }
130
131 15
        if ('' !== $alt_definitions) {
132 6
            $definition = $this->buildVariableDefinition($definition, $alt_definitions);
0 ignored issues
show
Compatibility introduced by
$definition of type object<Pinepain\JsSandbo...torDefinitionInterface> is not a sub-type of object<Pinepain\JsSandbo...torDefinitionInterface>. It seems like you assume a child interface of the interface Pinepain\JsSandbox\Extra...ctorDefinitionInterface to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
133
        }
134
135 15
        return $definition;
136
    }
137
138
    /**
139
     * @param PlainExtractorDefinitionInterface $definition
140
     * @param string $alt_definitions
141
     *
142
     * @return VariableExtractorDefinition
143
     * @throws ExtractorDefinitionBuilderException
144
     */
145 6
    protected function buildVariableDefinition(PlainExtractorDefinitionInterface $definition, string $alt_definitions): VariableExtractorDefinition
146
    {
147 6
        $alt = [$definition];
148
149 6
        while ('' !== $alt_definitions && preg_match($this->type_regexp, $alt_definitions, $matches)) {
150
            // build alt
151 6
            $alt[] = $this->buildExtractor($matches['name'], $matches['param'] ?? '', '', $this->getDepth($matches), $this->hasGroups($matches));
152
153 6
            $alt_definitions = trim($matches['alt'] ?? '');
154
        }
155
156 6
        if ('' !== $alt_definitions) {
157
            // UNEXPECTED
158
            // this should not be possible, but just in case we will ever get here
159
            throw new ExtractorDefinitionBuilderException('Invalid varying definition');
160
        }
161
162 6
        return new VariableExtractorDefinition(...$alt);
163
    }
164
165
    /**
166
     * @param null|ExtractorDefinitionInterface $definition
167
     * @param int $depth
168
     * @param bool $groups
169
     *
170
     * @return ExtractorDefinitionInterface
171
     * @throws ExtractorDefinitionBuilderException
172
     */
173 6
    protected function buildArrayDefinition(?ExtractorDefinitionInterface $definition, int $depth, bool $groups): ExtractorDefinitionInterface
174
    {
175 6
        if (!$definition && $groups) {
176 1
            throw new ExtractorDefinitionBuilderException('Empty group is not allowed');
177
        }
178
179 5
        while ($depth) {
180 5
            $depth--;
181
            // arrayed definition
182 5
            $definition = new PlainExtractorDefinition('[]', $definition);
183
        }
184
185 5
        return $definition;
186
    }
187
188
    /**
189
     * @param array $matches
190
     *
191
     * @return int
192
     */
193 17
    private function getDepth(array $matches): int
194
    {
195 17
        if (!isset($matches['arr']) || '' === $matches['arr']) {
196 13
            return 0;
197
        }
198
199 6
        return substr_count($matches['arr'], '[');
200
    }
201
202 17
    private function hasGroups(array $matches): bool
203
    {
204 17
        return isset($matches['group']) && '' !== $matches['group'];
205
    }
206
}
207