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

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

03 Apr 2026 01:13AM UTC coverage: 98.248% (-0.4%) from 98.654%
23929375043

Pull #125

github

Enno Woortmann
attribute tests
Pull Request #125: attributes

1496 of 1526 new or added lines in 66 files covered. (98.03%)

4 existing lines in 3 files now uncovered.

4374 of 4452 relevant lines covered (98.25%)

620.23 hits per line

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

98.65
/src/SchemaProcessor/SchemaProcessor.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\SchemaProcessor;
6

7
use PHPModelGenerator\Exception\SchemaException;
8
use PHPModelGenerator\Model\GeneratorConfiguration;
9
use PHPModelGenerator\Model\Property\CompositionPropertyDecorator;
10
use PHPModelGenerator\Model\Property\Property;
11
use PHPModelGenerator\Model\Property\PropertyInterface;
12
use PHPModelGenerator\Model\Property\PropertyType;
13
use PHPModelGenerator\Model\RenderJob;
14
use PHPModelGenerator\Model\Schema;
15
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
16
use PHPModelGenerator\Model\SchemaDefinition\SchemaDefinitionDictionary;
17
use PHPModelGenerator\Model\Validator;
18
use PHPModelGenerator\Model\Validator\AbstractComposedPropertyValidator;
19
use PHPModelGenerator\Model\Validator\ComposedPropertyValidator;
20
use PHPModelGenerator\Model\Validator\ConditionalPropertyValidator;
21
use PHPModelGenerator\Model\Validator\PropertyTemplateValidator;
22
use PHPModelGenerator\Model\Validator\Factory\Composition\AllOfValidatorFactory;
23
use PHPModelGenerator\Model\Validator\Factory\Composition\ComposedPropertiesValidatorFactoryInterface;
24
use PHPModelGenerator\PropertyProcessor\Decorator\Property\ObjectInstantiationDecorator;
25
use PHPModelGenerator\PropertyProcessor\Decorator\SchemaNamespaceTransferDecorator;
26
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\CompositionTypeHintDecorator;
27
use PHPModelGenerator\PropertyProcessor\PropertyFactory;
28
use PHPModelGenerator\SchemaProvider\SchemaProviderInterface;
29

30
/**
31
 * Class SchemaProcessor
32
 *
33
 * @package PHPModelGenerator\SchemaProcessor
34
 */
