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

nette / schema / 11189863618

05 Oct 2024 02:59AM UTC coverage: 97.384% (+0.7%) from 96.667%
11189863618

push

github

dg
Type::merge() merges arrays only according to the schema (BC break)

2 of 2 new or added lines in 1 file covered. (100.0%)

4 existing lines in 3 files now uncovered.

484 of 497 relevant lines covered (97.38%)

0.97 hits per line

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

97.87
/src/Schema/Elements/Structure.php
1
<?php
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
declare(strict_types=1);
9

10
namespace Nette\Schema\Elements;
11

12
use Nette;
13
use Nette\Schema\Context;
14
use Nette\Schema\Helpers;
15
use Nette\Schema\MergeMode;
16
use Nette\Schema\Schema;
17

18

19
final class Structure implements Schema
20
{
21
        use Base;
22

23
        /** @var Schema[] */
24
        private array $items;
25

26
        /** for array|list */
27
        private ?Schema $otherItems = null;
28

29
        /** @var array{?int, ?int} */
30
        private array $range = [null, null];
31
        private bool $skipDefaults = false;
32
        private MergeMode $mergeMode = MergeMode::AppendKeys;
33

34

35
        /**
36
         * @param  Schema[]  $shape
37
         */
38
        public function __construct(array $shape)
1✔
39
        {
40
                (function (Schema ...$items) {})(...array_values($shape));
1✔
41
                $this->items = $shape;
1✔
42
                $this->castTo('object');
1✔
43
                $this->required = true;
1✔
44
        }
1✔
45

46

47
        public function default(mixed $value): self
1✔
48
        {
49
                throw new Nette\InvalidStateException('Structure cannot have default value.');
1✔
50
        }
51

52

53
        public function min(?int $min): self
1✔
54
        {
55
                $this->range[0] = $min;
1✔
56
                return $this;
1✔
57
        }
58

59

60
        public function max(?int $max): self
1✔
61
        {
62
                $this->range[1] = $max;
1✔
63
                return $this;
1✔
64
        }
65

66

67
        public function otherItems(string|Schema $type = 'mixed'): self
1✔
68
        {
69
                $this->otherItems = $type instanceof Schema ? $type : new Type($type);
1✔
70
                return $this;
1✔
71
        }
72

73

74
        public function skipDefaults(bool $state = true): self
1✔
75
        {
76
                $this->skipDefaults = $state;
1✔
77
                return $this;
1✔
78
        }
79

80

81
        public function mergeMode(MergeMode $mode): self
1✔
82
        {
83
                $this->mergeMode = $mode;
1✔
84
                return $this;
1✔
85
        }
86

87

88
        public function extend(array|self $shape): self
1✔
89
        {
90
                $shape = $shape instanceof self ? $shape->items : $shape;
1✔
91
                return new self(array_merge($this->items, $shape));
1✔
92
        }
93

94

95
        public function getShape(): array
96
        {
97
                return $this->items;
1✔
98
        }
99

100

101
        /********************* processing ****************d*g**/
102

103

104
        public function normalize(mixed $value, Context $context): mixed
1✔
105
        {
106
                if ($prevent = (is_array($value) && isset($value[Helpers::PreventMerging]))) {
1✔
UNCOV
107
                        unset($value[Helpers::PreventMerging]);
×
108
                }
109

110
                $value = $this->doNormalize($value, $context);
1✔
111
                if (is_object($value)) {
1✔
112
                        $value = (array) $value;
1✔
113
                }
114

115
                if (is_array($value)) {
1✔
116
                        foreach ($value as $key => $val) {
1✔
117
                                $itemSchema = $this->items[$key] ?? $this->otherItems;
1✔
118
                                if ($itemSchema) {
1✔
119
                                        $context->path[] = $key;
1✔
120
                                        $value[$key] = $itemSchema->normalize($val, $context);
1✔
121
                                        array_pop($context->path);
1✔
122
                                }
123
                        }
124

125
                        if ($prevent) {
1✔
UNCOV
126
                                $value[Helpers::PreventMerging] = true;
×
127
                        }
128
                }
129

130
                return $value;
1✔
131
        }
132

133

134
        public function merge(mixed $value, mixed $base): mixed
1✔
135
        {
136
                if ($this->mergeMode === MergeMode::Replace || (is_array($value) && isset($value[Helpers::PreventMerging]))) {
1✔
137
                        unset($value[Helpers::PreventMerging]);
1✔
138
                        $base = null;
1✔
139
                }
140

141
                if (is_array($value) && is_array($base)) {
1✔
142
                        $index = $this->mergeMode === MergeMode::OverwriteKeys ? null : 0;
1✔
143
                        foreach ($value as $key => $val) {
1✔
144
                                if ($key === $index) {
1✔
145
                                        $base[] = $val;
1✔
146
                                        $index++;
1✔
147
                                } else {
148
                                        $base[$key] = array_key_exists($key, $base) && ($itemSchema = $this->items[$key] ?? $this->otherItems)
1✔
149
                                                ? $itemSchema->merge($val, $base[$key])
1✔
150
                                                : $val;
1✔
151
                                }
152
                        }
153

154
                        return $base;
1✔
155
                }
156

157
                return $value ?? $base;
1✔
158
        }
159

160

161
        public function complete(mixed $value, Context $context): mixed
1✔
162
        {
163
                if ($value === null) {
1✔
164
                        $value = []; // is unable to distinguish null from array in NEON
1✔
165
                }
166

167
                $this->doDeprecation($context);
1✔
168

169
                $isOk = $context->createChecker();
1✔
170
                Helpers::validateType($value, 'array', $context);
1✔
171
                $isOk() && Helpers::validateRange($value, $this->range, $context);
1✔
172
                $isOk() && $this->validateItems($value, $context);
1✔
173
                $isOk() && $value = $this->doTransform($value, $context);
1✔
174
                return $isOk() ? $value : null;
1✔
175
        }
176

177

178
        private function validateItems(array &$value, Context $context): void
1✔
179
        {
180
                $items = $this->items;
1✔
181
                if ($extraKeys = array_keys(array_diff_key($value, $items))) {
1✔
182
                        if ($this->otherItems) {
1✔
183
                                $items += array_fill_keys($extraKeys, $this->otherItems);
1✔
184
                        } else {
185
                                $keys = array_map('strval', array_keys($items));
1✔
186
                                foreach ($extraKeys as $key) {
1✔
187
                                        $hint = Nette\Utils\Helpers::getSuggestion($keys, (string) $key);
1✔
188
                                        $context->addError(
1✔
189
                                                'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'),
1✔
190
                                                Nette\Schema\Message::UnexpectedItem,
1✔
191
                                                ['hint' => $hint],
1✔
192
                                        )->path[] = $key;
1✔
193
                                }
194
                        }
195
                }
196

197
                foreach ($items as $itemKey => $itemVal) {
1✔
198
                        $context->path[] = $itemKey;
1✔
199
                        if (array_key_exists($itemKey, $value)) {
1✔
200
                                $value[$itemKey] = $itemVal->complete($value[$itemKey], $context);
1✔
201
                        } else {
202
                                $default = $itemVal->completeDefault($context); // checks required item
1✔
203
                                if (!$context->skipDefaults && !$this->skipDefaults) {
1✔
204
                                        $value[$itemKey] = $default;
1✔
205
                                }
206
                        }
207

208
                        array_pop($context->path);
1✔
209
                }
210
        }
1✔
211

212

213
        public function completeDefault(Context $context): mixed
1✔
214
        {
215
                return $this->required
1✔
216
                        ? $this->complete([], $context)
1✔
217
                        : null;
1✔
218
        }
219
}
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