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

nette / php-generator / 22836119803

09 Mar 2026 02:40AM UTC coverage: 94.264% (+0.2%) from 94.029%
22836119803

push

github

dg
added CLAUDE.md

1890 of 2005 relevant lines covered (94.26%)

0.94 hits per line

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

99.42
/src/PhpGenerator/Dumper.php
1
<?php declare(strict_types=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\PhpGenerator;
9

10
use Nette;
11
use function addcslashes, array_filter, array_keys, array_shift, count, dechex, implode, in_array, is_array, is_int, is_object, is_resource, is_string, ltrim, method_exists, ord, preg_match, preg_replace, preg_replace_callback, preg_split, range, serialize, str_contains, str_pad, str_repeat, str_replace, strlen, strrpos, strtoupper, substr, trim, unserialize, var_export;
12

13

14
/**
15
 * Generates a PHP representation of a variable.
16
 */
17
final class Dumper
18
{
19
        private const IndentLength = 4;
20

21
        public int $maxDepth = 50;
22
        public int $wrapLength = 120;
23
        public string $indentation = "\t";
24
        public bool $customObjects = true;
25
        public bool $references = false;
26
        public DumpContext $context = DumpContext::Expression;
27

28
        /** @var array<string, int> */
29
        private array $refMap = [];
30

31

32
        /**
33
         * Converts a value to its PHP code representation.
34
         * @param  int  $column  current column for array wrapping decisions
35
         */
36
        public function dump(mixed $var, int $column = 0): string
1✔
37
        {
38
                return $this->dumpReferences($var)
1✔
39
                        ?? $this->dumpVar($var, column: $column);
1✔
40
        }
41

42

43
        /** @param  array<mixed[]|object>  $parents */
44
        private function dumpVar(mixed $var, array $parents = [], int $level = 0, int $column = 0): string
1✔
45
        {
46
                if ($var === null) {
1✔
47
                        return 'null';
1✔
48

49
                } elseif (is_string($var)) {
1✔
50
                        return $this->dumpString($var);
1✔
51

52
                } elseif (is_array($var)) {
1✔
53
                        return $this->dumpArray($var, $parents, $level, $column);
1✔
54

55
                } elseif ($var instanceof Literal) {
1✔
56
                        return $this->dumpLiteral($var, $level);
1✔
57

58
                } elseif (is_object($var)) {
1✔
59
                        return $this->dumpObject($var, $parents, $level, $column);
1✔
60

61
                } elseif (is_resource($var)) {
1✔
62
                        throw new Nette\InvalidStateException('Cannot dump value of type resource.');
×
63

64
                } else {
65
                        return var_export($var, return: true);
1✔
66
                }
67
        }
68

69

70
        private function dumpString(string $s): string
1✔
71
        {
72
                $special = [
1✔
73
                        "\r" => '\r',
74
                        "\n" => '\n',
75
                        "\t" => '\t',
76
                        "\e" => '\e',
77
                        '\\' => '\\\\',
78
                ];
79

80
                $utf8 = preg_match('##u', $s);
1✔
81
                $escaped = preg_replace_callback(
1✔
82
                        $utf8 ? '#[\p{C}\\\]#u' : '#[\x00-\x1F\x7F-\xFF\\\]#',
1✔
83
                        fn($m) => $special[$m[0]] ?? (strlen($m[0]) === 1
1✔
84
                                        ? '\x' . str_pad(strtoupper(dechex(ord($m[0]))), 2, '0', STR_PAD_LEFT)
1✔
85
                                        : '\u{' . strtoupper(ltrim(dechex(self::utf8Ord($m[0])), '0')) . '}'),
1✔
86
                        $s,
1✔
87
                );
88
                return $s === str_replace('\\\\', '\\', $escaped)
1✔
89
                        ? "'" . preg_replace('#\'|\\\(?=[\'\\\]|$)#D', '\\\$0', $s) . "'"
1✔
90
                        : '"' . addcslashes($escaped, '"$') . '"';
1✔
91
        }
92

93

94
        private static function utf8Ord(string $c): int
1✔
95
        {
96
                $ord0 = ord($c[0]);
1✔
97
                return match (true) {
98
                        $ord0 < 0x80 => $ord0,
1✔
99
                        $ord0 < 0xE0 => ($ord0 << 6) + ord($c[1]) - 0x3080,
1✔
100
                        $ord0 < 0xF0 => ($ord0 << 12) + (ord($c[1]) << 6) + ord($c[2]) - 0xE2080,
1✔
101
                        default => ($ord0 << 18) + (ord($c[1]) << 12) + (ord($c[2]) << 6) + ord($c[3]) - 0x3C82080,
1✔
102
                };
103
        }
104

105

106
        /**
107
         * @param  mixed[]  $var
108
         * @param  array<mixed[]|object>  $parents
109
         */
110
        private function dumpArray(array $var, array $parents, int $level, int $column): string
1✔
111
        {
112
                if (empty($var)) {
1✔
113
                        return '[]';
1✔
114

115
                } elseif ($level > $this->maxDepth || !$this->references && in_array($var, $parents, strict: true)) {
1✔
116
                        throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.');
1✔
117
                }
118

119
                $parents[] = $var;
1✔
120
                $hideKeys = is_int(($keys = array_keys($var))[0]) && $keys === range($keys[0], $keys[0] + count($var) - 1);
1✔
121
                $pairs = [];
1✔
122

123
                foreach ($var as $k => $v) {
1✔
124
                        $keyPart = $hideKeys && ($k !== $keys[0] || $k === 0)
1✔
125
                                ? ''
1✔
126
                                : $this->dumpVar($k) . ' => ';
1✔
127

128
                        if (
129
                                $this->references
1✔
130
                                && ($refId = (\ReflectionReference::fromArrayElement($var, $k))?->getId())
1✔
131
                                && isset($this->refMap[$refId])
1✔
132
                        ) {
133
                                $pairs[] = $keyPart . '&$r[' . $this->refMap[$refId] . ']';
1✔
134
                        } else {
135
                                $pairs[] = $keyPart . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart) + 1); // 1 = comma after item
1✔
136
                        }
137
                }
138

139
                $line = '[' . implode(', ', $pairs) . ']';
1✔
140
                $space = str_repeat($this->indentation, $level);
1✔
141
                return !str_contains($line, "\n") && $level * self::IndentLength + $column + strlen($line) <= $this->wrapLength
1✔
142
                        ? $line
1✔
143
                        : "[\n$space" . $this->indentation . implode(",\n$space" . $this->indentation, $pairs) . ",\n$space]";
1✔
144
        }
145

146

147
        /** @param  array<mixed[]|object>  $parents */
148
        private function dumpObject(object $var, array $parents, int $level, int $column): string
1✔
149
        {
150
                if ($level > $this->maxDepth || in_array($var, $parents, strict: true)) {
1✔
151
                        throw new Nette\InvalidStateException('Nesting level too deep or recursive dependency.');
1✔
152
                } elseif ((new \ReflectionObject($var))->isAnonymous()) {
1✔
153
                        throw new Nette\InvalidStateException('Cannot dump an instance of an anonymous class.');
1✔
154
                }
155

156
                $class = $var::class;
1✔
157
                $parents[] = $var;
1✔
158

159
                if ($class === \stdClass::class) {
1✔
160
                        if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) {
1✔
161
                                throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
1✔
162
                        }
163

164
                        $var = (array) $var;
1✔
165
                        return '(object) ' . $this->dumpArray($var, $parents, $level, $column + 10);
1✔
166

167
                } elseif ($class === \DateTime::class || $class === \DateTimeImmutable::class) {
1✔
168
                        if (!in_array($this->context, [DumpContext::Expression, DumpContext::Parameter, DumpContext::Attribute], strict: true)) {
1✔
169
                                throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
1✔
170
                        }
171
                        assert($var instanceof \DateTimeInterface);
172
                        return $this->format(
1✔
173
                                "new \\$class(?, new \\DateTimeZone(?))",
1✔
174
                                $var->format('Y-m-d H:i:s.u'),
1✔
175
                                $var->getTimezone()->getName(),
1✔
176
                        );
177

178
                } elseif ($var instanceof \UnitEnum) {
1✔
179
                        return '\\' . $var::class . '::' . $var->name;
1✔
180

181
                } elseif ($var instanceof \Closure) {
1✔
182
                        $inner = Nette\Utils\Callback::unwrap($var);
1✔
183
                        if (is_callable($inner) && Nette\Utils\Callback::isStatic($inner)) {
1✔
184
                                return implode('::', (array) $inner) . '(...)';
1✔
185
                        }
186

187
                        throw new Nette\InvalidStateException('Cannot dump object of type Closure.');
1✔
188

189
                } elseif ($this->customObjects) {
1✔
190
                        if ($this->context !== DumpContext::Expression) {
1✔
191
                                throw new Nette\InvalidStateException("Cannot dump object of type $class in {$this->context->name} context.");
1✔
192
                        }
193
                        return $this->dumpCustomObject($var, $parents, $level);
1✔
194

195
                } else {
196
                        throw new Nette\InvalidStateException("Cannot dump object of type $class.");
1✔
197
                }
