Completed
Push — master ( a61833...39d240 )
by Viacheslav
15:14 queued 05:21
created

SchemaBuilder::pathFromDescription()   A

Complexity

Conditions 6
Paths 4

Size

Total Lines 10
Code Lines 6

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 3
CRAP Score 6

Importance

Changes 0
Metric Value
cc 6
eloc 6
c 0
b 0
f 0
nc 4
nop 2
dl 0
loc 10
ccs 3
cts 3
cp 1
crap 6
rs 9.2222
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
                    $types = PhpCode::varExport($this->schema->type);
107 8
                    $result = $this->createVarName
108 8
                        ? "{$this->varName} = (new ::schema())->setType($types);"
109
                        : "{$this->varName}->type = $types;";
110
            }
111
        } else {
112 11
            if ($this->createVarName) {
113 11
                $result = "{$this->varName} = new ::schema();";
114 11
            }
115
        }
116
117 11
        if (isset($result)) {
118
            $this->result->addSnippet(
119 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

119
                new PlaceholderString($result . "\n", array('::schema' => /** @scrutinizer ignore-deprecated */ new ReferenceTypeOf(Palette::schemaClass())))
Loading history...
120
            );
121 11
        }
122
    }
123 11
124
    private function processNamedClass()
125 8
    {
126 8
        if (!$this->skipProperties
127
            //&& $this->schema->type === Type::OBJECT
128
            && $this->schema->properties !== null
129
        ) {
130
            $class = $this->phpBuilder->getClass($this->schema, $this->path);
131
            if ($this->schema->id === 'http://json-schema.org/draft-04/schema#') {
132 8
                $this->result->addSnippet(
133 8
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
134 8
                        array('::class' => new TypeOf(Palette::schemaClass())))
135
                );
136
            } else {
137 8
                $this->result->addSnippet(
138
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
139 11
                        array('::class' => new TypeOf($class)))
140
                );
141
            }
142 11
            return true;
143
        }
144 11
        return false;
145
    }
146 11
147 11
    private function processRef()
148
    {
149 1
        if (!$this->skipProperties
150 1
            //&& $this->schema->type === Type::OBJECT
151
            && !$this->phpBuilder->minimizeRefs
152
            && $this->schema->getFromRefs()
153
        ) {
154
            $class = $this->phpBuilder->getClass($this->schema, $this->path);
155
            if ($this->schema->id === 'http://json-schema.org/draft-04/schema#') {
156 1
                $this->result->addSnippet(
157 1
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
158 1
                        array('::class' => new TypeOf(Palette::schemaClass())))
159
                );
160
            } else {
161 1
                $this->result->addSnippet(
162
                    new PlaceholderString("{$this->varName} = ::class::schema();\n",
163 11
                        array('::class' => new TypeOf($class)))
164
                );
165
            }
166
            return true;
167
        }
168
        return false;
169
    }
170 11
171
172 11
    /**
173 7
     * @throws Exception
174 3
     */
175 3
    private function processObject()
176 3
    {
177
        if ($this->schema->type === Type::OBJECT) {
178
            if (!$this->skipProperties) {
179 7
                $this->result->addSnippet(
180 7
                    new PlaceholderString("{$this->varName} = ::schema::object();\n",
181
                        array('::schema' => new TypeOf(Palette::schemaClass())))
182
                );
183
            } else {
184
                $this->result->addSnippet(
185
                    "{$this->varName}->type = 'object';\n"
186
                );
187 11
            }
188 4
189 3
        }
190 3
191 3
192 3
        if ($this->schema->additionalProperties !== null) {
193 3
            if ($this->schema->additionalProperties instanceof Schema) {
194 3
                $this->result->addSnippet(
195 3
                    $this->copyTo(new SchemaBuilder(
196
                        $this->schema->additionalProperties,
197
                        "{$this->varName}->additionalProperties",
198 3
                        $this->path . '->additionalProperties',
199 3
                        $this->phpBuilder
200 3
                    ))->build()
201
                );
202
            } else {
203
                $val = $this->schema->additionalProperties ? 'true' : 'false';
204
                $this->result->addSnippet(
205 11
                    "{$this->varName}->additionalProperties = $val;\n"
206 4
                );
207 4
            }
208 4
        }
209 4
210 4
        if ($this->schema->patternProperties !== null) {
211 4
            foreach ($this->schema->patternProperties as $pattern => $property) {
212 4
                if ($property instanceof Schema) {
213 4
                    $patternExp = var_export($pattern, true);
214 4
                    $this->result->addSnippet(
215
                        $this->copyTo(new SchemaBuilder(
216 4
                            $property,
217
                            "\$patternProperty",
218
                            $this->path . "->patternProperties->{{$pattern}}",
219
                            $this->phpBuilder
220 11
                        ))->build()
221
                    );
222
                    $this->result->addSnippet("{$this->varName}->setPatternProperty({$patternExp}, \$patternProperty);\n");
223
                }
224
            }
225 11
        }
226
    }
227 11
228
    /**
229 11
     * @throws Exception
230 2
     */
231 2
    private function processArray()
232 2
    {
233
        $schema = $this->schema;
234
235
        if (is_bool($schema->additionalItems)) {
236 11
            $val = $schema->additionalItems ? 'true' : 'false';
237 11
            $this->result->addSnippet(
238 2
                "{$this->varName}->additionalItems = $val;\n"
239 2
            );
240 2
        }
241 11
242 11
        $pathItems = 'items';
243 11
        if ($schema->items instanceof ClassStructure) { // todo better check for schema, `getJsonSchema` interface ?
244
            $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...
245
            $pathItems = 'items';
246
        } elseif ($schema->items === null) { // items defaults to empty schema so everything is valid
247
            $additionalItems = true;
248
        } else { // listed items
249
            $additionalItems = $schema->additionalItems;
250 11
            $pathItems = 'additionalItems';
251 11
        }
252 2
253 2
        if ($additionalItems instanceof ClassStructure) {
254 2
            $this->result->addSnippet(
255 2
                $this->copyTo(new SchemaBuilder(
256 2
                    $additionalItems,
257 2
                    "{$this->varName}->{$pathItems}",
258 2
                    $this->path . '->' . $pathItems,
259
                    $this->phpBuilder
260
                ))->build()
261
            );
262 11
        }
263
    }
264 11
265
    private function processEnum()
266 11
    {
267 7
        if (!empty($this->schema->enum)) {
268 7
            $this->result->addSnippet(
269
                "{$this->varName}->enum = array(\n"
270 7
            );
271 7
            foreach ($this->schema->enum as $i => $enumItem) {
272
                if (isset($this->schema->{Schema::ENUM_NAMES_PROPERTY}[$i])) {
273
                    $name = PhpCode::makePhpConstantName($this->schema->{Schema::ENUM_NAMES_PROPERTY}[$i]);
274 7
                } else {
275
                    $name = PhpCode::makePhpConstantName($enumItem);
276 7
                }
277 7
                $value = var_export($enumItem, true);
278 3
                if ($this->saveEnumConstInClass !== null && is_scalar($enumItem) && !is_bool($enumItem)) {
279 3
                    $this->saveEnumConstInClass->addConstant(new PhpConstant($name, $enumItem));
280 3
                    $this->result->addSnippet(
281
                        "    self::$name,\n"
282
                    );
283 6
                } else {
284 6
                    $this->result->addSnippet(
285
                        "    $value,\n"
286
                    );
287
                }
288
289 7
            }
290 7
            $this->result->addSnippet(
291
                ");\n"
292
            );
293
294 11
        }
295
    }
296
297
    private $skip = [];
298
299
    public function skipProperty($name)
300
    {
301
        $this->skip[$name] = 1;
302
        return $this;
303
    }
304 9
305
    private function copyTo(SchemaBuilder $schemaBuilder)
306 9
    {
307 9
        $schemaBuilder->skip = $this->skip;
308 9
        $schemaBuilder->saveEnumConstInClass = $this->saveEnumConstInClass;
309
        return $schemaBuilder;
310
    }
311 11
312
    /**
313 11
     * @throws \Swaggest\JsonSchema\InvalidValue
314 11
     */
315 1
    private function processOther()
