1 | <?php declare(strict_types=1); |
||
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 |
||
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"); |
||
180 | |||
181 | private function hasType(array $matches): bool |
||
185 | |||
186 | private function hasNullable(array $matches): bool |
||
190 | |||
191 | private function hasRest(array $matches): bool |
||
195 | |||
196 | private function hasDefault(array $matches): bool |
||
200 | |||
201 | private function guessTypeFromDefault($default): string |
||
223 | } |
||
224 |