Completed
Pull Request — master (#10)
by Viacheslav
07:30
created

SchemaBuilder   F

Complexity

Total Complexity 78

Size/Duplication

Total Lines 461
Duplicated Lines 0 %

Test Coverage

Coverage 93.05%

Importance

Changes 0
Metric Value
eloc 266
dl 0
loc 461
ccs 241
cts 259
cp 0.9305
rs 2.16
c 0
b 0
f 0
wmc 78

15 Methods

Rating   Name   Duplication   Size   Complexity  
B processObject() 0 47 8
A processNamedClass() 0 21 4
B processEnum() 0 27 7
A skipProperty() 0 4 1
D processType() 0 58 18
A __construct() 0 7 1
B processArray() 0 34 8
A copyTo() 0 5 1
A processRef() 0 22 5
B processOther() 0 51 8
A setSaveEnumConstInClass() 0 4 1
A setSkipProperties() 0 4 1
A build() 0 21 3
B processLogic() 0 39 7
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\Structure\ClassStructure;
10
use Swaggest\JsonSchema\Structure\ObjectItem;
11
use Swaggest\PhpCodeBuilder\PhpClass;
12
use Swaggest\PhpCodeBuilder\PhpCode;
13
use Swaggest\PhpCodeBuilder\PhpConstant;
14
use Swaggest\PhpCodeBuilder\Types\ReferenceTypeOf;
15
use Swaggest\PhpCodeBuilder\Types\TypeOf;
16
17
class SchemaBuilder
18
{
19
    /** @var Schema */
20
    private $schema;
21
    /** @var string */
22
    private $varName;
23
    /** @var bool */
24
    private $createVarName;
25
26
    /** @var PhpBuilder */
27
    private $phpBuilder;
28
    /** @var string */
29
    private $path;
30
31
    /** @var PhpCode */
32
    private $result;
33
34
    /** @var bool */
35
    private $skipProperties;
36
37
    /** @var PhpClass */
38
    private $saveEnumConstInClass;
39
40
    /**
41
     * SchemaBuilder constructor.
42
     * @param Schema $schema
43
     * @param string $varName
44
     * @param string $path
45
     * @param PhpBuilder $phpBuilder
46
     * @param bool $createVarName
47
     */
48 11
    public function __construct($schema, $varName, $path, PhpBuilder $phpBuilder, $createVarName = true)
49
    {
50 11
        $this->schema = $schema;
51 11
        $this->varName = $varName;
52 11
        $this->phpBuilder = $phpBuilder;
53 11
        $this->path = $path;
54 11
        $this->createVarName = $createVarName;
55 11
    }
56
57 11
    private function processType()
58
    {
59 11
        if ($this->schema->type !== null) {
60 7
            switch ($this->schema->type) {
61
                case Type::INTEGER:
62 4
                    $result = $this->createVarName
63 3
                        ? "{$this->varName} = ::schema::integer();"
64 4
                        : "{$this->varName}->type = ::schema::INTEGER;";
65 4
                    break;
66
67
                case Type::NUMBER:
68 5
                    $result = $this->createVarName
69 4
                        ? "{$this->varName} = ::schema::number();"
70 5
                        : "{$this->varName}->type = ::schema::NUMBER;";
71 5
                    break;
72
73
                case Type::BOOLEAN:
74 5
                    $result = $this->createVarName
75 5
                        ? "{$this->varName} = ::schema::boolean();"
76 5
                        : "{$this->varName}->type = ::schema::BOOLEAN;";
77 5
                    break;
78
79
                case Type::STRING:
80 6
                    $result = $this->createVarName
81 6
                        ? "{$this->varName} = ::schema::string();"
82 6
                        : "{$this->varName}->type = ::schema::STRING;";
83 6
                    break;
84
85
                case Type::ARR:
86 3
                    $result = $this->createVarName
87 3
                        ? "{$this->varName} = ::schema::arr();"
88 3
                        : "{$this->varName}->type = ::schema::_ARRAY;";
89 3
                    break;
90
91
                case Type::OBJECT:
92 7
                    return;
93
94
                case Type::NULL:
95 1
                    $result = $this->createVarName
96 1
                        ? "{$this->varName} = ::schema::null();"
97 1
                        : "{$this->varName}->type = ::schema::NULL;";
98 1
                    break;
99
100
                default:
101
                    $types = PhpCode::varExport($this->schema->type);
102
                    $result = $this->createVarName
103
                        ? "{$this->varName} = (new ::schema())->setType($types);"
104 7
                        : "{$this->varName}->type = $types;";
105
            }
106
        } else {
107 8
            if ($this->createVarName) {
108 8
                $result = "{$this->varName} = new ::schema();";
109
            }
110
        }
111
112 11
        if (isset($result)) {
113 11
            $this->result->addSnippet(
114 11
                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

114
                new PlaceholderString($result . "\n", array('::schema' => /** @scrutinizer ignore-deprecated */ new ReferenceTypeOf(Palette::schemaClass())))
Loading history...
115
            );
116
        }
117 11
    }
118
119 11
    private function processNamedClass()
120
    {
121 11
        if (!$this->skipProperties
122
            //&& $this->schema->type === Type::OBJECT
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
123 11
            && $this->schema->properties !== null
124
        ) {
125 8
            $class = $this->phpBuilder->getClass($this->schema, $this->path);
126 8
            if ($this->schema->id === 'http://json-schema.org/draft-04/schema#') {
127
                $this->result->addSnippet(
128
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
129
                        array('::class' => new TypeOf(Palette::schemaClass())))
130
                );
131
            } else {
132 8
                $this->result->addSnippet(
133 8
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
134 8
                        array('::class' => new TypeOf($class)))
135
                );
136
            }
137 8
            return true;
138
        }
139 11
        return false;
140
    }
