PhpBuilder::makeClass()   F
last analyzed

Complexity

Conditions 28
Paths 3073

Size

Total Lines 157
Code Lines 101

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 73
CRAP Score 29.0378

Importance

Changes 4
Bugs 0 Features 0
Metric Value
cc 28
eloc 101
c 4
b 0
f 0
nc 3073
nop 2
dl 0
loc 157
ccs 73
cts 82
cp 0.8902
crap 29.0378
rs 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\JsonSchema;
4
5
use Swaggest\CodeBuilder\AbstractTemplate;
6
use Swaggest\CodeBuilder\PlaceholderString;
7
use Swaggest\JsonSchema\Context;
8
use Swaggest\JsonSchema\JsonSchema;
9
use Swaggest\JsonSchema\Schema;
10
use Swaggest\JsonSchema\SchemaContract;
11
use Swaggest\JsonSchema\SchemaExporter;
12
use Swaggest\PhpCodeBuilder\Exception;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Swaggest\PhpCodeBuilder\JsonSchema\Exception. Consider defining an alias.

Let?s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let?s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
13
use Swaggest\PhpCodeBuilder\PhpAnyType;
14
use Swaggest\PhpCodeBuilder\PhpClass;
15
use Swaggest\PhpCodeBuilder\PhpClassProperty;
16
use Swaggest\PhpCodeBuilder\PhpCode;
17
use Swaggest\PhpCodeBuilder\PhpConstant;
18
use Swaggest\PhpCodeBuilder\PhpDoc;
19
use Swaggest\PhpCodeBuilder\PhpFlags;
20
use Swaggest\PhpCodeBuilder\PhpFunction;
21
use Swaggest\PhpCodeBuilder\PhpNamedVar;
22
use Swaggest\PhpCodeBuilder\PhpStdType;
23
use Swaggest\PhpCodeBuilder\Property\AdditionalPropertiesGetter;
24
use Swaggest\PhpCodeBuilder\Property\AdditionalPropertySetter;
25
use Swaggest\PhpCodeBuilder\Property\Getter;
26
use Swaggest\PhpCodeBuilder\Property\PatternPropertiesGetter;
27
use Swaggest\PhpCodeBuilder\Property\PatternPropertySetter;
28
use Swaggest\PhpCodeBuilder\Property\Setter;
29
use Swaggest\PhpCodeBuilder\Types\TypeOf;
30
31
class PhpBuilder
32
{
33
    const IMPORT_METHOD_PHPDOC_ID = '::import';
34
35
    const SCHEMA = 'schema';
36
    const ORIGIN = 'origin';
37
    const PROPERTY_NAME = 'property_name';
38
    const IMPORT_TYPE = 'import_type';
39
40
    /** @var \SplObjectStorage */
41
    private $generatedClasses;
42
43 12
    public function __construct()
44
    {
45 12
        $this->generatedClasses = new \SplObjectStorage();
46 12
    }
47
48
    public $buildGetters = false;
49
    public $buildSetters = false;
50
    public $makeEnumConstants = false;
51
    public $skipSchemaDescriptions = false;
52
53
    /**
54
     * Use title/description where available instead of keyword in names
55
     * @var bool
56
     */
57
    public $namesFromDescriptions = false;
58
59
    /**
60
     * Squish multiple $ref, a PHP class for each $ref will be created if false
61
     * @var bool
62
     */
63
    public $minimizeRefs = true;
64
65
    /** @var PhpBuilderClassHook */
66
    public $classCreatedHook;
67
68
    /** @var PhpBuilderClassHook */
69
    public $classPreparedHook;
70
71 12
    /**
72
     * Use default values to initialize properties
73 12
     * @var bool
74 12
     */
75
    public $declarePropertyDefaults = false;
76
77
    /**
78
     * Build setter and getter methods for additional properties
79
     * on a boolean true value for `additionalProperties`.
80
     * @var bool
81
     */
82
    public $buildAdditionalPropertyMethodsOnTrue = false;
83
84
    /**
85 11
     * @param SchemaContract $schema
86
     * @param string $path
87 11
     * @return PhpAnyType
88 10
     * @throws \Swaggest\PhpCodeBuilder\JsonSchema\Exception
89
     * @throws Exception
90 11
     */
91
    public function getType($schema, $path = '#')
92
    {
93
        if (!$schema instanceof Schema) {
94
            throw new Exception('Could not find Schema instance in SchemaContract: ' . get_class($schema));
95
        }
96
        $typeBuilder = new TypeBuilder($schema, $path, $this);
97
        return $typeBuilder->build();
98
    }
99
100
101 11
    /**
102
     * @param Schema $schema
103 11
     * @param string $path
104
     * @return PhpClass
105
     * @throws Exception
106 11
     * @throws \Swaggest\PhpCodeBuilder\JsonSchema\Exception
107 11
     */
108
    public function getClass($schema, $path)
109 11
    {
110 11
        if ($this->generatedClasses->contains($schema)) {
111 3
            return $this->generatedClasses[$schema]->class;
112
        } else {
113
            return $this->makeClass($schema, $path)->class;
114 11
        }
115 11
    }
116 4
117
    /**
118 11
     * @param Schema $schema
119
     * @param string $path
120 11
     * @return GeneratedClass
121
     * @throws Exception
122 11
     * @throws \Swaggest\PhpCodeBuilder\JsonSchema\Exception
123 11
     */
124
    private function makeClass($schema, $path)
125 11
    {
126 11
        if (empty($path)) {
127
            throw new Exception('Empty path');
128 11
        }
129
        $generatedClass = new GeneratedClass();
130 11
        $generatedClass->schema = $schema;
131 11
132
        $class = new PhpClass();
133 11
        if ($fromRefs = $schema->getFromRefs()) {
134 11
            $path = $fromRefs[count($fromRefs) - 1];
135
        }
136 11
137 11
        $class->setName(PhpCode::makePhpClassName($path));
138
        if ($this->classCreatedHook !== null) {
139
            $this->classCreatedHook->process($class, $path, $schema);
140
        }
141 11
        if (is_null($class->getExtends())) {
142 10
            $class->setExtends(Palette::classStructureClass());
143 10
        }
144 10
145
        $setupProperties = new PhpFunction('setUpProperties');
146 10
        $setupProperties
147 10
            ->setVisibility(PhpFlags::VIS_PUBLIC)
148 10
            ->setIsStatic(true);
149 10
        $setupProperties
150
            ->addArgument(new PhpNamedVar('properties', Palette::propertiesOrStaticClass()))
151 10
            ->addArgument(new PhpNamedVar('ownerSchema', Palette::schemaClass()));
152 10
153
        $body = new PhpCode();
154
155 10
        $class->addMeta($schema, self::SCHEMA);
156 4
        $class->addMethod($setupProperties);
157
158 10
        $generatedClass->class = $class;
159 10
        $generatedClass->path = $path;
160 10
161 10
        $this->generatedClasses->attach($schema, $generatedClass);
162 2
        if (null !== $this->dynamicIterator) {
163
            $this->dynamicIterator->push($generatedClass);
164 10
        }
165 10
166
        if ($schema->properties) {
167
            $phpNames = array();
168 10
            /**
169 5
             * @var string $name
170
             * @var Schema $property
171 10
             */
172 10
            foreach ($schema->properties as $name => $property) {
173
                $propertyName = PhpCode::makePhpName($name);
174 10
175 3
                $i = 2;
176 3
                $basePropertyName = $propertyName;
177
                while (isset($phpNames[$propertyName])) {
178
                    $propertyName = $basePropertyName . $i;
179
                    $i++;
180
                }
181 11
                $phpNames[$propertyName] = true;
182 2
183 2
                $schemaBuilder = new SchemaBuilder($property, '$properties->' . $propertyName, $path . '->' . $name, $this);
184
                if ($this->skipSchemaDescriptions) {
185
                    $schemaBuilder->skipProperty(JsonSchema::names()->description);
186 11
                }
187 4
                if ($this->makeEnumConstants) {
188 4
                    $schemaBuilder->setSaveEnumConstInClass($class);
189 4
                }
190
                $propertyType = $this->getType($property, $path . '->' . $name);
191 4
                $phpProperty = new PhpClassProperty($propertyName, $propertyType);
192 4
                $phpProperty->addMeta($property, self::SCHEMA);
193
                $phpProperty->addMeta($name, self::PROPERTY_NAME);
194
195
                if (!is_null($property->default) && $this->declarePropertyDefaults) {
196 11
                    $phpProperty->setDefault($property->default);
197 11
                }
198
199
                if ($this->schemaIsNullable($property)) {
200 11
                    $phpProperty->setIsMagical(true);
201 11
                }
202
203 11
                if ($property->description) {
204
                    $phpProperty->setDescription($property->description);
205 11
                }
206 11
                $class->addProperty($phpProperty);
207 11
                if ($this->buildGetters) {
208 7
                    $class->addMethod(new Getter($phpProperty));
209 7
                }
210 7
                if ($this->buildSetters) {
211 7
                    $class->addMethod(new Setter($phpProperty, true));
212
                }
213 7
                $body->addSnippet(
214 7
                    $schemaBuilder->build()
215
                );
216
                if ($propertyName != $name) {
217 7
                    $body->addSnippet('$ownerSchema->addPropertyMapping(' . var_export($name, true) . ', self::names()->'
218
                        . $propertyName . ");\n");
219
                }
220
            }
221 11
        }
222 2
223
        $additionalPropertiesType = null;
224
        $buildAdditionalPropertiesMethods = false;
225 11
        if ($schema->additionalProperties instanceof Schema) {
226
            $additionalPropertiesType = $this->getType($schema->additionalProperties);
227
            $buildAdditionalPropertiesMethods = true;
228
        } elseif ($this->buildAdditionalPropertyMethodsOnTrue && $schema->additionalProperties === true) {
229
            $additionalPropertiesType = PhpStdType::mixed();
230
            $buildAdditionalPropertiesMethods = true;
231
        }
232
233
        if ($buildAdditionalPropertiesMethods) {
234 7
            $class->addMethod(new AdditionalPropertiesGetter($additionalPropertiesType));
235
            $class->addMethod(new AdditionalPropertySetter($additionalPropertiesType));
236 7
        }
237 7
238 7
        if ($schema->patternProperties !== null) {
239
            foreach ($schema->patternProperties as $pattern => $patternProperty) {
240 7
                if ($patternProperty instanceof Schema) {
241 7
                    $const = new PhpConstant(PhpCode::makePhpConstantName($pattern . '_PROPERTY_PATTERN'), $pattern);
242 7
                    $class->addConstant($const);
243
244
                    $class->addMethod(new PatternPropertiesGetter($const, $this->getType($patternProperty)));
245
                    $class->addMethod(new PatternPropertySetter($const, $this->getType($patternProperty)));
246
                }
247
            }
248
        }
249
250
        $schemaBuilder = new SchemaBuilder($schema, '$ownerSchema', $path, $this, false);
251
        if ($this->skipSchemaDescriptions) {
252
            $schemaBuilder->skipProperty(JsonSchema::names()->description);
253
        }
254
        $schemaBuilder->setSkipProperties(true);
255
        $body->addSnippet($schemaBuilder->build());
256
257
        $setupProperties->setBody($body);
258
259
        $phpDoc = $class->getPhpDoc();
260
        $type = $this->getType($schema, $path);
261
        if (!$type instanceof PhpClass) {
0 ignored issues
show
introduced by
$type is never a sub-type of Swaggest\PhpCodeBuilder\PhpClass.
Loading history...
262
            $class->addMeta($type, self::IMPORT_TYPE);
263
            $phpDoc->add(
264
                PhpDoc::TAG_METHOD,
265
                new PlaceholderString(
266
                    'static :type import($data, :context $options = null)',
267
                    array(
268
                        ':type' => new TypeOf($type, true),
269
                        ':context' => new TypeOf(PhpClass::byFQN(Context::class))
270
                    )
271
                ),
272
                self::IMPORT_METHOD_PHPDOC_ID
273 7
            );
274
        }
275 7
276 7
        if ($this->classPreparedHook !== null) {
277
            $this->classPreparedHook->process($class, $path, $schema);
278
        }
279 7
280
        return $generatedClass;
281 7
    }
282
283
    /** @var DynamicIterator */
284 7
    private $dynamicIterator;
285
286 7
    /**
287 7
     * @return GeneratedClass[]|DynamicIterator
288 7
     */
289
    public function getGeneratedClasses()
290 7
    {
291 7
        $result = array();
292 7
        foreach ($this->generatedClasses as $schema) {
293 7
            $result[] = $this->generatedClasses[$schema];
294
        }
295
        $iterator = new DynamicIterator($result);
296
        $this->dynamicIterator = $iterator;
297
        return $iterator;
298
    }
299
300 7
    /**
301
     * @param AbstractTemplate $template
302 7
     * @return null|Schema
303
     */
304
    public static function getSchemaMeta(AbstractTemplate $template)
305 7
    {
306
        return $template->getMeta(self::SCHEMA);
307 7
    }
308 7
309
    /**
310
     * Returns true if null is allowed by schema.
311
     *
312
     * @param Schema $property
313
     * @return bool
314
     */
315
    private function schemaIsNullable($property)
316
    {
317
        if (!empty($property->enum) && !in_array(null, $property->enum)) {
318
            return false;
319
        }
320
321
        if ($property->const !== null) {
322
            return false;
323
        }
324
325
        if (!empty($property->anyOf)) {
326
            $nullable = false;
327
            foreach ($property->anyOf as $item) {
328
                if ($item instanceof Schema) {
329
                    if ($this->schemaIsNullable($item)) {
330
                        $nullable = true;
331
                        break;
332
                    }
333
                }
334
            }
335
            if (!$nullable) {
336
                return false;
337
            }
338
        }
339
340
        if (!empty($property->oneOf)) {
341
            $nullable = false;
342
            foreach ($property->oneOf as $item) {
343
                if ($item instanceof Schema) {
344
                    if ($this->schemaIsNullable($item)) {
345
                        $nullable = true;
346
                        break;
347
                    }
348
                }
349
            }
350
            if (!$nullable) {
351
                return false;
352
            }
353
        }
354
355
        if (!empty($property->allOf)) {
356
            foreach ($property->allOf as $item) {
357
                if ($item instanceof Schema) {
358
                    if (!$this->schemaIsNullable($item)) {
359
                        return false;
360
                    }
361
                }
362
            }
363
        }
364
365
        if (
366
            $property->type === null
367
            || $property->type === Schema::NULL
368
            || (is_array($property->type) && in_array(Schema::NULL, $property->type))
369
        ) {
370
            return true;
371
        }
372
373
        return false;
374
    }
375
}
376
377
378
class DynamicIterator implements \Iterator, \ArrayAccess
379
{
380
    private $rows;
381
    private $current;
382
    private $key;
383
    private $valid;
384
385
    public function push($item)
386
    {
387
        $this->rows[] = $item;
388
        return $this;
389
    }
390
391
    /**
392
     * DynamicIterator constructor.
393
     * @param array $rows
394
     */
395
    public function __construct($rows = array())
396
    {
397
        $this->rows = $rows;
398
    }
399
400
401
    #[\ReturnTypeWillChange]
402
    public function current()
403
    {
404
        return $this->current;
405
    }
406
407
    #[\ReturnTypeWillChange]
408
    public function next()
409
    {
410
        if (empty($this->rows)) {
411
            $this->valid = false;
412
            return;
413
        }
414
        $this->current = array_shift($this->rows);
415
        $this->valid = true;
416
        ++$this->key;
417
    }
418
419
    #[\ReturnTypeWillChange]
420
    public function key()
421
    {
422
        return $this->key;
423
    }
424
425
    #[\ReturnTypeWillChange]
426
    public function valid()
427
    {
428
        return $this->valid;
429
    }
430
431
    #[\ReturnTypeWillChange]
432
    public function rewind()
433
    {
434
        $this->next();
435
    }
436
437
    #[\ReturnTypeWillChange]
438
    public function offsetExists($offset)
439
    {
440
        return array_key_exists($offset, $this->rows);
441
    }
442
443
    #[\ReturnTypeWillChange]
444
    public function offsetGet($offset)
445
    {
446
        return $this->rows[$offset];
447
    }
448
449
    #[\ReturnTypeWillChange]
450
    public function offsetSet($offset, $value)
451
    {
452
        $this->rows[$offset] = $value;
453
    }
454
455
    #[\ReturnTypeWillChange]
456
    public function offsetUnset($offset)
457
    {
458
        unset($this->rows[$offset]);
459
    }
460
461
462
}