TypeBuilder::getTypeString()   F
last analyzed

Complexity

Conditions 56
Paths > 20000

Size

Total Lines 219
Code Lines 129

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 56
eloc 129
nc 1353646086
nop 2
dl 0
loc 219
rs 0
c 1
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Swaggest\PhpCodeBuilder\Markdown;
4
5
use Swaggest\CodeBuilder\TableRenderer;
6
use Swaggest\JsonSchema\Schema;
7
use Swaggest\PhpCodeBuilder\PhpCode;
8
9
class TypeBuilder
10
{
11
    const EXAMPLES = 'examples';
12
    const EXAMPLE = 'example';
13
14
    /** @var \SplObjectStorage */
15
    private $processed;
16
17
    public $trimNamePrefix = [
18
        '#/definitions'
19
    ];
20
21
    public $addNamePrefix = '';
22
23
    /**
24
     * Map of type name to type doc.
25
     * @var array<string,string>
26
     */
27
    public $types = [];
28
29
    public $uniqueTypeSchemas = [];
30
31
    public $file = '';
32
33
    public $confluence = false;
34
35
    public function __construct()
36
    {
37
        $this->processed = new \SplObjectStorage();
38
    }
39
40
    public function anchorLink($destinationHeader, $anchor = null)
41
    {
42
        if ($this->confluence) {
43
            $l = str_replace('`', '', $destinationHeader);
44
            $l = str_replace(' ', '-', $l);
45
            if (!is_string($l)) {
46
                return '#';
47
            }
48
49
            $l = urlencode($l);
50
51
            return '#' . $l;
52
        }
53
54
        if (!empty($anchor)) {
55
            $l = strtolower($anchor);
56
        } else {
57
            $l = strtolower($destinationHeader);
58
        }
59
60
        return '#' . $l;
61
    }
62
63
    public function header($text, $anchor = null)
64
    {
65
        if ($this->confluence) {
66
            return $text;
67
        }
68
69
        if (!empty($anchor)) {
70
            $l = strtolower($anchor);
71
        } else {
72
            $l = strtolower($text);
73
        }
74
        return '<a id="' . $l . '">' . '</a> ' . $text;
75
    }
76
77
    /**
78
     * @param Schema|boolean|null $schema
79
     * @param string $path
80
     * @return string
81
     */
82
    public function getTypeString($schema, $path = '')
83
    {
84
        if ($schema === null) {
85
            return '';
86
        }
87
88
        $schema = Schema::unboolSchema($schema);
89
90
        $isOptional = false;
91
        $isObject = false;
92
        $isArray = false;
93
        $isBoolean = false;
94
        $isString = false;
95
        $isNumber = false;
96
97
        if ($schema->const !== null) {
98
            return '`' . var_export($schema->const, true) . '`';
99
        }
100
101
        if (!empty($schema->enum)) {
102
            $res = '';
103
            if ($this->confluence) {
104
                foreach ($schema->enum as $value) {
105
                    $res .= '`' . var_export($value, true) . '`, ';
106
                }
107
                return substr($res, 0, -2);
108
            } else {
109
                foreach ($schema->enum as $value) {
110
                    $res .= '<br>`' . var_export($value, true) . '`, ';
111
                }
112
                return substr($res, 4, -2);
113
            }
114
        }
115
116
        if (!empty($schema->getFromRefs())) {
117
            $refs = $schema->getFromRefs();
118
            $path = $refs[0];
119
        }
120
121
        $type = $schema->type;
122
        if ($type === null) {
0 ignored issues
show
introduced by
The condition $type === null is always false.
Loading history...
123
            $type = [];
124
125
            if (!empty($schema->properties) || !empty($schema->additionalProperties) || !empty($schema->patternProperties)) {
126
                $type[] = Schema::OBJECT;
127
            }
128
129
            if (!empty($schema->items) || !empty($schema->additionalItems)) {
130
                $type[] = Schema::_ARRAY;
131
            }
132
        }
133
134
        if (!is_array($type)) {
135
            $type = [$type];
136
        }
137
138
        $or = [];
139
140
        if ($schema->oneOf !== null) {
141
            foreach ($schema->oneOf as $i => $item) {
142
                $or[] = $this->getTypeString($item, $path . '/oneOf/' . $i);
143
            }
144
        }
145
146
        if ($schema->anyOf !== null) {
147
            foreach ($schema->anyOf as $i => $item) {
148
                $or[] = $this->getTypeString($item, $path . '/anyOf/' . $i);
149
            }
150
        }
151
152
        if ($schema->allOf !== null) {
153
            foreach ($schema->allOf as $i => $item) {
154
                $or[] = $this->getTypeString($item, $path . '/allOf/' . $i);
155
            }
156
        }
157
158
        if ($schema->then !== null) {
159
            $or[] = $this->getTypeString($schema->then, $path . '/then');
160
        }
161
162
        if ($schema->else !== null) {
163
            $or[] = $this->getTypeString($schema->else, $path . '/else');
164
        }
165
166
        foreach ($type as $i => $t) {
167
            switch ($t) {
168
                case Schema::NULL:
169
                    $isOptional = true;
170
                    break;
171
172
                case Schema::OBJECT:
173
                    $isObject = true;
174
                    break;
175
176
                case Schema::_ARRAY:
177
                    $isArray = true;
178
                    break;
179
180
                case Schema::NUMBER:
181
                case Schema::INTEGER:
182
                    $isNumber = true;
183
                    break;
184
185
                case Schema::STRING:
186
                    $isString = true;
187
                    break;
188
189
                case Schema::BOOLEAN:
190
                    $isBoolean = true;
191
                    break;
192
193
            }
194
        }
195
196
197
        $namedTypeAdded = false;
198
        if (!empty($schema->properties) || $this->hasConstraints($schema)) {
199
            if ($this->processed->contains($schema)) {
200
                $or [] = $this->processed->offsetGet($schema);
201
                $namedTypeAdded = true;
202
            } else {
203
                if ($schema instanceof Schema) {
204
                    $typeName = $this->typeName($schema, $path);
205
                    $this->makeTypeDef($schema, $path);
206
207
                    $or [] = $typeName;
208
                    $namedTypeAdded = true;
209
                }
210
            }
211
        }
212
213
        if ($isObject) {
214
            $typeAdded = false;
215
216
            if ($namedTypeAdded) {
217
                $typeAdded = true;
218
            }
219
220
            if ($schema->additionalProperties instanceof Schema) {
221
                $typeName = $this->getTypeString($schema->additionalProperties, $path . '/additionalProperties');
222
                $or [] = "`Map<String,`$typeName`>`";
223
                $typeAdded = true;
224
            }
225
226
            if (!empty($schema->patternProperties)) {
227
                foreach ($schema->patternProperties as $pattern => $propertySchema) {
228
                    if ($propertySchema instanceof Schema) {
229
                        $typeName = $this->getTypeString($propertySchema, $path . '/patternProperties/' . $pattern);
230
                        $or [] = $typeName;
231
                        $typeAdded = true;
232
                    }
233
                }
234
            }
235
236
            if (!$typeAdded) {
237
                $or [] = '`Object`';
238
            }
239
        }
240
241
        if ($isArray) {
242
            $typeAdded = false;
243
244
            if ($schema->items instanceof Schema) {
245
                $typeName = $this->getTypeString($schema->items, $path . '/items');
246
                $or [] = "`Array<`$typeName`>`";
247
                $typeAdded = true;
248
            }
249
250
            if ($schema->additionalItems instanceof Schema) {
251
                $typeName = $this->getTypeString($schema->additionalItems, $path . '/additionalItems');
252
                $or [] = "`Array<`$typeName`>`";
253
                $typeAdded = true;
254
            }
255
256
            if (!$typeAdded) {
257
                $or [] = '`Array`';
258
            }
259
        }
260
261
        if ($isOptional) {
262
            $or [] = '`null`';
263
        }
264
265
        if ($isString) {
266
            $or [] = '`String`';
267
        }
268
269
        if ($isNumber) {
270
            $or [] = '`Number`';
271
        }
272
273
        if ($isBoolean) {
274
            $or [] = '`Boolean`';
275
        }
276
277
        if ($schema->format !== null) {
278
            $or [] = 'Format: `' . $schema->format . '`';
279
        }
280
281
        $res = '';
282
        foreach ($or as $item) {
283
            if (!empty($item) && $item !== '*') {
284
                $res .= ', ' . $item;
285
            }
286
        }
287
288
        if ($res !== '') {
289
            $res = substr($res, 2);
290
        } else {
291
            $res = '`*`';
292
        }
293
294
        if (empty($res)) {
295
            $res = '';
296
        }
297
298
        $res = str_replace('``', '', $res);
299
300
        return $res;
301
    }
302
303
    private function typeName(Schema $schema, $path, $raw = false)
304
    {
305
        if ($fromRefs = $schema->getFromRefs()) {
306
            $path = $fromRefs[count($fromRefs) - 1];
307
        }
308
309
        foreach ($this->trimNamePrefix as $prefix) {
310
            if ($prefix === substr($path, 0, strlen($prefix))) {
311
                $path = substr($path, strlen($prefix));
312
            }
313
        }
314
315
        if (($path === '#' || empty($path)) && !empty($schema->title)) {
316
            $path = $schema->title;
317
        }
318
319
        $name = PhpCode::makePhpName($this->addNamePrefix . '_' . $path, false);
320
321
        if ($raw) {
322
            return $name;
323
        }
324
325
        if ($this->confluence) {
326
            return '[' . $name . '](' . $this->anchorLink($name) . ')';
327
        }
328
329
        return '[`' . $name . '`](' . $this->anchorLink($name) . ')';
330
    }
331
332
    private static function constraints()
333
    {
334
        static $constraints;
335
336
        if ($constraints === null) {
337
            $names = Schema::names();
338
            $constraints = [
339
                $names->multipleOf,
340
                $names->maximum,
341
                $names->exclusiveMaximum,
342
                $names->minimum,
343
                $names->exclusiveMinimum,
344
                $names->maxLength,
345
                $names->minLength,
346
                $names->pattern,
347
                $names->maxItems,
348
                $names->minItems,
349
                $names->uniqueItems,
350
                $names->maxProperties,
351
                $names->minProperties,
352
            ];
353
        }
354
355
        return $constraints;
356
    }
357
358
    /**
359
     * @param Schema $schema
360
     */
361
    private function hasConstraints($schema)
362
    {
363
        foreach (self::constraints() as $name) {
364
            if ($schema->$name !== null) {
365
                return true;
366
            }
367
        }
368
369
        return false;
370
    }
371
372
    public function renderTypeDef(Schema $schema, $typeName, $path)
373
    {
374
        $head = '';
375
        if (!empty($schema->title) && $schema->title != $typeName) {
376
            $head .= $schema->title . "\n";
377
        }
378
379
        if (!empty($schema->description)) {
380
            $head .= $schema->description . "\n";
381
        }
382
383
        $examples = [];
384
        if (!empty($schema->{self::EXAMPLES})) {
385
            $examples = $schema->{self::EXAMPLES};
386
        }
387
388
        if (!empty($schema->{self::EXAMPLE})) {
389
            $examples[] = $schema->{self::EXAMPLE};
390
        }
391
392
        if (!empty($examples)) {
393
            $head .= "Example:\n\n";
394
            foreach ($examples as $example) {
395
                $head .= <<<MD
396
```json
397
$example
398
```
399
400
401
MD;
402
403
            }
404
        }
405
406
        $res = <<<MD
407
408
409
### {$this->header($typeName)}
410
$head
411
412
MD;
413
414
415
        $rows = [];
416
        foreach (self::constraints() as $name) {
417
            if ($schema->$name !== null) {
418
                $value = $schema->$name;
419
420
                if ($value instanceof Schema) {
421
                    $value = $this->typeName($value, $path . '/' . $name);
422
                }
423
424
                $rows [] = [
425
                    'Constraint' => $name,
426
                    'Value' => $value,
427
                ];
428
            }
429
        }
430
        $tr = TableRenderer::create(new \ArrayIterator($rows))
431
            ->stripEmptyColumns()
432
            ->setColDelimiter('|')
433
            ->setHeadRowDelimiter('-')
434
            ->setOutlineVertical(true)
435
            ->setShowHeader();
436
437
        if (!$this->confluence) {
438
            $tr->multilineCellDelimiter('<br>');
439
        }
440
441
        $res .= $tr;
442
443
        $res .= "\n\n";
444
445
        $rows = [];
446
        $hasDescription = false;
447
        if (!empty($schema->properties)) {
448
            foreach ($schema->properties as $propertyName => $propertySchema) {
449
                $typeString = $this->getTypeString($propertySchema, $path . '/' . $propertyName);
450
                $desc = $this->description($propertySchema);
451
                if (!empty($desc)) {
452
                    $hasDescription = true;
453
                }
454
                $isRequired = false;
455
                if (!empty($schema->required)) {
456
                    $isRequired = in_array($propertyName, $schema->required);
457
                }
458
                $rows [] = array(
459
                    'Property' => '`' . $propertyName . '`' . ($isRequired ? ' (required)' : ''),
460
                    'Type' => $typeString,
461
                    'Description' => $desc,
462
                );
463
            }
464
465
            if (!$hasDescription) {
466
                foreach ($rows as &$row) {
467
                    unset($row['Description']);
468
                }
469
            }
470
471
            $tr = TableRenderer::create(new \ArrayIterator($rows))
472
                ->stripEmptyColumns()
473
                ->setColDelimiter('|')
474
                ->setHeadRowDelimiter('-')
475
                ->setOutlineVertical(true)
476
                ->setShowHeader();
477
478
            if (!$this->confluence) {
479
                $tr->multilineCellDelimiter('<br>');
480
            }
481
482
            $res .= $tr;
483
        }
484
485
        $res .= <<<MD
486
487
MD;
488
489
        return $res;
490
    }
491
492
    private function makeTypeDef(Schema $schema, $path)
493
    {
494
        $tn = $this->typeName($schema, $path, true);
495
        $typeName = $this->typeName($schema, $path);
496
        $this->processed->attach($schema, $typeName);
497
498
        $res = $this->renderTypeDef($schema, $tn, $path);
499
500
        if (isset($this->uniqueTypeSchemas[$res])) {
501
            return $this->uniqueTypeSchemas[$res];
502
        }
503
504
        $this->types[$typeName] = $res;
505
        $this->uniqueTypeSchemas[$res] = $typeName;
506
        $this->file .= $res;
507
508
        return $typeName;
509
    }
510
511
    public function sortTypes()
512
    {
513
        ksort($this->types);
514
    }
515
516
    public function tableOfContents()
517
    {
518
        if (count($this->types) === 0) {
519
            return '';
520
        }
521
522
        $res = '# Types' . "\n\n";
523
524
        foreach ($this->types as $name => $doc) {
525
            $res .= '  * ' . $name . "\n";
526
        }
527
528
        $res .= "\n\n";
529
530
        return $res;
531
    }
532
533
    private function trim($s)
534
    {
535
        if (empty($s)) {
536
            return '';
537
        }
538
539
        return trim($s);
540
    }
541
542
    private function description(Schema $schema)
543
    {
544
        $res = str_replace("\n", " ", $this->trim($schema->title));
545
        if (!is_string($res)) {
0 ignored issues
show
introduced by
The condition is_string($res) is always true.
Loading history...
546
            return '';
547
        }
548
549
        if ($this->trim($schema->description)) {
550
            if ($res) {
551
                $res .= ". ";
552
            }
553
554
            $res .= str_replace("\n", " ", $this->trim($schema->description));
555
        }
556
        if ($res) {
557
            return rtrim($res, '.') . '.';
558
        }
559
560
        return '';
561
    }
562
}