|
| 1 | +<?php |
| 2 | + |
| 3 | +declare(strict_types=1); |
| 4 | + |
| 5 | +namespace PHPModelGenerator\SchemaProcessor\PostProcessor\Internal; |
| 6 | + |
| 7 | +use PHPModelGenerator\Model\GeneratorConfiguration; |
| 8 | +use PHPModelGenerator\Model\Property\PropertyInterface; |
| 9 | +use PHPModelGenerator\Model\Property\PropertyType; |
| 10 | +use PHPModelGenerator\Model\Schema; |
| 11 | +use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator; |
| 12 | +use PHPModelGenerator\Model\Validator\ComposedPropertyValidator; |
| 13 | +use PHPModelGenerator\Model\Validator\ConditionalPropertyValidator; |
| 14 | +use PHPModelGenerator\Model\Validator\PropertyValidatorInterface; |
| 15 | +use PHPModelGenerator\PropertyProcessor\ComposedValue\AllOfProcessor; |
| 16 | +use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor; |
| 17 | + |
| 18 | +/** |
| 19 | + * Promotes properties transferred from composition branches to non-nullable when the composition |
| 20 | + * structure guarantees the property is always present in a valid object. |
| 21 | + * |
| 22 | + * Rules: |
| 23 | + * allOf — property is required in any branch (all branches apply simultaneously) |
| 24 | + * anyOf — property is required in every branch (at least one always matches) |
| 25 | + * oneOf — property is required in every branch (exactly one always matches) |
| 26 | + * if/then/else — property is required in both then and else (one always applies) |
| 27 | + * |
| 28 | + * The property's isRequired() flag is intentionally left false so the template short-circuit |
| 29 | + * (which exits early when the key is absent) continues to work correctly during construction. |
| 30 | + * Only the nullable flag on the PropertyType is changed to false. |
| 31 | + */ |
| 32 | +class CompositionRequiredPromotionPostProcessor extends PostProcessor |
| 33 | +{ |
| 34 | + public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void |
| 35 | + { |
| 36 | + foreach ($schema->getBaseValidators() as $validator) { |
| 37 | + if (!($validator instanceof AbstractComposedPropertyValidator)) { |
| 38 | + continue; |
| 39 | + } |
| 40 | + |
| 41 | + foreach ($this->collectPromotablePropertyNames($validator) as $propertyName) { |
| 42 | + $this->promoteProperty($schema, $propertyName); |
| 43 | + } |
| 44 | + } |
| 45 | + } |
| 46 | + |
| 47 | + /** |
| 48 | + * Returns the names of all properties that are guaranteed to be present by the given validator. |
| 49 | + * |
| 50 | + * @return string[] |
| 51 | + */ |
| 52 | + private function collectPromotablePropertyNames(AbstractComposedPropertyValidator $validator): array |
| 53 | + { |
| 54 | + if ($validator instanceof ConditionalPropertyValidator) { |
| 55 | + return $this->collectFromConditional($validator); |
| 56 | + } |
| 57 | + |
| 58 | + return $this->collectFromComposed($validator); |
| 59 | + } |
| 60 | + |
| 61 | + /** |
| 62 | + * For if/then/else: a property is guaranteed only when both then and else are present and |
| 63 | + * both require the property. |
| 64 | + * |
| 65 | + * @return string[] |
| 66 | + */ |
| 67 | + private function collectFromConditional(ConditionalPropertyValidator $validator): array |
| 68 | + { |
| 69 | + $branches = $validator->getConditionBranches(); |
| 70 | + |
| 71 | + if (count($branches) < 2) { |
| 72 | + return []; |
| 73 | + } |
| 74 | + |
| 75 | + $requiredPerBranch = array_map( |
| 76 | + static fn($branch): array => |
| 77 | + $branch->getNestedSchema()?->getJsonSchema()->getJson()['required'] ?? [], |
| 78 | + $branches, |
| 79 | + ); |
| 80 | + |
| 81 | + return array_values(array_intersect(...$requiredPerBranch)); |
| 82 | + } |
| 83 | + |
| 84 | + /** |
| 85 | + * For allOf: a property is guaranteed when it is required in any branch. |
| 86 | + * For anyOf/oneOf: a property is guaranteed when it is required in every branch. |
| 87 | + * |
| 88 | + * @return string[] |
| 89 | + */ |
| 90 | + private function collectFromComposed(ComposedPropertyValidator $validator): array |
| 91 | + { |
| 92 | + $branches = $validator->getComposedProperties(); |
| 93 | + |
| 94 | + if (empty($branches)) { |
| 95 | + return []; |
| 96 | + } |
| 97 | + |
| 98 | + $requiredPerBranch = array_map( |
| 99 | + static fn($branch): array => |
| 100 | + $branch->getNestedSchema()?->getJsonSchema()->getJson()['required'] ?? [], |
| 101 | + $branches, |
| 102 | + ); |
| 103 | + |
| 104 | + if (is_a($validator->getCompositionProcessor(), AllOfProcessor::class, true)) { |
| 105 | + return array_values(array_unique(array_merge(...$requiredPerBranch))); |
| 106 | + } |
| 107 | + |
| 108 | + return array_values(array_intersect(...$requiredPerBranch)); |
| 109 | + } |
| 110 | + |
| 111 | + /** |
| 112 | + * Strips the nullable flag from the property's type if the property is not already required |
| 113 | + * at root level and has a type that can be promoted. |
| 114 | + */ |
| 115 | + private function promoteProperty(Schema $schema, string $propertyName): void |
| 116 | + { |
| 117 | + $property = null; |
| 118 | + |
| 119 | + foreach ($schema->getProperties() as $candidate) { |
| 120 | + if ($candidate->getName() === $propertyName) { |
| 121 | + $property = $candidate; |
| 122 | + break; |
| 123 | + } |
| 124 | + } |
| 125 | + |
| 126 | + if ($property === null) { |
| 127 | + return; |
| 128 | + } |
| 129 | + |
| 130 | + if ($property->isRequired()) { |
| 131 | + return; |
| 132 | + } |
| 133 | + |
| 134 | + $type = $property->getType(); |
| 135 | + $outputType = $property->getType(true); |
| 136 | + |
| 137 | + if ($type === null) { |
| 138 | + return; |
| 139 | + } |
| 140 | + |
| 141 | + $property->setType( |
| 142 | + new PropertyType($type->getNames(), false), |
| 143 | + new PropertyType($outputType->getNames(), false), |
| 144 | + ); |
| 145 | + } |
| 146 | +} |
0 commit comments