Completed
Push — master ( 4535bc...0feaba )
by Viacheslav
06:08
created

PhpBuilder   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 324
Duplicated Lines 0 %

Test Coverage

Coverage 83.46%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 158
dl 0
loc 324
ccs 111
cts 133
cp 0.8346
rs 6.4799
c 3
b 0
f 0
wmc 54

7 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 3 1
A getGeneratedClasses() 0 9 2
A getClass() 0 6 2
A getType() 0 7 2
F makeClass() 0 145 24
F schemaIsNullable() 0 59 22
A getSchemaMeta() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like PhpBuilder 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 PhpBuilder, and based on these observations, apply Extract Interface, too.

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