141
142 11
    private function processRef()
143
    {
144 11
        if (!$this->skipProperties
145
            //&& $this->schema->type === Type::OBJECT
0 ignored issues
show
Unused Code Comprehensibility introduced by
39% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
146 11
            && !$this->phpBuilder->minimizeRefs
147 11
            && $this->schema->getFromRefs()
148
        ) {
149 1
            $class = $this->phpBuilder->getClass($this->schema, $this->path);
150 1
            if ($this->schema->id === 'http://json-schema.org/draft-04/schema#') {
151
                $this->result->addSnippet(
152
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
153
                        array('::class' => new TypeOf(Palette::schemaClass())))
154
                );
155
            } else {
156 1
                $this->result->addSnippet(
157 1
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
158 1
                        array('::class' => new TypeOf($class)))
159
                );
160
            }
161 1
            return true;
162
        }
163 11
        return false;
164
    }
165
166
167
    /**
168
     * @throws Exception
169
     */
170 11
    private function processObject()
171
    {
172 11
        if ($this->schema->type === Type::OBJECT) {
173 7
            if (!$this->skipProperties) {
174 3
                $this->result->addSnippet(
175 3
                    new PlaceholderString("{$this->varName} = ::schema::object();\n",
176 3
                        array('::schema' => new TypeOf(Palette::schemaClass())))
177
                );
178
            } else {
179 7
                $this->result->addSnippet(
180 7
                    "{$this->varName}->type = 'object';\n"
181
                );
182
            }
183
184
        }
185
186
187 11
        if ($this->schema->additionalProperties !== null) {
188 4
            if ($this->schema->additionalProperties instanceof Schema) {
189 3
                $this->result->addSnippet(
190 3
                    $this->copyTo(new SchemaBuilder(
191 3
                        $this->schema->additionalProperties,
192 3
                        "{$this->varName}->additionalProperties",
193 3
                        $this->path . '->additionalProperties',
194 3
                        $this->phpBuilder
195 3
                    ))->build()
196
                );
197
            } else {
198 3
                $val = $this->schema->additionalProperties ? 'true' : 'false';
199 3
                $this->result->addSnippet(
200 3
                    "{$this->varName}->additionalProperties = $val;\n"
201
                );
202
            }
203
        }
204
205 11
        if ($this->schema->patternProperties !== null) {
206 4
            foreach ($this->schema->patternProperties as $pattern => $property) {
207 4
                $patternExp = var_export($pattern, true);
208 4
                $this->result->addSnippet(
209 4
                    $this->copyTo(new SchemaBuilder(
210 4
                        $property,
211 4
                        "\$patternProperty",
212 4
                        $this->path . '->patternProperties->{{$pattern}}',
213 4
                        $this->phpBuilder
214 4
                    ))->build()
215
                );
216 4
                $this->result->addSnippet("{$this->varName}->setPatternProperty({$patternExp}, \$patternProperty);\n");
217
218
            }
219
        }
220 11
    }
