1 | <?php |
||
2 | |||
3 | declare(strict_types=1); |
||
4 | |||
5 | namespace Yiisoft\VarDumper; |
||
6 | |||
7 | use __PHP_Incomplete_Class; |
||
8 | use Closure; |
||
9 | use DateTimeInterface; |
||
10 | use Exception; |
||
11 | use IteratorAggregate; |
||
12 | use JsonException; |
||
13 | use JsonSerializable; |
||
14 | use ReflectionException; |
||
15 | use ReflectionObject; |
||
16 | use Yiisoft\Arrays\ArrayableInterface; |
||
17 | use Yiisoft\VarDumper\Handler\EchoHandler; |
||
18 | |||
19 | use function array_keys; |
||
20 | use function gettype; |
||
21 | use function method_exists; |
||
22 | use function next; |
||
23 | use function spl_object_id; |
||
24 | use function str_repeat; |
||
25 | use function strtr; |
||
26 | use function trim; |
||
27 | use function var_export; |
||
28 | |||
29 | /** |
||
30 | * VarDumper provides enhanced versions of the PHP functions {@see var_dump()} and {@see var_export()}. |
||
31 | * It can: |
||
32 | * |
||
33 | * - Correctly identify the recursively referenced objects in a complex object structure. |
||
34 | * - Recursively control depth to avoid indefinite recursive display of some peculiar variables. |
||
35 | * - Export closures and objects. |
||
36 | * - Highlight output. |
||
37 | * - Format output. |
||
38 | */ |
||
39 | final class VarDumper |
||
40 | { |
||
41 | public const VAR_TYPE_PROPERTY = '$__type__$'; |
||
42 | public const OBJECT_ID_PROPERTY = '$__id__$'; |
||
43 | public const OBJECT_CLASS_PROPERTY = '$__class__$'; |
||
44 | public const DEPTH_LIMIT_EXCEEDED_PROPERTY = '$__depth_limit_exceeded__$'; |
||
45 | |||
46 | public const VAR_TYPE_ARRAY = 'array'; |
||
47 | public const VAR_TYPE_OBJECT = 'object'; |
||
48 | public const VAR_TYPE_RESOURCE = 'resource'; |
||
49 | private static ?HandlerInterface $defaultHandler = null; |
||
50 | |||
51 | /** |
||
52 | * @var string[] Variables using in closure scope. |
||
53 | */ |
||
54 | private array $useVarInClosures = []; |
||
55 | private bool $serializeObjects = true; |
||
56 | /** |
||
57 | * @var string Offset to use to indicate nesting level. |
||
58 | */ |
||
59 | private string $offset = ' '; |
||
60 | private static ?ClosureExporter $closureExporter = null; |
||
61 | |||
62 | /** |
||
63 | * @param mixed $variable Variable to dump. |
||
64 | */ |
||
65 | 149 | private function __construct(private mixed $variable) |
|
66 | { |
||
67 | 149 | } |
|
68 | |||
69 | public static function setDefaultHandler(HandlerInterface $handler): void |
||
70 | { |
||
71 | self::$defaultHandler = $handler; |
||
72 | } |
||
73 | |||
74 | 6 | public static function getDefaultHandler(): HandlerInterface |
|
75 | { |
||
76 | 6 | return self::$defaultHandler ??= new EchoHandler(); |
|
77 | } |
||
78 | |||
79 | /** |
||
80 | * @param mixed $variable Variable to dump. |
||
81 | * |
||
82 | * @return static An instance containing variable to dump. |
||
83 | */ |
||
84 | 149 | public static function create(mixed $variable): self |
|
85 | { |
||
86 | 149 | return new self($variable); |
|
87 | } |
||
88 | |||
89 | /** |
||
90 | * Prints a variable. |
||
91 | * |
||
92 | * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()} |
||
93 | * but is more robust when handling complex objects. |
||
94 | * |
||
95 | * @param mixed $variable Variable to be dumped. |
||
96 | * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10. |
||
97 | * @param bool $highlight Whether the result should be syntax-highlighted. |
||
98 | * |
||
99 | * @throws ReflectionException |
||
100 | */ |
||
101 | 6 | public static function dump(mixed $variable, int $depth = 10, bool $highlight = true): void |
|
102 | { |
||
103 | 6 | self::getDefaultHandler()->handle($variable, $depth, $highlight); |
|
104 | } |
||
105 | |||
106 | /** |
||
107 | * Sets offset string to use to indicate nesting level. |
||
108 | * |
||
109 | * @param string $offset The offset string. |
||
110 | * |
||
111 | * @return static New instance with a given offset. |
||
112 | */ |
||
113 | public function withOffset(string $offset): self |
||
114 | { |
||
115 | $new = clone $this; |
||
116 | $new->offset = $offset; |
||
117 | |||
118 | return $new; |
||
119 | } |
||
120 | |||
121 | /** |
||
122 | * Dumps a variable in terms of a string. |
||
123 | * |
||
124 | * This method achieves the similar functionality as {@see var_dump()} and {@see print_r()} |
||
125 | * but is more robust when handling complex objects. |
||
126 | * |
||
127 | * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10. |
||
128 | * @param bool $highlight Whether the result should be syntax-highlighted. |
||
129 | * |
||
130 | * @throws ReflectionException |
||
131 | * |
||
132 | * @return string The string representation of the variable. |
||
133 | */ |
||
134 | 35 | public function asString(int $depth = 10): string |
|
135 | { |
||
136 | 35 | return $this->dumpInternal($this->variable, true, $depth, 0); |
|
137 | } |
||
138 | |||
139 | /** |
||
140 | * Dumps a variable as a JSON string. |
||
141 | * |
||
142 | * This method provides similar functionality to the {@see json_encode()} |
||
143 | * but is more robust when handling complex objects. |
||
144 | * |
||
145 | * @param bool $format Use whitespaces in returned data to format it |
||
146 | * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10. |
||
147 | * |
||
148 | * @throws JsonException |
||
149 | * |
||
150 | * @return string The json representation of the variable. |
||
151 | */ |
||
152 | 30 | public function asJson(bool $format = true, int $depth = 10): string |
|
153 | { |
||
154 | /** @var mixed $output */ |
||
155 | 30 | $output = $this->asPrimitives($depth); |
|
156 | |||
157 | 30 | if ($format) { |
|
158 | 30 | return json_encode($output, JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT); |
|
159 | } |
||
160 | |||
161 | return json_encode($output, JSON_THROW_ON_ERROR); |
||
162 | } |
||
163 | |||
164 | /** |
||
165 | * Dumps the variable as PHP primitives in JSON decoded style. |
||
166 | * |
||
167 | * @param int $depth Maximum depth that the dumper should go into the variable. Defaults to 10. |
||
168 | * |
||
169 | * @throws ReflectionException |
||
170 | * |
||
171 | * @return mixed |
||
172 | */ |
||
173 | 60 | public function asPrimitives(int $depth = 10): mixed |
|
174 | { |
||
175 | 60 | return $this->exportPrimitives($this->variable, $depth, 0); |
|
176 | } |
||
177 | |||
178 | /** |
||
179 | * Exports a variable as a string containing PHP code. |
||
180 | * |
||
181 | * The string is a valid PHP expression that can be evaluated by PHP parser |
||
182 | * and the evaluation result will give back the variable value. |
||
183 | * |
||
184 | * This method is similar to {@see var_export()}. The main difference is that |
||
185 | * it generates more compact string representation using short array syntax. |
||
186 | * |
||
187 | * It also handles closures with {@see ClosureExporter} and objects |
||
188 | * by using the PHP functions {@see serialize()} and {@see unserialize()}. |
||
189 | * |
||
190 | * @param bool $format Whatever to format code. |
||
191 | * @param string[] $useVariables Array of variables used in `use` statement (['$params', '$config']) |
||
192 | * @param bool $serializeObjects If it is true all objects will be serialized except objects with closure(s). If it |
||
193 | * is false only objects of internal classes will be serialized. |
||
194 | * |
||
195 | * @throws ReflectionException |
||
196 | * |
||
197 | * @return string A PHP code representation of the variable. |
||
198 | */ |
||
199 | 56 | public function export(bool $format = true, array $useVariables = [], bool $serializeObjects = true): string |
|
200 | { |
||
201 | 56 | $this->useVarInClosures = $useVariables; |
|
202 | 56 | $this->serializeObjects = $serializeObjects; |
|
203 | 56 | return $this->exportInternal($this->variable, $format, 0); |
|
204 | } |
||
205 | |||
206 | /** |
||
207 | * @param mixed $var Variable to be dumped. |
||
208 | * @param bool $format Whatever to format code. |
||
209 | * @param int $depth Maximum depth. |
||
210 | * @param int $level Current depth. |
||
211 | * |
||
212 | * @throws ReflectionException |
||
213 | * |
||
214 | * @return string |
||
215 | */ |
||
216 | 35 | private function dumpInternal(mixed $var, bool $format, int $depth, int $level): string |
|
217 | { |
||
218 | 35 | switch (gettype($var)) { |
|
219 | 35 | case 'resource': |
|
220 | 34 | case 'resource (closed)': |
|
221 | 1 | return '{resource}'; |
|
222 | 34 | case 'NULL': |
|
223 | 1 | return 'null'; |
|
224 | 33 | case 'array': |
|
225 | 6 | if ($depth <= $level) { |
|
226 | 1 | return '[...]'; |
|
227 | } |
||
228 | |||
229 | 5 | if (empty($var)) { |
|
230 | 2 | return '[]'; |
|
231 | } |
||
232 | |||
233 | 3 | $output = ''; |
|
234 | 3 | $keys = array_keys($var); |
|
235 | 3 | $spaces = str_repeat($this->offset, $level); |
|
236 | 3 | $output .= '['; |
|
237 | |||
238 | 3 | foreach ($keys as $name) { |
|
239 | 3 | if ($format) { |
|
240 | 3 | $output .= "\n" . $spaces . $this->offset; |
|
241 | } |
||
242 | 3 | $output .= $this->exportVariable($name); |
|
243 | 3 | $output .= ' => '; |
|
244 | 3 | $output .= $this->dumpInternal($var[$name], $format, $depth, $level + 1); |
|
245 | } |
||
246 | |||
247 | 3 | return $format |
|
248 | 3 | ? $output . "\n" . $spaces . ']' |
|
249 | 3 | : $output . ']'; |
|
250 | 31 | case 'object': |
|
251 | 17 | if ($var instanceof Closure) { |
|
252 | 11 | return $this->exportClosure($var); |
|
253 | } |
||
254 | 8 | if ($var instanceof DateTimeInterface) { |
|
255 | 1 | return $this->exportDateTime($var); |
|
256 | } |
||
257 | |||
258 | 7 | if ($depth <= $level) { |
|
259 | 1 | return $this->getObjectDescription($var) . ' (...)'; |
|
260 | } |
||
261 | |||
262 | 6 | $spaces = str_repeat($this->offset, $level); |
|
263 | 6 | $output = $this->getObjectDescription($var) . "\n" . $spaces . '('; |
|
264 | 6 | $objectProperties = $this->getObjectProperties($var); |
|
265 | |||
266 | /** @psalm-var mixed $value */ |
||
267 | 6 | foreach ($objectProperties as $name => $value) { |
|
268 | 4 | $propertyName = strtr(trim((string) $name), "\0", '::'); |
|
269 | 4 | $output .= "\n" . $spaces . $this->offset . '[' . $propertyName . '] => '; |
|
270 | 4 | $output .= $this->dumpInternal($value, $format, $depth, $level + 1); |
|
271 | } |
||
272 | 6 | return $output . "\n" . $spaces . ')'; |
|
273 | default: |
||
274 | 16 | return $this->exportVariable($var); |
|
275 | } |
||
276 | } |
||
277 | |||
278 | /** |
||
279 | * @param mixed $variable Variable to be exported. |
||
280 | * @param bool $format Whatever to format code. |
||
281 | * @param int $level Current depth. |
||
282 | * |
||
283 | * @throws ReflectionException |
||
284 | * |
||
285 | * @return string |
||
286 | */ |
||
287 | 56 | private function exportInternal(mixed $variable, bool $format, int $level): string |
|
288 | { |
||
289 | 56 | $spaces = str_repeat($this->offset, $level); |
|
290 | 56 | switch (gettype($variable)) { |
|
291 | 56 | case 'NULL': |
|
292 | 2 | return 'null'; |
|
293 | 54 | case 'array': |
|
294 | 9 | if (empty($variable)) { |
|
295 | 2 | return '[]'; |
|
296 | } |
||
297 | |||
298 | 7 | $keys = array_keys($variable); |
|
299 | 7 | $outputKeys = $keys !== array_keys($keys); |
|
300 | 7 | $output = '['; |
|
301 | |||
302 | 7 | foreach ($keys as $key) { |
|
303 | 7 | if ($format) { |
|
304 | 4 | $output .= "\n" . $spaces . $this->offset; |
|
305 | } |
||
306 | 7 | if ($outputKeys) { |
|
307 | 3 | $output .= $this->exportVariable($key); |
|
308 | 3 | $output .= ' => '; |
|
309 | } |
||
310 | 7 | $output .= $this->exportInternal($variable[$key], $format, $level + 1); |
|
311 | 7 | if ($format || next($keys) !== false) { |
|
312 | 6 | $output .= ','; |
|
313 | } |
||
314 | } |
||
315 | |||
316 | 7 | return $format |
|
317 | 4 | ? $output . "\n" . $spaces . ']' |
|
318 | 7 | : $output . ']'; |
|
319 | 52 | case 'object': |
|
320 | 35 | if ($variable instanceof Closure) { |
|
321 | 25 | return $this->exportClosure($variable, $level); |
|
322 | } |
||
323 | 15 | if ($variable instanceof DateTimeInterface) { |
|
324 | 4 | return $this->exportDateTime($variable); |
|
325 | } |
||
326 | |||
327 | |||
328 | 11 | $reflectionObject = new ReflectionObject($variable); |
|
329 | try { |
||
330 | 11 | if ($this->serializeObjects || $reflectionObject->isInternal() || $reflectionObject->isAnonymous()) { |
|
331 | 9 | return "unserialize({$this->exportVariable(serialize($variable))})"; |
|
332 | } |
||
333 | |||
334 | 2 | return $this->exportObject($variable, $reflectionObject, $format, $level); |
|
335 | 6 | } catch (Exception) { |
|
336 | // Serialize may fail, for example: if object contains a `\Closure` instance so we use a fallback. |
||
337 | 6 | if ($this->serializeObjects && !$reflectionObject->isInternal() && !$reflectionObject->isAnonymous()) { |
|
338 | try { |
||
339 | 4 | return $this->exportObject($variable, $reflectionObject, $format, $level); |
|
340 | } catch (Exception) { |
||
341 | return $this->exportObjectFallback($variable, $format, $level); |
||
342 | } |
||
343 | } |
||
344 | |||
345 | 2 | return $this->exportObjectFallback($variable, $format, $level); |
|
346 | } |
||
347 | default: |
||
348 | 19 | return $this->exportVariable($variable); |
|
349 | } |
||
350 | } |
||
351 | |||
352 | /** |
||
353 | * @throws ReflectionException |
||
354 | * |
||
355 | * @return mixed |
||
356 | * @psalm-param mixed $var |
||
357 | */ |
||
358 | 60 | private function exportPrimitives(mixed $var, int $depth, int $level): mixed |
|
359 | { |
||
360 | 60 | switch (gettype($var)) { |
|
361 | 60 | case 'resource': |
|
362 | 58 | case 'resource (closed)': |
|
363 | /** @var resource $var */ |
||
364 | 4 | $id = get_resource_id($var); |
|
365 | 4 | $type = get_resource_type($var); |
|
366 | |||
367 | 4 | return [ |
|
368 | 4 | self::VAR_TYPE_PROPERTY => self::VAR_TYPE_RESOURCE, |
|
369 | 4 | 'id' => $id, |
|
370 | 4 | 'type' => $type, |
|
371 | 4 | 'closed' => !is_resource($var), |
|
372 | 4 | ]; |
|
373 | 56 | case 'array': |
|
374 | 12 | if ($depth <= $level) { |
|
375 | 2 | return [ |
|
376 | 2 | self::VAR_TYPE_PROPERTY => self::VAR_TYPE_ARRAY, |
|
377 | 2 | self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true, |
|
378 | 2 | ]; |
|
379 | } |
||
380 | |||
381 | /** @psalm-suppress MissingClosureReturnType */ |
||
382 | 12 | return array_map(fn ($value) => $this->exportPrimitives($value, $depth, $level + 1), $var); |
|
383 | 52 | case 'object': |
|
384 | 34 | if ($var instanceof Closure) { |
|
385 | 20 | return $this->exportClosure($var); |
|
386 | } |
||
387 | |||
388 | 16 | $objectClass = $var::class; |
|
389 | 16 | $objectId = $this->getObjectId($var); |
|
390 | 16 | if ($depth <= $level) { |
|
391 | 2 | return [ |
|
392 | 2 | self::VAR_TYPE_PROPERTY => self::VAR_TYPE_OBJECT, |
|
393 | 2 | self::OBJECT_ID_PROPERTY => $objectId, |
|
394 | 2 | self::OBJECT_CLASS_PROPERTY => $objectClass, |
|
395 | 2 | self::DEPTH_LIMIT_EXCEEDED_PROPERTY => true, |
|
396 | 2 | ]; |
|
397 | } |
||
398 | |||
399 | 16 | $objectProperties = $this->getObjectProperties($var); |
|
400 | |||
401 | 16 | $output = [ |
|
402 | 16 | self::OBJECT_ID_PROPERTY => $objectId, |
|
403 | 16 | self::OBJECT_CLASS_PROPERTY => $objectClass, |
|
404 | 16 | ]; |
|
405 | /** |
||
406 | * @psalm-var mixed $value |
||
407 | * @psalm-var string $name |
||
408 | */ |
||
409 | 16 | foreach ($objectProperties as $name => $value) { |
|
410 | 14 | $propertyName = $this->getPropertyName($name); |
|
411 | /** @psalm-suppress MixedAssignment */ |
||
412 | 14 | $output[$propertyName] = $this->exportPrimitives($value, $depth, $level + 1); |
|
413 | } |
||
414 | 16 | return $output; |
|
415 | default: |
||
416 | 28 | return $var; |
|
417 | } |
||
418 | } |
||
419 | |||
420 | 20 | private function getPropertyName(string|int $property): string |
|
421 | { |
||
422 | 20 | if (is_int($property)) { |
|
423 | 2 | return (string) $property; |
|
424 | } |
||
425 | 18 | $property = str_replace("\0", '::', trim($property)); |
|
426 | |||
427 | 18 | if (str_starts_with($property, '*::')) { |
|
428 | return substr($property, 3); |
||
429 | } |
||
430 | |||
431 | 18 | if (($pos = strpos($property, '::')) !== false) { |
|
432 | 6 | return substr($property, $pos + 2); |
|
433 | } |
||
434 | |||
435 | 12 | return $property; |
|
436 | } |
||
437 | |||
438 | /** |
||
439 | * @throws ReflectionException |
||
440 | * |
||
441 | * @return string |
||
442 | */ |
||
443 | 2 | private function exportObjectFallback(object $variable, bool $format, int $level): string |
|
444 | { |
||
445 | 2 | if ($variable instanceof ArrayableInterface) { |
|
446 | return $this->exportInternal($variable->toArray(), $format, $level); |
||
447 | } |
||
448 | |||
449 | 2 | if ($variable instanceof JsonSerializable) { |
|
450 | return $this->exportInternal($variable->jsonSerialize(), $format, $level); |
||
451 | } |
||
452 | |||
453 | 2 | if ($variable instanceof IteratorAggregate) { |
|
454 | return $this->exportInternal(iterator_to_array($variable), $format, $level); |
||
455 | } |
||
456 | |||
457 | /** @psalm-suppress RedundantCondition */ |
||
458 | 2 | if ('__PHP_Incomplete_Class' !== $variable::class && method_exists($variable, '__toString')) { |
|
459 | return $this->exportVariable($variable->__toString()); |
||
460 | } |
||
461 | |||
462 | 2 | return $this->exportVariable(self::create($variable)->asString()); |
|
463 | } |
||
464 | |||
465 | 6 | private function exportObject(object $variable, ReflectionObject $reflectionObject, bool $format, int $level): string |
|
466 | { |
||
467 | 6 | $spaces = str_repeat($this->offset, $level); |
|
468 | 6 | $objectProperties = $this->getObjectProperties($variable); |
|
469 | 6 | $class = $variable::class; |
|
470 | 6 | $use = $this->useVarInClosures === [] ? '' : ' use (' . implode(', ', $this->useVarInClosures) . ')'; |
|
471 | 6 | $lines = ['(static function ()' . $use . ' {',]; |
|
472 | 6 | if ($reflectionObject->getConstructor() === null) { |
|
473 | 2 | $lines = [ |
|
474 | 2 | ...$lines, |
|
475 | 2 | $this->offset . '$object = new ' . $class . '();', |
|
476 | 2 | $this->offset . '(function ()' . $use . ' {', |
|
477 | 2 | ]; |
|
478 | } else { |
||
479 | 4 | $lines = [ |
|
480 | 4 | ...$lines, |
|
481 | 4 | $this->offset . '$class = new \ReflectionClass(\'' . $class . '\');', |
|
482 | 4 | $this->offset . '$object = $class->newInstanceWithoutConstructor();', |
|
483 | 4 | $this->offset . '(function ()' . $use . ' {', |
|
484 | 4 | ]; |
|
485 | } |
||
486 | 6 | $endLines = [ |
|
487 | 6 | $this->offset . '})->bindTo($object, \'' . $class . '\')();', |
|
488 | 6 | '', |
|
489 | 6 | $this->offset . 'return $object;', |
|
490 | 6 | '})()', |
|
491 | 6 | ]; |
|
492 | |||
493 | /** |
||
494 | * @psalm-var mixed $value |
||
495 | * @psalm-var string $name |
||
496 | */ |
||
497 | 6 | foreach ($objectProperties as $name => $value) { |
|
498 | 6 | $propertyName = $this->getPropertyName($name); |
|
499 | 6 | $lines[] = $this->offset . $this->offset . '$this->' . $propertyName . ' = ' . |
|
500 | 6 | $this->exportInternal($value, $format, $level + 2) . ';'; |
|
501 | } |
||
502 | |||
503 | 6 | return implode("\n" . ($format ? $spaces : ''), array_merge($lines, $endLines)); |
|
504 | } |
||
505 | |||
506 | /** |
||
507 | * Exports a {@see \Closure} instance. |
||
508 | * |
||
509 | * @param Closure $closure Closure instance. |
||
510 | * |
||
511 | * @throws ReflectionException |
||
512 | * |
||
513 | * @return string |
||
514 | */ |
||
515 | 56 | private function exportClosure(Closure $closure, int $level = 0): string |
|
516 | { |
||
517 | 56 | if (self::$closureExporter === null) { |
|
518 | 1 | self::$closureExporter = new ClosureExporter(); |
|
519 | } |
||
520 | |||
521 | 56 | return self::$closureExporter->export($closure, $level); |
|
0 ignored issues
–
show
|
|||
522 | } |
||
523 | |||
524 | /** |
||
525 | * @return string |
||
526 | */ |
||
527 | 42 | private function exportVariable(mixed $variable): string |
|
528 | { |
||
529 | 42 | return var_export($variable, true); |
|
530 | } |
||
531 | |||
532 | 7 | private function getObjectDescription(object $object): string |
|
533 | { |
||
534 | 7 | return $object::class . '#' . $this->getObjectId($object); |
|
535 | } |
||
536 | |||
537 | 23 | private function getObjectId(object $object): string |
|
538 | { |
||
539 | 23 | return (string) spl_object_id($object); |
|
540 | } |
||
541 | |||
542 | 28 | private function getObjectProperties(object $var): array |
|
543 | { |
||
544 | 28 | if (!$var instanceof __PHP_Incomplete_Class && method_exists($var, '__debugInfo')) { |
|
545 | /** @var array $var */ |
||
546 | 3 | $var = $var->__debugInfo(); |
|
547 | } |
||
548 | |||
549 | 28 | return (array) $var; |
|
550 | } |
||
551 | |||
552 | 5 | private function exportDateTime(DateTimeInterface $variable): string |
|
553 | { |
||
554 | 5 | return sprintf( |
|
555 | 5 | "new \%s('%s', new \DateTimeZone('%s'))", |
|
556 | 5 | $variable::class, |
|
557 | 5 | $variable->format(DateTimeInterface::RFC3339_EXTENDED), |
|
558 | 5 | $variable->getTimezone()->getName() |
|
559 | 5 | ); |
|
560 | } |
||
561 | } |
||
562 |
This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.
This is most likely a typographical error or the method has been renamed.