SchemaBuilder   F
last analyzed

Complexity

Total Complexity 101

Size/Duplication

Total Lines 549
Duplicated Lines 0 %

Test Coverage

Coverage 93.05%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 318
c 3
b 0
f 0
dl 0
loc 549
ccs 241
cts 259
cp 0.9305
rs 2
wmc 101

16 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 10 2
B processObject() 0 50 9
A processNamedClass() 0 21 4
D processType() 0 65 20
B processArray() 0 30 6
A processRef() 0 22 5
B processEnum() 0 38 9
A skipProperty() 0 4 1
A copyTo() 0 5 1
A setSaveEnumConstInClass() 0 4 1
A setSkipProperties() 0 4 1
A build() 0 21 3
C processOther() 0 80 17
A pathFromDescription() 0 10 6
B processLogic() 0 53 11
A processFromRef() 0 14 5

How to fix   Complexity   

Complex Class

Complex classes like SchemaBuilder often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use SchemaBuilder, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace Swaggest\PhpCodeBuilder\JsonSchema;
4
5
6
use Swaggest\CodeBuilder\PlaceholderString;
7
use Swaggest\JsonSchema\Constraint\Type;
8
use Swaggest\JsonSchema\Schema;
9
use Swaggest\JsonSchema\SchemaContract;
10
use Swaggest\JsonSchema\Structure\ClassStructure;
11
use Swaggest\JsonSchema\Structure\ObjectItem;
12
use Swaggest\PhpCodeBuilder\PhpClass;
13
use Swaggest\PhpCodeBuilder\PhpCode;
14
use Swaggest\PhpCodeBuilder\PhpConstant;
15
use Swaggest\PhpCodeBuilder\Types\ReferenceTypeOf;
16
use Swaggest\PhpCodeBuilder\Types\TypeOf;
17
18
class SchemaBuilder
19
{
20
    /** @var Schema */
21
    private $schema;
22
    /** @var string */
23
    private $varName;
24
    /** @var bool */
25
    private $createVarName;
26
27
    /** @var PhpBuilder */
28
    private $phpBuilder;
29
    /** @var string */
30
    private $path;
31
32
    /** @var PhpCode */
33
    private $result;
34
35
    /** @var bool */
36
    private $skipProperties;
37
38
    /** @var PhpClass */
39
    private $saveEnumConstInClass;
40
41
    /**
42
     * SchemaBuilder constructor.
43
     * @param Schema|SchemaContract $schema
44
     * @param string $varName
45
     * @param string $path
46
     * @param PhpBuilder $phpBuilder
47
     * @param bool $createVarName
48 11
     * @throws \Exception
49
     */
50 11
    public function __construct($schema, $varName, $path, PhpBuilder $phpBuilder, $createVarName = true)
51 11
    {
52 11
        if (!$schema instanceof Schema) {
53 11
            throw new Exception('Could not find Schema instance in SchemaContract: ' . get_class($schema));
54 11
        }
55 11
        $this->schema = $schema;
56
        $this->varName = $varName;
57 11
        $this->phpBuilder = $phpBuilder;
58
        $this->path = $path;
59 11
        $this->createVarName = $createVarName;
60 7
    }
61
62 4
    private function processType()
63 3
    {
64 4
        if ($this->schema->type !== null) {
65 4
            switch ($this->schema->type) {
66
                case Type::INTEGER:
67
                    $result = $this->createVarName
68 5
                        ? "{$this->varName} = ::schema::integer();"
69 4
                        : "{$this->varName}->type = ::schema::INTEGER;";
70 5
                    break;
71 5
72
                case Type::NUMBER:
73
                    $result = $this->createVarName
74 5
                        ? "{$this->varName} = ::schema::number();"
75 5
                        : "{$this->varName}->type = ::schema::NUMBER;";
76 5
                    break;
77 5
78
                case Type::BOOLEAN:
79
                    $result = $this->createVarName
80 6
                        ? "{$this->varName} = ::schema::boolean();"
81 6
                        : "{$this->varName}->type = ::schema::BOOLEAN;";
82 6
                    break;
83 6
84
                case Type::STRING:
85
                    $result = $this->createVarName
86 3
                        ? "{$this->varName} = ::schema::string();"
87 3
                        : "{$this->varName}->type = ::schema::STRING;";
88 3
                    break;
89 3
90
                case Type::ARR:
91
                    $result = $this->createVarName
92 7
                        ? "{$this->varName} = ::schema::arr();"
93
                        : "{$this->varName}->type = ::schema::_ARRAY;";
94
                    break;
95 1
96 1
                case Type::OBJECT:
97 1
                    return;
98 1
99
                case Type::NULL:
100
                    $result = $this->createVarName
101
                        ? "{$this->varName} = ::schema::null();"
102
                        : "{$this->varName}->type = ::schema::NULL;";
103
                    break;
104 7
105
                default:
106
                    if (!is_array($this->schema->type)) {
107 8
                        throw new Exception('Unexpected type:' . $this->schema->type);
108 8
                    }
109
                    $types = [];
110
                    foreach ($this->schema->type as $type) {
111
                        $types[] = '::schema::' . PhpCode::makePhpConstantName($type);
112 11
                    }
113 11
                    $types = '[' . implode(', ', $types) . ']';
114 11
                    $result = $this->createVarName
115
                        ? "{$this->varName} = (new ::schema())->setType($types);"
116
                        : "{$this->varName}->type = $types;";
117 11
            }
118
        } else {
119 11
            if ($this->createVarName) {
120
                $result = "{$this->varName} = new ::schema();";
121 11
            }
122
        }
123 11
124
        if (isset($result)) {
125 8
            $this->result->addSnippet(
126 8
                new PlaceholderString($result . "\n", array('::schema' => new ReferenceTypeOf(Palette::schemaClass())))
0 ignored issues
show
Deprecated Code introduced by
The class Swaggest\PhpCodeBuilder\Types\ReferenceTypeOf has been deprecated: redundant by TypeOf ( Ignorable by Annotation )

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

126
                new PlaceholderString($result . "\n", array('::schema' => /** @scrutinizer ignore-deprecated */ new ReferenceTypeOf(Palette::schemaClass())))
Loading history...
127
            );
128
        }
129
    }
130
131
    private function processNamedClass()
132 8
    {
133 8
        if (!$this->skipProperties
134 8
            //&& $this->schema->type === Type::OBJECT
135
            && $this->schema->properties !== null
136
        ) {
137 8
            $class = $this->phpBuilder->getClass($this->schema, $this->path);
138
            if ($this->schema->id === 'http://json-schema.org/draft-04/schema#') {
139 11
                $this->result->addSnippet(
140
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
141
                        array('::class' => new TypeOf(Palette::schemaClass())))
142 11
                );
143
            } else {
144 11
                $this->result->addSnippet(
145
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
146 11
                        array('::class' => new TypeOf($class)))
147 11
                );
148
            }
149 1
            return true;
150 1
        }
