Completed
Push — master ( 989bc3...da76d1 )
by Viacheslav
13:41 queued 03:34
created

PhpBuilder   B

Complexity

Total Complexity 52

Size/Duplication

Total Lines 314
Duplicated Lines 0 %

Test Coverage

Coverage 83.46%

Importance

Changes 3
Bugs 0 Features 0
Metric Value
eloc 155
c 3
b 0
f 0
dl 0
loc 314
ccs 111
cts 133
cp 0.8346
rs 7.44
wmc 52

7 Methods

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

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