Passed
Push — new-features ( 1b4a6c...36d73f )
by Bogdan
03:00
created

ExtractorDefinitionBuilder   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 177
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 3

Test Coverage

Coverage 97.83%

Importance

Changes 0
Metric Value
wmc 26
lcom 1
cbo 3
dl 0
loc 177
ccs 45
cts 46
cp 0.9783
rs 10
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A build() 0 21 4
D buildExtractor() 0 33 9
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
    /**
65
     * {@inheritdoc}
66
     */
67 20
    public function build(string $definition): ExtractorDefinitionInterface
68
    {
69 20
        $definition = trim($definition);
70
71 20
        if (!$definition) {
72 1
            throw new ExtractorDefinitionBuilderException('Definition must be non-empty string');
73
        }
74
75
        try {
76 19
            if (preg_match($this->type_regexp, $definition, $matches)) {
77 18
                $extractor = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches));
78
79 16
                return $extractor;
80
            }
81 3
        } catch (ExtractorDefinitionBuilderException $e) {
82
            // We don't care about what specific issue we hit inside,
83
            // for API user it means that the definition is invalid
84
        }
85
86 4
        throw new ExtractorDefinitionBuilderException("Unable to parse definition: '{$definition}'");
87
    }
88
89
    /**
90
     * @param string $name
91
     * @param null|string $param
92
     * @param null|string $alt_definitions
93
     * @param int $depth
94
     * @param bool $groups
95
     *
96
     * @return null|ExtractorDefinitionInterface
97
     * @throws ExtractorDefinitionBuilderException
98
     */
99 18
    protected function buildExtractor(string $name, string $param, string $alt_definitions, int $depth, bool $groups): ExtractorDefinitionInterface
100
    {
101 18
        $next = null;
102
103 18
        if ('' !== $param && preg_match($this->type_regexp, $param, $matches)) {
104 10
            $next = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches));
105
        }
106
107 18
        if ($name) {
108 13
            $definition = new PlainExtractorDefinition($name, $next);
109
        } else {
110 11
            $definition = $next;
111
        }
112
113 18
        if ($depth > 0) {
114
            // special case for blank brackets [] which should be the same as any[]
115 6
            if (!$groups && !$definition) {
116 2
                $definition = new PlainExtractorDefinition('any');
117
            }
118
119 6
            $definition = $this->buildArrayDefinition($definition, $depth, $groups);
120
        }
121
122 17
        if (!$definition) {
123 2
            throw new ExtractorDefinitionBuilderException('Empty group is not allowed');
124
        }
125
126 15
        if ('' !== $alt_definitions) {
127 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...
128
        }
129
130 15
        return $definition;
131
    }
132
133
    /**
134
     * @param PlainExtractorDefinitionInterface $definition
135
     * @param string $alt_definitions
136
     *
137
     * @return VariableExtractorDefinition
138
     * @throws ExtractorDefinitionBuilderException
139
     */
140 6
    protected function buildVariableDefinition(PlainExtractorDefinitionInterface $definition, string $alt_definitions): VariableExtractorDefinition
141
    {
142 6
        $alt = [$definition];
143
144 6
        while ('' !== $alt_definitions && preg_match($this->type_regexp, $alt_definitions, $matches)) {
145
            // build alt
146 6
            $alt[] = $this->buildExtractor($matches['name'], $matches['param'] ?? '', '', $this->getDepth($matches), $this->hasGroups($matches));
147
148 6
            $alt_definitions = trim($matches['alt'] ?? '');
149
        }
150
151 6
        if ('' !== $alt_definitions) {
152
            // UNEXPECTED
153
            // this should not be possible, but just in case we will ever get here
154
            throw new ExtractorDefinitionBuilderException('Invalid varying definition');
155
        }
156
157 6
        return new VariableExtractorDefinition(...$alt);
158
    }
159
160
    /**
161
     * @param null|ExtractorDefinitionInterface $definition
162
     * @param int $depth
163
     * @param bool $groups
164
     *
165
     * @return ExtractorDefinitionInterface
166
     * @throws ExtractorDefinitionBuilderException
167
     */
168 6
    protected function buildArrayDefinition(?ExtractorDefinitionInterface $definition, int $depth, bool $groups): ExtractorDefinitionInterface
169
    {
170 6
        if (!$definition && $groups) {
171 1
            throw new ExtractorDefinitionBuilderException('Empty group is not allowed');
172
        }
173
174 5
        while ($depth) {
175 5
            $depth--;
176
            // arrayed definition
177 5
            $definition = new PlainExtractorDefinition('[]', $definition);
178
        }
179
180 5
        return $definition;
181
    }
182
183
    /**
184
     * @param array $matches
185
     *
186
     * @return int
187
     */
188 18
    private function getDepth(array $matches): int
189
    {
190 18
        if (!isset($matches['arr']) || '' === $matches['arr']) {
191 14
            return 0;
192
        }
193
194 7
        return substr_count($matches['arr'], '[');
195
    }
196
197 18
    private function hasGroups(array $matches): bool
198
    {
199 18
        return isset($matches['group']) && '' !== $matches['group'];
200
    }
201
}
202