151
        return false;
152
    }
153
154
    private function processRef()
155
    {
156 1
        if (!$this->skipProperties
157 1
            //&& $this->schema->type === Type::OBJECT
158 1
            && !$this->phpBuilder->minimizeRefs
159
            && $this->schema->getFromRefs()
160
        ) {
161 1
            $class = $this->phpBuilder->getClass($this->schema, $this->path);
162
            if ($this->schema->id === 'http://json-schema.org/draft-04/schema#') {
163 11
                $this->result->addSnippet(
164
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
165
                        array('::class' => new TypeOf(Palette::schemaClass())))
166
                );
167
            } else {
168
                $this->result->addSnippet(
169
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
170 11
                        array('::class' => new TypeOf($class)))
171
                );
172 11
            }
173 7
            return true;
174 3
        }
175 3
        return false;
176 3
    }
177
178
179 7
    /**
180 7
     * @throws Exception
181
     */
182
    private function processObject()
183
    {
184
        if ($this->schema->type === Type::OBJECT) {
185
            if (!$this->skipProperties) {
186
                $this->result->addSnippet(
187 11
                    new PlaceholderString("{$this->varName} = ::schema::object();\n",
188 4
                        array('::schema' => new TypeOf(Palette::schemaClass())))
189 3
                );
190 3
            } else {
191 3
                $this->result->addSnippet(
192 3
                    new PlaceholderString("{$this->varName}->type = ::schema::OBJECT;\n",
193 3
                        array('::schema' => new TypeOf(Palette::schemaClass())))
194 3
                );
195 3
            }
196
197
        }
198 3
199 3
200 3
        if ($this->schema->additionalProperties !== null) {
201
            if ($this->schema->additionalProperties instanceof Schema) {
202
                $this->result->addSnippet(
203
                    $this->copyTo(new SchemaBuilder(
204
                        $this->schema->additionalProperties,
205 11
                        "{$this->varName}->additionalProperties",
206 4
                        $this->path . '->additionalProperties',
207 4
                        $this->phpBuilder
208 4
                    ))->build()
209 4
                );
210 4
            } else {
211 4
                $val = $this->schema->additionalProperties ? 'true' : 'false';
212 4
                $this->result->addSnippet(
213 4
                    "{$this->varName}->additionalProperties = $val;\n"
214 4
                );
215
            }
216 4
        }
217
218
        if ($this->schema->patternProperties !== null) {
219
            foreach ($this->schema->patternProperties as $pattern => $property) {
220 11
                if ($property instanceof Schema) {
221
                    $varName = '$' . PhpCode::makePhpName($pattern);
222
                    $patternExp = var_export($pattern, true);
223
                    $this->result->addSnippet(
224
                        $this->copyTo(new SchemaBuilder(
225 11
                            $property,
226
                            $varName,
227 11
                            $this->path . "->patternProperties->{{$pattern}}",
228
                            $this->phpBuilder
229 11
                        ))->build()
230 2
                    );
231 2
                    $this->result->addSnippet("{$this->varName}->setPatternProperty({$patternExp}, $varName);\n");
232 2
                }
233
            }
234
        }
235
    }
