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

nette / schema / 29631259250

18 Jul 2026 04:51AM UTC coverage: 98.361% (+1.6%) from 96.735%
29631259250

push

github

dg
DOCS

540 of 549 relevant lines covered (98.36%)

0.98 hits per line

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

98.99
/src/Schema/Elements/Structure.php
1
<?php declare(strict_types=1);
1✔
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
namespace Nette\Schema\Elements;
9

10
use Nette;
11
use Nette\Schema\Context;
12
use Nette\Schema\Helpers;
13
use Nette\Schema\MergeMode;
14
use Nette\Schema\Schema;
15
use function array_diff_key, array_fill_keys, array_key_exists, array_keys, array_map, array_merge, array_pop, array_values, is_array, is_object, strval;
16

17

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

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

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

28
        /** @var array{?int, ?int} */
29
        private array $range = [null, null];
30
        private bool $skipDefaults = false;
31

32

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

42

43
        /**
44
         * Not supported for structures; always throws.
45
         */
46
        public function default(mixed $value): self
1✔
47
        {
48
                throw new Nette\InvalidStateException('Structure cannot have default value.');
1✔
49
        }
50

51

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

58

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

65

66
        /**
67
         * Allows extra keys not defined in the shape, validating their values against the given type.
68
         */
69
        public function otherItems(string|Schema $type = 'mixed'): self
1✔
70
        {
71
                $this->otherItems = $type instanceof Schema ? $type : new Type($type);
1✔
72
                return $this;
1✔
73
        }
74

75

76
        /**
77
         * When enabled, properties whose value equals the default are omitted from the output.
78
         */
79
        public function skipDefaults(bool $state = true): self
1✔
80
        {
81
                $this->skipDefaults = $state;
1✔
82
                return $this;
1✔
83
        }
84

85

86
        /**
87
         * Creates a new structure by merging this shape with additional properties.
88
         * @param  Schema[]|self  $shape
89
         */
90
        public function extend(array|self $shape): self
1✔
91
        {
92
                $shape = $shape instanceof self ? $shape->items : $shape;
1✔
93
                return new self(array_merge($this->items, $shape));
1✔
94
        }
95

96

97
        /** @return Schema[] */
98
        public function getShape(): array
99
        {
100
                return $this->items;
1✔
101
        }
102

103

104
        /********************* processing ****************d*g**/
105

106

107
        public function normalize(mixed $value, Context $context): mixed
1✔
108
        {
109
                $value = $this->doNormalize($value, $context);
1✔
110
                if (is_object($value)) {
1✔
111
                        $value = (array) $value;
1✔
112
                }
113

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

125
                return $value;
1✔
126
        }
127

128

129
        public function merge(mixed $value, mixed $base, Context $context): mixed
1✔
130
        {
131
                if ($this->mergeWith) {
1✔
132
                        return ($this->mergeWith)($value, $base);
×
133
                }
134

135
                $mode = $this->mergeMode ?? ($this->otherItems === null ? MergeMode::OverwriteKeys : MergeMode::AppendKeys);
1✔
136
                if ($mode === MergeMode::Replace) {
1✔
137
                        return $value;
1✔
138
                }
139

140
                if (is_array($value) && is_array($base)) {
1✔
141
                        $index = $mode === MergeMode::OverwriteKeys ? null : 0;
1✔
142
                        foreach ($value as $key => $val) {
1✔
143
                                if ($key === $index) {
1✔
144
                                        $base[] = $val;
1✔
145
                                        $index++;
1✔
146
                                } elseif (!array_key_exists($key, $base)) {
1✔
147
                                        $base[$key] = $val;
1✔
148
                                } elseif ($itemSchema = $this->items[$key] ?? $this->otherItems) {
1✔
149
                                        $context->path[] = $key;
1✔
150
                                        $base[$key] = $itemSchema->merge($val, $base[$key], $context);
1✔
151
                                        array_pop($context->path);
1✔
152
                                } else {
153
                                        if (is_array($val) && is_array($base[$key]) && $this->mergeMode === null) {
1✔
154
                                                $context->addError(
1✔
155
                                                        'Cannot merge %path%: the schema does not describe the item, use otherItems() or mergeMode().',
1✔
156
                                                        Nette\Schema\Message::CannotMerge,
1✔
157
                                                )->path[] = $key;
1✔
158
                                        }
159
                                        $base[$key] = $val;
1✔
160
                                }
161
                        }
162

163
                        return $base;
1✔
164
                }
165

166
                return $value === null && is_array($base) ? $base : $value;
1✔
167
        }
168

169

170
        public function complete(mixed $value, Context $context): mixed
1✔
171
        {
172
                if ($value === null) {
1✔
173
                        $value = []; // is unable to distinguish null from array in NEON
1✔
174
                }
175

176
                $this->doDeprecation($context);
1✔
177

178
                $isOk = $context->createChecker();
1✔
179
                Helpers::validateType($value, 'array', $context);
1✔
180
                $isOk() && Helpers::validateRange($value, $this->range, $context);
1✔
181
                $isOk() && $this->validateItems($value, $context);
1✔
182
                $isOk() && $value = $this->doTransform($value, $context);
1✔
183
                return $isOk() ? $value : null;
1✔
184
        }
185

186

187
        /** @param  array<mixed>  $value */
188
        private function validateItems(array &$value, Context $context): void
1✔
189
        {
190
                $items = $this->items;
1✔
191
                if ($extraKeys = array_keys(array_diff_key($value, $items))) {
1✔
192
                        if ($this->otherItems) {
1✔
193
                                $items += array_fill_keys($extraKeys, $this->otherItems);
1✔
194
                        } else {
195
                                $keys = array_map(strval(...), array_keys($items));
1✔
196
                                foreach ($extraKeys as $key) {
1✔
197
                                        $hint = Nette\Utils\Helpers::getSuggestion($keys, (string) $key);
1✔
198
                                        $context->addError(
1✔
199
                                                'Unexpected item %path%' . ($hint ? ", did you mean '%hint%'?" : '.'),
1✔
200
                                                Nette\Schema\Message::UnexpectedItem,
1✔
201
                                                ['hint' => $hint],
1✔
202
                                        )->path[] = $key;
1✔
203
                                }
204
                        }
205
                }
206

207
                foreach ($items as $itemKey => $itemVal) {
1✔
208
                        $context->path[] = $itemKey;
1✔
209
                        if (array_key_exists($itemKey, $value)) {
1✔
210
                                $value[$itemKey] = $itemVal->complete($value[$itemKey], $context);
1✔
211
                        } else {
212
                                $default = $itemVal->completeDefault($context); // checks required item
1✔
213
                                if (!$context->skipDefaults && !$this->skipDefaults) {
1✔
214
                                        $value[$itemKey] = $default;
1✔
215
                                }
216
                        }
217

218
                        array_pop($context->path);
1✔
219
                }
220
        }
1✔
221

222

223
        public function completeDefault(Context $context): mixed
1✔
224
        {
225
                return $this->required
1✔
226
                        ? $this->complete([], $context)
1✔
227
                        : null;
1✔
228
        }
229
}
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