198
        }
199

200

201
        /** @param  array<mixed[]|object>  $parents */
202
        private function dumpCustomObject(object $var, array $parents, int $level): string
1✔
203
        {
204
                $class = $var::class;
1✔
205
                $space = str_repeat($this->indentation, $level);
1✔
206
                $out = "\n";
1✔
207

208
                if (method_exists($var, '__serialize')) {
1✔
209
                        $arr = $var->__serialize();
1✔
210
                } else {
211
                        $arr = (array) $var;
1✔
212
                        if (method_exists($var, '__sleep')) {
1✔
213
                                foreach ($var->__sleep() as $v) {
1✔
214
                                        $props[$v] = $props["\x00*\x00$v"] = $props["\x00$class\x00$v"] = true;
1✔
215
                                }
216
                        }
217
                }
218

219
                foreach ($arr as $k => $v) {
1✔
220
                        if (!isset($props) || isset($props[$k])) {
1✔
221
                                $out .= $space . $this->indentation
1✔
222
                                        . ($keyPart = $this->dumpVar($k) . ' => ')
1✔
223
                                        . $this->dumpVar($v, $parents, $level + 1, strlen($keyPart))
1✔
224
                                        . ",\n";
1✔
225
                        }
226
                }
227

228
                return '\\' . self::class . "::createObject(\\$class::class, [$out$space])";
1✔
229
        }
230

231

232
        private function dumpLiteral(Literal $var, int $level): string
1✔
233
        {
234
                $s = $var->formatWith($this);
1✔
235
                $s = Nette\Utils\Strings::unixNewLines($s);
1✔
236
                $s = Nette\Utils\Strings::indent(trim($s), $level, $this->indentation);
1✔
237
                return ltrim($s, $this->indentation);
1✔
238
        }
239

240

241
        private function dumpReferences(mixed $var): ?string
1✔
242
        {
243
                $this->refMap = $refs = [];
1✔
244
                if (!$this->references || !is_array($var)) {
1✔
245
                        return null;
1✔
246
                }
247

248
                $this->collectReferences($var, $refs);
1✔
249
                $refs = array_filter($refs, fn($ref) => $ref[0] >= 2);
1✔
250
                if (!$refs) {
1✔
251
                        return null;
1✔
252
                }
253

254
                $n = 0;
1✔
255
                foreach ($refs as $refId => $_) {
1✔
256
                        $this->refMap[$refId] = ++$n;
1✔
257
                }
258

259
                $preamble = '';
1✔
260
                foreach ($this->refMap as $refId => $n) {
1✔
261
                        $preamble .= '$r[' . $n . '] = ' . $this->dumpVar($refs[$refId][1]) . '; ';
1✔
262
                }
263

264
                return '(static function () { ' . $preamble . 'return ' . $this->dumpVar($var) . '; })()';
1✔
265
        }
266

267

268
        /**
269
         * @param  mixed[]  $var
270
         * @param  array<string, array{int, mixed}>  $refs
271
         */
272
        private function collectReferences(array $var, array &$refs): void
