Completed
Pull Request — master (#29)
by
unknown
06:32
created

PhpBuilder::getSchemaMeta()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 1

Importance

Changes 0
Metric Value
cc 1
eloc 1
c 0
b 0
f 0
nc 1
nop 1
dl 0
loc 3
ccs 1
cts 1
cp 1
crap 1
rs 10
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 (!is_null($property->default)) {
180
                    $phpProperty->setDefault($property->default);
181 11
                }
182 2
183 2
                if ($this->schemaIsNullable($property)) {
184
                    $phpProperty->setIsMagical(true);
185
                }
186 11
187 4
                if ($property->description) {
188 4
                    $phpProperty->setDescription($property->description);
189 4
                }
190
                $class->addProperty($phpProperty);
191 4
                if ($this->buildGetters) {
192 4
                    $class->addMethod(new Getter($phpProperty));
193
                }
194
                if ($this->buildSetters) {
195
                    $class->addMethod(new Setter($phpProperty, true));
196 11
                }
197 11
                $body->addSnippet(
198
                    $schemaBuilder->build()
199
                );
200 11
                if ($propertyName != $name) {
201 11
                    $body->addSnippet('$ownerSchema->addPropertyMapping(' . var_export($name, true) . ', self::names()->'
202
                        . $propertyName . ");\n");
203 11
                }
204
            }
205 11
        }
206 11
207 11
        if ($schema->additionalProperties instanceof Schema) {
208 7
            $class->addMethod(new AdditionalPropertiesGetter($this->getType($schema->additionalProperties)));
209 7
            $class->addMethod(new AdditionalPropertySetter($this->getType($schema->additionalProperties)));
210 7
        }
211 7
212
        if ($schema->patternProperties !== null) {
213 7
            foreach ($schema->patternProperties as $pattern => $patternProperty) {
214 7
                if ($patternProperty instanceof Schema) {
215
                    $const = new PhpConstant(PhpCode::makePhpConstantName($pattern . '_PROPERTY_PATTERN'), $pattern);
216
                    $class->addConstant($const);
217 7
218
                    $class->addMethod(new PatternPropertiesGetter($const, $this->getType($patternProperty)));
219
                    $class->addMethod(new PatternPropertySetter($const, $this->getType($patternProperty)));
220
                }
221 11
            }
222 2
        }
223
224
        $schemaBuilder = new SchemaBuilder($schema, '$ownerSchema', $path, $this, false);
225 11
        if ($this->skipSchemaDescriptions) {
226
            $schemaBuilder->skipProperty(JsonSchema::names()->description);
227
        }
228
        $schemaBuilder->setSkipProperties(true);
229
        $body->addSnippet($schemaBuilder->build());
230
231
        $setupProperties->setBody($body);
232
233
        $phpDoc = $class->getPhpDoc();
234 7
        $type = $this->getType($schema, $path);
235
        if (!$type instanceof PhpClass) {
0 ignored issues
show
introduced by
$type is never a sub-type of Swaggest\PhpCodeBuilder\PhpClass.
Loading history...
236 7
            $class->addMeta($type, self::IMPORT_TYPE);
237 7
            $phpDoc->add(
238 7
                PhpDoc::TAG_METHOD,
239
                new PlaceholderString(
240 7
                    'static :type import($data, :context $options = null)',
241 7
                    array(
242 7
                        ':type' => new TypeOf($type, true),
243
                        ':context' => new TypeOf(PhpClass::byFQN(Context::class))
244
                    )
245
                ),
246
                self::IMPORT_METHOD_PHPDOC_ID
247
            );
248
        }
249
250
        if ($this->classPreparedHook !== null) {
251
            $this->classPreparedHook->process($class, $path, $schema);
252
        }
253
254
        return $generatedClass;
255
    }
256
257
    /** @var DynamicIterator */
258
    private $dynamicIterator;
259
260
    /**
261
     * @return GeneratedClass[]|DynamicIterator
262
     */
