• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

wol-soft / php-json-schema-model-generator / 37939486788

09 Oct 2026 01:48PM UTC coverage: 98.87% (-0.02%) from 98.892%
37939486788

Pull #193

github

web-flow
Merge 2da2151bf into f7cfd60d0
Pull Request #193: Run filters of branch properties only while their branch is active

305 of 310 new or added lines in 10 files covered. (98.39%)

5 existing lines in 1 file now uncovered.

8221 of 8315 relevant lines covered (98.87%)

1651.84 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

98.9
/src/SchemaProcessor/PostProcessor/Internal/SerializationPostProcessor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\SchemaProcessor\PostProcessor\Internal;
6

7
use JsonSerializable;
8
use PHPModelGenerator\Filter\TransformingFilterInterface;
9
use PHPModelGenerator\Interfaces\SerializationInterface;
10
use PHPModelGenerator\Model\GeneratorConfiguration;
11
use PHPModelGenerator\Model\Property\Property;
12
use PHPModelGenerator\Model\Property\PropertyInterface;
13
use PHPModelGenerator\Model\Property\PropertyType;
14
use PHPModelGenerator\Model\Schema;
15
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
16
use PHPModelGenerator\Model\Validator;
17
use PHPModelGenerator\Model\Validator\AdditionalPropertiesValidator;
18
use PHPModelGenerator\Model\Validator\FilterValidator;
19
use PHPModelGenerator\Model\Validator\PatternPropertiesValidator;
20
use PHPModelGenerator\Model\Validator\PropertyValidatorInterface;
21
use PHPModelGenerator\SchemaProcessor\Hook\SchemaHookResolver;
22
use PHPModelGenerator\SchemaProcessor\Hook\SerializationHookInterface;
23
use PHPModelGenerator\SchemaProcessor\PostProcessor\PostProcessor;
24
use PHPModelGenerator\SchemaProcessor\PostProcessor\RenderedMethod;
25
use PHPModelGenerator\Traits\SerializableTrait;
26
use PHPModelGenerator\Utils\FilterReflection;
27
use PHPModelGenerator\Utils\RenderHelper;
28
use PHPModelGenerator\Utils\TypeCheck;
29
use ReflectionException;
30

31
/**
32
 * Class SerializationPostProcessor
33
 *
34
 * @package PHPModelGenerator\SchemaProcessor\PostProcessor
35
 */