1✔
273
        {
274
                foreach ($var as $k => $v) {
1✔
275
                        $refId = (\ReflectionReference::fromArrayElement($var, $k))?->getId();
1✔
276
                        if ($refId !== null) {
1✔
277
                                $refs[$refId] ??= [0, $v];
1✔
278
                                $refs[$refId][0]++;
1✔
279
                        }
280

281
                        if (is_array($v) && ($refId === null || $refs[$refId][0] === 1)) {
1✔
282
                                $this->collectReferences($v, $refs);
1✔
283
                        }
284
                }
285
        }
1✔
286

287

288
        /**
289
         * Formats a PHP expression using placeholders.
290
         * Supported placeholders: ? (value), \? (literal ?), $? (variable), ->? (property access),
291
         * ::? (static access), ...? (spread array), ...?: (named spread), ?* (alias for ...?)
292
         */
293
        public function format(string $statement, mixed ...$args): string
1✔
294
        {
295
                $tokens = preg_split('#(\.\.\.\?:?|\$\?|->\?|::\?|\\\\\?|\?\*|\?(?!\w))#', $statement, -1, PREG_SPLIT_DELIM_CAPTURE);
1✔
296
                $res = '';
1✔
297
                foreach ($tokens as $n => $token) {
1✔
298
                        if ($n % 2 === 0) {
1✔
299
                                $res .= $token;
1✔
300
                        } elseif ($token === '\?') {
1✔
301
                                $res .= '?';
1✔
302
                        } elseif (!$args) {
1✔
303
                                throw new Nette\InvalidArgumentException('Insufficient number of arguments.');
1✔
304
                        } elseif ($token === '?') {
1✔
305
                                $res .= $this->dump(array_shift($args), strlen($res) - strrpos($res, "\n"));
1✔
306
                        } elseif ($token === '...?' || $token === '...?:' || $token === '?*') {
1✔
307
                                $arg = array_shift($args);
1✔
308
                                if (!is_array($arg)) {
1✔
309
                                        throw new Nette\InvalidArgumentException('Argument must be an array.');
1✔
310
                                }
311

312
                                $res .= $this->dumpArguments($arg, strlen($res) - strrpos($res, "\n"), $token === '...?:');
1✔
313

314
                        } else { // $  ->  ::
315
                                $arg = array_shift($args);
1✔
316
                                if ($arg instanceof Literal || !Helpers::isIdentifier($arg)) {
1✔
317
                                        $arg = '{' . $this->dumpVar($arg) . '}';
1✔
318
                                }
319

320
                                $res .= substr($token, 0, -1) . $arg;
1✔
321
                        }
322
                }
323

324
                if ($args) {
1✔
325
                        throw new Nette\InvalidArgumentException('Insufficient number of placeholders.');
1✔
326
                }
327

328
                return $res;
1✔
329
        }
330

331

332
        /** @param  mixed[]  $args */
333
        private function dumpArguments(array $args, int $column, bool $named): string
1✔
334
        {
335
                $pairs = [];
1✔
336
                foreach ($args as $k => $v) {
1✔
337
                        $name = $named && !is_int($k) ? $k . ': ' : '';
1✔
338
                        $pairs[] = $name . $this->dumpVar($v, [$args], 0, $column + strlen($name) + 1); // 1 = ) after args
1✔
339
                }
340

341
                $line = implode(', ', $pairs);
1✔
342
                return count($args) < 2 || (!str_contains($line, "\n") && $column + strlen($line) <= $this->wrapLength)
1✔
343
                        ? $line
1✔
344
                        : "\n" . $this->indentation . implode(",\n" . $this->indentation, $pairs) . ",\n";
1✔
345
        }
346

347

348
        /**
349
         * @param  class-string  $class
350
         * @param  mixed[]  $props
351
         * @internal
352
         */
353
        public static function createObject(string $class, array $props): object
1✔
354
        {
355
                if (method_exists($class, '__unserialize')) {
1✔
356
                        $obj = (new \ReflectionClass($class))->newInstanceWithoutConstructor();
1✔
357
                        $obj->__unserialize($props);
1✔
358
                        return $obj;
1✔
359
                }
360
                return unserialize('O' . substr(serialize($class), 1, -1) . substr(serialize($props), 1));
1✔
361
        }
362
}
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