1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace EventSauce\EventSourcing\CodeGeneration; |
||
6 | |||
7 | use LogicException; |
||
8 | use const null; |
||
9 | use function array_filter; |
||
10 | use function sprintf; |
||
11 | use function ucfirst; |
||
12 | use function var_export; |
||
13 | |||
14 | class CodeDumper |
||
15 | { |
||
16 | /** |
||
17 | * @var DefinitionGroup |
||
18 | */ |
||
19 | private $definitionGroup; |
||
20 | |||
21 | 12 | public function dump(DefinitionGroup $definitionGroup, bool $withHelpers = true, bool $withSerialization = true): string |
|
22 | { |
||
23 | 12 | $this->definitionGroup = $definitionGroup; |
|
24 | 12 | $eventsCode = $this->dumpEvents($definitionGroup->events(), $withHelpers, $withSerialization); |
|
25 | 12 | $commandCode = $this->dumpCommands($definitionGroup->commands()); |
|
26 | 11 | $namespace = $definitionGroup->namespace(); |
|
27 | 11 | $allCode = implode(array_filter([$eventsCode, $commandCode]), "\n\n"); |
|
0 ignored issues
–
show
|
|||
28 | |||
29 | 11 | if ($withSerialization) { |
|
30 | 11 | $namespace .= "; |
|
31 | |||
32 | use EventSauce\EventSourcing\Serialization\SerializableEvent"; |
||
33 | } |
||
34 | |||
35 | return <<<EOF |
||
36 | <?php |
||
37 | |||
38 | 11 | namespace $namespace; |
|
39 | |||
40 | 11 | $allCode |
|
41 | |||
42 | EOF; |
||
43 | } |
||
44 | |||
45 | 12 | private function dumpEvents(array $events, bool $withHelpers, bool $withSerialization): string |
|
46 | { |
||
47 | 12 | $code = []; |
|
48 | |||
49 | 12 | if (empty($events)) { |
|
50 | 2 | return ''; |
|
51 | } |
||
52 | |||
53 | 10 | foreach ($events as $event) { |
|
54 | 10 | $name = $event->name(); |
|
55 | 10 | $fields = $this->dumpFields($event); |
|
56 | 10 | $constructor = $this->dumpConstructor($event); |
|
57 | 10 | $methods = $this->dumpMethods($event); |
|
58 | 10 | $deserializer = $this->dumpSerializationMethods($event); |
|
59 | 10 | $testHelpers = $withHelpers ? $this->dumpTestHelpers($event) : ''; |
|
60 | 10 | $implements = $withSerialization ? ' implements SerializableEvent' : ''; |
|
61 | |||
62 | 10 | $code[] = <<<EOF |
|
63 | 10 | final class $name$implements |
|
64 | { |
||
65 | 10 | $fields$constructor$methods$deserializer |
|
66 | |||
67 | 10 | $testHelpers} |
|
68 | |||
69 | |||
70 | EOF; |
||
71 | } |
||
72 | |||
73 | 10 | return rtrim(implode('', $code)); |
|
74 | } |
||
75 | |||
76 | 12 | private function dumpFields(DefinitionWithFields $definition): string |
|
77 | { |
||
78 | 12 | $fields = $this->fieldsFromDefinition($definition); |
|
79 | 11 | $code = []; |
|
80 | 11 | $code[] = <<<EOF |
|
81 | |||
82 | EOF; |
||
83 | |||
84 | 11 | foreach ($fields as $field) { |
|
85 | 10 | $name = $field['name']; |
|
86 | 10 | $type = $this->definitionGroup->resolveTypeAlias($field['type']); |
|
87 | |||
88 | 10 | $code[] = <<<EOF |
|
89 | /** |
||
90 | 10 | * @var $type |
|
91 | */ |
||
92 | 10 | private \$$name; |
|
93 | |||
94 | |||
95 | EOF; |
||
96 | } |
||
97 | |||
98 | 11 | return implode('', $code); |
|
99 | } |
||
100 | |||
101 | 11 | private function dumpConstructor(DefinitionWithFields $definition): string |
|
102 | { |
||
103 | 11 | $arguments = []; |
|
104 | 11 | $assignments = []; |
|
105 | 11 | $fields = $this->fieldsFromDefinition($definition); |
|
106 | |||
107 | 11 | if (empty($fields)) { |
|
108 | 1 | return ''; |
|
109 | } |
||
110 | |||
111 | 10 | foreach ($fields as $field) { |
|
112 | 10 | $resolvedType = $this->definitionGroup->resolveTypeAlias($field['type']); |
|
113 | 10 | $arguments[] = sprintf(' %s $%s', $resolvedType, $field['name']); |
|
114 | 10 | $assignments[] = sprintf(' $this->%s = $%s;', $field['name'], $field['name']); |
|
115 | } |
||
116 | |||
117 | 10 | $arguments = implode(",\n", $arguments); |
|
118 | 10 | $assignments = implode("\n", $assignments); |
|
119 | |||
120 | return <<<EOF |
||
121 | public function __construct( |
||
122 | 10 | $arguments |
|
123 | ) { |
||
124 | 10 | $assignments |
|
125 | } |
||
126 | |||
127 | |||
128 | EOF; |
||
129 | } |
||
130 | |||
131 | 11 | private function dumpMethods(DefinitionWithFields $command): string |
|
132 | { |
||
133 | 11 | $methods = []; |
|
134 | |||
135 | 11 | foreach ($this->fieldsFromDefinition($command) as $field) { |
|
136 | 10 | $methods[] = <<<EOF |
|
137 | 10 | public function {$field['name']}(): {$this->definitionGroup->resolveTypeAlias($field['type'])} |
|
138 | { |
||
139 | 10 | return \$this->{$field['name']}; |
|
140 | } |
||
141 | |||
142 | |||
143 | EOF; |
||
144 | } |
||
145 | |||
146 | 11 | return empty($methods) ? '' : rtrim(implode('', $methods)) . "\n"; |
|
147 | } |
||
148 | |||
149 | 10 | private function dumpSerializationMethods(EventDefinition $event) |
|
150 | { |
||
151 | 10 | $name = $event->name(); |
|
152 | 10 | $arguments = []; |
|
153 | 10 | $serializers = []; |
|
154 | |||
155 | 10 | foreach ($this->fieldsFromDefinition($event) as $field) { |
|
156 | 9 | $type = $this->definitionGroup->resolveTypeAlias($field['type']); |
|
157 | 9 | $parameter = sprintf('$payload[\'%s\']', $field['name']); |
|
158 | 9 | $template = $event->deserializerForField($field['name']) |
|
159 | 9 | ?: $event->deserializerForType($field['type']); |
|
160 | 9 | $arguments[] = trim(strtr($template, ['{type}' => $type, '{param}' => $parameter])); |
|
161 | |||
162 | 9 | $property = sprintf('$this->%s', $field['name']); |
|
163 | 9 | $template = $event->serializerForField($field['name']) |
|
164 | 9 | ?: $event->serializerForType($field['type']); |
|
165 | 9 | $template = sprintf("'%s' => %s", $field['name'], $template); |
|
166 | 9 | $serializers[] = trim(strtr($template, ['{type}' => $type, '{param}' => $property])); |
|
167 | } |
||
168 | |||
169 | 10 | $arguments = preg_replace('/^.{2,}$/m', ' $0', implode(",\n", $arguments)); |
|
170 | |||
171 | 10 | if ( ! empty($arguments)) { |
|
172 | 9 | $arguments = "\n$arguments"; |
|
173 | } |
||
174 | |||
175 | 10 | $serializers = preg_replace('/^.{2,}$/m', ' $0', implode(",\n", $serializers)); |
|
176 | |||
177 | 10 | if ( ! empty($serializers)) { |
|
178 | 9 | $serializers = "\n$serializers,\n "; |
|
179 | } |
||
180 | |||
181 | return <<<EOF |
||
182 | public static function fromPayload(array \$payload): SerializableEvent |
||
183 | { |
||
184 | 10 | return new $name($arguments); |
|
185 | } |
||
186 | |||
187 | public function toPayload(): array |
||
188 | { |
||
189 | 10 | return [$serializers]; |
|
190 | } |
||
191 | EOF; |
||
192 | } |
||
193 | |||
194 | 8 | private function dumpTestHelpers(EventDefinition $event): string |
|
195 | { |
||
196 | 8 | $constructor = []; |
|
197 | 8 | $constructorArguments = ''; |
|
198 | 8 | $constructorValues = []; |
|
199 | 8 | $helpers = []; |
|
200 | |||
201 | 8 | foreach ($this->fieldsFromDefinition($event) as $field) { |
|
202 | 7 | $resolvedType = $this->definitionGroup->resolveTypeAlias($field['type']); |
|
203 | |||
204 | 7 | if (null === $field['example']) { |
|
205 | 3 | $constructor[] = ucfirst($field['name']); |
|
206 | |||
207 | 3 | if ('' !== $constructorArguments) { |
|
208 | 1 | $constructorArguments .= ', '; |
|
209 | } |
||
210 | |||
211 | 3 | $constructorArguments .= sprintf('%s $%s', $resolvedType, $field['name']); |
|
212 | 3 | $constructorValues[] = sprintf('$%s', $field['name']); |
|
213 | } else { |
||
214 | 5 | $constructorValues[] = $this->dumpConstructorValue($field, $event); |
|
215 | 5 | $method = sprintf('with%s', ucfirst($field['name'])); |
|
216 | 5 | $helpers[] = <<<EOF |
|
217 | /** |
||
218 | * @codeCoverageIgnore |
||
219 | */ |
||
220 | 5 | public function $method({$resolvedType} \${$field['name']}): {$event->name()} |
|
221 | { |
||
222 | 5 | \$this->{$field['name']} = \${$field['name']}; |
|
223 | |||
224 | return \$this; |
||
225 | } |
||
226 | |||
227 | |||
228 | EOF; |
||
229 | } |
||
230 | } |
||
231 | |||
232 | 8 | $constructor = sprintf('with%s', implode('And', $constructor)); |
|
233 | 8 | $constructorValues = implode(",\n ", $constructorValues); |
|
234 | |||
235 | 8 | if ('' !== $constructorValues) { |
|
236 | 7 | $constructorValues = "\n $constructorValues\n "; |
|
237 | } |
||
238 | |||
239 | 8 | $helpers[] = <<<EOF |
|
240 | /** |
||
241 | * @codeCoverageIgnore |
||
242 | */ |
||
243 | 8 | public static function $constructor($constructorArguments): {$event->name()} |
|
244 | { |
||
245 | 8 | return new {$event->name()}($constructorValues); |
|
246 | } |
||
247 | |||
248 | |||
249 | EOF; |
||
250 | |||
251 | 8 | return rtrim(implode('', $helpers)) . "\n"; |
|
252 | } |
||
253 | |||
254 | 5 | private function dumpConstructorValue(array $field, EventDefinition $event): string |
|
255 | { |
||
256 | 5 | $parameter = rtrim($field['example']); |
|
257 | 5 | $resolvedType = $this->definitionGroup->resolveTypeAlias($field['type']); |
|
258 | |||
259 | 5 | if (gettype($parameter) === $resolvedType) { |
|
260 | 4 | $parameter = var_export($parameter, true); |
|
261 | } |
||
262 | |||
263 | 5 | $template = $event->deserializerForField($field['name']) |
|
264 | 5 | ?: $event->deserializerForType($field['type']); |
|
265 | |||
266 | 5 | return rtrim(strtr($template, ['{type}' => $resolvedType, '{param}' => $parameter])); |
|
267 | } |
||
268 | |||
269 | /** |
||
270 | * @param CommandDefinition[] $commands |
||
271 | * |
||
272 | * @return string |
||
273 | */ |
||
274 | 12 | private function dumpCommands(array $commands): string |
|
275 | { |
||
276 | 12 | $code = []; |
|
277 | |||
278 | 12 | foreach ($commands as $command) { |
|
279 | 4 | $code[] = <<<EOF |
|
280 | 5 | final class {$command->name()} |
|
281 | { |
||
282 | 5 | {$this->dumpFields($command)}{$this->dumpConstructor($command)}{$this->dumpMethods($command)}} |
|
283 | |||
284 | |||
285 | EOF; |
||
286 | } |
||
287 | |||
288 | 11 | return rtrim(implode('', $code)); |
|
289 | } |
||
290 | |||
291 | /** |
||
292 | * @param DefinitionWithFields $definition |
||
293 | * |
||
294 | * @return array |
||
295 | */ |
||
296 | 12 | private function fieldsFromDefinition(DefinitionWithFields $definition): array |
|
297 | { |
||
298 | 12 | $fields = $this->fieldsFrom($definition->fieldsFrom()); |
|
299 | |||
300 | 11 | foreach ($definition->fields() as $field) { |
|
301 | 10 | array_push($fields, $field); |
|
302 | } |
||
303 | |||
304 | 11 | return $fields; |
|
305 | } |
||
306 | |||
307 | 12 | private function fieldsFrom(string $fieldsFrom): array |
|
308 | { |
||
309 | 12 | if (empty($fieldsFrom)) { |
|
310 | 11 | return []; |
|
311 | } |
||
312 | |||
313 | 2 | foreach ($this->definitionGroup->events() as $event) { |
|
314 | 1 | if ($event->name() === $fieldsFrom) { |
|
315 | 1 | return $event->fields(); |
|
316 | } |
||
317 | } |
||
318 | |||
319 | 2 | foreach ($this->definitionGroup->commands() as $command) { |
|
320 | 2 | if ($command->name() === $fieldsFrom) { |
|
321 | 1 | return $command->fields(); |
|
322 | } |
||
323 | } |
||
324 | |||
325 | 1 | throw new LogicException("Could not inherit fields from {$fieldsFrom}."); |
|
326 | } |
||
327 | } |
||
328 |
This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.
If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress. Please note the @ignore annotation hint above.