ParameterSpecBuilder::build()   C
last analyzed

Complexity

Conditions 7
Paths 13

Size

Total Lines 33
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 14
CRAP Score 7

Importance

Changes 0
Metric Value
dl 0
loc 33
rs 6.7272
c 0
b 0
f 0
ccs 14
cts 14
cp 1
cc 7
eloc 17
nc 13
nop 1
crap 7
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\Specs\Builder;
17
18
19
use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderException;
20
use Pinepain\JsSandbox\Extractors\ExtractorDefinitionBuilderInterface;
21
use Pinepain\JsSandbox\Specs\Builder\Exceptions\ArgumentValueBuilderException;
22
use Pinepain\JsSandbox\Specs\Builder\Exceptions\ParameterSpecBuilderException;
23
use Pinepain\JsSandbox\Specs\Parameters\MandatoryParameterSpec;
24
use Pinepain\JsSandbox\Specs\Parameters\OptionalParameterSpec;
25
use Pinepain\JsSandbox\Specs\Parameters\ParameterSpecInterface;
26
use Pinepain\JsSandbox\Specs\Parameters\VariadicParameterSpec;
27
28
29
class ParameterSpecBuilder implements ParameterSpecBuilderInterface
30
{
31
    protected $regexp = '/
32
        ^
33
        (?:
34
            (?<rest>\.{3})
35
            \s*
36
        )?
37
        (?<name>[_a-z]\w*)
38
        \s*
39
        (?<nullable>\?)?
40
        \s*
41
        (?:
42
            \s* = \s*
43
            (?<default>
44
                (?:[+-]?[0-9]+\.?[0-9]*)    # numbers (no exponential notation)
45
                |
46
                (?:\\\'[^\\\']*\\\')        # single-quoted string
47
                |
48
                (?:\"[^\"]*\")              # double-quoted string
49
                |
50
                (?:\[\s*\])                 # empty array
51
                |
52
                (?:\{\s*\})                 # empty object
53
                |
54
                true | false | null
55
            )
56
            \s*
57
        )?
58
        (?:
59
            \s*
60
            \:
61
            \s*
62
            (?<type>([\w\-]*(?:\(.*\))?(?:\[\s*\])?)(?:\s*\|\s*(?-1))*)
63
            \s*
64
        )?
65
        $
66
        /xi';
67
    /**
68
     * @var ExtractorDefinitionBuilderInterface
69 39
     */
70
    private $extractor;
71 39
    /**
72 39
     * @var ArgumentValueBuilderInterface
73
     */
74
    private $argument;
75
76
    /**
77
     * @param ExtractorDefinitionBuilderInterface $extractor
78
     * @param ArgumentValueBuilderInterface $argument
79
     */
80 39
    public function __construct(ExtractorDefinitionBuilderInterface $extractor, ArgumentValueBuilderInterface $argument)
81
    {
82 39
        $this->extractor = $extractor;
83
        $this->argument  = $argument;
84 39
    }
85 1
86
    /**
87
     * @param string $definition
88 38
     *
89
     * @return ParameterSpecInterface
90 37
     * @throws ParameterSpecBuilderException
91 2
     */
92
    public function build(string $definition): ParameterSpecInterface
93
    {
94 35
        $definition = trim($definition);
95 31
96
        if (!$definition) {
97
            throw new ParameterSpecBuilderException('Definition must be non-empty string');
98 4
        }
99 2
100 1
        if (preg_match($this->regexp, $definition, $matches)) {
101
102
            $matches = $this->prepareDefinition($matches);
103
104 1
            try {
105
                if ($this->hasRest($matches)) {
106
                    return $this->buildVariadicParameterSpec($matches);
107 2
                }
108
109 2
                if ($this->hasDefault($matches)) {
110 1
                    return $this->buildOptionalParameterSpec($matches, $matches['default']);
111
                }
112
113 1
                if ($this->hasNullable($matches)) {
114
                    return $this->buildOptionalParameterSpec($matches, null);
115
                }
116 31
117
                return $this->buildMandatoryParameterSpec($matches);
118 31
            } catch (ExtractorDefinitionBuilderException $e) {
119
                throw new ParameterSpecBuilderException("Unable to parse definition because of extractor failure: " . $e->getMessage());
120 31
            }
121
        }
122
123 4
        throw new ParameterSpecBuilderException("Unable to parse definition: '{$definition}'");
124
    }
125 4
126
    protected function buildVariadicParameterSpec(array $matches): VariadicParameterSpec
127
    {
128 31
        return new VariadicParameterSpec($matches['name'], $this->extractor->build($matches['type']));
129
    }
130 31
131 4
    protected function buildOptionalParameterSpec(array $matches, ?string $default): OptionalParameterSpec
132 2
    {
133
        if (null !== $default) {
134
            $default_definition = $matches['default'];
135 2
            try {
136
                $default = $this->argument->build($default_definition, false);
137
            } catch (ArgumentValueBuilderException $e) {
138 27
                throw new ParameterSpecBuilderException("Unknown or unsupported default value format '{$default_definition}'");
139
            }
140 3
141
            if (!$this->hasType($matches)) {
142 3
                $matches['type'] = $this->guessTypeFromDefault($default);
143
            }
144 3
        }
145
146
        if ($this->hasNullable($matches)) {
147
            // nullable means that null is a valid value and thus we should explicitly enable null extractor here
148
            $matches['type'] = 'null|' . $matches['type'];
149 18
        }
150
151
        return new OptionalParameterSpec($matches['name'], $this->extractor->build($matches['type']), $default);
152
    }
153
154
    protected function buildMandatoryParameterSpec(array $matches): MandatoryParameterSpec
155 18
    {
156 2
        return new MandatoryParameterSpec($matches['name'], $this->extractor->build($matches['type']));
157
    }
158
159 16
    protected function prepareDefinition(array $matches): array
160 2
    {
161
        if ($this->hasNullable($matches) && $this->hasRest($matches)) {
162
            throw new ParameterSpecBuilderException("Variadic parameter could not be nullable");
163 14
        }
164 14
165 14
        if ($this->hasNullable($matches) && $this->hasDefault($matches)) {
166
            throw new ParameterSpecBuilderException("Nullable parameter could not have default value");
167
        }
168
169
        if ($this->hasRest($matches) && $this->hasDefault($matches)) {
170
            throw new ParameterSpecBuilderException('Variadic parameter could have no default value');
171
        }
172
173 18
        if (!$this->hasDefault($matches) && !$this->hasType($matches)) {
174
            // special case when no default value set and no type provided
175 18
            $matches['type'] = 'any';
176
        }
177 18
178
        return $matches;
179
    }
180
181
    private function hasType(array $matches): bool
182
    {
183
        return isset($matches['type']) && '' !== $matches['type'];
184
    }
185
186
    private function hasNullable(array $matches): bool
187
    {
188
        return isset($matches['nullable']) && '' !== $matches['nullable'];
189
    }
190
191
    private function hasRest(array $matches): bool
192
    {
193
        return isset($matches['rest']) && '' !== $matches['rest'];
194
    }
195
196
    private function hasDefault(array $matches): bool
197
    {
198
        return isset($matches['default']) && '' !== $matches['default'];
199
    }
200
201
    private function guessTypeFromDefault($default): string
202
    {
203
        if (is_array($default)) {
204
            return '[]';
205
        }
206
207
        if (is_numeric($default)) {
208
            return 'number';
209
        }
210
211
        if (is_bool($default)) {
212
            return 'bool';
213
        }
214
215
        if (is_string($default)) {
216
            return 'string';
217
        }
218
219
        // it looks like we have nullable parameter which could be anything
220
221
        return 'any';
222
    }
223
}
224