ExtractorDefinitionBuilder::buildExtractor()   C
last analyzed

Complexity

Conditions 7
Paths 24

Size

Total Lines 28
Code Lines 15

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 56

Importance

Changes 0
Metric Value
dl 0
loc 28
rs 6.7272
c 0
b 0
f 0
ccs 0
cts 0
cp 0
cc 7
eloc 15
nc 24
nop 5
crap 56
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\RecursiveExtractorDefinition;
23
use Pinepain\JsSandbox\Extractors\Definition\VariableExtractorDefinition;
24
25
26
class ExtractorDefinitionBuilder implements ExtractorDefinitionBuilderInterface
27
{
28
    /**
29
     * @var string
30
     */
31
    protected $type_regexp = '/
32
    ^
33
    (
34
        (?<name>
35 8
            [-_\w]*
36
        )
37 8
        (?:
38
            \s*
39 8
            (?<group>
40 1
                \(
41
                \s*
42
                (?<param>
43 7
                    (?-4)*
44 6
                    |
45
                    [\w\\\\]+
46
                )
47 1
                \s*
48
                \)
49
            )
50
        )?
51
        (?:
52
            \s*
53
            (?<arr>(?:\s*\[\s*\]\s*)+)
54
        )?
55
        (?:
56
            \s*
57
            \|
58 6
            \s*
59
            (?<alt>(?-6))
60 6
        )?
61
    )
62 6
    $
63 3
    /xi';
64
65
    /**
66 6
     * {@inheritdoc}
67
     */
68 6
    public function build(string $definition): ExtractorDefinitionInterface
69 2
    {
70
        $definition = trim($definition);
71
72 6
        if (!$definition) {
73
            throw new ExtractorDefinitionBuilderException('Definition must be non-empty string');
74
        }
75
76
        try {
77
            if (preg_match($this->type_regexp, $definition, $matches)) {
78
                $extractor = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches));
79
80
                return $extractor;
81
            }
82 2
        } catch (ExtractorDefinitionBuilderException $e) {
83
            // We don't care about what specific issue we hit inside,
84 2
            // for API user it means that the definition is invalid
85
        }
86 2
87
        throw new ExtractorDefinitionBuilderException("Unable to parse definition: '{$definition}'");
88 2
    }
89
90 2
    /**
91
     * @param string $name
92
     * @param null|string $param
93 2
     * @param null|string $alt_definitions
94
     * @param int $depth
95
     * @param bool $groups
96
     *
97
     * @return null|ExtractorDefinitionInterface
98 2
     * @throws ExtractorDefinitionBuilderException
99
     */
100
    protected function buildExtractor(string $name, string $param, string $alt_definitions, int $depth, bool $groups): ExtractorDefinitionInterface
101
    {
102
        $next = null;
103
104
        if ('' !== $param && preg_match($this->type_regexp, $param, $matches)) {
105
            $next = $this->buildExtractor($matches['name'], $matches['param'] ?? '', $matches['alt'] ?? '', $this->getDepth($matches), $this->hasGroups($matches));
106
        }
107
108
        if ($name) {
109
            $definition = $this->buildPlainExtractor($name, $next);
110
        } else {
111
            $definition = $next;
112
        }
113
114
        if ($depth > 0) {
115
            $definition = $this->buildArrayDefinition($definition, $depth, $groups);
116
        }
117
118
        if (!$definition) {
119
            throw new ExtractorDefinitionBuilderException('Empty group is not allowed');
120
        }
121
122
        if ('' !== $alt_definitions) {
123
            $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...
124
        }
125
126
        return $definition;
127
    }
128
129
    /**
130
     * @param PlainExtractorDefinitionInterface $definition
131
     * @param string $alt_definitions
132
     *
133
     * @return VariableExtractorDefinition
134
     * @throws ExtractorDefinitionBuilderException
135
     */
136
    protected function buildVariableDefinition(PlainExtractorDefinitionInterface $definition, string $alt_definitions): VariableExtractorDefinition
137
    {
138
        $alt = [$definition];
139
140
        while ('' !== $alt_definitions && preg_match($this->type_regexp, $alt_definitions, $matches)) {
141
            // build alt
142
            $alt[] = $this->buildExtractor($matches['name'], $matches['param'] ?? '', '', $this->getDepth($matches), $this->hasGroups($matches));
143
144
            $alt_definitions = trim($matches['alt'] ?? '');
145
        }
146
147
        if ('' !== $alt_definitions) {
148
            // UNEXPECTED
149
            // this should not be possible, but just in case we will ever get here
150
            throw new ExtractorDefinitionBuilderException('Invalid varying definition');
151
        }
152
153
        return new VariableExtractorDefinition(...$alt);
154
    }
155
156
    /**
157
     * @param null|ExtractorDefinitionInterface $definition
158
     * @param int $depth
159
     * @param bool $groups
160
     *
161
     * @return ExtractorDefinitionInterface
162
     * @throws ExtractorDefinitionBuilderException
163
     */
164
    protected function buildArrayDefinition(?ExtractorDefinitionInterface $definition, int $depth, bool $groups): ExtractorDefinitionInterface
165
    {
166
        // special case for blank brackets [] which should be the same as any[]
167
        if (!$definition) {
168
            if ($groups) {
169
                throw new ExtractorDefinitionBuilderException('Empty group is not allowed');
170
            }
171
172
            $definition = $this->buildPlainExtractor('any');
173
        }
174
175
        while ($depth) {
176
            $depth--;
177
            // arrayed definition
178
            $definition = $this->buildPlainExtractor('[]', $definition);
179
        }
180
181
        return $definition;
182
    }
183
184
    private function getDepth(array $matches): int
185
    {
186
        if (!isset($matches['arr']) || '' === $matches['arr']) {
187
            return 0;
188
        }
189
190
        return substr_count($matches['arr'], '[');
191
    }
192
193
    private function hasGroups(array $matches): bool
194
    {
195
        return isset($matches['group']) && '' !== $matches['group'];
196
    }
197
198
    private function buildPlainExtractor(string $name, ?ExtractorDefinitionInterface $next = null): PlainExtractorDefinitionInterface
199
    {
200
        if ('any' === $name && !$next) {
201
            return new RecursiveExtractorDefinition($name);
202
        }
203
204
        return new PlainExtractorDefinition($name, $next);
205
    }
206
}
207