263
    public function getGeneratedClasses()
264
    {
265
        $result = array();
266
        foreach ($this->generatedClasses as $schema) {
267
            $result[] = $this->generatedClasses[$schema];
268
        }
269
        $iterator = new DynamicIterator($result);
270
        $this->dynamicIterator = $iterator;
271
        return $iterator;
272
    }
273 7
274
    /**
275 7
     * @param AbstractTemplate $template
276 7
     * @return null|Schema
277
     */
278
    public static function getSchemaMeta(AbstractTemplate $template)
279 7
    {
280
        return $template->getMeta(self::SCHEMA);
281 7
    }
282
283
    /**
284 7
     * Returns true if null is allowed by schema.
285
     *
286 7
     * @param Schema $property
287 7
     * @return bool
288 7
     */
289
    private function schemaIsNullable($property)
290 7
    {
291 7
        if (!empty($property->enum) && !in_array(null, $property->enum)) {
292 7
            return false;
293 7
        }
294
295
        if ($property->const !== null) {
296
            return false;
297
        }
298
299
        if (!empty($property->anyOf)) {
300 7
            $nullable = false;
301
            foreach ($property->anyOf as $item) {
302 7
                if ($item instanceof Schema) {
303
                    if ($this->schemaIsNullable($item)) {
304
                        $nullable = true;
305 7
                        break;
306
                    }
307 7
                }
308 7
            }
309
            if (!$nullable) {
310
                return false;
311
            }
312
        }
313
314
        if (!empty($property->oneOf)) {
315
            $nullable = false;
316
            foreach ($property->oneOf as $item) {
317
                if ($item instanceof Schema) {
318
                    if ($this->schemaIsNullable($item)) {
319
                        $nullable = true;
320
                        break;
321
                    }
322
                }
323
            }
324
            if (!$nullable) {
325
                return false;
326
            }
327
        }
328
329
        if (!empty($property->allOf)) {
330
            foreach ($property->allOf as $item) {
331
                if ($item instanceof Schema) {
332
                    if (!$this->schemaIsNullable($item)) {
333
                        return false;
334
                    }
335
                }
336
            }
337
        }
338
339
        if (
340
            $property->type === null
341
            || $property->type === Schema::NULL
342
            || (is_array($property->type) && in_array(Schema::NULL, $property->type))
343
        ) {
344
            return true;
345
        }
346
347
        return false;
348
    }
349
}
350
351
352
class DynamicIterator implements \Iterator, \ArrayAccess
353
{
354
    private $rows;
355
    private $current;
356
    private $key;
357
    private $valid;
358
359
    public function push($item)
360
    {
361
        $this->rows[] = $item;
362
        return $this;
363
    }
364
365
    /**
366
     * DynamicIterator constructor.
367
     * @param array $rows
368
     */
369
    public function __construct($rows = array())
370
    {
371
        $this->rows = $rows;
372
    }
373
374
375
    public function current()
376
    {
377
        return $this->current;
378
    }
379
380
    public function next()
381
    {
382
        if (empty($this->rows)) {
383
            $this->valid = false;
384
            return;
385
        }
386
        $this->current = array_shift($this->rows);
387
        $this->valid = true;
388
        ++$this->key;
389
    }
390
391
    public function key()
392
    {
393
        return $this->key;
394
    }
395
396
    public function valid()
397
    {
398
        return $this->valid;
399
    }
400
401
    public function rewind()
402
    {
403
        $this->next();
404
    }
405
406
    public function offsetExists($offset)
407
    {
408
        return array_key_exists($offset, $this->rows);
409
    }
410
411
    public function offsetGet($offset)
412
    {
413
        return $this->rows[$offset];
414
    }
415
416
    public function offsetSet($offset, $value)
417
    {
418
        $this->rows[$offset] = $value;
419
    }
420
421
    public function offsetUnset($offset)
422
    {
423
        unset($this->rows[$offset]);
424
    }
425
426
427
}