316
    {
317 1
        static $skip = null, $emptySchema = null, $names = null;
318 1
        if ($skip === null) {
319 1
            $emptySchema = new Schema();
320 1
            $names = Schema::names();
321 1
            $skip = array(
322 1
                $names->type => 1,
323 1
                Schema::PROP_REF => 1,
324 1
                $names->items => 1,
325 1
                $names->additionalItems => 1,
326 1
                $names->properties => 1,
327 1
                $names->additionalProperties => 1,
328 1
                $names->patternProperties => 1,
329 1
                $names->allOf => 1, // @todo process
330 1
                $names->anyOf => 1,
331 1
                $names->oneOf => 1,
332 1
                $names->not => 1,
333 1
                $names->definitions => 1,
334 1
                $names->enum => 1,
335
                $names->if => 1,
336
                $names->then => 1,
337 11
                $names->else => 1,
338 11
            );
339 11
        }
340 11
        $schemaData = Schema::export($this->schema);
341
        foreach ((array)$schemaData as $key => $value) {
342 7
            if (isset($skip[$key])) {
343
                continue;
344
            }
345
            if (isset($this->skip[$key])) {
346
                continue;
347
            }
348 7
349
            if (!property_exists($emptySchema, $key) && $key !== $names->const && $key[0] !== '$') {
350
                continue;
351 7
            }
352 2
353 7
            if ($names->required == $key && is_array($value)) {
354 2
                $export = "array(\n";
355
                foreach ($value as $item) {
356 7
                    if (PhpCode::makePhpName($item) === $item) {
357
                        $expItem = 'self::names()->' . $item;
358
                    } else {
359 7
                        $expItem = PhpCode::varExport($item);
360 7
                    }
361 7
                    $export .= '    ' . $expItem . ",\n";
362
                }
363
                $export .= ")";
364 11
                $this->result->addSnippet(
365
                    "{$this->varName}->{$key} = " . $export . ";\n"
366 11
                );
367
                continue;
368 11
            }
369 11
370 11
            //$this->result->addSnippet('/* ' . print_r($value, 1) . '*/' . "\n");
371 4
            //echo "{$this->varName}->{$key}\n";
372 4
            if ($value instanceof ObjectItem) {
373 4
                //$value = $value->jsonSerialize();
374 4
                $export = 'new \stdClass()';
375 4
            } elseif ($value instanceof \stdClass) {
376 4
                $export = '(object)' . PhpCode::varExport((array)$value);
377 4
            } elseif (is_string($value)) {
378
                $export = '"' . str_replace(array('\\', "\n", "\r", "\t", '"'), array('\\\\', '\n', '\r', '\t', '\"'), $value) . '"';
379
            } else {
380
                $export = PhpCode::varExport($value);
381
            }
382
383 11
            $key = PhpCode::makePhpName($key);
384 11
            $this->result->addSnippet(
385 6
                "{$this->varName}->{$key} = " . $export . ";\n"
386 6
            );
387 6
        }
388 6
    }
389 6
390 6
    /**
391 6
     * @throws Exception
392 6
     * @throws \Exception
393
     */
394 6
    private function processLogic()
