Passed
Push — master ( deacd9...fc5bf0 )
by Francis
01:53
created

SwaggerModelGenerator::generateSubClass()   A

Complexity

Conditions 6
Paths 13

Size

Total Lines 30
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
eloc 16
c 1
b 0
f 0
dl 0
loc 30
rs 9.1111
cc 6
nc 13
nop 3
1
<?php
2
3
declare(strict_types=1);
4
5
namespace Prometee\SwaggerClientBuilder\Swagger;
6
7
use Exception;
8
use Prometee\SwaggerClientBuilder\PhpBuilder\Object\ClassBuilderInterface;
9
use Prometee\SwaggerClientBuilder\PhpBuilder\Object\Method\ConstructorBuilderInterface;
10
use Prometee\SwaggerClientBuilder\PhpBuilder\Object\Other\MethodsBuilderInterface;
11
use Prometee\SwaggerClientBuilder\Swagger\Helper\SwaggerModelHelperInterface;
12
use Prometee\SwaggerClientBuilder\Swagger\PhpBuilder\Factory\ModelClassFactoryInterface;
13
use Prometee\SwaggerClientBuilder\Swagger\PhpBuilder\Factory\ModelMethodFactoryInterface;
14
use Prometee\SwaggerClientBuilder\Swagger\PhpBuilder\Model\Attribute\ModelPropertyBuilderInterface;
15
use Prometee\SwaggerClientBuilder\Swagger\PhpBuilder\Model\Method\ModelConstructorBuilderInterface;
16
use Prometee\SwaggerClientBuilder\Swagger\PhpBuilder\Model\Other\ModelPropertiesBuilderInterface;
17
18
class SwaggerModelGenerator implements SwaggerModelGeneratorInterface
19
{
20
    /** @var ModelClassFactoryInterface */
21
    protected $classFactory;
22
    /** @var ModelMethodFactoryInterface */
23
    protected $methodFactory;
24
    /** @var SwaggerModelHelperInterface */
25
    protected $helper;
26
27
    /** @var string */
28
    protected $folder;
29
    /** @var string */
30
    protected $namespace;
31
    /** @var string */
32
    protected $indent;
33
    /** @var array */
34
    protected $definitions = [];
35
    /** @var bool */
36
    protected $overwrite = false;
37
38
    /**
39
     * @param ModelClassFactoryInterface $classFactory
40
     * @param ModelMethodFactoryInterface $methodFactory
41
     * @param SwaggerModelHelperInterface $helper
42
     */
43
    public function __construct(
44
        ModelClassFactoryInterface $classFactory,
45
        ModelMethodFactoryInterface $methodFactory,
46
        SwaggerModelHelperInterface $helper
47
    )
48
    {
49
        $this->classFactory = $classFactory;
50
        $this->methodFactory = $methodFactory;
51
        $this->helper = $helper;
52
    }
53
54
    /**
55
     * @param string $folder
56
     * @param string $namespace
57
     * @param string $indent
58
     */
59
    public function configure(string $folder, string $namespace, string $indent = '    '): void
60
    {
61
        $this->folder = $folder;
62
        $this->namespace = $namespace;
63
        $this->indent = $indent;
64
        $this->definitions = [];
65
    }
66
67
    /**
68
     * {@inheritDoc}
69
     *
70
     * @throws Exception
71
     */
72
    public function generate(): bool
73
    {
74
        foreach ($this->definitions as $definitionName => $definition) {
75
            if (!isset($definition['type'])) {
76
                return false;
77
            }
78
            if ($definition['type'] !== 'object') {
79
                continue;
80
            }
81
            $this->generateClass($definitionName);
82
        }
83
84
        return true;
85
    }
86
87
    /**
88
     * {@inheritDoc}
89
     *
90
     * @throws Exception
91
     */
92
    public function generateClass(string $definitionName): ?ClassBuilderInterface
93
    {
94
        $filePath = $this->getFilePathFromDefinitionName($definitionName);
95
        if (!$this->overwrite && is_file($filePath)) {
96
            return null;
97
        }
98
99
        // Class
100
        $classBuilder = $this->classFactory->createClassBuilder();
101
        $this->configureClassBuilder(
102
            $classBuilder,
103
            $definitionName
104
        );
105
106
        // Properties
107
        /** @var ModelPropertiesBuilderInterface $modelPropertiesBuilder */
108
        $modelPropertiesBuilder = $classBuilder->getPropertiesBuilder();
109
        $this->configurePropertiesBuilder(
110
            $classBuilder,
111
            $modelPropertiesBuilder,
112
            $definitionName
113
        );
114
115
        // Constructor
116
        $constructorBuilder = $this->methodFactory->createModelConstructorBuilder($classBuilder->getUsesBuilder());
117
        $this->configureConstructorBuilder(
118
            $classBuilder->getMethodsBuilder(),
119
            $modelPropertiesBuilder,
120
            $constructorBuilder
121
        );
122
123
        // File creation
124
        $directory = dirname($filePath);
125
        if (!is_dir($directory)) {
126
            mkdir($directory, 0777, true);
127
        }
128
129
        if (false === file_put_contents($filePath, $classBuilder->build($this->indent))) {
130
            throw new Exception(sprintf('Unable to generate the class : "%s" !', $filePath));
131
        }
132
133
        return $classBuilder;
134
    }
135
136
    /**
137
     * {@inheritDoc}
138
     *
139
     * @throws Exception
140
     */
141
    public function generateSubClass(
142
        string $currentDefinitionName,
143
        string $currentProperty,
144
        array $currentConfig
145
    ): ?string
146
    {
147
        if (!isset($currentConfig['type'])) {
148
            return null;
149
        }
150
151
        $subConfig = null;
152
        if ($currentConfig['type'] === 'object') {
153
            $subConfig = $currentConfig;
154
        }
155
        if ($currentConfig['type'] === 'array') {
156
            $subConfig = $currentConfig['items'];
157
        }
158
159
        if (null === $subConfig) {
160
            return null;
161
        }
162
163
        $subDefinitionName = $currentDefinitionName . '/' . ucfirst($currentProperty);
164
        $this->definitions[$subDefinitionName] = $subConfig;
165
        $this->generateClass($subDefinitionName);
166
        [$subNamespace, $subClassName] = $this->getClassNameAndNamespaceFromDefinitionName($subDefinitionName);
167
        $type = '\\' . $subNamespace . '\\' . $subClassName;
168
        $type .= $currentConfig['type'] === 'array' ? '[]' : '';
169
170
        return $type;
171
    }
172
173
    /**
174
     * {@inheritDoc}
175
     */
176
    public function getClassNameAndNamespaceFromDefinitionName(
177
        string $definitionName,
178
        string $classPrefix = '',
179
        string $classSuffix = ''
180
    ): array
181
    {
182
        $classPath = $this->helper::camelize($definitionName);
183
        $classParts = explode('/', $classPath);
184
        $className = array_pop($classParts);
185
        $namespace = implode('\\', $classParts);
186
        $namespace = $this->namespace . ($namespace === '' ? '' : $namespace);
187
188
        return [
189
            $namespace,
190
            $classPrefix . $className . $classSuffix,
191
        ];
192
    }
193
194
    /**
195
     * {@inheritDoc}
196
     */
197
    protected function getFilePathFromDefinitionName(string $definitionName): string
198
    {
199
        return sprintf('%s/%s.php', $this->folder, $this->helper::camelize($definitionName));
200
    }
201
202
    /**
203
     * {@inheritDoc}
204
     */
205
    public function getPhpTypeFromPropertyConfig(array $config): string
206
    {
207
        $type = $this->helper::getPhpTypeFromSwaggerConfiguration($config);
208
209
        if (null === $type) {
210
            return 'null';
211
        }
212
213
        if (false === $this->hasDefinition($type)) {
214
            return $type;
215
        }
216
217
        if (1 === preg_match('#^\\\\#', $type)) {
218
            return $type;
219
        }
220
221
        $singleType = rtrim($type, '[]');
222
        [$propertyNamespace, $propertyClassName] = $this->getClassNameAndNamespaceFromDefinitionName($singleType);
223
        return $propertyNamespace . '\\' . $propertyClassName;
224
    }
225
226
    /**
227
     * {@inheritDoc}
228
     *
229
     * @throws Exception
230
     */
231
    public function configureClassBuilder(
232
        ClassBuilderInterface $classBuilder,
233
        string $definitionName
234
    ): ?ClassBuilderInterface
235
    {
236
        $subClassBuilder = null;
237
        $extendClass = null;
238
        if (isset($this->definitions[$definitionName]['allOf'])) {
239
            $allOfConfig = $this->definitions[$definitionName]['allOf'][0];
240
            $subDefinitionName = $this->helper::getPhpTypeFromSwaggerConfiguration($allOfConfig);
241
            if (null === $subDefinitionName) {
242
                return null;
243
            }
244
            $subClassBuilder = $this->generateClass($subDefinitionName);
245
            $extendClass = $this->getPhpTypeFromPropertyConfig($allOfConfig);
246
        }
247
        [$namespace, $className] = $this->getClassNameAndNamespaceFromDefinitionName($definitionName);
248
        $classBuilder->configure($namespace, $className, $extendClass);
249
250
        return $subClassBuilder;
251
    }
252
253
    /**
254
     * {@inheritDoc}
255
     *
256
     * @throws Exception
257
     */
258
    public function configurePropertiesBuilder(
259
        ClassBuilderInterface $classBuilder,
260
        ModelPropertiesBuilderInterface $modelPropertiesBuilder,
261
        string $definitionName
262
    ): void
263
    {
264
        $properties = $this->flattenPropertiesDefinition($this->definitions[$definitionName]);
265
        $requires = $this->flattenRequiresDefinition($this->definitions[$definitionName]);
266
        $ownedProperties = $this->foundNotInheritedProperties($this->definitions[$definitionName]);
267
        $inheritedProperties = array_diff(array_keys($properties), array_keys($ownedProperties));
268
269
        foreach ($properties as $propertyName => $configuration) {
270
            $required = false !== array_search($propertyName, $requires);
271
            $inherited = false !== array_search($propertyName, $inheritedProperties);
272
            $this->processProperty(
273
                $modelPropertiesBuilder,
274
                $classBuilder->getMethodsBuilder(),
275
                $definitionName,
276
                $propertyName,
277
                $configuration,
278
                $required,
279
                $inherited
280
            );
281
        }
282
    }
283
284
    /**
285
     * {@inheritDoc}
286
     */
287
    public function flattenPropertiesDefinition(array $definition): array
288
    {
289
        if (isset($definition['properties'])) {
290
            return $definition['properties'];
291
        }
292
293
        if (!isset($definition['allOf'])) {
294
            return [];
295
        }
296
297
        $allOf = $definition['allOf'];
298
        $inheritedPropertyName = $this->helper::getPhpTypeFromSwaggerDefinitionName($allOf[0]['$ref']);
299
300
        $properties = [];
301
        if (isset($allOf[1]['properties'])) {
302
            $properties = $allOf[1]['properties'];
303
        }
304
305
        $inheritedProperties = $this->flattenPropertiesDefinition($this->definitions[$inheritedPropertyName]);
306
        return array_merge($properties, $inheritedProperties);
307
    }
308
309
    /**
310
     * {@inheritDoc}
311
     */
312
    public function flattenRequiresDefinition(array $definition): array
313
    {
314
        if (isset($definition['required'])) {
315
            return $definition['required'];
316
        }
317
318
        if (!isset($definition['allOf'])) {
319
            return [];
320
        }
321
322
        $allOf = $definition['allOf'];
323
        $inheritedPropertyName = $this->helper::getPhpTypeFromSwaggerDefinitionName($allOf[0]['$ref']);
324
325
        $requires = [];
326
        if (isset($allOf[1]['required'])) {
327
            $requires = $allOf[1]['required'];
328
        }
329
330
        $inheritedRequires = $this->flattenRequiresDefinition($this->definitions[$inheritedPropertyName]);
331
        return array_merge($requires, $inheritedRequires);
332
    }
333
334
    public function foundNotInheritedProperties(array $definition): array
335
    {
336
        if (isset($definition['properties'])) {
337
            return $definition['properties'];
338
        }
339
340
        if (!isset($definition['allOf'])) {
341
            return [];
342
        }
343
344
        $allOf = $definition['allOf'];
345
        if (!isset($allOf[1]['properties'])) {
346
            return [];
347
        }
348
349
        return $allOf[1]['properties'];
350
    }
351
352
    /**
353
     * {@inheritDoc}
354
     *
355
     * @throws Exception
356
     */
357
    public function processProperty(
358
        ModelPropertiesBuilderInterface $modelPropertiesBuilder,
359
        MethodsBuilderInterface $methodsBuilder,
360
        string $definitionName,
361
        string $propertyName,
362
        array $configuration,
363
        bool $required = false,
364
        bool $inherited = false
365
    ): void {
366
        $types = $this->findPropertyTypes(
367
            $definitionName,
368
            $propertyName,
369
            $configuration
370
        );
371
372
        $cleanPropertyName = $this->helper::cleanStr($propertyName);
373
        $description = isset($configuration['description']) ? $configuration['description'] : null;
374
375
        /** @var ModelPropertyBuilderInterface $propertyBuilder */
376
        $propertyBuilder = $this->classFactory->createPropertyBuilder(
377
            $modelPropertiesBuilder->getUsesBuilder()
378
        );
379
        $modelPropertiesBuilder->addPropertyFromSwaggerPropertyDefinition(
380
            $propertyBuilder,
381
            $cleanPropertyName,
382
            $types,
383
            $required,
384
            $inherited,
385
            $description
386
        );
387
388
        if ($inherited) {
389
            return;
390
        }
391
392
        $getterSetterBuilder = $this->methodFactory->createPropertyMethodsBuilder(
393
            $methodsBuilder->getUsesBuilder()
394
        );
395
        $definition = &$this->definitions[$definitionName];
396
        $readOnly = isset($definition['readOnly']) && $definition['readOnly'] === 'true';
397
        $writeOnly = isset($definition['writeOnly']) && $definition['writeOnly'] === 'true';
398
        $getterSetterBuilder->configure($propertyBuilder, $readOnly, $writeOnly);
399
400
        $methodBuilders = $getterSetterBuilder->getMethods($this->methodFactory, $this->indent);
401
        $methodsBuilder->addMultipleMethod($methodBuilders);
402
    }
403
404
    /**
405
     * {@inheritDoc}
406
     */
407
    public function configureConstructorBuilder(
408
        MethodsBuilderInterface $methodsBuilder,
409
        ModelPropertiesBuilderInterface $modelPropertiesBuilder,
410
        ModelConstructorBuilderInterface $constructorBuilder
411
    ): void
412
    {
413
        $constructorBuilder->configureFromPropertiesBuilder($modelPropertiesBuilder);
414
        $methodsBuilder->addMethod($constructorBuilder);
415
    }
416
417
    /**
418
     * {@inheritDoc}
419
     *
420
     * @throws Exception
421
     */
422
    public function findPropertyTypes(
423
        string $definitionName,
424
        string $propertyName,
425
        array $configuration
426
    ): array
427
    {
428
        $cleanPropertyName = $this->helper::cleanStr($propertyName);
429
430
        $type = $this->generateSubClass($definitionName, $cleanPropertyName, $configuration);
431
        if (null === $type) {
432
            $type = $this->getPhpTypeFromPropertyConfig($configuration);
433
        }
434
435
        $types = [$type];
436
437
        if ($this->helper::isNullableBySwaggerConfiguration($propertyName, $this->definitions[$definitionName])) {
438
            $types[] = 'null';
439
        }
440
441
        return $types;
442
    }
443
444
    /**
445
     * {@inheritDoc}
446
     */
447
    public function hasDefinition(string $definitionName): bool
448
    {
449
        return isset($this->definitions[$definitionName]);
450
    }
451
452
    /**
453
     * {@inheritDoc}
454
     */
455
    public function getDefinitions(): array
456
    {
457
        return $this->definitions;
458
    }
459
460
    /**
461
     * {@inheritDoc}
462
     */
463
    public function setDefinitions(array $definitions): void
464
    {
465
        $this->definitions = $definitions;
466
    }
467
468
    /**
469
     * {@inheritDoc}
470
     */
471
    public function getHelper(): SwaggerModelHelperInterface
472
    {
473
        return $this->helper;
474
    }
475
476
    /**
477
     * {@inheritDoc}
478
     */
479
    public function setHelper(SwaggerModelHelperInterface $helper): void
480
    {
481
        $this->helper = $helper;
482
    }
483
484
    /**
485
     * {@inheritDoc}
486
     */
487
    public function isOverwrite(): bool
488
    {
489
        return $this->overwrite;
490
    }
491
492
    /**
493
     * {@inheritDoc}
494
     */
495
    public function setOverwrite(bool $overwrite): void
496
    {
497
        $this->overwrite = $overwrite;
498
    }
499
}
500