35
class SchemaProcessor
36
{
37
    protected string $currentClassPath;
38
    protected string $currentClassName;
39

40
    /** @var Schema[] Collect processed schemas to avoid duplicated classes */
41
    protected array $processedSchema = [];
42
    /** @var PropertyInterface[] Collect processed schemas to avoid duplicated classes */
43
    protected array $processedMergedProperties = [];
44
    /**
45
     * Global index of schemas keyed by the canonical file path or URL returned by
46
     * SchemaProviderInterface::getRef(). Used to deduplicate external $ref resolutions across
47
     * all schema processings, making class generation order-independent.
48
     *
49
     * When a $ref triggers processTopLevelSchema() for a file that the provider has not yet
50
     * reached, the canonical Schema is registered here before property processing begins. If
51
     * the provider later iterates the same file, generateModel() detects the match via the
52
     * combined file-path + content-signature check and returns the already-registered Schema
53
     * without creating a duplicate render job.
54
     *
55
     * Note: for providers such as OpenAPIv3Provider that yield multiple distinct schemas from
56
     * a single source file, each schema has a unique content signature; the signature check
57
     * prevents false-positive deduplication across schemas that merely share the same file.
58
     *
59
     * @var Schema[]
60
     */
61
    protected array $processedFileSchemas = [];
62
    /** @var string[] */
63
    protected array $generatedFiles = [];
64

65
    public function __construct(
2,202✔
66
        protected SchemaProviderInterface $schemaProvider,
67
        protected string $destination,
68
        protected GeneratorConfiguration $generatorConfiguration,
69
        protected RenderQueue $renderQueue,
70
    ) {}
2,202✔
71

72
    /**
73
     * Process a given json schema file
74
     *
75
     * @throws SchemaException
76
     */
77
    public function process(JsonSchema $jsonSchema): void
2,182✔
78
    {
79
        $this->setCurrentClassPath($jsonSchema->getFile());
2,182✔
80
        $this->currentClassName = $this->generatorConfiguration->getClassNameGenerator()->getClassName(
2,182✔
81
            str_ireplace('.json', '', basename($jsonSchema->getFile())),
2,182✔
82
            $jsonSchema,
2,182✔
83
            false,
2,182✔
84
        );
2,182✔
85

86
        $this->processSchema(
2,182✔
87
            $jsonSchema,
2,182✔
88
            $this->currentClassPath,
2,182✔
89
            $this->currentClassName,
2,182✔
90
            new SchemaDefinitionDictionary($jsonSchema),
2,182✔
91
            true,
2,182✔
92
        );
2,182✔
93
    }
94

95
    /**
96
     * Process a JSON schema stored as an associative array
97
     *
98
     * @param SchemaDefinitionDictionary $dictionary   If a nested object of a schema is processed import the
99
     *                                                 definitions of the parent schema to make them available for the
100
     *                                                 nested schema as well
101
     * @param bool                       $initialClass Is it an initial class or a nested class?
102
     *
103
     * @throws SchemaException
104
     */
105
    public function processSchema(
2,182✔
106
        JsonSchema $jsonSchema,
107
        string $classPath,
108
        string $className,
109
        SchemaDefinitionDictionary $dictionary,
110
        bool $initialClass = false,
111
    ): ?Schema {
112
        if (
113
            (!isset($jsonSchema->getJson()['type']) || $jsonSchema->getJson()['type'] !== 'object') &&
2,182✔
114
            !array_intersect(array_keys($jsonSchema->getJson()), ['anyOf', 'allOf', 'oneOf', 'if', '$ref'])
2,182✔
115
        ) {
116
            // skip the JSON schema as neither an object, a reference nor a composition is defined on the root level
117
            return null;
4✔
118
        }
119

120
        return $this->generateModel($classPath, $className, $jsonSchema, $dictionary, $initialClass);
2,182✔
121
    }
122

123
    /**
124
     * Generate a model and store the model to the file system
125
     *
126
     * @throws SchemaException
127
     */
128
    protected function generateModel(
2,182✔
129
        string $classPath,
130
        string $className,
131
        JsonSchema $jsonSchema,
132
        SchemaDefinitionDictionary $dictionary,
133
        bool $initialClass,
134
    ): Schema {
135
        $schemaSignature = $jsonSchema->getSignature();
2,182✔
136

137
        if (!$initialClass && isset($this->processedSchema[$schemaSignature])) {
2,182✔
138
            if ($this->generatorConfiguration->isOutputEnabled()) {
101✔
139
                echo "Duplicated signature $schemaSignature for class $className." .
1✔
140
                    " Redirecting to {$this->processedSchema[$schemaSignature]->getClassName()}\n";
1✔
141
            }
142

143
            return $this->processedSchema[$schemaSignature];
101✔
144
        }
145

146
        // For initial-class calls: if this exact file+content was already processed eagerly via
147
        // processTopLevelSchema() (triggered by a $ref resolution), reuse that schema to avoid a
148
        // duplicate render job. Both checks are required:
149
        // - The file-path check detects that this file was already processed via a $ref.
150
        // - The signature check ensures we do not short-circuit when a different schema shares
151
        //   the same source file (e.g. OpenAPI v3 where all component schemas are yielded from
152
        //   the same spec file — each has a unique signature).
153
        if (
154
            $initialClass
2,182✔
155
            && isset($this->processedSchema[$schemaSignature])
2,182✔
156
            && $this->getProcessedFileSchema($jsonSchema->getFile()) !== null
2,182✔
157
        ) {
158
            return $this->processedSchema[$schemaSignature];
9✔
159
        }
160

161
        $schema = new Schema(
2,182✔
162
            $this->getTargetFileName($classPath, $className),
2,182✔
163
            $classPath,
2,182✔
164
            $className,
2,182✔
165
            $jsonSchema,
2,182✔
166
            $dictionary,
2,182✔
167
            $initialClass,
2,182✔
168
            $this->generatorConfiguration,
2,182✔
169
        );
2,182✔
170

171
        // Register by content signature (secondary dedup for content-identical inline schemas).
172
        $this->processedSchema[$schemaSignature] = $schema;
2,182✔
173
        // Register by canonical file path/URL (primary dedup for external $ref resolutions).
174
        // Registering here — before property processing — ensures that any $ref back to this
175
        // file encountered while processing the referencing schema finds this canonical schema
176
        // immediately, regardless of which schema was discovered first by the provider.
177
        $this->registerProcessedFileSchema($jsonSchema->getFile(), $schema);
2,182✔
178
        $json = $jsonSchema->getJson();
2,182✔
179
        $json['type'] = 'base';
2,182✔
180

181
        (new PropertyFactory())->create(
2,182✔
182
            $this,
2,182✔
183
            $schema,
2,182✔
184
            $className,
2,182✔
185
            $jsonSchema->withJson($json),
2,182✔
186
        );
2,182✔
187

188
        $this->generateClassFile($schema);
2,121✔
189

190
        return $schema;
2,121✔
191
    }
192

193
    /**
194
     * Attach a new class file render job to the render proxy
195
     */
196
    public function generateClassFile(Schema $schema): void
2,121✔
197
    {
198
        $this->renderQueue->addRenderJob(new RenderJob($schema));
2,121✔
199

200
        if ($this->generatorConfiguration->isOutputEnabled()) {
2,121✔
201
            echo sprintf(
2✔
202
                "Generated class %s\n",
2✔
203
                join(
2✔
204
                    '\\',
2✔
205
                    array_filter([
2✔
206
                        $this->generatorConfiguration->getNamespacePrefix(),
2✔
207
                        $schema->getClassPath(),
2✔
208
                        $schema->getClassName(),
2✔
209
                    ]),
2✔
210
                ),
2✔
211
            );
2✔
212
        }
213

214
        $this->generatedFiles[] = $schema->getTargetFileName();
2,121✔
215
    }
216

217

218
    /**
219
     * Gather all nested object properties and merge them together into a single merged property
220
     *
221
     * @param CompositionPropertyDecorator[] $compositionProperties
222
     *
223
     * @throws SchemaException
224
     */
225
    public function createMergedProperty(
186✔
226
        Schema $schema,
227
        PropertyInterface $property,
228
        array $compositionProperties,
229
        JsonSchema $propertySchema,
230
    ): ?PropertyInterface {
231
        $redirectToProperty = $this->redirectMergedProperty($compositionProperties);
186✔
232
        if ($redirectToProperty === null || $redirectToProperty instanceof PropertyInterface) {
186✔
233
            if ($redirectToProperty) {
84✔
234
                $property->addTypeHintDecorator(new CompositionTypeHintDecorator($redirectToProperty));
20✔
235
            }
236

237
            return $redirectToProperty;
84✔
238
        }
239

240
        /** @var JsonSchema $jsonSchema */
241
        $jsonSchema = $propertySchema->getJson()['propertySchema'];
102✔
242
        $schemaSignature = $jsonSchema->getSignature();
102✔
243

244
        if (!isset($this->processedMergedProperties[$schemaSignature])) {
102✔
245
            $mergedClassName = $this
102✔
246
                ->getGeneratorConfiguration()
102✔
247
                ->getClassNameGenerator()
102✔
248
                ->getClassName(
102✔
249
                    $property->getName(),
102✔
250
                    $propertySchema,
102✔
251
                    true,
102✔
252
                    $this->getCurrentClassName(),
102✔
253
                );
102✔
254

255
            $mergedPropertySchema = new Schema(
102✔
256
                $this->getTargetFileName($schema->getClassPath(), $mergedClassName),
102✔
257
                $schema->getClassPath(),
102✔
258
                $mergedClassName,
102✔
259
                $propertySchema,
102✔
260
            );
102✔
261

262
            $this->processedMergedProperties[$schemaSignature] = (new Property(
102✔
263
                'MergedProperty',
102✔
264
                new PropertyType($mergedClassName),
102✔
265
                $mergedPropertySchema->getJsonSchema(),
102✔
266
            ))
102✔
267
                ->addDecorator(new ObjectInstantiationDecorator($mergedClassName, $this->getGeneratorConfiguration()))
102✔
268
                ->setNestedSchema($mergedPropertySchema);
102✔
269

270
            $this->transferPropertiesToMergedSchema($schema, $mergedPropertySchema, $compositionProperties);
102✔
271

272
            // make sure the merged schema knows all imports of the parent schema
273
            $mergedPropertySchema->addNamespaceTransferDecorator(new SchemaNamespaceTransferDecorator($schema));
102✔
274

275
            $this->generateClassFile($mergedPropertySchema);
102✔
276
        }
277

278
        $mergedSchema = $this->processedMergedProperties[$schemaSignature]->getNestedSchema();
102✔
279
        $schema->addUsedClass(
102✔
280
            join(
102✔
281
                '\\',
102✔
282
                array_filter([
102✔
283
                    $this->generatorConfiguration->getNamespacePrefix(),
102✔
284
                    $mergedSchema->getClassPath(),
102✔
285
                    $mergedSchema->getClassName(),
102✔
286
                ]),
102✔
287
            )
102✔
288
        );
102✔
289

290
        $property->addTypeHintDecorator(
102✔
291
            new CompositionTypeHintDecorator($this->processedMergedProperties[$schemaSignature]),
102✔
292
        );
102✔
293

294
        return $this->processedMergedProperties[$schemaSignature];
102✔
295
    }
296

297
    /**
298
     * Check if multiple $compositionProperties contain nested schemas. Only in this case a merged property must be
299
     * created. If no nested schemas are detected null will be returned. If only one $compositionProperty contains a
300
     * nested schema the $compositionProperty will be used as a replacement for the merged property.
301
     *
302
     * Returns false if a merged property must be created.
303
     *
304
     * @param CompositionPropertyDecorator[] $compositionProperties
305
     *
306
     * @return PropertyInterface|null|false
307
     */
308
    private function redirectMergedProperty(array $compositionProperties)
186✔
309
    {
310
        $redirectToProperty = null;
186✔
311
        foreach ($compositionProperties as $property) {
186✔
312
            if ($property->getNestedSchema()) {
186✔
313
                if ($redirectToProperty !== null) {
122✔
314
                    return false;
102✔
315
                }
316

317
                $redirectToProperty = $property;
122✔
318
            }
319
        }
320

321
        return $redirectToProperty;
84✔
322
    }
323

324
    /**
325
     * @param PropertyInterface[] $compositionProperties
326
     */
327
    private function transferPropertiesToMergedSchema(
102✔
328
        Schema $schema,
329
        Schema $mergedPropertySchema,
330
        array $compositionProperties,
331
    ): void {
332
        foreach ($compositionProperties as $property) {
102✔
333
            if (!$property->getNestedSchema()) {
102✔
334
                continue;
26✔
335
            }
336

337
            $property->getNestedSchema()->onAllPropertiesResolved(
102✔
338
                function () use ($property, $schema, $mergedPropertySchema): void {
102✔
339
                    foreach ($property->getNestedSchema()->getProperties() as $nestedProperty) {
102✔
340
                        $mergedPropertySchema->addProperty(
102✔
341
                        // don't validate fields in merged properties. All fields were validated before
342
                        // corresponding to the defined constraints of the composition property.
343
                            (clone $nestedProperty)->filterValidators(static fn(): bool => false),
102✔
344
                        );
102✔
345
                    }
346
                },
102✔
347
            );
102✔
348
        }
349
    }
350

351
    /**
352
     * Get the class path out of the file path of a schema file
353
     */
354
    protected function setCurrentClassPath(string $jsonSchemaFile): void
2,182✔
355
    {
356
        $fileDir  = str_replace('\\', '/', dirname($jsonSchemaFile));
2,182✔
357
        $baseDir  = str_replace('\\', '/', $this->schemaProvider->getBaseDirectory());
2,182✔
358
        $relative = str_replace($baseDir, '', $fileDir);
2,182✔
359

360
        // If the file is outside the provider's base directory, str_replace leaves the absolute
361
        // path untouched. In that case fall back to using just the last directory component so
362
        // the generated class path stays sensible rather than encoding an absolute path.
363
        if ($relative === $fileDir) {
2,182✔
364
            $relative = basename($fileDir);
×
365
        }
366

367
        $pieces = array_map(
2,182✔
368
            static fn(string $directory): string => ucfirst((string) preg_replace('/\W/', '', $directory)),
2,182✔
369
            explode('/', $relative),
2,182✔
370
        );
2,182✔
371

372
        $this->currentClassPath = join('\\', array_filter($pieces));
2,182✔
373
    }
374

375
    public function getCurrentClassPath(): string
980✔
376
    {
377
        return $this->currentClassPath;
980✔
378
    }
379

380
    public function getCurrentClassName(): string
960✔
381
    {
382
        return $this->currentClassName;
960✔
383
    }
384

385
    public function getGeneratedFiles(): array
2,095✔
386
    {
387
        return $this->generatedFiles;
2,095✔
388
    }
389

390
    public function getGeneratorConfiguration(): GeneratorConfiguration
2,191✔
391
    {
392
        return $this->generatorConfiguration;
2,191✔
393
    }
394

395
    public function getSchemaProvider(): SchemaProviderInterface
208✔
396
    {
397
        return $this->schemaProvider;
208✔
398
    }
399

400
    public function getProcessedFileSchema(string $fileKey): ?Schema
203✔
401
    {
402
        return $this->processedFileSchemas[$this->normaliseFileKey($fileKey)] ?? null;
203✔
403
    }
404

405
    public function registerProcessedFileSchema(string $fileKey, Schema $schema): void
2,182✔
406
    {
407
        $this->processedFileSchemas[$this->normaliseFileKey($fileKey)] = $schema;
2,182✔
408
    }
409

410
    /**
411
     * Normalise a file path or URL to a consistent key for processedFileSchemas.
412
     * On Windows, RecursiveDirectoryIterator may produce backslash-separated paths while
413
     * RefResolverTrait produces forward-slash paths for the same file. Normalising to forward
414
     * slashes ensures the two representations map to the same key.
415
     */
416
    private function normaliseFileKey(string $fileKey): string
2,182✔
417
    {
418
        return str_replace('\\', '/', $fileKey);
2,182✔
419
    }
420

421
    /**
422
     * Process an external schema file with its canonical class name and path, exactly as
423
     * process() would, but without overwriting the current class path / class name context
424
     * (which belongs to the schema that triggered the $ref resolution).
425
     *
426
     * Returns the resulting Schema, or null if the file does not define an object/composition.
427
     *
428
     * @throws SchemaException
429
     */
430
    public function processTopLevelSchema(JsonSchema $jsonSchema): ?Schema
13✔
431
    {
432
        $savedClassPath  = $this->currentClassPath;
13✔
433
        $savedClassName  = $this->currentClassName;
13✔
434

435
        $this->setCurrentClassPath($jsonSchema->getFile());
13✔
436
        $this->currentClassName = $this->generatorConfiguration->getClassNameGenerator()->getClassName(
13✔
437
            str_ireplace('.json', '', basename($jsonSchema->getFile())),
13✔
438
            $jsonSchema,
13✔
439
            false,
13✔
440
        );
13✔
441

442
        $schema = $this->processSchema(
13✔
443
            $jsonSchema,
13✔
444
            $this->currentClassPath,
13✔
445
            $this->currentClassName,
13✔
446
            new SchemaDefinitionDictionary($jsonSchema),
13✔
447
            true,
13✔
448
        );
13✔
449

450
        $this->currentClassPath = $savedClassPath;
13✔
451
        $this->currentClassName = $savedClassName;
13✔
452

453
        return $schema;
13✔
454
    }
455

456
    /**
457
     * Transfer properties of composed properties to the given schema to offer a complete model
458
     * including all composed properties.
459
     *
460
     * This is an internal pipeline mechanic (Q5.1): not a JSON Schema keyword and therefore not
461
     * a Draft modifier. It is called as an explicit post-step from generateModel after all Draft
462
     * modifiers have run on the root-level BaseProperty.
463
     *
464
     * @throws SchemaException
465
     */
466
    public function transferComposedPropertiesToSchema(PropertyInterface $property, Schema $schema): void
2,122✔
467
    {
468
        foreach ($property->getValidators() as $validator) {
2,122✔
469
            $validator = $validator->getValidator();
364✔
470

471
            if (!is_a($validator, AbstractComposedPropertyValidator::class)) {
364✔
NEW
472
                continue;
×
473
            }
474

475
            // If the transferred validator of the composed property is also a composed property
476
            // strip the nested composition validations from the added validator. The nested
477
            // composition will be validated in the object generated for the nested composition
478
            // which will be executed via an instantiation. Consequently, the validation must not
479
            // be executed in the outer composition.
480
            $schema->addBaseValidator(
364✔
481
                ($validator instanceof ComposedPropertyValidator)
364✔
482
                    ? $validator->withoutNestedCompositionValidation()
328✔
483
                    : $validator,
364✔
484
            );
364✔
485

486
            if (
487
                !is_a(
364✔
488
                    $validator->getCompositionProcessor(),
364✔
489
                    ComposedPropertiesValidatorFactoryInterface::class,
364✔
490
                    true,
364✔
491
                )
364✔
492
            ) {
NEW
493
                continue;
×
494
            }
495

496
            $branchesForValidator = $validator instanceof ConditionalPropertyValidator
364✔
497
                ? $validator->getConditionBranches()
54✔
498
                : $validator->getComposedProperties();
328✔
499

500
            $totalBranches = count($branchesForValidator);
364✔
501
            $resolvedPropertiesCallbacks = 0;
364✔
502
            $seenBranchPropertyNames = [];
364✔
503

504
            foreach ($validator->getComposedProperties() as $composedProperty) {
364✔
505
                $composedProperty->onResolve(function () use (
363✔
506
                    $composedProperty,
363✔
507
                    $property,
363✔
508
                    $validator,
363✔
509
                    $branchesForValidator,
363✔
510
                    $totalBranches,
363✔
511
                    $schema,
363✔
512
                    &$resolvedPropertiesCallbacks,
363✔
513
                    &$seenBranchPropertyNames,
363✔
514
                ): void {
363✔
515
                    if (!$composedProperty->getNestedSchema()) {
363✔
516
                        throw new SchemaException(
1✔
517
                            sprintf(
1✔
518
                                "No nested schema for composed property %s in file %s found",
1✔
519
                                $property->getName(),
1✔
520
                                $property->getJsonSchema()->getFile(),
1✔
521
                            )
1✔
522
                        );
1✔
523
                    }
524

525
                    $isBranchForValidator = in_array($composedProperty, $branchesForValidator, true);
362✔
526

527
                    $composedProperty->getNestedSchema()->onAllPropertiesResolved(
362✔
528
                        function () use (
362✔
529
                            $composedProperty,
362✔
530
                            $validator,
362✔
531
                            $isBranchForValidator,
362✔
532
                            $totalBranches,
362✔
533
                            $schema,
362✔
534
                            &$resolvedPropertiesCallbacks,
362✔
535
                            &$seenBranchPropertyNames,
362✔
536
                        ): void {
362✔
537
                            foreach ($composedProperty->getNestedSchema()->getProperties() as $branchProperty) {
362✔
538
                                $schema->addProperty(
360✔
539
                                    $this->cloneTransferredProperty(
360✔
540
                                        $branchProperty,
360✔
541
                                        $composedProperty,
360✔
542
                                        $validator,
360✔
543
                                    ),
360✔
544
                                    $validator->getCompositionProcessor(),
360✔
545
                                );
360✔
546

547
                                $composedProperty->appendAffectedObjectProperty($branchProperty);
358✔
548
                                $seenBranchPropertyNames[$branchProperty->getName()] = true;
358✔
549
                            }
550

551
                            if ($isBranchForValidator && ++$resolvedPropertiesCallbacks === $totalBranches) {
360✔
552
                                foreach (array_keys($seenBranchPropertyNames) as $branchPropertyName) {
359✔
553
                                    $schema->getPropertyMerger()->checkForTotalConflict(
357✔
554
                                        $branchPropertyName,
357✔
555
                                        $totalBranches,
357✔
556
                                    );
357✔
557
                                }
558
                            }
559
                        },
362✔
560
                    );
362✔
561
                });
363✔
562
            }
563
        }
564
    }
565

566
    /**
567
     * Clone the provided property to transfer it to a schema. Sets the nullability and required
568
     * flag based on the composition processor used to set up the composition. Widens the type to
569
     * mixed when the property is exclusive to one anyOf/oneOf branch and at least one other branch
570
     * allows additional properties, preventing TypeError when raw input values of an arbitrary
571
     * type are stored in the property slot.
572
     */
573
    private function cloneTransferredProperty(
360✔
574
        PropertyInterface $property,
575
        CompositionPropertyDecorator $sourceBranch,
576
        AbstractComposedPropertyValidator $validator,
577
    ): PropertyInterface {
578
        $compositionProcessor = $validator->getCompositionProcessor();
360✔
579

580
        $transferredProperty = (clone $property)
360✔
581
            ->filterValidators(static fn(Validator $v): bool =>
360✔
582
                is_a($v->getValidator(), PropertyTemplateValidator::class));
360✔
583

584
        if (!is_a($compositionProcessor, AllOfValidatorFactory::class, true)) {
360✔
585
            $transferredProperty->setRequired(false);
250✔
586

587
            if ($transferredProperty->getType()) {
250✔
588
                $transferredProperty->setType(
225✔
589
                    new PropertyType($transferredProperty->getType()->getNames(), true),
225✔
590
                    new PropertyType($transferredProperty->getType(true)->getNames(), true),
225✔
591
                );
225✔
592
            }
593

594
            $wideningBranches = $validator instanceof ConditionalPropertyValidator
250✔
595
                ? $validator->getConditionBranches()
54✔
596
                : $validator->getComposedProperties();
196✔
597

598
            if ($this->exclusiveBranchPropertyNeedsWidening($property->getName(), $sourceBranch, $wideningBranches)) {
250✔
599
                $transferredProperty->setType(null, null, reset: true);
184✔
600
            }
601
        }
602

603
        return $transferredProperty;
360✔
604
    }
605

606
    /**
607
     * Returns true when the property named $propertyName is exclusive to $sourceBranch and at
608
     * least one other anyOf/oneOf branch allows additional properties (i.e. does NOT declare
609
     * additionalProperties: false). In that case the property slot can receive an
610
     * arbitrarily-typed raw input value from a non-matching branch, so the type hint is removed.
611
     *
612
     * Returns false when the property appears in another branch too (Schema::addProperty handles
613
     * that via type merging) or when all other branches have additionalProperties: false (making
614
     * the property mutually exclusive with the other branches' properties).
615
     *
616
     * @param CompositionPropertyDecorator[] $allBranches
617
     */
618
    private function exclusiveBranchPropertyNeedsWidening(
250✔
619
        string $propertyName,
620
        CompositionPropertyDecorator $sourceBranch,
621
        array $allBranches,
622
    ): bool {
623
        foreach ($allBranches as $branch) {
250✔
624
            if ($branch === $sourceBranch) {
250✔
625
                continue;
250✔
626
            }
627

628
            $branchPropertyNames = $branch->getNestedSchema()
249✔
629
                ? array_map(
249✔
630
                    static fn(PropertyInterface $p): string => $p->getName(),
249✔
631
                    $branch->getNestedSchema()->getProperties(),
249✔
632
                )
249✔
NEW
633
                : [];
×
634

635
            if (in_array($propertyName, $branchPropertyNames, true)) {
249✔
636
                return false;
106✔
637
            }
638
        }
639

640
        foreach ($allBranches as $branch) {
186✔
641
            if ($branch === $sourceBranch) {
186✔
642
                continue;
145✔
643
            }
644

645
            if (($branch->getBranchSchema()->getJson()['additionalProperties'] ?? true) !== false) {
185✔
646
                return true;
184✔
647
            }
648
        }
649

650
        return false;
16✔
651
    }
652

653
    private function getTargetFileName(string $classPath, string $className): string
2,182✔
654
    {
655
        return join(
2,182✔
656
            DIRECTORY_SEPARATOR,
2,182✔
657
            array_filter([$this->destination, str_replace('\\', DIRECTORY_SEPARATOR, $classPath), $className]),
2,182✔
658
        ) . '.php';
2,182✔
659
    }
660
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc