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

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

18 Jul 2026 09:07PM UTC coverage: 98.744% (-0.05%) from 98.79%
29661031835

push

github

web-flow
Merge pull request #165 from wol-soft/claude/validation-error-messages-c73hox

Validation error message readability (issue #131): jsonPointer + real array-item names

72 of 76 new or added lines in 7 files covered. (94.74%)

7078 of 7168 relevant lines covered (98.74%)

552.49 hits per line

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

98.53
/src/Model/Property/Property.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace PHPModelGenerator\Model\Property;
6

7
use PHPModelGenerator\Attributes\Internal;
8
use PHPModelGenerator\Exception\SchemaException;
9
use PHPModelGenerator\Model\Attributes\PhpAttribute;
10
use PHPModelGenerator\Model\Schema;
11
use PHPModelGenerator\Model\SchemaDefinition\JsonSchema;
12
use PHPModelGenerator\Model\Validator;
13
use PHPModelGenerator\Model\Validator\PropertyValidatorInterface;
14
use PHPModelGenerator\PropertyProcessor\Decorator\Property\PropertyDecoratorInterface;
15
use PHPModelGenerator\PropertyProcessor\Decorator\TypeHint\TypeHintDecoratorInterface;
16

17
/**
18
 * Class Property
19
 *
20
 * @package PHPModelGenerator\Model\Property
21
 */
22
class Property extends AbstractProperty
23
{
24
    /** @var PropertyType|null */
25
    protected $outputType;
26
    /** @var bool */
27
    protected $isPropertyRequired = true;
28
    /** @var bool */
29
    protected $isPropertyArrayItem = false;
30
    /** @var bool */
31
    protected $isPropertyReadOnly = false;
32
    /** @var bool */
33
    protected $isPropertyWriteOnly = false;
34
    /** @var bool */
35
    protected $isPropertyInternal = false;
36
    /** @var string|null */
37
    protected $comment = null;
38
    /** @var array */
39
    protected $examples = [];
40
    /** @var mixed */
41
    protected $defaultValue;
42

43
    /** @var Validator[] */
44
    protected $validators = [];
45
    /** @var Schema */
46
    protected $nestedSchema;
47
    /** @var PropertyDecoratorInterface[] */
48
    public $decorators = [];
49
    /** @var TypeHintDecoratorInterface[] */
50
    public $typeHintDecorators = [];
51

52
    private array $renderedTypeHints = [];
53
    /** Track the amount of unresolved validators */
54
    private int $pendingValidators = 0;
55

56
    /**
57
     * Property constructor.
58
     *
59
     * @throws SchemaException
60
     */
61
    public function __construct(
2,647✔
62
        string $name,
63
        protected ?PropertyType $type,
64
        JsonSchema $jsonSchema,
65
        protected string $description = '',
66
    ) {
67
        parent::__construct($name, $jsonSchema);
2,647✔
68

69
        $this->resolve();
2,647✔
70
    }
71

72
    /**
73
     * @inheritdoc
74
     */
75
    public function getType(bool $outputType = false): ?PropertyType
2,504✔
76
    {
77
        // If the output type differs from the input type (transforming filter case), return a union
78
        // so the setter can express that it accepts either the raw input or the already-transformed value.
79
        if (
80
            !$outputType
2,504✔
81
            && $this->type
2,504✔
82
            && $this->outputType
2,504✔
83
            && $this->outputType->getNames() !== $this->type->getNames()
2,504✔
84
        ) {
85
            return new PropertyType(
103✔
86
                array_merge($this->type->getNames(), $this->outputType->getNames()),
103✔
87
                $this->type->isNullable() ?? $this->outputType->isNullable(),
103✔
88
            );
103✔
89
        }
90

91
        return $outputType && $this->outputType !== null ? $this->outputType : $this->type;
2,504✔
92
    }
93

94
    /**
95
     * @inheritdoc
96
     */
97
    public function setType(
1,531✔
98
        ?PropertyType $type = null,
99
        ?PropertyType $outputType = null,
100
        bool $reset = false,
101
    ): PropertyInterface {
102
        if ($reset) {
1,531✔
103
            $this->typeHintDecorators = [];
230✔
104
        }
105

106
        $this->type = $type;
1,531✔
107
        $this->outputType = $outputType;
1,531✔
108

109
        return $this;
1,531✔
110
    }
111

112
    /**
113
     * @inheritdoc
114
     */
115
    public function getTypeHint(bool $outputType = false, array $skipDecorators = []): string
2,438✔
116
    {
117
        if (isset($this->renderedTypeHints[$outputType])) {
2,438✔
118
            return $this->renderedTypeHints[$outputType];
2,430✔
119
        }
120

121
        static $skipDec = [];
2,438✔
122

123
        $additionalSkips = array_diff($skipDecorators, $skipDec);
2,438✔
124
        $skipDec = array_merge($skipDec, $additionalSkips);
2,438✔
125

126
        $input = [$outputType && $this->outputType !== null ? $this->outputType : $this->type];
2,438✔
127

128
        // If the output type differs from an input type also accept the output type
129
        if (!$outputType && $this->outputType !== null && $this->outputType !== $this->type) {
2,438✔
130
            $input = [$this->type, $this->outputType];
117✔
131
        }
132

133
        $input = join(
2,438✔
134
            '|',
2,438✔
135
            array_filter(array_map(function (?PropertyType $input) use ($outputType, $skipDec): string {
2,438✔
136
                $typeHint = $input ? $input->getNames()[0] : '';
2,438✔
137

138
                $filteredDecorators = array_filter(
2,438✔
139
                    $this->typeHintDecorators,
2,438✔
140
                    static fn(TypeHintDecoratorInterface $decorator): bool => !in_array($decorator::class, $skipDec),
2,438✔
141
                );
2,438✔
142

143
                foreach ($filteredDecorators as $decorator) {
2,438✔
144
                    $typeHint = $decorator->decorate($typeHint, $outputType);
973✔
145
                }
146

147
                return $typeHint;
2,438✔
148
            }, $input)),
2,438✔
149
        );
2,438✔
150

151
        $skipDec = array_diff($skipDec, $additionalSkips);
2,438✔
152

153
        return $this->renderedTypeHints[$outputType] = $input ?: 'mixed';
2,438✔
154
    }
155

156
    /**
157
     * @inheritdoc
158
     */
159
    public function addTypeHintDecorator(TypeHintDecoratorInterface $typeHintDecorator): PropertyInterface
1,347✔
160
    {
161
        $this->typeHintDecorators[] = $typeHintDecorator;
1,347✔
162
        $this->renderedTypeHints = [];
1,347✔
163

164
        return $this;
1,347✔
165
    }
166

167
    /**
168
     * @inheritdoc
169
     */
170
    public function getDescription(): string
2,430✔
171
    {
172
        return $this->description;
2,430✔
173
    }
174

175
    public function getComment(): ?string
2,387✔
176
    {
177
        return $this->comment;
2,387✔
178
    }
179

180
    public function setComment(string $comment): PropertyInterface
3✔
181
    {
182
        $this->comment = $comment;
3✔
183

184
        return $this;
3✔
185
    }
186

187
    public function getExamples(): array
2,387✔
188
    {
189
        return $this->examples;
2,387✔
190
    }
191

192
    public function setExamples(array $examples): PropertyInterface
5✔
193
    {
194
        $this->examples = $examples;
5✔
195

196
        return $this;
5✔
197
    }
198

199
    /**
200
     * @inheritdoc
201
     */
202
    public function addValidator(
2,546✔
203
        PropertyValidatorInterface $validator,
204
        int $priority = 99,
205
        ?string $sourceKey = null,
206
    ): PropertyInterface {
207
        if (!$validator->isResolved()) {
2,546✔
208
            $this->isResolved = false;
124✔
209

210
            $this->pendingValidators++;
124✔
211

212
            $validator->onResolve(function (): void {
124✔
213
                if (--$this->pendingValidators === 0) {
76✔
214
                    $this->resolve();
76✔
215
                }
216
            });
124✔
217
        }
218

219
        $wrapper = new Validator($validator, $priority);
2,546✔
220
        if ($sourceKey !== null) {
2,546✔
221
            $wrapper->setSourceKey($sourceKey);
45✔
222
        }
223

224
        $this->validators[] = $wrapper;
2,546✔
225

226
        return $this;
2,546✔
227
    }
228

229
    /**
230
     * @inheritdoc
231
     */
232
    public function getValidators(): array
2,628✔
233
    {
234
        return $this->validators;
2,628✔
235
    }
236

237
    /**
238
     * @inheritdoc
239
     */
240
    public function filterValidators(callable $filter): PropertyInterface
1,594✔
241
    {
242
        $this->validators = array_filter($this->validators, $filter);
1,594✔
243

244
        return $this;
1,594✔
245
    }
246

247
    /**
248
     * @inheritdoc
249
     */
250
    public function getOrderedValidators(): array
2,439✔
251
    {
252
        usort(
2,439✔
253
            $this->validators,
2,439✔
254
            static fn(Validator $validator, Validator $comparedValidator): int =>
2,439✔
255
                $validator->getPriority() <=> $comparedValidator->getPriority(),
2,439✔
256
        );
2,439✔
257

258
        return array_map(
2,439✔
259
            static fn(Validator $validator): PropertyValidatorInterface => $validator->getValidator(),
2,439✔
260
            $this->validators,
2,439✔
261
        );
2,439✔
262
    }
263

264
    /**
265
     * @inheritdoc
266
     */
267
    public function addDecorator(PropertyDecoratorInterface $decorator): PropertyInterface
1,246✔
268
    {
269
        $this->decorators[] = $decorator;
1,246✔
270

271
        return $this;
1,246✔
272
    }
273

274
    /**
275
     * @inheritdoc
276
     */
277
    public function filterDecorators(callable $filter): PropertyInterface
408✔
278
    {
279
        $this->decorators = array_values(array_filter($this->decorators, $filter));
408✔
280

281
        return $this;
408✔
282
    }
283

284
    /**
285
     * @inheritdoc
286
     */
287
    public function resolveDecorator(string $input, bool $nestedProperty): string
800✔
288
    {
289
        foreach ($this->decorators as $decorator) {
800✔
290
            $input = $decorator->decorate($input, $this, $nestedProperty);
800✔
291
        }
292

293
        return $input;
800✔
294
    }
295

296
    /**
297
     * @inheritdoc
298
     */
299
    public function getDecorators(): array
2,427✔
300
    {
301
        return $this->decorators;
2,427✔
302
    }
303

304
    /**
305
     * @inheritdoc
306
     */
307
    public function setRequired(bool $isPropertyRequired): PropertyInterface
2,587✔
308
    {
309
        $this->isPropertyRequired = $isPropertyRequired;
2,587✔
310

311
        return $this;
2,587✔
312
    }
313

314
    /**
315
     * @inheritdoc
316
     */
317
    public function setArrayItem(bool $isArrayItem): PropertyInterface
2,577✔
318
    {
319
        $this->isPropertyArrayItem = $isArrayItem;
2,577✔
320

321
        return $this;
2,577✔
322
    }
323

324
    /**
325
     * @inheritdoc
326
     */
NEW
327
    public function isArrayItem(): bool
×
328
    {
NEW
329
        return $this->isPropertyArrayItem;
×
330
    }
331

332
    /**
333
     * @inheritdoc
334
     */
335
    public function setReadOnly(bool $isPropertyReadOnly): PropertyInterface
2,577✔
336
    {
337
        $this->isPropertyReadOnly = $isPropertyReadOnly;
2,577✔
338

339
        return $this;
2,577✔
340
    }
341

342
    /**
343
     * @inheritdoc
344
     */
345
    public function setWriteOnly(bool $isPropertyWriteOnly): PropertyInterface
2,577✔
346
    {
347
        $this->isPropertyWriteOnly = $isPropertyWriteOnly;
2,577✔
348

349
        return $this;
2,577✔
350
    }
351

352
    /**
353
     * @inheritdoc
354
     */
355
    public function setDefaultValue($defaultValue, bool $raw = false): PropertyInterface
642✔
356
    {
357
        $this->defaultValue = $defaultValue !== null && !$raw ? var_export($defaultValue, true) : $defaultValue;
642✔
358

359
        return $this;
642✔
360
    }
361

362
    /**
363
     * @inheritdoc
364
     */
365
    public function getDefaultValue(): ?string
2,459✔
366
    {
367
        return $this->defaultValue;
2,459✔
368
    }
369

370
    /**
371
     * @inheritdoc
372
     */
373
    public function isRequired(): bool
2,565✔
374
    {
375
        return $this->isPropertyRequired || $this->isPropertyArrayItem;
2,565✔
376
    }
377

378
    /**
379
     * @inheritdoc
380
     */
381
    public function isReadOnly(): bool
2,390✔
382
    {
383
        return $this->isPropertyReadOnly;
2,390✔
384
    }
385

386
    /**
387
     * @inheritdoc
388
     */
389
    public function isWriteOnly(): bool
2,393✔
390
    {
391
        return $this->isPropertyWriteOnly;
2,393✔
392
    }
393

394
    /**
395
     * @inheritdoc
396
     */
397
    public function setNestedSchema(Schema $schema): PropertyInterface
1,041✔
398
    {
399
        $this->nestedSchema = $schema;
1,041✔
400
        return $this;
1,041✔
401
    }
402

403
    /**
404
     * @inheritdoc
405
     */
406
    public function getNestedSchema(): ?Schema
1,517✔
407
    {
408
        return $this->nestedSchema;
1,517✔
409
    }
410

411
    /**
412
     * @inheritdoc
413
     */
414
    public function setInternal(bool $isPropertyInternal): PropertyInterface
582✔
415
    {
416
        // Internal is a one-way flag: once set to true the #[Internal] attribute has been
417
        // added to the property's attribute list and cannot be removed. Subsequent calls
418
        // with false (or repeated calls with true) are therefore silently ignored.
419
        if ($isPropertyInternal && !$this->isPropertyInternal) {
582✔
420
            $this->isPropertyInternal = true;
582✔
421
            $this->addAttribute(new PhpAttribute(Internal::class));
582✔
422
        }
423

424
        return $this;
582✔
425
    }
426

427
    /**
428
     * @inheritdoc
429
     */
430
    public function isInternal(): bool
2,479✔
431
    {
432
        return $this->isPropertyInternal;
2,479✔
433
    }
434
}
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