395 6
    {
396 6
        $names = Schema::names();
397 6
        /** @var string $keyword */
398 6
        foreach (array($names->not, $names->if, $names->then, $names->else) as $keyword) {
399 6
            if ($this->schema->$keyword !== null) {
400 6
                $schema = $this->schema->$keyword;
401
                $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

401
                $path = $this->path . '->' . /** @scrutinizer ignore-type */ $keyword;
Loading history...
402 2
                if ($schema instanceof Schema) {
403 2
                    $path = $this->pathFromDescription($path, $schema);
404 2
                    if (!empty($schema->getFromRefs())) {
405
                        $path = $schema->getFromRefs()[0];
406
                    }
407
                }
408
                $this->result->addSnippet(
409
                    $this->copyTo(new SchemaBuilder(
410
                        $schema,
411
                        "{$this->varName}->{$keyword}",
412 11
                        $path,
413
                        $this->phpBuilder
414
                    ))->build()
415
                );
416
            }
417
418 11
        }
419
420 11
        foreach (array($names->anyOf, $names->oneOf, $names->allOf) as $keyword) {
421
            if ($this->schema->$keyword !== null) {
422 11
                foreach ($this->schema->$keyword as $index => $schema) {
423 8
                    $path = $this->path . "->{$keyword}[{$index}]";
424
                    $path = $this->pathFromDescription($path, $schema);
425
                    if ($schema instanceof Schema && !empty($schema->getFromRefs())) {
426 11
                        $path = $schema->getFromRefs()[0];
427 1
                    }
428
                    $varName = '$' . PhpCode::makePhpName("{$this->varName}->{$keyword}[{$index}]");
429
                    $schemaInit = $this->copyTo(new SchemaBuilder(
430 11
                        $schema,
431 11
                        $varName,
432 11
                        $path,
433 11
                        $this->phpBuilder
434 11
                    ))->build();
435 11
436 11
                    if (count($schemaInit->snippets) === 1) { // Init in single statement can be just assigned.
437
                        $this->result->addSnippet($this->copyTo(new SchemaBuilder(
438 11
                            $schema,
439
                            "{$this->varName}->{$keyword}[{$index}]",
440
                            $this->path . "->{$keyword}[{$index}]",
441
                            $this->phpBuilder
442 11
                        ))->build());
443
                    } else {
444 11
                        $this->result->addSnippet($schemaInit);
445 10
                        $this->result->addSnippet(<<<PHP
446 3
{$this->varName}->{$keyword}[{$index}] = {$varName};
447 3
448 3
PHP
449
                        );
450 10
                    }
451
                }
452 1
            }
453 1
        }
454 1
    }
455 1
456
    /**
457
     * @param string $path
458 1
     * @param Schema $schema
459
     * @return string
460
     */
461
    private function pathFromDescription($path, $schema)
462
    {
463
        if ($this->phpBuilder->namesFromDescriptions) {
464 11
            if ($schema->title && strlen($schema->title) < 30) {
465
                $path = $this->path . "->{$schema->title}";
466 11
            } elseif ($schema->description && strlen($schema->description) < 30) {
467 11
                $path = $this->path . "->{$schema->description}";
468
            }
469
        }
470
        return $path;
471
    }
472
473
    /**
474 4
     * @return PhpCode
475
     * @throws Exception
476 4
     * @throws \Swaggest\JsonSchema\InvalidValue
477 4
     */
478
    public function build()
479
    {
480
        $this->result = new PhpCode();
481
482
        if ($this->processNamedClass()) {
483
            return $this->result;
484
        }
485
486
        if ($this->processRef()) {
487
            return $this->result;
488
        }
489
490
        $this->processType();
491
        $this->processObject();
492
        $this->processArray();
493
        $this->processLogic();
494
        $this->processEnum();
495
        $this->processOther();
496
        $this->processFromRef();
497
498
        return $this->result;
499
500
    }
501
502
    private function processFromRef()
503
    {
504
        if ($this->phpBuilder->minimizeRefs) {
505
            if ($fromRefs = $this->schema->getFromRefs()) {
506
                $fromRef = $fromRefs[count($fromRefs) - 1];
507
                $value = var_export($fromRef, true);
508
                $this->result->addSnippet("{$this->varName}->setFromRef($value);\n");
509
            }
510
            return;
511
        }
512
        if ($fromRefs = $this->schema->getFromRefs()) {
513
            foreach ($fromRefs as $fromRef) {
514
                $value = var_export($fromRef, true);
515
                $this->result->addSnippet("{$this->varName}->setFromRef($value);\n");
516
            }
517
        }
518
    }
519
520
    /**
521
     * @param boolean $skipProperties
522
     * @return $this
523
     */
524
    public function setSkipProperties($skipProperties)
525
    {
526
        $this->skipProperties = $skipProperties;
527
        return $this;
528
    }
529
530
    /**
531
     * @param PhpClass $saveEnumConstInClass
532
     * @return $this
533
     */
534
    public function setSaveEnumConstInClass($saveEnumConstInClass)
535
    {
536
        $this->saveEnumConstInClass = $saveEnumConstInClass;
537
        return $this;
538
    }
539
540
541
}