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

azjezz / psl / 22680186990

04 Mar 2026 05:04PM UTC coverage: 97.34% (-1.0%) from 98.375%
22680186990

Pull #610

github

azjezz
perf: replace PSL calls with native PHP builtins and fix O(n2) algorithms

Signed-off-by: azjezz <azjezz@protonmail.com>
Pull Request #610: perf: replace PSL calls with native PHP builtins and fix O(n2) algorithms

319 of 355 new or added lines in 90 files covered. (89.86%)

64 existing lines in 12 files now uncovered.

9258 of 9511 relevant lines covered (97.34%)

34.97 hits per line

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

99.06
/src/Psl/Type/Internal/ShapeType.php
1
<?php
2

3
declare(strict_types=1);
4

5
namespace Psl\Type\Internal;
6

7
use Override;
8
use Psl\Type;
9
use Psl\Type\Exception\AssertException;
10
use Psl\Type\Exception\CoercionException;
11
use stdClass;
12
use Throwable;
13

14
use function array_diff_key;
15
use function array_filter;
16
use function array_intersect_key;
17
use function array_keys;
18
use function implode;
19
use function is_array;
20
use function is_int;
21
use function is_iterable;
22

23
/**
24
 * @template Tk of array-key
25
 * @template Tv
26
 *
27
 * @extends Type\Type<array<Tk, Tv>>
28
 *
29
 * @mago-expect lint:kan-defect
30
 */
