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
|
|||
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
|
|||
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 | } |
Let?s assume that you have a directory layout like this:
and let?s assume the following content of
Bar.php
:If both files
OtherDir/Foo.php
andSomeDir/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 beforeOtherDir/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: