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

PhpBuilder::makeClass()   F

Complexity

Conditions 22
Paths 513

Size

Total Lines 141
Code Lines 90

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 78
CRAP Score 22.1055

Importance

Changes 3
Bugs 0 Features 0
Metric Value
cc 22
eloc 90
c 3
b 0
f 0
nc 513
nop 2
dl 0
loc 141
ccs 78
cts 83
cp 0.9398
crap 22.1055
rs 0.6763

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\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
}