221
222
    /**
223
     * @throws Exception
224
     */
225 11
    private function processArray()
226
    {
227 11
        $schema = $this->schema;
228
229 11
        if (is_bool($schema->additionalItems)) {
230 2
            $val = $schema->additionalItems ? 'true' : 'false';
231 2
            $this->result->addSnippet(
232 2
                "{$this->varName}->additionalItems = $val;\n"
233
            );
234
        }
235
236 11
        $pathItems = 'items';
237 11
        if ($schema->items instanceof ClassStructure) { // todo better check for schema, `getJsonSchema` interface ?
238 2
            $items = array();
239 2
            $additionalItems = $schema->items;
240 2
            $pathItems = 'items';
241 11
        } elseif ($schema->items === null) { // items defaults to empty schema so everything is valid
242 11
            $items = array();
243 11
            $additionalItems = true;
244
        } else { // listed items
245
            $items = $schema->items;
246
            $additionalItems = $schema->additionalItems;
247
            $pathItems = 'additionalItems';
248
        }
249
250 11
        if ($items !== null || $additionalItems !== null) {
0 ignored issues
show
introduced by
The condition $items !== null is always true.
Loading history...
251 11
            if ($additionalItems instanceof ClassStructure) {
252 2
                $this->result->addSnippet(
253 2
                    $this->copyTo(new SchemaBuilder(
254 2
                        $additionalItems,
255 2
                        "{$this->varName}->{$pathItems}",
256 2
                        $this->path . '->' . $pathItems,
257 2
                        $this->phpBuilder
258 2
                    ))->build()
259
                );
260
            }
261
        }
262 11
    }
263
264 11
    private function processEnum()
265
    {
266 11
        if (!empty($this->schema->enum)) {
267 7
            $this->result->addSnippet(
268 7
                "{$this->varName}->enum = array(\n"
269
            );
270 7
            foreach ($this->schema->enum as $i => $enumItem) {
271 7
                if (isset($this->schema->{Schema::ENUM_NAMES_PROPERTY}[$i])) {
272
                    $name = PhpCode::makePhpConstantName($this->schema->{Schema::ENUM_NAMES_PROPERTY}[$i]);
273
                } else {
274 7
                    $name = PhpCode::makePhpConstantName($enumItem);
275
                }
276 7
                $value = var_export($enumItem, true);
277 7
                if ($this->saveEnumConstInClass !== null && is_scalar($enumItem) && !is_bool($enumItem)) {
278 3
                    $this->saveEnumConstInClass->addConstant(new PhpConstant($name, $enumItem));
279 3
                    $this->result->addSnippet(
280 3
                        "    self::$name,\n"
281
                    );
282
                } else {
283 6
                    $this->result->addSnippet(
284 6
                        "    $value,\n"
285
                    );
286
                }
287
288
            }
289 7
            $this->result->addSnippet(
290 7
                ");\n"
291
            );
292
293
        }
294 11
    }