31
final readonly class ShapeType extends Type\Type
32
{
33
    /**
34
     * @var array<Tk, Type\TypeInterface<Tv>>
35
     */
36
    private array $requiredElements;
37

38
    /**
39
     * @psalm-mutation-free
40
     *
41
     * @param array<Tk, Type\TypeInterface<Tv>> $elements_types
42
     */
43
    public function __construct(
44
        private array $elements_types,
45
        private bool $allow_unknown_fields = false,
46
    ) {
47
        $this->requiredElements = array_filter(
150✔
48
            $elements_types,
150✔
49
            static fn(Type\TypeInterface $element): bool => !$element->isOptional(),
150✔
50
        );
150✔
51
    }
52

53
    /**
54
     * @psalm-assert-if-true array<Tk, Tv> $value
55
     */
56
    #[Override]
57
    public function matches(mixed $value): bool
58
    {
59
        if (!is_array($value)) {
51✔
60
            return false;
22✔
61
        }
62

63
        foreach ($this->elements_types as $element => $type) {
29✔
64
            if (array_key_exists($element, $value)) {
29✔
65
                if (!$type->matches($value[$element])) {
29✔
66
                    return false;
8✔
67
                }
68

69
                continue;
29✔
70
            }
71

72
            if (!$type->isOptional()) {
22✔
73
                return false;
8✔
74
            }
75
        }
76

77
        if (!$this->allow_unknown_fields) {
17✔
78
            foreach ($value as $k => $_v) {
16✔
79
                if (!array_key_exists($k, $this->elements_types)) {
16✔
NEW
80
                    return false;
×
81
                }
82
            }
83
        }
84

85
        return true;
17✔
86
    }
87

88
    /**
89
     * @throws CoercionException
90
     *
91
     * @return array<Tk, Tv>
92
     */
93
    #[Override]
94
    public function coerce(mixed $value): array
95
    {
96
        if ($value instanceof stdClass) {
66✔
97
            $value = (array) $value;
2✔
98
        }
99

100
        // To whom reads this: yes, I hate this stuff as passionately as you do :-)
101
        if (!is_array($value)) {
66✔
102
            // Fallback to slow implementation - unhappy path
103
            return $this->coerceIterable($value);
26✔
104
        }
105

106
        if (array_keys(array_intersect_key($value, $this->requiredElements)) !== array_keys($this->requiredElements)) {
46✔
107
            // Fallback to slow implementation - unhappy path
108
            return $this->coerceIterable($value);
12✔
109
        }
110

111
        if (!$this->allow_unknown_fields && array_keys($value) !== array_keys($this->elements_types)) {
39✔
112
            // Fallback to slow implementation - unhappy path
113
            return $this->coerceIterable($value);
17✔
114
        }
115

116
        $coerced = [];
36✔
117

118
        try {
119
            foreach (array_intersect_key($this->elements_types, $value) as $key => $type) {
36✔
120
                $coerced[$key] = $type->coerce($value[$key]);
36✔
121
            }
122
        } catch (CoercionException) {
9✔
123
            // Fallback to slow implementation - unhappy path. Prevents having to eagerly compute traces.
124
            $this->coerceIterable($value);
9✔
125
        }
126

127
        /** @var mixed $additionalValue */
128
        foreach (array_diff_key($value, $this->elements_types) as $key => $additionalValue) {
27✔
129
            $coerced[$key] = $additionalValue;
1✔
130
        }
131

132
        return $coerced;
27✔
133
    }
134

135
    /**
136
     * @throws CoercionException
137
     *
138
     * @return array<Tk, Tv>
139
     */
140
    private function coerceIterable(mixed $value): array
141
    {
142
        if (!is_iterable($value)) {
54✔
143
            throw CoercionException::withValue($value, $this->toString());
14✔
144
        }
145

146
        $arrayKeyType = Type\array_key();
40✔
147
        $array = [];
40✔
148
        $k = null;
40✔
149
        try {
150
            /**
151
             * @var Tk $k
152
             * @var Tv $v
153
             */
154
            foreach ($value as $k => $v) {
40✔
155
                // @mago-expect analysis:redundant-type-comparison
156
                if (!$arrayKeyType->matches($k)) {
37✔
157
                    continue;
2✔
158
                }
159

160
                $array[$k] = $v;
35✔
161
            }
162
        } catch (Throwable $e) {
3✔
163
            throw CoercionException::withValue(null, $this->toString(), PathExpression::iteratorError($k), $e);
3✔
164
        }
165

166
        $result = [];
37✔
167
        $element = null;
37✔
168
        $element_value_found = false;
37✔
169

170
        try {
171
            foreach ($this->elements_types as $element => $type) {
37✔
172
                $element_value_found = false;
37✔
173
                if (array_key_exists($element, $array)) {
37✔
174
                    $element_value_found = true;
34✔
175
                    $result[$element] = $type->coerce($array[$element]);
34✔
176

177
                    continue;
32✔
178
                }
179

180
                if ($type->isOptional()) {
28✔
181
                    continue;
14✔
182
                }
183

184
                throw CoercionException::withValue(null, $this->toString(), PathExpression::path($element));
14✔
185
            }
186
        } catch (CoercionException $e) {
18✔
187
            throw match (true) {
188
                $element_value_found => CoercionException::withValue(
18✔
189
                    null === $element ? null : $array[$element] ?? null,
18✔
190
                    $this->toString(),
18✔
191
                    PathExpression::path($element),
18✔
192
                    $e,
18✔
193
                ),
18✔
194
                default => $e,
14✔
195
            };
196
        }
197

198
        if ($this->allow_unknown_fields) {
19✔
199
            foreach ($array as $k => $v) {
1✔
200
                if (array_key_exists($k, $result)) {
1✔
201
                    continue;
1✔
202
                }
203

204
                $result[$k] = $v;
1✔
205
            }
206
        }
207

208
        return $result;
19✔
209
    }
210

211
    /**
212
     * @throws AssertException
213
     *
214
     * @return array<Tk, Tv>
215
     *
216
     * @psalm-assert array<Tk, Tv> $value
217
     */
218
    #[Override]
219
    public function assert(mixed $value): array
220
    {
221
        if (!is_array($value)) {
59✔
222
            throw AssertException::withValue($value, $this->toString());
22✔
223
        }
224

225
        $result = [];
37✔
226
        $element = null;
37✔
227
        $element_value_found = false;
37✔
228

229
        try {
230
            foreach ($this->elements_types as $element => $type) {
37✔
231
                $element_value_found = false;
37✔
232
                if (array_key_exists($element, $value)) {
37✔
233
                    $element_value_found = true;
36✔
234
                    $result[$element] = $type->assert($value[$element]);
36✔
235

236
                    continue;
33✔
237
                }
238

239
                if ($type->isOptional()) {
23✔
240
                    continue;
14✔
241
                }
242

243
                throw AssertException::withValue(null, $this->toString(), PathExpression::path($element));
9✔
244
            }
245
        } catch (AssertException $e) {
16✔
246
            throw match (true) {
247
                $element_value_found => AssertException::withValue(
16✔
248
                    null === $element ? null : $value[$element] ?? null,
16✔
249
                    $this->toString(),
16✔
250
                    PathExpression::path($element),
16✔
251
                    $e,
16✔
252
                ),
16✔
253
                default => $e,
9✔
254
            };
255
        }
256

257
        /**
258
         * @var Tv $v
259
         */
260
        foreach ($value as $k => $v) {
21✔
261
            if (array_key_exists($k, $result)) {
21✔
262
                continue;
21✔
263
            }
264

265
            if ($this->allow_unknown_fields) {
2✔
266
                $result[$k] = $v;
1✔
267
                continue;
1✔
268
            }
269

270
            throw AssertException::withValue($v, $this->toString(), PathExpression::path($k));
1✔
271
        }
272

273
        return $result;
20✔
274
    }
275

276
    /**
277
     * Returns a string representation of the shape.
278
     */
279
    #[Override]
280
    public function toString(): string
281
    {
282
        $nodes = [];
79✔
283
        foreach ($this->elements_types as $element => $type) {
79✔
284
            $nodes[] = $this->getElementName($element) . ($type->isOptional() ? '?' : '') . ': ' . $type->toString();
79✔
285
        }
286

287
        return 'array{' . implode(', ', $nodes) . '}';
79✔
288
    }
289

290
    private function getElementName(string|int $element): string
291
    {
292
        return is_int($element) ? (string) $element : '\'' . $element . '\'';
79✔
293
    }
294
}
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