36
class SerializationPostProcessor extends PostProcessor
37
{
38
    /**
39
     * Add serialization support to the provided schema
40
     */
41
    public function process(Schema $schema, GeneratorConfiguration $generatorConfiguration): void
191 ✔
42
    {
43
        $schema
191 ✔
44
            ->addTrait(SerializableTrait::class)
191 ✔
45
            ->addInterface(JsonSerializable::class)
191 ✔
46
            ->addInterface(SerializationInterface::class);
191 ✔
47

48
        $this->addSerializeFunctionsForTransformingFilters($schema, $generatorConfiguration);
191 ✔
49
        $this->addSerializationHookMethod($schema, $generatorConfiguration);
191 ✔
50
        $this->addSkipNotProvidedPropertiesMap($schema, $generatorConfiguration);
191 ✔
51
        $this->addWriteOnlyExclusion($schema, $generatorConfiguration);
191 ✔
52

53
        $json = $schema->getJsonSchema()->getJson();
191 ✔
54
        if (isset($json['additionalProperties']) && $json['additionalProperties'] !== false) {
191 ✔
55
            $this->addAdditionalPropertiesTransformingFilterSerializer($schema, $generatorConfiguration);
36 ✔
56
        }
57
    }
58

59
    /**
60
     * @param FilterValidator[] $filterValidators
61
     *
62
     * @throws ReflectionException
63
     */
64
    private function renderGuardedSerializers(array $filterValidators, PropertyInterface $property): string
9 ✔
65
    {
66
        $code = ['$value = $this->' . $property->getAttribute(true) . ';', ''];
9 ✔
67

68
        foreach ($filterValidators as $filterValidator) {
9 ✔
69
            [$serializerClass, $serializerMethod] = $filterValidator->getFilter()->getSerializer();
9 ✔
70
            $returnTypeNames = FilterReflection::getReturnTypeNames($filterValidator->getFilter(), $property);
9 ✔
71

72
            $serialization = "return \\{$serializerClass}::{$serializerMethod}("
9 ✔
73
                . '$value, ' . RenderHelper::varExportArray($filterValidator->getFilterOptions()) . ');';
9 ✔
74

75
            if ($returnTypeNames === []) {
9 ✔
NEW
76
                $code[] = $serialization;
×
77

NEW
78
                return implode("\n", $code);
×
79
            }
80

81
            array_push(
9 ✔
82
                $code,
9 ✔
83
                'if ' . TypeCheck::buildCompound($returnTypeNames) . ' {',
9 ✔
84
                "    $serialization",
9 ✔
85
                '}',
9 ✔
86
                '',
9 ✔
87
            );
9 ✔
88
        }
89

90
        $code[] = 'return $value;';
9 ✔
91

92
        return implode("\n", $code);
9 ✔
93
    }
94

95
    /**
96
     * Each transforming filter must provide a method to serialize the value. Add a method to the schema to call the
97
     * serialization for each property with a transforming filter
98
     */
99
    private function addSerializeFunctionsForTransformingFilters(
191 ✔
100
        Schema $schema,
101
        GeneratorConfiguration $generatorConfiguration,
102
    ): void {
103
        foreach ($schema->getProperties() as $property) {
191 ✔
104
            $transformingFilterValidators = array_values(array_filter(
188 ✔
105
                array_map(
188 ✔
106
                    static fn(Validator $wrapper): PropertyValidatorInterface => $wrapper->getValidator(),
188 ✔
107
                    $property->getValidators(),
188 ✔
108
                ),
188 ✔
109
                static fn(PropertyValidatorInterface $validator): bool => $validator instanceof FilterValidator
188 ✔
110
                    && $validator->getFilter() instanceof TransformingFilterInterface,
188 ✔
111
            ));
188 ✔
112

113
            if ($transformingFilterValidators === []) {
188 ✔
114
                continue;
143 ✔
115
            }
116

117
            $templateValues = ['property' => $property, 'guardedSerializers' => ''];
79 ✔
118

119
            // A filter which is owned by a branch of a composition may not have run: the property then still holds
120
            // the value as provided. Only a value of the output type of a filter is serialized by it.
121
            $ownedByBranch = array_filter(
79 ✔
122
                $transformingFilterValidators,
79 ✔
123
                static fn(FilterValidator $validator): bool => !$validator->isExecuted(),
79 ✔
124
            );
79 ✔
125

126
            if ($ownedByBranch !== []) {
79 ✔
127
                $templateValues['guardedSerializers'] = $this->renderGuardedSerializers(
9 ✔
128
                    $transformingFilterValidators,
9 ✔
129
                    $property,
9 ✔
130
                );
9 ✔
131
            } else {
132
                $validator = $transformingFilterValidators[0];
79 ✔
133
                [$serializerClass, $serializerMethod] = $validator->getFilter()->getSerializer();
79 ✔
134

135
                $templateValues += [
79 ✔
136
                    'serializerClass' => $serializerClass,
79 ✔
137
                    'serializerMethod' => $serializerMethod,
79 ✔
138
                    'serializerOptions' => RenderHelper::varExportArray($validator->getFilterOptions()),
79 ✔
139
                ];
79 ✔
140
            }
141

142
            $schema->addMethod(
79 ✔
143
                "_serialize{$property->getAttribute()}",
79 ✔
144
                new RenderedMethod(
79 ✔
145
                    $schema,
79 ✔
146
                    $generatorConfiguration,
79 ✔
147
                    join(DIRECTORY_SEPARATOR, ['Serialization', 'TransformingFilterSerializer.phptpl']),
79 ✔
148
                    $templateValues,
79 ✔
149
                ),
79 ✔
150
            );
79 ✔
151
        }
152

153
        foreach ($schema->getBaseValidators() as $validator) {
191 ✔
154
            if ($validator instanceof PatternPropertiesValidator) {
81 ✔
155
                foreach ($validator->getValidationProperty()->getValidators() as $patternPropertyValidator) {
21 ✔
156
                    $filterValidator = $patternPropertyValidator->getValidator();
21 ✔
157

158
                    if (
159
                        $filterValidator instanceof FilterValidator &&
21 ✔
160
                        $filterValidator->getFilter() instanceof TransformingFilterInterface
21 ✔
161
                    ) {
162
                        [$serializerClass, $serializerMethod] = $filterValidator->getFilter()->getSerializer();
6 ✔
163

164
                        $schema->addMethod(
6 ✔
165
                            "_serialize{$validator->getKey()}",
6 ✔
166
                            new RenderedMethod(
6 ✔
167
                                $schema,
6 ✔
168
                                $generatorConfiguration,
6 ✔
169
                                join(
6 ✔
170
                                    DIRECTORY_SEPARATOR,
6 ✔
171
                                    ['Serialization', 'PatternPropertyTransformingFilterSerializer.phptpl'],
6 ✔
172
                                ),
6 ✔
173
                                [
6 ✔
174
                                    'key' => $validator->getKey(),
6 ✔
175
                                    'serializerClass' => $serializerClass,
6 ✔
176
                                    'serializerMethod' => $serializerMethod,
6 ✔
177
                                    'serializerOptions' => RenderHelper::varExportArray(
6 ✔
178
                                        $filterValidator->getFilterOptions(),
6 ✔
179
                                    ),
6 ✔
180
                                ],
6 ✔
181
                            )
6 ✔
182
                        );
6 ✔
183
                    }
184
                }
185
            }
186
        }
187
    }
188

189
    private function addSerializationHookMethod(Schema $schema, GeneratorConfiguration $generatorConfiguration): void
191 ✔
190
    {
191
        $schema->addMethod(
191 ✔
192
            '_resolveSerializationHook',
191 ✔
193
            new RenderedMethod(
191 ✔
194
                $schema,
191 ✔
195
                $generatorConfiguration,
191 ✔
196
                join(DIRECTORY_SEPARATOR, ['Serialization', 'SerializationHook.phptpl']),
191 ✔
197
                [
191 ✔
198
                    'schemaHookResolver' => new SchemaHookResolver($schema),
191 ✔
199
                ],
191 ✔
200
            )
191 ✔
201
        );
191 ✔
202
    }
203

204
    /**
205
     * When additional properties have a transforming filter, override _serializeAdditionalProperties on the model
206
     * so that the filter's deserializer runs before values are serialized.
207
     *
208
     * For the generic case (no transforming filter), SerializableTrait._serializeAdditionalProperties handles
209
     * serialization directly — no model-side override is needed.
210
     */
211
    public function addAdditionalPropertiesTransformingFilterSerializer(
48 ✔
212
        Schema $schema,
213
        GeneratorConfiguration $generatorConfiguration,
214
    ): void {
215
        $validationProperty = null;
48 ✔
216
        foreach ($schema->getBaseValidators() as $validator) {
48 ✔
217
            if (is_a($validator, AdditionalPropertiesValidator::class)) {
48 ✔
218
                $validationProperty = $validator->getValidationProperty();
36 ✔
219
            }
220
        }
221

222
        $transformingFilterValidator = null;
48 ✔
223
        $serializerClass = null;
48 ✔
224
        $serializerMethod = null;
48 ✔
225

226
        if ($validationProperty) {
48 ✔
227
            foreach ($validationProperty->getValidators() as $validator) {
36 ✔
228
                $validator = $validator->getValidator();
36 ✔
229

230
                if (
231
                    $validator instanceof FilterValidator &&
36 ✔
232
                    $validator->getFilter() instanceof TransformingFilterInterface
36 ✔
233
                ) {
234
                    $transformingFilterValidator = $validator;
9 ✔
235
                    [$serializerClass, $serializerMethod] = $validator->getFilter()->getSerializer();
9 ✔
236
                }
237
            }
238
        }
239

240
        // Only generate the model-side override when a transforming filter is present.
241
        // The trait's default _serializeAdditionalProperties handles the generic case.
242
        if (!$transformingFilterValidator) {
48 ✔
243
            return;
39 ✔
244
        }
245

246
        $schema->addMethod(
9 ✔
247
            '_serializeAdditionalProperties',
9 ✔
248
            new RenderedMethod(
9 ✔
249
                $schema,
9 ✔
250
                $generatorConfiguration,
9 ✔
251
                'Serialization/AdditionalPropertiesSerializer.phptpl',
9 ✔
252
                [
9 ✔
253
                    'serializerClass' => $serializerClass,
9 ✔
254
                    'serializerMethod' => $serializerMethod,
9 ✔
255
                    'serializerOptions' => RenderHelper::varExportArray(
9 ✔
256
                        $transformingFilterValidator->getFilterOptions(),
9 ✔
257
                    ),
9 ✔
258
                ],
9 ✔
259
            )
9 ✔
260
        );
9 ✔
261
    }
262

263
    private function addWriteOnlyExclusion(
191 ✔
264
        Schema $schema,
265
        GeneratorConfiguration $generatorConfiguration,
266
    ): void {
267
        $writeOnlyAttributes = array_map(
191 ✔
268
            static fn(PropertyInterface $property): string => $property->getAttribute(true),
191 ✔
269
            array_filter(
191 ✔
270
                $schema->getProperties(),
191 ✔
271
                static fn(PropertyInterface $property): bool => $property->isWriteOnly(),
191 ✔
272
            ),
191 ✔
273
        );
191 ✔
274

275
        if (!$writeOnlyAttributes) {
191 ✔
276
            return;
185 ✔
277
        }
278

279
        $keysExport = RenderHelper::varExportArray(array_values($writeOnlyAttributes));
9 ✔
280

281
        $schema->addSchemaHook(
9 ✔
282
            new class ($keysExport) implements SerializationHookInterface
9 ✔
283
            {
9 ✔
284
                public function __construct(private readonly string $keysExport)
285
                {}
9 ✔
286

287
                public function getCode(): string
288
                {
289
                    return sprintf(
9 ✔
290
                        <<<'CODE'
9 ✔
291
                        foreach (%s as $_writeOnlyKey) {
292
                            unset($data[$_writeOnlyKey]);
293
                        }
294
                        CODE,
9 ✔
295
                        $this->keysExport,
9 ✔
296
                    );
9 ✔
297
                }
298
            },
9 ✔
299
        );
9 ✔
300
    }
301

302
    private function addSkipNotProvidedPropertiesMap(
191 ✔
303
        Schema $schema,
304
        GeneratorConfiguration $generatorConfiguration,
305
    ): void {
306
        if ($generatorConfiguration->isImplicitNullAllowed()) {
191 ✔
307
            return;
145 ✔
308
        }
309

310
        $skipNotProvidedValues = array_map(
46 ✔
311
            static fn(PropertyInterface $property): string => $property->getName(),
46 ✔
312
            array_filter(
46 ✔
313
                $schema->getProperties(),
46 ✔
314
                static fn(PropertyInterface $property): bool =>
46 ✔
315
                    !$property->isRequired() && !$property->getDefaultValue(),
46 ✔
316
            )
46 ✔
317
        );
46 ✔
318

319
        $schema->addProperty(
46 ✔
320
            (new Property(
46 ✔
321
                'skipNotProvidedPropertiesMap',
46 ✔
322
                new PropertyType('array'),
46 ✔
323
                new JsonSchema(__FILE__, []),
46 ✔
324
                'Values which might be skipped for serialization if not provided',
46 ✔
325
            ))
46 ✔
326
                ->setDefaultValue($skipNotProvidedValues)
46 ✔
327
                ->setInternal(true),
46 ✔
328
        );
46 ✔
329
    }
330
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc