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

orisai / object-mapper / 29745748821

20 Jul 2026 09:29AM UTC coverage: 88.132%. First build
29745748821

push

github

mabar
PHPStan: analysis fixes

12 of 19 new or added lines in 4 files covered. (63.16%)

5859 of 6648 relevant lines covered (88.13%)

121.05 hits per line

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

96.1
/src/Processing/DefaultProcessor.php
1
<?php declare(strict_types = 1);
2

3
namespace Orisai\ObjectMapper\Processing;
4

5
use Closure;
6
use Nette\Utils\Helpers;
7
use Orisai\ObjectMapper\Args\Args;
8
use Orisai\ObjectMapper\Callbacks\AfterMappingCallback;
9
use Orisai\ObjectMapper\Callbacks\AfterValidationCallback;
10
use Orisai\ObjectMapper\Callbacks\BeforeValidationCallback;
11
use Orisai\ObjectMapper\Callbacks\Callback;
12
use Orisai\ObjectMapper\Callbacks\Context\CallbackBaseContext;
13
use Orisai\ObjectMapper\Callbacks\Context\FieldContext;
14
use Orisai\ObjectMapper\Callbacks\Context\ObjectContext;
15
use Orisai\ObjectMapper\Exception\InvalidData;
16
use Orisai\ObjectMapper\Exception\ValueDoesNotMatch;
17
use Orisai\ObjectMapper\MappedObject;
18
use Orisai\ObjectMapper\Meta\MetaLoader;
19
use Orisai\ObjectMapper\Meta\Runtime\ClassRuntimeMeta;
20
use Orisai\ObjectMapper\Meta\Runtime\FieldRuntimeMeta;
21
use Orisai\ObjectMapper\Meta\Runtime\NodeRuntimeMeta;
22
use Orisai\ObjectMapper\Meta\Runtime\RuntimeMeta;
23
use Orisai\ObjectMapper\Processing\Context\DynamicContext;
24
use Orisai\ObjectMapper\Processing\Context\ProcessorCallContext;
25
use Orisai\ObjectMapper\Processing\Context\PropertyContext;
26
use Orisai\ObjectMapper\Processing\Context\ServicesContext;
27
use Orisai\ObjectMapper\Rules\MappedObjectArgs;
28
use Orisai\ObjectMapper\Rules\MappedObjectRule;
29
use Orisai\ObjectMapper\Rules\RuleManager;
30
use Orisai\ObjectMapper\Types\MappedObjectType;
31
use Orisai\ObjectMapper\Types\MessageType;
32
use function array_key_exists;
33
use function array_keys;
34
use function array_map;
35
use function assert;
36
use function is_array;
37

38
final class DefaultProcessor implements Processor
39
{
40

41
        private MetaLoader $metaLoader;
42

43
        private RuleManager $ruleManager;
44

45
        private ObjectCreator $objectCreator;
46

47
        private RawValuesMap $rawValuesMap;
48

49
        private ServicesContext $services;
50

51
        /** @var Closure(MappedObject, string, mixed): void */
52
        private Closure $setFunction;
53

54
        /** @var Closure(MappedObject, string): void */
55
        private Closure $unsetFunction;
56

57
        /** @var array<class-string<MappedObject>, RuntimeMeta> */
58
        private array $metaCache = [];
59

60
        /** @var array<class-string, array<string, PropertyContext>> */
61
        private array $propertyContextCache = [];
62

63
        public function __construct(MetaLoader $metaLoader, RuleManager $ruleManager, ObjectCreator $objectCreator)
64
        {
65
                $this->metaLoader = $metaLoader;
3,108✔
66
                $this->ruleManager = $ruleManager;
3,108✔
67
                $this->objectCreator = $objectCreator;
3,108✔
68
                $this->rawValuesMap = new RawValuesMap();
3,108✔
69
                $this->services = new ServicesContext($metaLoader, $ruleManager, $this);
3,108✔
70
                // phpcs:disable SlevomatCodingStandard.Functions.StaticClosure
71
                $this->setFunction = function (MappedObject $object, string $name, $value): void {
3,108✔
72
                        $object->$name = $value;
33✔
73
                };
3,108✔
74
                $this->unsetFunction = function (MappedObject $object, string $name): void {
3,108✔
75
                        unset($object->$name);
×
76
                };
3,108✔
77
                // phpcs:enable
78
        }
79

80
        public function reset(): void
81
        {
82
                $this->metaCache = [];
×
83
                $this->propertyContextCache = [];
×
84
                $this->objectCreator->reset();
×
85
        }
86

87
        /**
88
         * @param mixed $data
89
         * @throws InvalidData
90
         */
91
        public function process($data, string $class, ?Options $options = null): MappedObject
92
        {
93
                [
424✔
94
                        $processedData,
424✔
95
                        $call,
424✔
96
                        $dynamic,
424✔
97
                ] = $this->processBase($data, $class, $options, true);
424✔
98

99
                $object = $call->getObjectHolder()->getInstance();
336✔
100
                $this->fillObject($object, $processedData, $data, $call, $dynamic);
336✔
101

102
                $meta = $call->getMeta();
336✔
103
                $classMeta = $meta->class;
336✔
104

105
                if ($classMeta->callbacks !== []) {
336✔
106
                        $callbackContext = new ObjectContext($this->services, $dynamic, $call);
48✔
107

108
                        $this->handleClassCallbacks(
48✔
109
                                [],
48✔
110
                                $call,
48✔
111
                                $callbackContext,
48✔
112
                                $classMeta,
48✔
113
                                AfterMappingCallback::class,
48✔
114
                        );
48✔
115
                }
116

117
                return $object;
336✔
118
        }
119

120
        /**
121
         * @param mixed                      $data
122
         * @param class-string<MappedObject> $class
123
         * @return array<int|string, mixed>
124
         * @throws InvalidData
125
         */
126
        public function processWithoutMapping($data, string $class, ?Options $options = null): array
127
        {
128
                [$processedData] = $this->processBase($data, $class, $options, false);
64✔
129

130
                return $processedData;
40✔
131
        }
132

133
        /**
134
         * @template T of MappedObject
135
         * @param mixed           $data
136
         * @param class-string<T> $class
137
         * @return array{array<mixed>, ProcessorCallContext<T>, DynamicContext}
138
         * @throws InvalidData
139
         */
140
        private function processBase($data, string $class, ?Options $options, bool $initializeObjects): array
141
        {
142
                $meta = $this->metaCache[$class] ??= $this->metaLoader->load($class);
464✔
143

144
                $options ??= new Options();
464✔
145

146
                $dynamic = new DynamicContext($options, $initializeObjects);
464✔
147
                $call = new ProcessorCallContext(
464✔
148
                        new ObjectHolder($this->objectCreator, $class, $meta->class),
464✔
149
                        $meta,
464✔
150
                        fn (): MappedObjectType => $this->createMappedObjectType($class, $dynamic),
464✔
151
                );
464✔
152

153
                $processedData = $this->processData($data, $call, $dynamic);
464✔
154

155
                return [$processedData, $call, $dynamic];
352✔
156
        }
157

158
        // /////////////// //
159
        // Base processing //
160
        // /////////////// //
161

162
        /**
163
         * @param mixed                              $data
164
         * @param ProcessorCallContext<MappedObject> $call
165
         * @return array<int|string, mixed>
166
         * @throws InvalidData
167
         */
168
        private function processData(
169
                $data,
170
                ProcessorCallContext $call,
171
                DynamicContext $dynamic
172
        ): array
173
        {
174
                $meta = $call->getMeta();
464✔
175
                $classMeta = $meta->class;
464✔
176

177
                if ($classMeta->callbacks !== []) {
464✔
178
                        $callbackContext = new ObjectContext($this->services, $dynamic, $call);
96✔
179

180
                        $data = $this->handleClassCallbacks(
96✔
181
                                $data,
96✔
182
                                $call,
96✔
183
                                $callbackContext,
96✔
184
                                $classMeta,
96✔
185
                                BeforeValidationCallback::class,
96✔
186
                        );
96✔
187
                }
188

189
                $data = $this->ensureDataProcessable($data, $call);
456✔
190
                $data = $this->handleFields($data, $call, $dynamic);
416✔
191

192
                if (isset($callbackContext)) {
376✔
193
                        $data = $this->handleClassCallbacks(
72✔
194
                                $data,
72✔
195
                                $call,
72✔
196
                                $callbackContext,
72✔
197
                                $classMeta,
72✔
198
                                AfterValidationCallback::class,
72✔
199
                        );
72✔
200
                        assert(is_array($data)); // After class callbacks are forced to return array
48✔
201
                }
202

203
                return $data;
352✔
204
        }
205

206
        /**
207
         * @param class-string<MappedObject> $class
208
         */
209
        private function createMappedObjectType(string $class, DynamicContext $dynamic): MappedObjectType
210
        {
211
                return $this->ruleManager->getRule(MappedObjectRule::class)->createType(
152✔
212
                        new MappedObjectArgs($class),
152✔
213
                        $this->services,
152✔
214
                        $dynamic,
152✔
215
                );
152✔
216
        }
217

218
        /**
219
         * @param mixed $data
220
         * @param ProcessorCallContext<MappedObject> $context
221
         * @return array<mixed>
222
         * @throws InvalidData
223
         */
224
        private function ensureDataProcessable($data, ProcessorCallContext $context): array
225
        {
226
                if (!is_array($data)) {
456✔
227
                        $type = $context->getType();
80✔
228
                        $type->markInvalid();
80✔
229

230
                        throw InvalidData::create($type, Value::of($data));
80✔
231
                }
232

233
                return $data;
416✔
234
        }
235

236
        // /////////////////// //
237
        // Properties / Fields //
238
        // /////////////////// //
239

240
        /**
241
         * @param array<int|string, mixed>           $data
242
         * @param ProcessorCallContext<MappedObject> $call
243
         * @return array<int|string, mixed>
244
         * @throws InvalidData
245
         */
246
        private function handleFields(
247
                array $data,
248
                ProcessorCallContext $call,
249
                DynamicContext $dynamic
250
        ): array
251
        {
252
                $meta = $call->getMeta();
416✔
253
                $fieldsMeta = $meta->fields;
416✔
254

255
                $data = $this->handleSentFields($data, $call, $dynamic, $fieldsMeta);
416✔
256
                $data = $this->handleMissingFields($data, $call, $dynamic, $fieldsMeta);
416✔
257

258
                $type = $call->getTypeIfInitialized();
416✔
259

260
                if ($type !== null && $type->hasInvalidFields()) {
416✔
261
                        throw InvalidData::create($type, Value::none());
48✔
262
                }
263

264
                return $data;
376✔
265
        }
266

267
        /**
268
         * @param array<int|string, mixed>           $data
269
         * @param ProcessorCallContext<MappedObject> $call
270
         * @param array<int|string, FieldRuntimeMeta> $fieldsMeta
271
         * @param-out array<int|string, FieldRuntimeMeta> $fieldsMeta
272
         * @return array<int|string, mixed>
273
         */
274
        private function handleSentFields(
275
                array $data,
276
                ProcessorCallContext $call,
277
                DynamicContext $dynamic,
278
                array &$fieldsMeta
279
        ): array
280
        {
281
                $type = null;
416✔
282
                $options = $dynamic->getOptions();
416✔
283

284
                $hintedFieldNames = null;
416✔
285

286
                foreach ($data as $fieldName => $value) {
416✔
287
                        // Skip invalid field
288
                        if ($type !== null && $type->isFieldInvalid($fieldName)) {
352✔
289
                                unset($fieldsMeta[$fieldName]); // Remaining fields are handled as missing
×
290

291
                                continue;
×
292
                        }
293

294
                        $fieldMeta = $fieldsMeta[$fieldName] ?? null;
352✔
295
                        unset($fieldsMeta[$fieldName]); // Remaining fields are handled as missing
352✔
296

297
                        // Unknown field
298
                        if ($fieldMeta === null) {
352✔
299
                                // Remove field from data
300
                                unset($data[$fieldName]);
32✔
301

302
                                if ($options->isAllowUnknownFields()) {
32✔
303
                                        continue;
8✔
304
                                }
305

306
                                $hintedFieldName = Helpers::getSuggestion(
24✔
307
                                        $hintedFieldNames ??= array_map(
24✔
308
                                                static fn ($fieldName) => (string) $fieldName,
24✔
309
                                                array_keys($fieldsMeta),
24✔
310
                                        ),
24✔
311
                                        (string) $fieldName,
24✔
312
                                );
24✔
313
                                $hint = $hintedFieldName !== null && !array_key_exists($hintedFieldName, $data)
24✔
314
                                        ? ", did you mean '$hintedFieldName'?"
8✔
315
                                        : '.';
24✔
316

317
                                // Add error to type
318
                                $type ??= $call->getType();
24✔
319
                                $type->overwriteInvalidField(
24✔
320
                                        $fieldName,
24✔
321
                                        ValueDoesNotMatch::create(
24✔
322
                                                new MessageType("Field is unknown$hint"),
24✔
323
                                                Value::of($value),
24✔
324
                                        ),
24✔
325
                                );
24✔
326

327
                                continue;
24✔
328
                        }
329

330
                        $property = $fieldMeta->property;
336✔
331
                        $className = $property->declaringClass;
336✔
332
                        $propertyName = $property->name;
336✔
333
                        $propertyContext = $this->propertyContextCache[$className][$propertyName]
336✔
334
                                ?? (
336✔
335
                                $this->propertyContextCache[$className][$propertyName] = new PropertyContext(
336✔
336
                                        $fieldMeta->default,
336✔
337
                                        $fieldMeta->property->name,
336✔
338
                                        $fieldName,
336✔
339
                                ));
336✔
340

341
                        // Process field value with property rules
342
                        try {
343
                                $data[$fieldName] = $this->processProperty(
336✔
344
                                        $value,
336✔
345
                                        $propertyContext,
336✔
346
                                        $dynamic,
336✔
347
                                        $call,
336✔
348
                                        $fieldMeta,
336✔
349
                                );
336✔
350
                        } catch (ValueDoesNotMatch | InvalidData $exception) {
24✔
351
                                $type ??= $call->getType();
24✔
352
                                $type->overwriteInvalidField($fieldName, $exception);
24✔
353
                        }
354
                }
355

356
                return $data;
416✔
357
        }
358

359
        /**
360
         * @param array<int|string, mixed>           $data
361
         * @param ProcessorCallContext<MappedObject> $call
362
         * @param array<int|string, FieldRuntimeMeta> $fieldsMeta
363
         * @return array<int|string, mixed>
364
         */
365
        private function handleMissingFields(
366
                array $data,
367
                ProcessorCallContext $call,
368
                DynamicContext $dynamic,
369
                array $fieldsMeta
370
        ): array
371
        {
372
                $type = null;
416✔
373
                $options = $dynamic->getOptions();
416✔
374
                $initializeObjects = $dynamic->shouldInitializeObjects();
416✔
375

376
                $requiredFields = $options->getRequiredFields();
416✔
377
                $fillDefaultValues = $initializeObjects || $options->isPrefillDefaultValues();
416✔
378

379
                // $fieldsMeta contains only missing fields at this point
380
                foreach ($fieldsMeta as $fieldName => $fieldMeta) {
416✔
381
                        $fieldMeta = $fieldsMeta[$fieldName];
154✔
382
                        $defaultMeta = $fieldMeta->default;
154✔
383

384
                        if ($requiredFields === RequiredFields::nonDefault() && $defaultMeta->hasValue()) {
154✔
385
                                // Add default value if defaults are not required and should be used
386
                                // If VOs are initialized then values are always prefilled - user can work with them in after callback,
387
                                //   and they are defined by VO anyway
388
                                if ($fillDefaultValues) {
146✔
389
                                        $data[$fieldName] = $defaultMeta->getValue();
146✔
390
                                }
391
                        } elseif (
392
                                $requiredFields !== RequiredFields::none()
16✔
393
                                && ($type === null || !$type->isFieldInvalid($fieldName))
16✔
394
                        ) {
395
                                // Field is missing and have no default value, mark as invalid
396
                                $fieldRuleMeta = $fieldMeta->rule;
8✔
397
                                $fieldRule = $this->ruleManager->getRule($fieldRuleMeta->type);
8✔
398
                                $type ??= $call->getType();
8✔
399
                                $type->overwriteInvalidField(
8✔
400
                                        $fieldName,
8✔
401
                                        ValueDoesNotMatch::create(
8✔
402
                                                $fieldRule->createType(
8✔
403
                                                        $fieldRuleMeta->args,
8✔
404
                                                        $this->services,
8✔
405
                                                        $dynamic,
8✔
406
                                                ),
8✔
407
                                                Value::none(),
8✔
408
                                        ),
8✔
409
                                );
8✔
410
                        }
411
                }
412

413
                return $data;
416✔
414
        }
415

416
        // //////////////// //
417
        // Property / Field //
418
        // //////////////// //
419

420
        /**
421
         * @param mixed                              $value
422
         * @param ProcessorCallContext<MappedObject> $call
423
         * @return mixed
424
         * @throws ValueDoesNotMatch
425
         * @throws InvalidData
426
         */
427
        private function processProperty(
428
                $value,
429
                PropertyContext $property,
430
                DynamicContext $dynamic,
431
                ProcessorCallContext $call,
432
                FieldRuntimeMeta $meta
433
        )
434
        {
435
                if ($meta->callbacks !== []) {
336✔
436
                        $callbackContext = new FieldContext(
96✔
437
                                $this->services,
96✔
438
                                $dynamic,
96✔
439
                                $property,
96✔
440
                                $call,
96✔
441
                        );
96✔
442

443
                        $value = $this->applyCallbacks($value, $callbackContext, $call, $meta, BeforeValidationCallback::class);
96✔
444
                }
445

446
                $value = $this->processPropertyRules($value, $property, $dynamic, $meta);
336✔
447

448
                if (isset($callbackContext)) {
328✔
449
                        $value = $this->applyCallbacks($value, $callbackContext, $call, $meta, AfterValidationCallback::class);
88✔
450
                }
451

452
                return $value;
328✔
453
        }
454

455
        /**
456
         * @param mixed $value
457
         * @return mixed
458
         * @throws ValueDoesNotMatch
459
         * @throws InvalidData
460
         */
461
        private function processPropertyRules(
462
                $value,
463
                PropertyContext $property,
464
                DynamicContext $dynamic,
465
                FieldRuntimeMeta $meta
466
        )
467
        {
468
                $ruleMeta = $meta->rule;
336✔
469
                $rule = $this->ruleManager->getRule($ruleMeta->type);
336✔
470

471
                return $rule->processValue(
336✔
472
                        $value,
336✔
473
                        $ruleMeta->args,
336✔
474
                        $this->services,
336✔
475
                        $property,
336✔
476
                        $dynamic,
336✔
477
                );
336✔
478
        }
479

480
        // ///////// //
481
        // Callbacks //
482
        // ///////// //
483

484
        /**
485
         * @param mixed                              $data
486
         * @param ProcessorCallContext<MappedObject> $call
487
         * @param class-string<Callback<Args>>       $callbackType
488
         * @return mixed
489
         * @throws InvalidData
490
         */
491
        private function handleClassCallbacks(
492
                $data,
493
                ProcessorCallContext $call,
494
                ObjectContext $callbackContext,
495
                ClassRuntimeMeta $meta,
496
                string $callbackType
497
        )
498
        {
499
                try {
500
                        $data = $this->applyCallbacks($data, $callbackContext, $call, $meta, $callbackType);
96✔
501
                } catch (ValueDoesNotMatch | InvalidData $exception) {
32✔
502
                        $type = $callbackContext->getType();
32✔
503
                        $caughtType = $exception->getType();
32✔
504

505
                        // User thrown type is not the actual type from MappedObjectContext
506
                        if ($caughtType !== $type) {
32✔
507
                                $type->addError($exception);
24✔
508

509
                                throw InvalidData::create($type, Value::none());
24✔
510
                        }
511

512
                        throw InvalidData::create($type, $exception->getValue());
8✔
513
                }
514

515
                return $data;
88✔
516
        }
517

518
        /**
519
         * @param mixed $data
520
         * @param ObjectContext|FieldContext $callbackContext
521
         * @param ProcessorCallContext<MappedObject> $call
522
         * @param ClassRuntimeMeta|FieldRuntimeMeta $meta
523
         * @param class-string<Callback<Args>> $callbackType
524
         * @return mixed
525
         * @throws ValueDoesNotMatch
526
         * @throws InvalidData
527
         */
528
        private function applyCallbacks(
529
                $data,
530
                CallbackBaseContext $callbackContext,
531
                ProcessorCallContext $call,
532
                NodeRuntimeMeta $meta,
533
                string $callbackType
534
        )
535
        {
536
                $holder = $call->getObjectHolder();
176✔
537

538
                foreach ($meta->getCallbacksByType($callbackType) as $callback) {
176✔
539
                        $data = $callbackType::invoke(
176✔
540
                                $data,
176✔
541
                                $callback->args,
176✔
542
                                $holder,
176✔
543
                                $callbackContext,
176✔
544
                        );
176✔
545
                }
546

547
                return $data;
168✔
548
        }
549

550

551
        // ///////////// //
552
        // Mapped Object //
553
        // ///////////// //
554

555
        /**
556
         * @param array<int|string, mixed>           $data
557
         * @param mixed                              $rawData
558
         * @param ProcessorCallContext<MappedObject> $call
559
         */
560
        private function fillObject(
561
                MappedObject $object,
562
                array $data,
563
                $rawData,
564
                ProcessorCallContext $call,
565
                DynamicContext $dynamic
566
        ): void
567
        {
568
                $options = $dynamic->getOptions();
336✔
569
                $meta = $call->getMeta();
336✔
570

571
                // Set raw data
572
                if ($options->isTrackRawValues()) {
336✔
573
                        $this->rawValuesMap->setRawValues($object, $rawData);
16✔
574
                }
575

576
                $fieldsMeta = $meta->fields;
336✔
577

578
                if ($dynamic->getOptions()->getRequiredFields() === RequiredFields::none()) {
336✔
579
                        $unsetter = $this->unsetFunction;
8✔
580

581
                        // Reset mapped properties state
582
                        foreach ($fieldsMeta as $fieldMeta) {
8✔
583
                                $property = $fieldMeta->property;
8✔
584
                                $name = $property->name;
8✔
585

586
                                if ($property->isPublicSet) {
8✔
587
                                        unset($object->$name);
8✔
588
                                } else {
NEW
589
                                        $boundUnsetter = $unsetter->bindTo($object, $property->declaringClass);
×
NEW
590
                                        assert($boundUnsetter !== null);
×
NEW
591
                                        $boundUnsetter($object, $name);
×
592
                                }
593
                        }
594
                }
595

596
                $setter = $this->setFunction;
336✔
597

598
                // Set processed data
599
                foreach ($data as $fieldName => $value) {
336✔
600
                        $property = $fieldsMeta[$fieldName]->property;
304✔
601
                        $name = $property->name;
304✔
602

603
                        if ($property->isPublicSet) {
304✔
604
                                $object->$name = $value;
287✔
605
                        } else {
606
                                $boundSetter = $setter->bindTo($object, $property->declaringClass);
33✔
607
                                assert($boundSetter !== null);
33✔
608
                                $boundSetter($object, $name, $value);
33✔
609
                        }
610
                }
611
        }
612

613
        public function getRawValues(MappedObject $object)
614
        {
615
                return $this->rawValuesMap->getRawValues($object);
16✔
616
        }
617

618
}
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