236 11
237 11
    /**
238 2
     * @throws Exception
239 2
     */
240 2
    private function processArray()
241 11
    {
242 11
        $schema = $this->schema;
243 11
244
        if (is_bool($schema->additionalItems)) {
245
            $val = $schema->additionalItems ? 'true' : 'false';
246
            $this->result->addSnippet(
247
                "{$this->varName}->additionalItems = $val;\n"
248
            );
249
        }
250 11
251 11
        $pathItems = 'items';
252 2
        if ($schema->items instanceof ClassStructure) { // todo better check for schema, `getJsonSchema` interface ?
253 2
            $additionalItems = $schema->items;
0 ignored issues
show
Documentation Bug introduced by
It seems like $schema->items can also be of type Swaggest\JsonSchema\Structure\ClassStructure. However, the property $items is declared as type Swaggest\JsonSchema\Sche...a\SchemaContract[]|null. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
254 2
            $pathItems = 'items';
255 2
        } elseif ($schema->items === null) { // items defaults to empty schema so everything is valid
256 2
            $additionalItems = true;
257 2
        } else { // listed items
258 2
            $additionalItems = $schema->additionalItems;
259
            $pathItems = 'additionalItems';
260
        }
261
262 11
        if ($additionalItems instanceof ClassStructure) {
263
            $this->result->addSnippet(
264 11
                $this->copyTo(new SchemaBuilder(
265
                    $additionalItems,
266 11
                    "{$this->varName}->{$pathItems}",
267 7
                    $this->path . '->' . $pathItems,
268 7
                    $this->phpBuilder
269
                ))->build()
270 7
            );
271 7
        }
272
    }
273
274 7
    private function processEnum()
275
    {
276 7
        if (!empty($this->schema->enum)) {
277 7
            $this->result->addSnippet(
278 3
                "{$this->varName}->enum = array(\n"
279 3
            );
280 3
            foreach ($this->schema->enum as $i => $enumItem) {
281
                if (isset($this->schema->{Schema::ENUM_NAMES_PROPERTY}[$i])) {
282
                    $name = PhpCode::makePhpConstantName($this->schema->{Schema::ENUM_NAMES_PROPERTY}[$i]);
283 6
                } else {
284 6
                    $name = PhpCode::makePhpConstantName($enumItem);
285
                }
286
                $value = var_export($enumItem, true);
287
                if ($this->saveEnumConstInClass !== null && is_scalar($enumItem) && !is_bool($enumItem)) {
288
                    $checkName = $name;
289 7
                    $i = 1;
290 7
                    do {
291
                        try {
292
                            $this->saveEnumConstInClass->addConstant(new PhpConstant($checkName, $enumItem));
293
                            $name = $checkName;
294 11
                            break;
295
                        } catch (\Swaggest\PhpCodeBuilder\Exception $exception) {
296
                            $i++;
297
                            $checkName = $name . $i;
298
                        }
299
                    } while(true);
300
                    $this->result->addSnippet(
301
                        "    self::$name,\n"
302
                    );
303
                } else {
304 9
                    $this->result->addSnippet(
305
                        "    $value,\n"
306 9
                    );
307 9
                }
308 9
309
            }
310
            $this->result->addSnippet(
311 11
                ");\n"
312
            );
313 11
314 11
        }
315 1
    }
316
317 1
    private $skip = [];
318 1
319 1
    public function skipProperty($name)
320 1
    {
321 1
        $this->skip[$name] = 1;
322 1
        return $this;
323 1
    }
324 1
325 1
    private function copyTo(SchemaBuilder $schemaBuilder)
326 1
    {
327 1
        $schemaBuilder->skip = $this->skip;
328 1
        $schemaBuilder->saveEnumConstInClass = $this->saveEnumConstInClass;
329 1
        return $schemaBuilder;
330 1
    }
331 1
332 1
    /**
333 1
     * @throws \Swaggest\JsonSchema\InvalidValue
334 1
     */
335
    private function processOther()
336
    {
337 11
        static $skip = null, $emptySchema = null, $names = null;
338 11
        if ($skip === null) {
339 11
            $emptySchema = new Schema();
340 11
            $names = Schema::names();
341
            $skip = array(
342 7
                $names->type => 1,
343
                Schema::PROP_REF => 1,
344
                $names->items => 1,
345
                $names->additionalItems => 1,
346
                $names->properties => 1,
347
                $names->additionalProperties => 1,
348 7
                $names->patternProperties => 1,
349
                $names->allOf => 1, // @todo process
350
                $names->anyOf => 1,
351 7
                $names->oneOf => 1,
352 2
                $names->not => 1,
353 7
                $names->definitions => 1,
354 2
                $names->enum => 1,
355
                $names->if => 1,
356 7
                $names->then => 1,
357
                $names->else => 1,
358
            );
359 7
        }
360 7
        $schemaData = Schema::export($this->schema);
361 7
        foreach ((array)$schemaData as $key => $value) {
362
            if (isset($skip[$key])) {
363
                continue;
364 11
            }
365
            if (isset($this->skip[$key])) {
366 11
                continue;
367
            }
368 11
369 11
            if (!property_exists($emptySchema, $key) && $key !== $names->const && $key !== $names->default
370 11
                && $key[0] !== '$') {
371 4
                continue;
372 4
            }
373 4
374 4
            if ($names->required == $key && is_array($value)) {
375 4
                $export = "array(\n";
376 4
                foreach ($value as $item) {
377 4
                    if (PhpCode::makePhpName($item) === $item) {
378
                        $expItem = 'self::names()->' . $item;
379
                    } else {
380
                        $expItem = PhpCode::varExport($item);
381
                    }
382
                    $export .= '    ' . $expItem . ",\n";
383 11
                }
384 11
                $export .= ")";
385 6
                $this->result->addSnippet(
386 6
                    "{$this->varName}->{$key} = " . $export . ";\n"
387 6
                );
388 6
                continue;
389 6
            }
390 6
391 6
            //$this->result->addSnippet('/* ' . print_r($value, 1) . '*/' . "\n");
392 6
            //echo "{$this->varName}->{$key}\n";
393
            if ($value instanceof ObjectItem) {
394 6
                //$value = $value->jsonSerialize();
395 6
                $export = 'new \stdClass()';
396 6
            } elseif ($value instanceof \stdClass) {
397 6
                $export = '(object)' . PhpCode::varExport((array)$value);
398 6
            } elseif (is_string($value)) {
399 6
                switch ($key) {
400 6
                    case 'pattern':
401
                        $export = '"' . str_replace(array('\\', "\n", "\r", "\t", '"', '${', '{$'), array('\\\\', '\n', '\r', '\t', '\"', '\${', '{\$'), $value) . '"';
402 2
                        break;
403 2
                    default:
404 2
                        $export = '"' . str_replace(array('\\', "\n", "\r", "\t", '"', '$'), array('\\\\', '\n', '\r', '\t', '\"', '\$'), $value) . '"';
405
                        break;
406
                }
407
                
408
            } else {
409
                $export = PhpCode::varExport($value);
410
            }
411
412 11
            $key = PhpCode::makePhpName($key);
413
            $this->result->addSnippet(
414
                "{$this->varName}->{$key} = " . $export . ";\n"
415
            );
416
        }
417
    }
418 11
419
    /**
420 11
     * @throws Exception
421
     * @throws \Exception
422 11
     */
423 8
    private function processLogic()