295
296
    private $skip = [];
297
298
    public function skipProperty($name)
299
    {
300
        $this->skip[$name] = 1;
301
        return $this;
302
    }
303
304 9
    private function copyTo(SchemaBuilder $schemaBuilder)
305
    {
306 9
        $schemaBuilder->skip = $this->skip;
307 9
        $schemaBuilder->saveEnumConstInClass = $this->saveEnumConstInClass;
308 9
        return $schemaBuilder;
309
    }
310
311 11
    private function processOther()
312
    {
313 11
        static $skip = null;
314 11
        if ($skip === null) {
315 1
            $names = Schema::names();
316
            $skip = array(
317 1
                (string)$names->type => 1,
318 1
                '$ref' => 1,
319 1
                (string)$names->items => 1,
320 1
                (string)$names->additionalItems => 1,
321 1
                (string)$names->properties => 1,
322 1
                (string)$names->additionalProperties => 1,
323 1
                (string)$names->patternProperties => 1,
324 1
                (string)$names->allOf => 1, // @todo process
325 1
                (string)$names->anyOf => 1,
326 1
                (string)$names->oneOf => 1,
327 1
                (string)$names->not => 1,
328 1
                (string)$names->definitions => 1,
329 1
                (string)$names->enum => 1,
330 1
                (string)$names->if => 1,
331 1
                (string)$names->then => 1,
332 1
                (string)$names->else => 1,
333 1
                (string)$names->fromRef => 1,
334 1
                (string)$names->originPath => 1,
335
            );
336
        }
337 11
        $schemaData = Schema::export($this->schema);
338 11
        foreach ((array)$schemaData as $key => $value) {
339 11
            if (isset($skip[$key])) {
340 11
                continue;
341
            }
342 7
            if (isset($this->skip[$key])) {
343
                continue;
344
            }
345
346
            //$this->result->addSnippet('/* ' . print_r($value, 1) . '*/' . "\n");
0 ignored issues
show
Unused Code Comprehensibility introduced by
52% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
347
            //echo "{$this->varName}->{$key}\n";
348 7
            if ($value instanceof ObjectItem) {
349
                //$value = $value->jsonSerialize();
0 ignored issues
show
Unused Code Comprehensibility introduced by
60% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
350
                $export = 'new \stdClass()';
351 7
            } elseif ($value instanceof \stdClass) {
352 2
                $export = '(object)' . PhpCode::varExport((array)$value);
353 7
            } elseif (is_string($value)) {
354 2
                $export = '"' . str_replace(array('\\', "\n", "\r", "\t", '"'), array('\\\\', '\n', '\r', '\t', '\"'), $value) . '"';
355
            } else {
356 7
                $export = PhpCode::varExport($value);
357
            }
358
359 7
            $key = PhpCode::makePhpName($key);
360 7
            $this->result->addSnippet(
361 7
                "{$this->varName}->{$key} = " . $export . ";\n"
362
            );
363
        }
364 11
    }
365
366 11
    private function processLogic()
