Completed
Push — master ( eba306...4ab485 )
by Frank
02:41 queued 42s
created

src/CodeGeneration/CodeDumper.php (1 issue)

Severity
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
The call to implode() has too many arguments starting with ' '. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

27
        $allCode = /** @scrutinizer ignore-call */ implode(array_filter([$eventsCode, $commandCode]), "\n\n");

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.

Loading history...
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 7
        \$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 2
                return $command->fields();
322
            }
323
        }
324
325 1
        throw new LogicException("Could not inherit fields from {$fieldsFrom}.");
326
    }
327
}
328