424
    {
425
        $names = Schema::names();
426 11
        /** @var string $keyword */
427 1
        foreach (array($names->not, $names->if, $names->then, $names->else) as $keyword) {
428
            if ($this->schema->$keyword !== null) {
429
                $schema = $this->schema->$keyword;
430 11
                $path = $this->path . '->' . $keyword;
0 ignored issues
show
Bug introduced by
Are you sure $keyword of type Swaggest\JsonSchema\SchemaContract|string can be used in concatenation? ( Ignorable by Annotation )

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

430
                $path = $this->path . '->' . /** @scrutinizer ignore-type */ $keyword;
Loading history...
431 11
                if ($schema instanceof Schema) {
432 11
                    $path = $this->pathFromDescription($path, $schema);
433 11
                    if (!empty($schema->getFromRefs())) {
434 11
                        $path = $schema->getFromRefs()[0];
435 11
                    }
436 11
                }
437
                $this->result->addSnippet(
438 11
                    $this->copyTo(new SchemaBuilder(
439
                        $schema,
440
                        "{$this->varName}->{$keyword}",
441
                        $path,
442 11
                        $this->phpBuilder
443
                    ))->build()
444 11
                );
445 10
            }
446 3
447 3
        }
448 3
449
        foreach (array($names->anyOf, $names->oneOf, $names->allOf) as $keyword) {
450 10
            if ($this->schema->$keyword !== null) {
451
                foreach ($this->schema->$keyword as $index => $schema) {
452 1
                    $path = $this->path . "->{$keyword}[{$index}]";
453 1
                    $path = $this->pathFromDescription($path, $schema);
454 1
                    if ($schema instanceof Schema && !empty($schema->getFromRefs())) {
455 1
                        $path = $schema->getFromRefs()[0];
456
                    }
457
                    $varName = '$' . PhpCode::makePhpName("{$this->varName}->{$keyword}[{$index}]");
458 1
                    $schemaInit = $this->copyTo(new SchemaBuilder(
459
                        $schema,
460
                        $varName,
461
                        $path,
462
                        $this->phpBuilder
463
                    ))->build();
464 11
465
                    if (count($schemaInit->snippets) === 1) { // Init in single statement can be just assigned.
466 11
                        $this->result->addSnippet($this->copyTo(new SchemaBuilder(
467 11
                            $schema,
468
                            "{$this->varName}->{$keyword}[{$index}]",
469
                            $this->path . "->{$keyword}[{$index}]",
470
                            $this->phpBuilder
471
                        ))->build());
472
                    } else {
473
                        $this->result->addSnippet($schemaInit);
474 4
                        $this->result->addSnippet(<<<PHP
475
{$this->varName}->{$keyword}[{$index}] = {$varName};
476 4
477 4
PHP
478
                        );
479
                    }
480
                }
481
            }
482
        }
483
    }
484
485
    /**
486
     * @param string $path
487
     * @param Schema $schema
488
     * @return string
489
     */
490
    private function pathFromDescription($path, $schema)
491
    {
492
        if ($this->phpBuilder->namesFromDescriptions) {
493
            if ($schema->title && strlen($schema->title) < 30) {
494
                $path = $this->path . "->{$schema->title}";
495
            } elseif ($schema->description && strlen($schema->description) < 30) {
496
                $path = $this->path . "->{$schema->description}";
497
            }
498
        }
499
        return $path;
500
    }
501
502
    /**
503
     * @return PhpCode
504
     * @throws Exception
505
     * @throws \Swaggest\JsonSchema\InvalidValue
506
     */
507
    public function build()
508
    {
509
        $this->result = new PhpCode();
510
511
        if ($this->processNamedClass()) {
512
            return $this->result;
513
        }
514
515
        if ($this->processRef()) {
516
            return $this->result;
517
        }
518
519
        $this->processType();
520
        $this->processObject();
521
        $this->processArray();
522
        $this->processLogic();
523
        $this->processEnum();
524
        $this->processOther();
525
        $this->processFromRef();
526
527
        return $this->result;
528
529
    }
530
531
    private function processFromRef()
532
    {
533
        if ($this->phpBuilder->minimizeRefs) {
534
            if ($fromRefs = $this->schema->getFromRefs()) {
535
                $fromRef = $fromRefs[count($fromRefs) - 1];
536
                $value = var_export($fromRef, true);
537
                $this->result->addSnippet("{$this->varName}->setFromRef($value);\n");
538
            }
539
            return;
540
        }
541
        if ($fromRefs = $this->schema->getFromRefs()) {
542
            foreach ($fromRefs as $fromRef) {
543
                $value = var_export($fromRef, true);
544
                $this->result->addSnippet("{$this->varName}->setFromRef($value);\n");
545
            }
546
        }
547
    }
548
549
    /**
550
     * @param boolean $skipProperties
551
     * @return $this
552
     */
553
    public function setSkipProperties($skipProperties)
554
    {
555
        $this->skipProperties = $skipProperties;
556
        return $this;
557
    }
558
559
    /**
560
     * @param PhpClass $saveEnumConstInClass
561
     * @return $this
562
     */
563
    public function setSaveEnumConstInClass($saveEnumConstInClass)
564
    {
565
        $this->saveEnumConstInClass = $saveEnumConstInClass;
566
        return $this;
567
    }
568
569
570
}