367
    {
368 11
        $names = Schema::names();
369 11
        foreach (array($names->not, $names->if, $names->then, $names->else) as $keyword) {
370 11
            if ($this->schema->$keyword !== null) {
371 4
                $this->result->addSnippet(
372 4
                    $this->copyTo(new SchemaBuilder(
373 4
                        $this->schema->$keyword,
374 4
                        "{$this->varName}->{$keyword}",
375 4
                        $this->path . '->' . $keyword,
0 ignored issues
show
Bug introduced by
Are you sure $keyword of type Swaggest\JsonSchema\Schema|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

375
                        $this->path . '->' . /** @scrutinizer ignore-type */ $keyword,
Loading history...
376 4
                        $this->phpBuilder
377 4
                    ))->build()
378
                );
379
            }
380
381
        }
382
383 11
        foreach (array($names->anyOf, $names->oneOf, $names->allOf) as $keyword) {
384 11
            if ($this->schema->$keyword !== null) {
385 6
                foreach ($this->schema->$keyword as $index => $schema) {
386 6
                    $varName = '$' . PhpCode::makePhpName("{$this->varName}->{$keyword}[{$index}]");
387 6
                    $schemaInit = $this->copyTo(new SchemaBuilder(
388 6
                        $schema,
389 6
                        $varName,
390 6
                        $this->path . "->{$keyword}[{$index}]",
391 6
                        $this->phpBuilder
392 6
                    ))->build();
393
394 6
                    if (count($schemaInit->snippets) === 1) { // Init in single statement can be just assigned.
395 6
                        $this->result->addSnippet($this->copyTo(new SchemaBuilder(
396 6
                            $schema,
397 6
                            "{$this->varName}->{$keyword}[{$index}]",
398 6
                            $this->path . "->{$keyword}[{$index}]",
399 6
                            $this->phpBuilder
400 6
                        ))->build());
401
                    } else {
402 2
                        $this->result->addSnippet($schemaInit);
403 2
                        $this->result->addSnippet(<<<PHP
404 2
{$this->varName}->{$keyword}[{$index}] = {$varName};
405
406
PHP
407
                        );
408
                    }
409
                }
410
            }
411
        }
412 11
    }
413
414
    /**
415
     * @return PhpCode
416
     * @throws Exception
417
     */
418 11
    public function build()
419
    {
420 11
        $this->result = new PhpCode();
421
422 11
        if ($this->processNamedClass()) {
423 8
            return $this->result;
424
        }
425
426 11
        if ($this->processRef()) {
427 1
            return $this->result;
428
        }
429
430 11
        $this->processType();
431 11
        $this->processObject();
432 11
        $this->processArray();
433 11
        $this->processLogic();
434 11
        $this->processEnum();
435 11
        $this->processOther();
436 11
        $this->processFromRef();
437
438 11
        return $this->result;
439
440
    }
441
442 11
    private function processFromRef()
443
    {
444 11
        if ($this->phpBuilder->minimizeRefs) {
445 10
            if ($fromRefs = $this->schema->getFromRefs()) {
446 3
                $fromRef = $fromRefs[count($fromRefs) - 1];
0 ignored issues
show
Bug introduced by
$fromRefs of type string is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

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

446
                $fromRef = $fromRefs[count(/** @scrutinizer ignore-type */ $fromRefs) - 1];
Loading history...
447 3
                $value = var_export($fromRef, 1);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $value is correct as var_export($fromRef, 1) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
448 3
                $this->result->addSnippet("{$this->varName}->setFromRef($value);\n");
449
            }
450 10
            return;
451
        }
452 1
        if ($fromRefs = $this->schema->getFromRefs()) {
453 1
            foreach ($fromRefs as $fromRef) {
0 ignored issues
show
Bug introduced by
The expression $fromRefs of type string is not traversable.
Loading history...
454 1
                $value = var_export($fromRef, 1);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $value is correct as var_export($fromRef, 1) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
455 1
                $this->result->addSnippet("{$this->varName}->setFromRef($value);\n");
456
            }
457
        }
458 1
    }
459
460
    /**
461
     * @param boolean $skipProperties
462
     * @return $this
463
     */
464 11
    public function setSkipProperties($skipProperties)
465
    {
466 11
        $this->skipProperties = $skipProperties;
467 11
        return $this;
468
    }
469
470
    /**
471
     * @param PhpClass $saveEnumConstInClass
472
     * @return $this
473
     */
474 4
    public function setSaveEnumConstInClass($saveEnumConstInClass)
475
    {
476 4
        $this->saveEnumConstInClass = $saveEnumConstInClass;
477 4
        return $this;
478
    }
479
480
481
}