Completed
Pull Request — master (#54)
by Frank
03:15
created

CodeDumper::dumpEvents()   A

Complexity

Conditions 5
Paths 6

Size

Total Lines 29
Code Lines 17

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 17
CRAP Score 5

Importance

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