diff --git a/.gitattributes b/.gitattributes index 84c7add0..14c3c359 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,3 @@ /Tests export-ignore /phpunit.xml.dist export-ignore -/.gitattributes export-ignore -/.gitignore export-ignore +/.git* export-ignore diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..4689c4da --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +Please do not submit any Pull Requests here. They will be closed. +--- + +Please submit your PR here instead: +https://github.com/symfony/symfony + +This repository is what we call a "subtree split": a read-only subset of that main repository. +We're looking forward to your PR there! diff --git a/.github/workflows/close-pull-request.yml b/.github/workflows/close-pull-request.yml new file mode 100644 index 00000000..e55b4781 --- /dev/null +++ b/.github/workflows/close-pull-request.yml @@ -0,0 +1,20 @@ +name: Close Pull Request + +on: + pull_request_target: + types: [opened] + +jobs: + run: + runs-on: ubuntu-latest + steps: + - uses: superbrothers/close-pull-request@v3 + with: + comment: | + Thanks for your Pull Request! We love contributions. + + However, you should instead open your PR on the main repository: + https://github.com/symfony/symfony + + This repository is what we call a "subtree split": a read-only subset of that main repository. + We're looking forward to your PR there! diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa4daf2..7481cff1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,19 @@ CHANGELOG ========= +6.4 +--- + + * Dump uninitialized properties + +6.3 +--- + + * Add caster for `WeakMap` + * Add support of named arguments to `dd()` and `dump()` to display the argument name + * Add support for `Relay\Relay` + * Add display of invisible characters + 6.2 --- diff --git a/Caster/AmqpCaster.php b/Caster/AmqpCaster.php index dc3b6219..22026f46 100644 --- a/Caster/AmqpCaster.php +++ b/Caster/AmqpCaster.php @@ -46,6 +46,9 @@ class AmqpCaster \AMQP_EX_TYPE_HEADERS => 'AMQP_EX_TYPE_HEADERS', ]; + /** + * @return array + */ public static function castConnection(\AMQPConnection $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -79,6 +82,9 @@ public static function castConnection(\AMQPConnection $c, array $a, Stub $stub, return $a; } + /** + * @return array + */ public static function castChannel(\AMQPChannel $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -102,6 +108,9 @@ public static function castChannel(\AMQPChannel $c, array $a, Stub $stub, bool $ return $a; } + /** + * @return array + */ public static function castQueue(\AMQPQueue $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -125,6 +134,9 @@ public static function castQueue(\AMQPQueue $c, array $a, Stub $stub, bool $isNe return $a; } + /** + * @return array + */ public static function castExchange(\AMQPExchange $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -153,6 +165,9 @@ public static function castExchange(\AMQPExchange $c, array $a, Stub $stub, bool return $a; } + /** + * @return array + */ public static function castEnvelope(\AMQPEnvelope $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; diff --git a/Caster/Caster.php b/Caster/Caster.php index b6a84fcb..d9577e7a 100644 --- a/Caster/Caster.php +++ b/Caster/Caster.php @@ -32,17 +32,22 @@ class Caster public const EXCLUDE_EMPTY = 128; public const EXCLUDE_NOT_IMPORTANT = 256; public const EXCLUDE_STRICT = 512; + public const EXCLUDE_UNINITIALIZED = 1024; public const PREFIX_VIRTUAL = "\0~\0"; public const PREFIX_DYNAMIC = "\0+\0"; public const PREFIX_PROTECTED = "\0*\0"; + // usage: sprintf(Caster::PATTERN_PRIVATE, $class, $property) + public const PATTERN_PRIVATE = "\0%s\0%s"; + + private static array $classProperties = []; /** * Casts objects to arrays and adds the dynamic property prefix. * * @param bool $hasDebugInfo Whether the __debugInfo method exists on $obj or not */ - public static function castObject(object $obj, string $class, bool $hasDebugInfo = false, string $debugClass = null): array + public static function castObject(object $obj, string $class, bool $hasDebugInfo = false, ?string $debugClass = null): array { if ($hasDebugInfo) { try { @@ -59,20 +64,17 @@ public static function castObject(object $obj, string $class, bool $hasDebugInfo return $a; } + $classProperties = self::$classProperties[$class] ??= self::getClassProperties(new \ReflectionClass($class)); + $a = array_replace($classProperties, $a); + if ($a) { - static $publicProperties = []; $debugClass ??= get_debug_type($obj); $i = 0; $prefixedKeys = []; foreach ($a as $k => $v) { if ("\0" !== ($k[0] ?? '')) { - if (!isset($publicProperties[$class])) { - foreach ((new \ReflectionClass($class))->getProperties(\ReflectionProperty::IS_PUBLIC) as $prop) { - $publicProperties[$class][$prop->name] = true; - } - } - if (!isset($publicProperties[$class][$k])) { + if (!isset($classProperties[$k])) { $prefixedKeys[$i] = self::PREFIX_DYNAMIC.$k; } } elseif ($debugClass !== $class && 1 === strpos($k, $class)) { @@ -129,6 +131,8 @@ public static function filter(array $a, int $filter, array $listedProperties = [ $type |= self::EXCLUDE_EMPTY & $filter; } elseif (false === $v || '' === $v || '0' === $v || 0 === $v || 0.0 === $v || [] === $v) { $type |= self::EXCLUDE_EMPTY & $filter; + } elseif ($v instanceof UninitializedStub) { + $type |= self::EXCLUDE_UNINITIALIZED & $filter; } if ((self::EXCLUDE_NOT_IMPORTANT & $filter) && !\in_array($k, $listedProperties, true)) { $type |= self::EXCLUDE_NOT_IMPORTANT; @@ -167,4 +171,28 @@ public static function castPhpIncompleteClass(\__PHP_Incomplete_Class $c, array return $a; } + + private static function getClassProperties(\ReflectionClass $class): array + { + $classProperties = []; + $className = $class->name; + + if ($parent = $class->getParentClass()) { + $classProperties += self::$classProperties[$parent->name] ??= self::getClassProperties($parent); + } + + foreach ($class->getProperties() as $p) { + if ($p->isStatic()) { + continue; + } + + $classProperties[match (true) { + $p->isPublic() => $p->name, + $p->isProtected() => self::PREFIX_PROTECTED.$p->name, + default => "\0".$className."\0".$p->name, + }] = new UninitializedStub($p); + } + + return $classProperties; + } } diff --git a/Caster/ClassStub.php b/Caster/ClassStub.php index fb8377a0..bf0d056c 100644 --- a/Caster/ClassStub.php +++ b/Caster/ClassStub.php @@ -24,7 +24,7 @@ class ClassStub extends ConstStub * @param string $identifier A PHP identifier, e.g. a class, method, interface, etc. name * @param callable $callable The callable targeted by the identifier when it is ambiguous or not a real PHP identifier */ - public function __construct(string $identifier, callable|array|string $callable = null) + public function __construct(string $identifier, callable|array|string|null $callable = null) { $this->value = $identifier; @@ -56,9 +56,7 @@ public function __construct(string $identifier, callable|array|string $callable } if (str_contains($identifier, "@anonymous\0")) { - $this->value = $identifier = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { - return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; - }, $identifier); + $this->value = $identifier = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $identifier); } if (null !== $callable && $r instanceof \ReflectionFunctionAbstract) { @@ -87,6 +85,9 @@ public function __construct(string $identifier, callable|array|string $callable } } + /** + * @return mixed + */ public static function wrapCallable(mixed $callable) { if (\is_object($callable) || !\is_callable($callable)) { diff --git a/Caster/ConstStub.php b/Caster/ConstStub.php index d7d1812b..587c6c39 100644 --- a/Caster/ConstStub.php +++ b/Caster/ConstStub.php @@ -20,7 +20,7 @@ */ class ConstStub extends Stub { - public function __construct(string $name, string|int|float $value = null) + public function __construct(string $name, string|int|float|null $value = null) { $this->class = $name; $this->value = 1 < \func_num_args() ? $value : $name; diff --git a/Caster/DOMCaster.php b/Caster/DOMCaster.php index 8b770b8a..4135fbfe 100644 --- a/Caster/DOMCaster.php +++ b/Caster/DOMCaster.php @@ -23,7 +23,7 @@ class DOMCaster { private const ERROR_CODES = [ - \DOM_PHP_ERR => 'DOM_PHP_ERR', + 0 => 'DOM_PHP_ERR', \DOM_INDEX_SIZE_ERR => 'DOM_INDEX_SIZE_ERR', \DOMSTRING_SIZE_ERR => 'DOMSTRING_SIZE_ERR', \DOM_HIERARCHY_REQUEST_ERR => 'DOM_HIERARCHY_REQUEST_ERR', @@ -63,6 +63,9 @@ class DOMCaster \XML_NAMESPACE_DECL_NODE => 'XML_NAMESPACE_DECL_NODE', ]; + /** + * @return array + */ public static function castException(\DOMException $e, array $a, Stub $stub, bool $isNested) { $k = Caster::PREFIX_PROTECTED.'code'; @@ -73,6 +76,9 @@ public static function castException(\DOMException $e, array $a, Stub $stub, boo return $a; } + /** + * @return array + */ public static function castLength($dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -82,6 +88,9 @@ public static function castLength($dom, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castImplementation(\DOMImplementation $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -92,6 +101,9 @@ public static function castImplementation(\DOMImplementation $dom, array $a, Stu return $a; } + /** + * @return array + */ public static function castNode(\DOMNode $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -116,6 +128,9 @@ public static function castNode(\DOMNode $dom, array $a, Stub $stub, bool $isNes return $a; } + /** + * @return array + */ public static function castNameSpaceNode(\DOMNameSpaceNode $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -132,22 +147,21 @@ public static function castNameSpaceNode(\DOMNameSpaceNode $dom, array $a, Stub return $a; } + /** + * @return array + */ public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ 'doctype' => $dom->doctype, 'implementation' => $dom->implementation, 'documentElement' => new CutStub($dom->documentElement), - 'actualEncoding' => $dom->actualEncoding, 'encoding' => $dom->encoding, 'xmlEncoding' => $dom->xmlEncoding, - 'standalone' => $dom->standalone, 'xmlStandalone' => $dom->xmlStandalone, - 'version' => $dom->version, 'xmlVersion' => $dom->xmlVersion, 'strictErrorChecking' => $dom->strictErrorChecking, 'documentURI' => $dom->documentURI ? new LinkStub($dom->documentURI) : $dom->documentURI, - 'config' => $dom->config, 'formatOutput' => $dom->formatOutput, 'validateOnParse' => $dom->validateOnParse, 'resolveExternals' => $dom->resolveExternals, @@ -166,6 +180,9 @@ public static function castDocument(\DOMDocument $dom, array $a, Stub $stub, boo return $a; } + /** + * @return array + */ public static function castCharacterData(\DOMCharacterData $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -176,6 +193,9 @@ public static function castCharacterData(\DOMCharacterData $dom, array $a, Stub return $a; } + /** + * @return array + */ public static function castAttr(\DOMAttr $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -189,6 +209,9 @@ public static function castAttr(\DOMAttr $dom, array $a, Stub $stub, bool $isNes return $a; } + /** + * @return array + */ public static function castElement(\DOMElement $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -199,6 +222,9 @@ public static function castElement(\DOMElement $dom, array $a, Stub $stub, bool return $a; } + /** + * @return array + */ public static function castText(\DOMText $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -208,6 +234,9 @@ public static function castText(\DOMText $dom, array $a, Stub $stub, bool $isNes return $a; } + /** + * @return array + */ public static function castDocumentType(\DOMDocumentType $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -222,6 +251,9 @@ public static function castDocumentType(\DOMDocumentType $dom, array $a, Stub $s return $a; } + /** + * @return array + */ public static function castNotation(\DOMNotation $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -232,20 +264,23 @@ public static function castNotation(\DOMNotation $dom, array $a, Stub $stub, boo return $a; } + /** + * @return array + */ public static function castEntity(\DOMEntity $dom, array $a, Stub $stub, bool $isNested) { $a += [ 'publicId' => $dom->publicId, 'systemId' => $dom->systemId, 'notationName' => $dom->notationName, - 'actualEncoding' => $dom->actualEncoding, - 'encoding' => $dom->encoding, - 'version' => $dom->version, ]; return $a; } + /** + * @return array + */ public static function castProcessingInstruction(\DOMProcessingInstruction $dom, array $a, Stub $stub, bool $isNested) { $a += [ @@ -256,6 +291,9 @@ public static function castProcessingInstruction(\DOMProcessingInstruction $dom, return $a; } + /** + * @return array + */ public static function castXPath(\DOMXPath $dom, array $a, Stub $stub, bool $isNested) { $a += [ diff --git a/Caster/DateCaster.php b/Caster/DateCaster.php index 07e8c5ea..a0cbddb7 100644 --- a/Caster/DateCaster.php +++ b/Caster/DateCaster.php @@ -24,10 +24,13 @@ class DateCaster { private const PERIOD_LIMIT = 3; + /** + * @return array + */ public static function castDateTime(\DateTimeInterface $d, array $a, Stub $stub, bool $isNested, int $filter) { $prefix = Caster::PREFIX_VIRTUAL; - $location = $d->getTimezone()->getLocation(); + $location = $d->getTimezone() ? $d->getTimezone()->getLocation() : null; $fromNow = (new \DateTimeImmutable())->diff($d); $title = $d->format('l, F j, Y') @@ -47,6 +50,9 @@ public static function castDateTime(\DateTimeInterface $d, array $a, Stub $stub, return $a; } + /** + * @return array + */ public static function castInterval(\DateInterval $interval, array $a, Stub $stub, bool $isNested, int $filter) { $now = new \DateTimeImmutable('@0', new \DateTimeZone('UTC')); @@ -76,6 +82,9 @@ private static function formatInterval(\DateInterval $i): string return $i->format(rtrim($format)); } + /** + * @return array + */ public static function castTimeZone(\DateTimeZone $timeZone, array $a, Stub $stub, bool $isNested, int $filter) { $location = $timeZone->getLocation(); @@ -87,6 +96,9 @@ public static function castTimeZone(\DateTimeZone $timeZone, array $a, Stub $stu return $filter & Caster::EXCLUDE_VERBOSE ? $z : $z + $a; } + /** + * @return array + */ public static function castPeriod(\DatePeriod $p, array $a, Stub $stub, bool $isNested, int $filter) { $dates = []; diff --git a/Caster/DoctrineCaster.php b/Caster/DoctrineCaster.php index 129b2cb4..3120c3d9 100644 --- a/Caster/DoctrineCaster.php +++ b/Caster/DoctrineCaster.php @@ -25,6 +25,9 @@ */ class DoctrineCaster { + /** + * @return array + */ public static function castCommonProxy(CommonProxy $proxy, array $a, Stub $stub, bool $isNested) { foreach (['__cloner__', '__initializer__'] as $k) { @@ -37,6 +40,9 @@ public static function castCommonProxy(CommonProxy $proxy, array $a, Stub $stub, return $a; } + /** + * @return array + */ public static function castOrmProxy(OrmProxy $proxy, array $a, Stub $stub, bool $isNested) { foreach (['_entityPersister', '_identifier'] as $k) { @@ -49,6 +55,9 @@ public static function castOrmProxy(OrmProxy $proxy, array $a, Stub $stub, bool return $a; } + /** + * @return array + */ public static function castPersistentCollection(PersistentCollection $coll, array $a, Stub $stub, bool $isNested) { foreach (['snapshot', 'association', 'typeClass'] as $k) { diff --git a/Caster/DsPairStub.php b/Caster/DsPairStub.php index 22112af9..afa2727b 100644 --- a/Caster/DsPairStub.php +++ b/Caster/DsPairStub.php @@ -18,7 +18,7 @@ */ class DsPairStub extends Stub { - public function __construct(string|int $key, mixed $value) + public function __construct(mixed $key, mixed $value) { $this->value = [ Caster::PREFIX_VIRTUAL.'key' => $key, diff --git a/Caster/ExceptionCaster.php b/Caster/ExceptionCaster.php index 14caf71f..3dff5dca 100644 --- a/Caster/ExceptionCaster.php +++ b/Caster/ExceptionCaster.php @@ -11,6 +11,7 @@ namespace Symfony\Component\VarDumper\Caster; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Exception\ThrowingCasterException; @@ -41,21 +42,30 @@ class ExceptionCaster \E_USER_ERROR => 'E_USER_ERROR', \E_USER_WARNING => 'E_USER_WARNING', \E_USER_NOTICE => 'E_USER_NOTICE', - \E_STRICT => 'E_STRICT', + 2048 => 'E_STRICT', ]; private static array $framesCache = []; + /** + * @return array + */ public static function castError(\Error $e, array $a, Stub $stub, bool $isNested, int $filter = 0) { return self::filterExceptionArray($stub->class, $a, "\0Error\0", $filter); } + /** + * @return array + */ public static function castException(\Exception $e, array $a, Stub $stub, bool $isNested, int $filter = 0) { return self::filterExceptionArray($stub->class, $a, "\0Exception\0", $filter); } + /** + * @return array + */ public static function castErrorException(\ErrorException $e, array $a, Stub $stub, bool $isNested) { if (isset($a[$s = Caster::PREFIX_PROTECTED.'severity'], self::$errorTypes[$a[$s]])) { @@ -65,6 +75,9 @@ public static function castErrorException(\ErrorException $e, array $a, Stub $st return $a; } + /** + * @return array + */ public static function castThrowingCasterException(ThrowingCasterException $e, array $a, Stub $stub, bool $isNested) { $trace = Caster::PREFIX_VIRTUAL.'trace'; @@ -83,6 +96,9 @@ public static function castThrowingCasterException(ThrowingCasterException $e, a return $a; } + /** + * @return array + */ public static function castSilencedErrorContext(SilencedErrorContext $e, array $a, Stub $stub, bool $isNested) { $sPrefix = "\0".SilencedErrorContext::class."\0"; @@ -110,6 +126,9 @@ public static function castSilencedErrorContext(SilencedErrorContext $e, array $ return $a; } + /** + * @return array + */ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, bool $isNested) { if (!$isNested) { @@ -184,6 +203,9 @@ public static function castTraceStub(TraceStub $trace, array $a, Stub $stub, boo return $a; } + /** + * @return array + */ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, bool $isNested) { if (!$isNested) { @@ -267,6 +289,19 @@ public static function castFrameStub(FrameStub $frame, array $a, Stub $stub, boo return $a; } + /** + * @return array + */ + public static function castFlattenException(FlattenException $e, array $a, Stub $stub, bool $isNested) + { + if ($isNested) { + $k = sprintf(Caster::PATTERN_PRIVATE, FlattenException::class, 'traceAsString'); + $a[$k] = new CutStub($a[$k]); + } + + return $a; + } + private static function filterExceptionArray(string $xClass, array $a, string $xPrefix, int $filter): array { if (isset($a[$xPrefix.'trace'])) { @@ -285,12 +320,10 @@ private static function filterExceptionArray(string $xClass, array $a, string $x if (empty($a[$xPrefix.'previous'])) { unset($a[$xPrefix.'previous']); } - unset($a[$xPrefix.'string'], $a[Caster::PREFIX_DYNAMIC.'xdebug_message'], $a[Caster::PREFIX_DYNAMIC.'__destructorException']); + unset($a[$xPrefix.'string'], $a[Caster::PREFIX_DYNAMIC.'xdebug_message']); if (isset($a[Caster::PREFIX_PROTECTED.'message']) && str_contains($a[Caster::PREFIX_PROTECTED.'message'], "@anonymous\0")) { - $a[Caster::PREFIX_PROTECTED.'message'] = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)[0-9a-fA-F]++/', function ($m) { - return class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0]; - }, $a[Caster::PREFIX_PROTECTED.'message']); + $a[Caster::PREFIX_PROTECTED.'message'] = preg_replace_callback('/[a-zA-Z_\x7f-\xff][\\\\a-zA-Z0-9_\x7f-\xff]*+@anonymous\x00.*?\.php(?:0x?|:[0-9]++\$)?[0-9a-fA-F]++/', fn ($m) => class_exists($m[0], false) ? (get_parent_class($m[0]) ?: key(class_implements($m[0])) ?: 'class').'@anonymous' : $m[0], $a[Caster::PREFIX_PROTECTED.'message']); } if (isset($a[Caster::PREFIX_PROTECTED.'file'], $a[Caster::PREFIX_PROTECTED.'line'])) { diff --git a/Caster/FFICaster.php b/Caster/FFICaster.php index 7d90e7b5..ffed9f31 100644 --- a/Caster/FFICaster.php +++ b/Caster/FFICaster.php @@ -97,7 +97,7 @@ private static function castFFIFunction(Stub $stub, CType $type): array return [Caster::PREFIX_VIRTUAL.'returnType' => $returnType]; } - private static function castFFIPointer(Stub $stub, CType $type, CData $data = null): array + private static function castFFIPointer(Stub $stub, CType $type, ?CData $data = null): array { $ptr = $type->getPointerType(); @@ -115,11 +115,21 @@ private static function castFFIPointer(Stub $stub, CType $type, CData $data = nu private static function castFFIStringValue(CData $data): string|CutStub { $result = []; + $ffi = \FFI::cdef(<<zend_get_page_size(); + + // get cdata address + $start = $ffi->cast('uintptr_t', $ffi->cast('char*', $data))->cdata; + // accessing memory in the same page as $start is safe + $max = min(self::MAX_STRING_LENGTH, ($start | ($pageSize - 1)) - $start); + + for ($i = 0; $i < $max; ++$i) { $result[$i] = $data[$i]; - if ("\0" === $result[$i]) { + if ("\0" === $data[$i]) { return implode('', $result); } } @@ -132,7 +142,7 @@ private static function castFFIStringValue(CData $data): string|CutStub return $stub; } - private static function castFFIStructLike(CType $type, CData $data = null): array + private static function castFFIStructLike(CType $type, ?CData $data = null): array { $isUnion = ($type->getAttributes() & CType::ATTR_UNION) === CType::ATTR_UNION; diff --git a/Caster/FiberCaster.php b/Caster/FiberCaster.php index c74a9e59..b797dbd6 100644 --- a/Caster/FiberCaster.php +++ b/Caster/FiberCaster.php @@ -20,6 +20,9 @@ */ final class FiberCaster { + /** + * @return array + */ public static function castFiber(\Fiber $fiber, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; diff --git a/Caster/IntlCaster.php b/Caster/IntlCaster.php index 1ed91d4d..a4590f4b 100644 --- a/Caster/IntlCaster.php +++ b/Caster/IntlCaster.php @@ -21,6 +21,9 @@ */ class IntlCaster { + /** + * @return array + */ public static function castMessageFormatter(\MessageFormatter $c, array $a, Stub $stub, bool $isNested) { $a += [ @@ -31,6 +34,9 @@ public static function castMessageFormatter(\MessageFormatter $c, array $a, Stub return self::castError($c, $a); } + /** + * @return array + */ public static function castNumberFormatter(\NumberFormatter $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ @@ -108,6 +114,9 @@ public static function castNumberFormatter(\NumberFormatter $c, array $a, Stub $ return self::castError($c, $a); } + /** + * @return array + */ public static function castIntlTimeZone(\IntlTimeZone $c, array $a, Stub $stub, bool $isNested) { $a += [ @@ -125,6 +134,9 @@ public static function castIntlTimeZone(\IntlTimeZone $c, array $a, Stub $stub, return self::castError($c, $a); } + /** + * @return array + */ public static function castIntlCalendar(\IntlCalendar $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ @@ -142,6 +154,9 @@ public static function castIntlCalendar(\IntlCalendar $c, array $a, Stub $stub, return self::castError($c, $a); } + /** + * @return array + */ public static function castIntlDateFormatter(\IntlDateFormatter $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $a += [ diff --git a/Caster/LinkStub.php b/Caster/LinkStub.php index 4113e3a2..4930436d 100644 --- a/Caster/LinkStub.php +++ b/Caster/LinkStub.php @@ -23,7 +23,7 @@ class LinkStub extends ConstStub private static array $vendorRoots; private static array $composerRoots = []; - public function __construct(string $label, int $line = 0, string $href = null) + public function __construct(string $label, int $line = 0, ?string $href = null) { $this->value = $label; @@ -60,7 +60,7 @@ public function __construct(string $label, int $line = 0, string $href = null) } } - private function getComposerRoot(string $file, bool &$inVendor) + private function getComposerRoot(string $file, bool &$inVendor): string|false { if (!isset(self::$vendorRoots)) { self::$vendorRoots = []; diff --git a/Caster/MemcachedCaster.php b/Caster/MemcachedCaster.php index bc1fd63f..2f161e8c 100644 --- a/Caster/MemcachedCaster.php +++ b/Caster/MemcachedCaster.php @@ -23,6 +23,9 @@ class MemcachedCaster private static array $optionConstants; private static array $defaultOptions; + /** + * @return array + */ public static function castMemcached(\Memcached $c, array $a, Stub $stub, bool $isNested) { $a += [ diff --git a/Caster/PdoCaster.php b/Caster/PdoCaster.php index 0372a630..d68eae21 100644 --- a/Caster/PdoCaster.php +++ b/Caster/PdoCaster.php @@ -59,6 +59,9 @@ class PdoCaster ], ]; + /** + * @return array + */ public static function castPdo(\PDO $c, array $a, Stub $stub, bool $isNested) { $attr = []; @@ -108,6 +111,9 @@ public static function castPdo(\PDO $c, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castPdoStatement(\PDOStatement $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; diff --git a/Caster/PgSqlCaster.php b/Caster/PgSqlCaster.php index d8e5b525..0d8b3d91 100644 --- a/Caster/PgSqlCaster.php +++ b/Caster/PgSqlCaster.php @@ -69,6 +69,9 @@ class PgSqlCaster 'function' => \PGSQL_DIAG_SOURCE_FUNCTION, ]; + /** + * @return array + */ public static function castLargeObject($lo, array $a, Stub $stub, bool $isNested) { $a['seek position'] = pg_lo_tell($lo); @@ -76,6 +79,9 @@ public static function castLargeObject($lo, array $a, Stub $stub, bool $isNested return $a; } + /** + * @return array + */ public static function castLink($link, array $a, Stub $stub, bool $isNested) { $a['status'] = pg_connection_status($link); @@ -108,6 +114,9 @@ public static function castLink($link, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castResult($result, array $a, Stub $stub, bool $isNested) { $a['num rows'] = pg_num_rows($result); diff --git a/Caster/ProxyManagerCaster.php b/Caster/ProxyManagerCaster.php index e7120191..eb6c88db 100644 --- a/Caster/ProxyManagerCaster.php +++ b/Caster/ProxyManagerCaster.php @@ -21,6 +21,9 @@ */ class ProxyManagerCaster { + /** + * @return array + */ public static function castProxy(ProxyInterface $c, array $a, Stub $stub, bool $isNested) { if ($parent = get_parent_class($c)) { diff --git a/Caster/RdKafkaCaster.php b/Caster/RdKafkaCaster.php index 82b8c8af..fcaa1b76 100644 --- a/Caster/RdKafkaCaster.php +++ b/Caster/RdKafkaCaster.php @@ -31,6 +31,9 @@ */ class RdKafkaCaster { + /** + * @return array + */ public static function castKafkaConsumer(KafkaConsumer $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -51,6 +54,9 @@ public static function castKafkaConsumer(KafkaConsumer $c, array $a, Stub $stub, return $a; } + /** + * @return array + */ public static function castTopic(Topic $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -62,6 +68,9 @@ public static function castTopic(Topic $c, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castTopicPartition(TopicPartition $c, array $a) { $prefix = Caster::PREFIX_VIRTUAL; @@ -75,6 +84,9 @@ public static function castTopicPartition(TopicPartition $c, array $a) return $a; } + /** + * @return array + */ public static function castMessage(Message $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -86,6 +98,9 @@ public static function castMessage(Message $c, array $a, Stub $stub, bool $isNes return $a; } + /** + * @return array + */ public static function castConf(Conf $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -97,6 +112,9 @@ public static function castConf(Conf $c, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castTopicConf(TopicConf $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -108,6 +126,9 @@ public static function castTopicConf(TopicConf $c, array $a, Stub $stub, bool $i return $a; } + /** + * @return array + */ public static function castRdKafka(\RdKafka $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -121,6 +142,9 @@ public static function castRdKafka(\RdKafka $c, array $a, Stub $stub, bool $isNe return $a; } + /** + * @return array + */ public static function castCollectionMetadata(CollectionMetadata $c, array $a, Stub $stub, bool $isNested) { $a += iterator_to_array($c); @@ -128,6 +152,9 @@ public static function castCollectionMetadata(CollectionMetadata $c, array $a, S return $a; } + /** + * @return array + */ public static function castTopicMetadata(TopicMetadata $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -140,6 +167,9 @@ public static function castTopicMetadata(TopicMetadata $c, array $a, Stub $stub, return $a; } + /** + * @return array + */ public static function castPartitionMetadata(PartitionMetadata $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -153,6 +183,9 @@ public static function castPartitionMetadata(PartitionMetadata $c, array $a, Stu return $a; } + /** + * @return array + */ public static function castBrokerMetadata(BrokerMetadata $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -166,6 +199,9 @@ public static function castBrokerMetadata(BrokerMetadata $c, array $a, Stub $stu return $a; } + /** + * @return array + */ private static function extractMetadata(KafkaConsumer|\RdKafka $c) { $prefix = Caster::PREFIX_VIRTUAL; diff --git a/Caster/RedisCaster.php b/Caster/RedisCaster.php index eac25a12..6ff04675 100644 --- a/Caster/RedisCaster.php +++ b/Caster/RedisCaster.php @@ -11,6 +11,7 @@ namespace Symfony\Component\VarDumper\Caster; +use Relay\Relay; use Symfony\Component\VarDumper\Cloner\Stub; /** @@ -23,15 +24,15 @@ class RedisCaster { private const SERIALIZERS = [ - \Redis::SERIALIZER_NONE => 'NONE', - \Redis::SERIALIZER_PHP => 'PHP', + 0 => 'NONE', // Redis::SERIALIZER_NONE + 1 => 'PHP', // Redis::SERIALIZER_PHP 2 => 'IGBINARY', // Optional Redis::SERIALIZER_IGBINARY ]; private const MODES = [ - \Redis::ATOMIC => 'ATOMIC', - \Redis::MULTI => 'MULTI', - \Redis::PIPELINE => 'PIPELINE', + 0 => 'ATOMIC', // Redis::ATOMIC + 1 => 'MULTI', // Redis::MULTI + 2 => 'PIPELINE', // Redis::PIPELINE ]; private const COMPRESSION_MODES = [ @@ -46,7 +47,10 @@ class RedisCaster \RedisCluster::FAILOVER_DISTRIBUTE_SLAVES => 'DISTRIBUTE_SLAVES', ]; - public static function castRedis(\Redis $c, array $a, Stub $stub, bool $isNested) + /** + * @return array + */ + public static function castRedis(\Redis|Relay $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -72,6 +76,9 @@ public static function castRedis(\Redis $c, array $a, Stub $stub, bool $isNested ]; } + /** + * @return array + */ public static function castRedisArray(\RedisArray $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -84,6 +91,9 @@ public static function castRedisArray(\RedisArray $c, array $a, Stub $stub, bool ]; } + /** + * @return array + */ public static function castRedisCluster(\RedisCluster $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -102,9 +112,9 @@ public static function castRedisCluster(\RedisCluster $c, array $a, Stub $stub, return $a; } - private static function getRedisOptions(\Redis|\RedisArray|\RedisCluster $redis, array $options = []): EnumStub + private static function getRedisOptions(\Redis|Relay|\RedisArray|\RedisCluster $redis, array $options = []): EnumStub { - $serializer = $redis->getOption(\Redis::OPT_SERIALIZER); + $serializer = $redis->getOption(\defined('Redis::OPT_SERIALIZER') ? \Redis::OPT_SERIALIZER : 1); if (\is_array($serializer)) { foreach ($serializer as &$v) { if (isset(self::SERIALIZERS[$v])) { @@ -136,11 +146,11 @@ private static function getRedisOptions(\Redis|\RedisArray|\RedisCluster $redis, } $options += [ - 'TCP_KEEPALIVE' => \defined('Redis::OPT_TCP_KEEPALIVE') ? $redis->getOption(\Redis::OPT_TCP_KEEPALIVE) : 0, - 'READ_TIMEOUT' => $redis->getOption(\Redis::OPT_READ_TIMEOUT), + 'TCP_KEEPALIVE' => \defined('Redis::OPT_TCP_KEEPALIVE') ? $redis->getOption(\Redis::OPT_TCP_KEEPALIVE) : Relay::OPT_TCP_KEEPALIVE, + 'READ_TIMEOUT' => $redis->getOption(\defined('Redis::OPT_READ_TIMEOUT') ? \Redis::OPT_READ_TIMEOUT : Relay::OPT_READ_TIMEOUT), 'COMPRESSION' => $compression, 'SERIALIZER' => $serializer, - 'PREFIX' => $redis->getOption(\Redis::OPT_PREFIX), + 'PREFIX' => $redis->getOption(\defined('Redis::OPT_PREFIX') ? \Redis::OPT_PREFIX : Relay::OPT_PREFIX), 'SCAN' => $retry, ]; diff --git a/Caster/ReflectionCaster.php b/Caster/ReflectionCaster.php index 00675dfd..1bd156c2 100644 --- a/Caster/ReflectionCaster.php +++ b/Caster/ReflectionCaster.php @@ -35,6 +35,9 @@ class ReflectionCaster 'isVariadic' => 'isVariadic', ]; + /** + * @return array + */ public static function castClosure(\Closure $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; @@ -42,7 +45,7 @@ public static function castClosure(\Closure $c, array $a, Stub $stub, bool $isNe $a = static::castFunctionAbstract($c, $a, $stub, $isNested, $filter); - if (!str_contains($c->name, '{closure}')) { + if (!str_contains($c->name, '{closure')) { $stub->class = isset($a[$prefix.'class']) ? $a[$prefix.'class']->value.'::'.$c->name : $c->name; unset($a[$prefix.'class']); } @@ -71,6 +74,9 @@ public static function castClosure(\Closure $c, array $a, Stub $stub, bool $isNe return $a; } + /** + * @return array + */ public static function unsetClosureFileInfo(\Closure $c, array $a) { unset($a[Caster::PREFIX_VIRTUAL.'file'], $a[Caster::PREFIX_VIRTUAL.'line']); @@ -78,20 +84,23 @@ public static function unsetClosureFileInfo(\Closure $c, array $a) return $a; } - public static function castGenerator(\Generator $c, array $a, Stub $stub, bool $isNested) + public static function castGenerator(\Generator $c, array $a, Stub $stub, bool $isNested): array { // Cannot create ReflectionGenerator based on a terminated Generator try { $reflectionGenerator = new \ReflectionGenerator($c); + + return self::castReflectionGenerator($reflectionGenerator, $a, $stub, $isNested); } catch (\Exception) { $a[Caster::PREFIX_VIRTUAL.'closed'] = true; return $a; } - - return self::castReflectionGenerator($reflectionGenerator, $a, $stub, $isNested); } + /** + * @return array + */ public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -114,16 +123,28 @@ public static function castType(\ReflectionType $c, array $a, Stub $stub, bool $ return $a; } + /** + * @return array + */ public static function castAttribute(\ReflectionAttribute $c, array $a, Stub $stub, bool $isNested) { - self::addMap($a, $c, [ + $map = [ 'name' => 'getName', 'arguments' => 'getArguments', - ]); + ]; + + if (\PHP_VERSION_ID >= 80400) { + unset($map['name']); + } + + self::addMap($a, $c, $map); return $a; } + /** + * @return array + */ public static function castReflectionGenerator(\ReflectionGenerator $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -159,6 +180,9 @@ public static function castReflectionGenerator(\ReflectionGenerator $c, array $a return $a; } + /** + * @return array + */ public static function castClass(\ReflectionClass $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; @@ -190,6 +214,9 @@ public static function castClass(\ReflectionClass $c, array $a, Stub $stub, bool return $a; } + /** + * @return array + */ public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, array $a, Stub $stub, bool $isNested, int $filter = 0) { $prefix = Caster::PREFIX_VIRTUAL; @@ -204,7 +231,7 @@ public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, arra if (isset($a[$prefix.'returnType'])) { $v = $a[$prefix.'returnType']; $v = $v instanceof \ReflectionNamedType ? $v->getName() : (string) $v; - $a[$prefix.'returnType'] = new ClassStub($a[$prefix.'returnType'] instanceof \ReflectionNamedType && $a[$prefix.'returnType']->allowsNull() && 'mixed' !== $v ? '?'.$v : $v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']); + $a[$prefix.'returnType'] = new ClassStub($a[$prefix.'returnType'] instanceof \ReflectionNamedType && $a[$prefix.'returnType']->allowsNull() && !\in_array($v, ['mixed', 'null'], true) ? '?'.$v : $v, [class_exists($v, false) || interface_exists($v, false) || trait_exists($v, false) ? $v : '', '']); } if (isset($a[$prefix.'class'])) { $a[$prefix.'class'] = new ClassStub($a[$prefix.'class']); @@ -248,6 +275,9 @@ public static function castFunctionAbstract(\ReflectionFunctionAbstract $c, arra return $a; } + /** + * @return array + */ public static function castClassConstant(\ReflectionClassConstant $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); @@ -258,6 +288,9 @@ public static function castClassConstant(\ReflectionClassConstant $c, array $a, return $a; } + /** + * @return array + */ public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); @@ -265,6 +298,9 @@ public static function castMethod(\ReflectionMethod $c, array $a, Stub $stub, bo return $a; } + /** + * @return array + */ public static function castParameter(\ReflectionParameter $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -305,6 +341,9 @@ public static function castParameter(\ReflectionParameter $c, array $a, Stub $st return $a; } + /** + * @return array + */ public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'modifiers'] = implode(' ', \Reflection::getModifierNames($c->getModifiers())); @@ -315,6 +354,9 @@ public static function castProperty(\ReflectionProperty $c, array $a, Stub $stub return $a; } + /** + * @return array + */ public static function castReference(\ReflectionReference $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'id'] = $c->getId(); @@ -322,6 +364,9 @@ public static function castReference(\ReflectionReference $c, array $a, Stub $st return $a; } + /** + * @return array + */ public static function castExtension(\ReflectionExtension $c, array $a, Stub $stub, bool $isNested) { self::addMap($a, $c, [ @@ -338,6 +383,9 @@ public static function castExtension(\ReflectionExtension $c, array $a, Stub $st return $a; } + /** + * @return array + */ public static function castZendExtension(\ReflectionZendExtension $c, array $a, Stub $stub, bool $isNested) { self::addMap($a, $c, [ @@ -350,6 +398,9 @@ public static function castZendExtension(\ReflectionZendExtension $c, array $a, return $a; } + /** + * @return string + */ public static function getSignature(array $a) { $prefix = Caster::PREFIX_VIRTUAL; @@ -362,7 +413,7 @@ public static function getSignature(array $a) if (!$type instanceof \ReflectionNamedType) { $signature .= $type.' '; } else { - if (!$param->isOptional() && $param->allowsNull() && 'mixed' !== $type->getName()) { + if ($param->allowsNull() && !\in_array($type->getName(), ['mixed', 'null'], true)) { $signature .= '?'; } $signature .= substr(strrchr('\\'.$type->getName(), '\\'), 1).' '; @@ -402,7 +453,7 @@ public static function getSignature(array $a) return $signature; } - private static function addExtra(array &$a, \Reflector $c) + private static function addExtra(array &$a, \Reflector $c): void { $x = isset($a[Caster::PREFIX_VIRTUAL.'extra']) ? $a[Caster::PREFIX_VIRTUAL.'extra']->value : []; @@ -418,7 +469,7 @@ private static function addExtra(array &$a, \Reflector $c) } } - private static function addMap(array &$a, object $c, array $map, string $prefix = Caster::PREFIX_VIRTUAL) + private static function addMap(array &$a, object $c, array $map, string $prefix = Caster::PREFIX_VIRTUAL): void { foreach ($map as $k => $m) { if ('isDisabled' === $k) { diff --git a/Caster/ResourceCaster.php b/Caster/ResourceCaster.php index 6db234e8..f3bbf3be 100644 --- a/Caster/ResourceCaster.php +++ b/Caster/ResourceCaster.php @@ -27,6 +27,9 @@ public static function castCurl(\CurlHandle $h, array $a, Stub $stub, bool $isNe return curl_getinfo($h); } + /** + * @return array + */ public static function castDba($dba, array $a, Stub $stub, bool $isNested) { $list = dba_list(); @@ -35,12 +38,15 @@ public static function castDba($dba, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castProcess($process, array $a, Stub $stub, bool $isNested) { return proc_get_status($process); } - public static function castStream($stream, array $a, Stub $stub, bool $isNested) + public static function castStream($stream, array $a, Stub $stub, bool $isNested): array { $a = stream_get_meta_data($stream) + static::castStreamContext($stream, $a, $stub, $isNested); if ($a['uri'] ?? false) { @@ -50,11 +56,17 @@ public static function castStream($stream, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castStreamContext($stream, array $a, Stub $stub, bool $isNested) { return @stream_context_get_params($stream) ?: $a; } + /** + * @return array + */ public static function castGd($gd, array $a, Stub $stub, bool $isNested) { $a['size'] = imagesx($gd).'x'.imagesy($gd); @@ -63,6 +75,9 @@ public static function castGd($gd, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castOpensslX509($h, array $a, Stub $stub, bool $isNested) { $stub->cut = -1; diff --git a/Caster/ScalarStub.php b/Caster/ScalarStub.php new file mode 100644 index 00000000..3bb1935b --- /dev/null +++ b/Caster/ScalarStub.php @@ -0,0 +1,27 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Caster; + +use Symfony\Component\VarDumper\Cloner\Stub; + +/** + * Represents any arbitrary value. + * + * @author Alexandre Daubois + */ +class ScalarStub extends Stub +{ + public function __construct(mixed $value) + { + $this->value = $value; + } +} diff --git a/Caster/SplCaster.php b/Caster/SplCaster.php index 448afbad..814d824d 100644 --- a/Caster/SplCaster.php +++ b/Caster/SplCaster.php @@ -29,16 +29,25 @@ class SplCaster \SplFileObject::READ_CSV => 'READ_CSV', ]; + /** + * @return array + */ public static function castArrayObject(\ArrayObject $c, array $a, Stub $stub, bool $isNested) { return self::castSplArray($c, $a, $stub, $isNested); } + /** + * @return array + */ public static function castArrayIterator(\ArrayIterator $c, array $a, Stub $stub, bool $isNested) { return self::castSplArray($c, $a, $stub, $isNested); } + /** + * @return array + */ public static function castHeap(\Iterator $c, array $a, Stub $stub, bool $isNested) { $a += [ @@ -48,6 +57,9 @@ public static function castHeap(\Iterator $c, array $a, Stub $stub, bool $isNest return $a; } + /** + * @return array + */ public static function castDoublyLinkedList(\SplDoublyLinkedList $c, array $a, Stub $stub, bool $isNested) { $prefix = Caster::PREFIX_VIRTUAL; @@ -63,6 +75,9 @@ public static function castDoublyLinkedList(\SplDoublyLinkedList $c, array $a, S return $a; } + /** + * @return array + */ public static function castFileInfo(\SplFileInfo $c, array $a, Stub $stub, bool $isNested) { static $map = [ @@ -139,6 +154,9 @@ public static function castFileInfo(\SplFileInfo $c, array $a, Stub $stub, bool return $a; } + /** + * @return array + */ public static function castFileObject(\SplFileObject $c, array $a, Stub $stub, bool $isNested) { static $map = [ @@ -176,6 +194,9 @@ public static function castFileObject(\SplFileObject $c, array $a, Stub $stub, b return $a; } + /** + * @return array + */ public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $stub, bool $isNested) { $storage = []; @@ -184,10 +205,10 @@ public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $s $clone = clone $c; foreach ($clone as $obj) { - $storage[] = [ + $storage[] = new EnumStub([ 'object' => $obj, 'info' => $clone->getInfo(), - ]; + ]); } $a += [ @@ -197,6 +218,9 @@ public static function castObjectStorage(\SplObjectStorage $c, array $a, Stub $s return $a; } + /** + * @return array + */ public static function castOuterIterator(\OuterIterator $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'innerIterator'] = $c->getInnerIterator(); @@ -204,6 +228,9 @@ public static function castOuterIterator(\OuterIterator $c, array $a, Stub $stub return $a; } + /** + * @return array + */ public static function castWeakReference(\WeakReference $c, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'object'] = $c->get(); @@ -211,6 +238,27 @@ public static function castWeakReference(\WeakReference $c, array $a, Stub $stub return $a; } + /** + * @return array + */ + public static function castWeakMap(\WeakMap $c, array $a, Stub $stub, bool $isNested) + { + $map = []; + + foreach (clone $c as $obj => $data) { + $map[] = new EnumStub([ + 'object' => $obj, + 'data' => $data, + ]); + } + + $a += [ + Caster::PREFIX_VIRTUAL.'map' => $map, + ]; + + return $a; + } + private static function castSplArray(\ArrayObject|\ArrayIterator $c, array $a, Stub $stub, bool $isNested): array { $prefix = Caster::PREFIX_VIRTUAL; @@ -221,7 +269,11 @@ private static function castSplArray(\ArrayObject|\ArrayIterator $c, array $a, S $a = Caster::castObject($c, $c::class, method_exists($c, '__debugInfo'), $stub->class); $c->setFlags($flags); } + + unset($a["\0ArrayObject\0storage"], $a["\0ArrayIterator\0storage"]); + $a += [ + $prefix.'storage' => $c->getArrayCopy(), $prefix.'flag::STD_PROP_LIST' => (bool) ($flags & \ArrayObject::STD_PROP_LIST), $prefix.'flag::ARRAY_AS_PROPS' => (bool) ($flags & \ArrayObject::ARRAY_AS_PROPS), ]; diff --git a/Caster/StubCaster.php b/Caster/StubCaster.php index 32ead7c2..4b93ff76 100644 --- a/Caster/StubCaster.php +++ b/Caster/StubCaster.php @@ -22,6 +22,9 @@ */ class StubCaster { + /** + * @return array + */ public static function castStub(Stub $c, array $a, Stub $stub, bool $isNested) { if ($isNested) { @@ -43,11 +46,17 @@ public static function castStub(Stub $c, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castCutArray(CutArrayStub $c, array $a, Stub $stub, bool $isNested) { return $isNested ? $c->preservedSubset : $a; } + /** + * @return array + */ public static function cutInternals($obj, array $a, Stub $stub, bool $isNested) { if ($isNested) { @@ -59,6 +68,9 @@ public static function cutInternals($obj, array $a, Stub $stub, bool $isNested) return $a; } + /** + * @return array + */ public static function castEnum(EnumStub $c, array $a, Stub $stub, bool $isNested) { if ($isNested) { @@ -81,4 +93,15 @@ public static function castEnum(EnumStub $c, array $a, Stub $stub, bool $isNeste return $a; } + + /** + * @return array + */ + public static function castScalar(ScalarStub $scalarStub, array $a, Stub $stub) + { + $stub->type = Stub::TYPE_SCALAR; + $stub->attr['value'] = $scalarStub->value; + + return $a; + } } diff --git a/Caster/SymfonyCaster.php b/Caster/SymfonyCaster.php index 3b13666c..ebc00f90 100644 --- a/Caster/SymfonyCaster.php +++ b/Caster/SymfonyCaster.php @@ -31,6 +31,9 @@ class SymfonyCaster 'format' => 'getRequestFormat', ]; + /** + * @return array + */ public static function castRequest(Request $request, array $a, Stub $stub, bool $isNested) { $clone = null; @@ -46,6 +49,9 @@ public static function castRequest(Request $request, array $a, Stub $stub, bool return $a; } + /** + * @return array + */ public static function castHttpClient($client, array $a, Stub $stub, bool $isNested) { $multiKey = sprintf("\0%s\0multi", $client::class); @@ -56,6 +62,9 @@ public static function castHttpClient($client, array $a, Stub $stub, bool $isNes return $a; } + /** + * @return array + */ public static function castHttpClientResponse($response, array $a, Stub $stub, bool $isNested) { $stub->cut += \count($a); @@ -68,6 +77,9 @@ public static function castHttpClientResponse($response, array $a, Stub $stub, b return $a; } + /** + * @return array + */ public static function castLazyObjectState($state, array $a, Stub $stub, bool $isNested) { if (!$isNested) { @@ -93,6 +105,9 @@ public static function castLazyObjectState($state, array $a, Stub $stub, bool $i return $a; } + /** + * @return array + */ public static function castUuid(Uuid $uuid, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'toBase58'] = $uuid->toBase58(); @@ -106,6 +121,9 @@ public static function castUuid(Uuid $uuid, array $a, Stub $stub, bool $isNested return $a; } + /** + * @return array + */ public static function castUlid(Ulid $ulid, array $a, Stub $stub, bool $isNested) { $a[Caster::PREFIX_VIRTUAL.'toBase58'] = $ulid->toBase58(); diff --git a/Caster/TraceStub.php b/Caster/TraceStub.php index 5eea1c87..d215d8db 100644 --- a/Caster/TraceStub.php +++ b/Caster/TraceStub.php @@ -25,7 +25,7 @@ class TraceStub extends Stub public $sliceLength; public $numberingOffset; - public function __construct(array $trace, bool $keepArgs = true, int $sliceOffset = 0, int $sliceLength = null, int $numberingOffset = 0) + public function __construct(array $trace, bool $keepArgs = true, int $sliceOffset = 0, ?int $sliceLength = null, int $numberingOffset = 0) { $this->value = $trace; $this->keepArgs = $keepArgs; diff --git a/Caster/UninitializedStub.php b/Caster/UninitializedStub.php new file mode 100644 index 00000000..a9bdd9b8 --- /dev/null +++ b/Caster/UninitializedStub.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Caster; + +/** + * Represents an uninitialized property. + * + * @author Nicolas Grekas + */ +class UninitializedStub extends ConstStub +{ + public function __construct(\ReflectionProperty $property) + { + parent::__construct('?'.($property->hasType() ? ' '.$property->getType() : ''), 'Uninitialized property'); + } +} diff --git a/Caster/XmlReaderCaster.php b/Caster/XmlReaderCaster.php index 506746b4..1cfcf4dd 100644 --- a/Caster/XmlReaderCaster.php +++ b/Caster/XmlReaderCaster.php @@ -43,6 +43,9 @@ class XmlReaderCaster \XMLReader::XML_DECLARATION => 'XML_DECLARATION', ]; + /** + * @return array + */ public static function castXmlReader(\XMLReader $reader, array $a, Stub $stub, bool $isNested) { try { @@ -82,6 +85,7 @@ public static function castXmlReader(\XMLReader $reader, array $a, Stub $stub, b $info[$props]->cut = $count; } + $a = Caster::filter($a, Caster::EXCLUDE_UNINITIALIZED, [], $count); $info = Caster::filter($info, Caster::EXCLUDE_EMPTY, [], $count); // +2 because hasValue and hasAttributes are always filtered $stub->cut += $count + 2; diff --git a/Caster/XmlResourceCaster.php b/Caster/XmlResourceCaster.php index ba55fced..0cf42584 100644 --- a/Caster/XmlResourceCaster.php +++ b/Caster/XmlResourceCaster.php @@ -47,6 +47,9 @@ class XmlResourceCaster \XML_ERROR_EXTERNAL_ENTITY_HANDLING => 'XML_ERROR_EXTERNAL_ENTITY_HANDLING', ]; + /** + * @return array + */ public static function castXml($h, array $a, Stub $stub, bool $isNested) { $a['current_byte_index'] = xml_get_current_byte_index($h); diff --git a/Cloner/AbstractCloner.php b/Cloner/AbstractCloner.php index 6c3efdeb..fc330bae 100644 --- a/Cloner/AbstractCloner.php +++ b/Cloner/AbstractCloner.php @@ -28,6 +28,7 @@ abstract class AbstractCloner implements ClonerInterface 'Symfony\Component\VarDumper\Caster\CutArrayStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castCutArray'], 'Symfony\Component\VarDumper\Caster\ConstStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castStub'], 'Symfony\Component\VarDumper\Caster\EnumStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castEnum'], + 'Symfony\Component\VarDumper\Caster\ScalarStub' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'castScalar'], 'Fiber' => ['Symfony\Component\VarDumper\Caster\FiberCaster', 'castFiber'], @@ -94,6 +95,7 @@ abstract class AbstractCloner implements ClonerInterface 'Symfony\Component\VarDumper\Caster\TraceStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castTraceStub'], 'Symfony\Component\VarDumper\Caster\FrameStub' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFrameStub'], 'Symfony\Component\VarDumper\Cloner\AbstractCloner' => ['Symfony\Component\VarDumper\Caster\StubCaster', 'cutInternals'], + 'Symfony\Component\ErrorHandler\Exception\FlattenException' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castFlattenException'], 'Symfony\Component\ErrorHandler\Exception\SilencedErrorContext' => ['Symfony\Component\VarDumper\Caster\ExceptionCaster', 'castSilencedErrorContext'], 'Imagine\Image\ImageInterface' => ['Symfony\Component\VarDumper\Caster\ImagineCaster', 'castImage'], @@ -125,9 +127,11 @@ abstract class AbstractCloner implements ClonerInterface 'SplObjectStorage' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castObjectStorage'], 'SplPriorityQueue' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castHeap'], 'OuterIterator' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castOuterIterator'], + 'WeakMap' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castWeakMap'], 'WeakReference' => ['Symfony\Component\VarDumper\Caster\SplCaster', 'castWeakReference'], 'Redis' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedis'], + 'Relay\Relay' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedis'], 'RedisArray' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisArray'], 'RedisCluster' => ['Symfony\Component\VarDumper\Caster\RedisCaster', 'castRedisCluster'], @@ -215,7 +219,7 @@ abstract class AbstractCloner implements ClonerInterface * * @see addCasters */ - public function __construct(array $casters = null) + public function __construct(?array $casters = null) { $this->addCasters($casters ?? static::$defaultCasters); } @@ -229,6 +233,8 @@ public function __construct(array $casters = null) * see e.g. static::$defaultCasters. * * @param callable[] $casters A map of casters + * + * @return void */ public function addCasters(array $casters) { @@ -239,6 +245,8 @@ public function addCasters(array $casters) /** * Sets the maximum number of items to clone past the minimum depth in nested structures. + * + * @return void */ public function setMaxItems(int $maxItems) { @@ -247,6 +255,8 @@ public function setMaxItems(int $maxItems) /** * Sets the maximum cloned length for strings. + * + * @return void */ public function setMaxString(int $maxString) { @@ -256,6 +266,8 @@ public function setMaxString(int $maxString) /** * Sets the minimum tree depth where we are guaranteed to clone all the items. After this * depth is reached, only setMaxItems items will be cloned. + * + * @return void */ public function setMinDepth(int $minDepth) { diff --git a/Cloner/Data.php b/Cloner/Data.php index 6ecb883e..16f51b0c 100644 --- a/Cloner/Data.php +++ b/Cloner/Data.php @@ -17,7 +17,7 @@ /** * @author Nicolas Grekas */ -class Data implements \ArrayAccess, \Countable, \IteratorAggregate +class Data implements \ArrayAccess, \Countable, \IteratorAggregate, \Stringable { private array $data; private int $position = 0; @@ -121,6 +121,9 @@ public function getIterator(): \Traversable yield from $value; } + /** + * @return mixed + */ public function __get(string $key) { if (null !== $data = $this->seek($key)) { @@ -211,6 +214,11 @@ public function withContext(array $context): static return $data; } + public function getContext(): array + { + return $this->context; + } + /** * Seeks to a specific key in nested data structures. */ @@ -257,21 +265,21 @@ public function seek(string|int $key): ?static /** * Dumps data with a DumperInterface dumper. + * + * @return void */ public function dump(DumperInterface $dumper) { $refs = [0]; $cursor = new Cursor(); + $cursor->hashType = -1; + $cursor->attr = $this->context[SourceContextProvider::class] ?? []; + $label = $this->context['label'] ?? ''; - if ($cursor->attr = $this->context[SourceContextProvider::class] ?? []) { - $cursor->attr['if_links'] = true; - $cursor->hashType = -1; - $dumper->dumpScalar($cursor, 'default', '^'); - $cursor->attr = ['if_links' => true]; - $dumper->dumpScalar($cursor, 'default', ' '); - $cursor->hashType = 0; + if ($cursor->attr || '' !== $label) { + $dumper->dumpScalar($cursor, 'label', $label); } - + $cursor->hashType = 0; $this->dumpItem($dumper, $cursor, $refs, $this->data[$this->position][$this->key]); } @@ -280,7 +288,7 @@ public function dump(DumperInterface $dumper) * * @param mixed $item A Stub object or the original value being dumped */ - private function dumpItem(DumperInterface $dumper, Cursor $cursor, array &$refs, mixed $item) + private function dumpItem(DumperInterface $dumper, Cursor $cursor, array &$refs, mixed $item): void { $cursor->refIndex = 0; $cursor->softRefTo = $cursor->softRefHandle = $cursor->softRefCount = 0; @@ -362,6 +370,10 @@ private function dumpItem(DumperInterface $dumper, Cursor $cursor, array &$refs, $dumper->leaveHash($cursor, $item->type, $item->class, $withChildren, $cut); break; + case Stub::TYPE_SCALAR: + $dumper->dumpScalar($cursor, 'default', $item->attr['value']); + break; + default: throw new \RuntimeException(sprintf('Unexpected Stub type: "%s".', $item->type)); } @@ -402,7 +414,7 @@ private function dumpChildren(DumperInterface $dumper, Cursor $parentCursor, arr return $hashCut; } - private function getStub(mixed $item) + private function getStub(mixed $item): mixed { if (!$item || !\is_array($item)) { return $item; diff --git a/Cloner/DumperInterface.php b/Cloner/DumperInterface.php index 61d02d24..4c5b315b 100644 --- a/Cloner/DumperInterface.php +++ b/Cloner/DumperInterface.php @@ -20,6 +20,8 @@ interface DumperInterface { /** * Dumps a scalar value. + * + * @return void */ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|null $value); @@ -29,6 +31,8 @@ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|n * @param string $str The string being dumped * @param bool $bin Whether $str is UTF-8 or binary encoded * @param int $cut The number of characters $str has been cut by + * + * @return void */ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut); @@ -38,6 +42,8 @@ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut); * @param int $type A Cursor::HASH_* const for the type of hash * @param string|int|null $class The object class, resource type or array count * @param bool $hasChild When the dump of the hash has child item + * + * @return void */ public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild); @@ -48,6 +54,8 @@ public function enterHash(Cursor $cursor, int $type, string|int|null $class, boo * @param string|int|null $class The object class, resource type or array count * @param bool $hasChild When the dump of the hash has child item * @param int $cut The number of items the hash has been cut by + * + * @return void */ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut); } diff --git a/Cloner/Internal/NoDefault.php b/Cloner/Internal/NoDefault.php new file mode 100644 index 00000000..ed9db988 --- /dev/null +++ b/Cloner/Internal/NoDefault.php @@ -0,0 +1,25 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Cloner\Internal; + +/** + * Flags a typed property that has no default value. + * + * This dummy object is used to distinguish a property with a default value of null + * from a property that is uninitialized by default. + * + * @internal + */ +enum NoDefault +{ + case NoDefault; +} diff --git a/Cloner/Stub.php b/Cloner/Stub.php index 1c5b8871..a377d2b9 100644 --- a/Cloner/Stub.php +++ b/Cloner/Stub.php @@ -11,6 +11,8 @@ namespace Symfony\Component\VarDumper\Cloner; +use Symfony\Component\VarDumper\Cloner\Internal\NoDefault; + /** * Represents the main properties of a PHP variable. * @@ -23,6 +25,7 @@ class Stub public const TYPE_ARRAY = 3; public const TYPE_OBJECT = 4; public const TYPE_RESOURCE = 5; + public const TYPE_SCALAR = 6; public const STRING_BINARY = 1; public const STRING_UTF8 = 2; @@ -49,15 +52,20 @@ public function __sleep(): array $properties = []; if (!isset(self::$defaultProperties[$c = static::class])) { - self::$defaultProperties[$c] = get_class_vars($c); + $reflection = new \ReflectionClass($c); + self::$defaultProperties[$c] = []; + + foreach ($reflection->getProperties() as $p) { + if ($p->isStatic()) { + continue; + } - foreach ((new \ReflectionClass($c))->getStaticProperties() as $k => $v) { - unset(self::$defaultProperties[$c][$k]); + self::$defaultProperties[$c][$p->name] = $p->hasDefaultValue() ? $p->getDefaultValue() : ($p->hasType() ? NoDefault::NoDefault : null); } } foreach (self::$defaultProperties[$c] as $k => $v) { - if ($this->$k !== $v) { + if (NoDefault::NoDefault === $v || $this->$k !== $v) { $properties[] = $k; } } diff --git a/Cloner/VarCloner.php b/Cloner/VarCloner.php index 4068c7c3..e168d0d3 100644 --- a/Cloner/VarCloner.php +++ b/Cloner/VarCloner.php @@ -16,7 +16,6 @@ */ class VarCloner extends AbstractCloner { - private static string $gid; private static array $arrayCache = []; protected function doClone(mixed $var): array @@ -41,7 +40,6 @@ protected function doClone(mixed $var): array $stub = null; // Stub capturing the main properties of an original item value // or null if the original value is used directly - $gid = self::$gid ??= md5(random_bytes(6)); // Unique string used to detect the special $GLOBALS variable $arrayStub = new Stub(); $arrayStub->type = Stub::TYPE_ARRAY; $fromObjCast = false; diff --git a/Command/ServerDumpCommand.php b/Command/ServerDumpCommand.php index 1b06e694..b64a884b 100644 --- a/Command/ServerDumpCommand.php +++ b/Command/ServerDumpCommand.php @@ -54,7 +54,7 @@ public function __construct(DumpServer $server, array $descriptors = []) parent::__construct(); } - protected function configure() + protected function configure(): void { $this ->addOption('format', null, InputOption::VALUE_REQUIRED, sprintf('The output format (%s)', implode(', ', $this->getAvailableFormats())), 'cli') diff --git a/Dumper/AbstractDumper.php b/Dumper/AbstractDumper.php index 2740b879..53165ba6 100644 --- a/Dumper/AbstractDumper.php +++ b/Dumper/AbstractDumper.php @@ -26,10 +26,13 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface public const DUMP_COMMA_SEPARATOR = 4; public const DUMP_TRAILING_COMMA = 8; + /** @var callable|resource|string|null */ public static $defaultOutput = 'php://output'; protected $line = ''; + /** @var callable|null */ protected $lineDumper; + /** @var resource|null */ protected $outputStream; protected $decimalPoint = '.'; protected $indentPad = ' '; @@ -42,7 +45,7 @@ abstract class AbstractDumper implements DataDumperInterface, DumperInterface * @param string|null $charset The default character encoding to use for non-UTF8 strings * @param int $flags A bit field of static::DUMP_* constants to fine tune dumps representation */ - public function __construct($output = null, string $charset = null, int $flags = 0) + public function __construct($output = null, ?string $charset = null, int $flags = 0) { $this->flags = $flags; $this->setCharset($charset ?: \ini_get('php.output_encoding') ?: \ini_get('default_charset') ?: 'UTF-8'); @@ -55,9 +58,9 @@ public function __construct($output = null, string $charset = null, int $flags = /** * Sets the output destination of the dumps. * - * @param callable|resource|string $output A line dumper callable, an opened stream or an output path + * @param callable|resource|string|null $output A line dumper callable, an opened stream or an output path * - * @return callable|resource|string The previous output destination + * @return callable|resource|string|null The previous output destination */ public function setOutput($output) { @@ -155,6 +158,8 @@ public function dump(Data $data, $output = null): ?string * * @param int $depth The recursive depth in the dumped structure for the line being dumped, * or -1 to signal the end-of-dump to the line dumper callable + * + * @return void */ protected function dumpLine(int $depth) { @@ -164,6 +169,8 @@ protected function dumpLine(int $depth) /** * Generic line dumper callback. + * + * @return void */ protected function echoLine(string $line, int $depth, string $indentPad) { diff --git a/Dumper/CliDumper.php b/Dumper/CliDumper.php index 660ca321..e36cee6a 100644 --- a/Dumper/CliDumper.php +++ b/Dumper/CliDumper.php @@ -11,6 +11,7 @@ namespace Symfony\Component\VarDumper\Dumper; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\VarDumper\Cloner\Cursor; use Symfony\Component\VarDumper\Cloner\Stub; @@ -22,6 +23,7 @@ class CliDumper extends AbstractDumper { public static $defaultColors; + /** @var callable|resource|string|null */ public static $defaultOutput = 'php://stdout'; protected $colors; @@ -51,6 +53,7 @@ class CliDumper extends AbstractDumper "\r" => '\r', "\033" => '\e', ]; + protected static $unicodeCharsRx = "/[\u{00A0}\u{00AD}\u{034F}\u{061C}\u{115F}\u{1160}\u{17B4}\u{17B5}\u{180E}\u{2000}-\u{200F}\u{202F}\u{205F}\u{2060}-\u{2064}\u{206A}-\u{206F}\u{3000}\u{2800}\u{3164}\u{FEFF}\u{FFA0}\u{1D159}\u{1D173}-\u{1D17A}]/u"; protected $collapseNextHash = false; protected $expandNextHash = false; @@ -61,7 +64,7 @@ class CliDumper extends AbstractDumper private bool $handlesHrefGracefully; - public function __construct($output = null, string $charset = null, int $flags = 0) + public function __construct($output = null, ?string $charset = null, int $flags = 0) { parent::__construct($output, $charset, $flags); @@ -80,11 +83,13 @@ public function __construct($output = null, string $charset = null, int $flags = ]); } - $this->displayOptions['fileLinkFormat'] = \ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l'; + $this->displayOptions['fileLinkFormat'] = class_exists(FileLinkFormatter::class) ? new FileLinkFormatter() : (\ini_get('xdebug.file_link_format') ?: get_cfg_var('xdebug.file_link_format') ?: 'file://%f#L%l'); } /** * Enables/disables colored output. + * + * @return void */ public function setColors(bool $colors) { @@ -93,6 +98,8 @@ public function setColors(bool $colors) /** * Sets the maximum number of characters per line for dumped strings. + * + * @return void */ public function setMaxStringWidth(int $maxStringWidth) { @@ -103,6 +110,8 @@ public function setMaxStringWidth(int $maxStringWidth) * Configures styles. * * @param array $styles A map of style names to style definitions + * + * @return void */ public function setStyles(array $styles) { @@ -113,15 +122,21 @@ public function setStyles(array $styles) * Configures display options. * * @param array $displayOptions A map of display options to customize the behavior + * + * @return void */ public function setDisplayOptions(array $displayOptions) { $this->displayOptions = $displayOptions + $this->displayOptions; } + /** + * @return void + */ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|null $value) { $this->dumpKey($cursor); + $this->collapseNextHash = $this->expandNextHash = false; $style = 'const'; $attr = $cursor->attr; @@ -131,6 +146,11 @@ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|n $style = 'default'; break; + case 'label': + $this->styles += ['label' => $this->styles['default']]; + $style = 'label'; + break; + case 'integer': $style = 'num'; @@ -174,9 +194,13 @@ public function dumpScalar(Cursor $cursor, string $type, string|int|float|bool|n $this->endValue($cursor); } + /** + * @return void + */ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) { $this->dumpKey($cursor); + $this->collapseNextHash = $this->expandNextHash = false; $attr = $cursor->attr; if ($bin) { @@ -262,11 +286,15 @@ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) } } + /** + * @return void + */ public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) { $this->colors ??= $this->supportsColors(); $this->dumpKey($cursor); + $this->expandNextHash = false; $attr = $cursor->attr; if ($this->collapseNextHash) { @@ -298,6 +326,9 @@ public function enterHash(Cursor $cursor, int $type, string|int|null $class, boo } } + /** + * @return void + */ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) { if (empty($cursor->attr['cut_hash'])) { @@ -313,6 +344,8 @@ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, boo * * @param bool $hasChild When the dump of the hash has child item * @param int $cut The number of items the hash has been cut by + * + * @return void */ protected function dumpEllipsis(Cursor $cursor, bool $hasChild, int $cut) { @@ -329,6 +362,8 @@ protected function dumpEllipsis(Cursor $cursor, bool $hasChild, int $cut) /** * Dumps a key in a hash structure. + * + * @return void */ protected function dumpKey(Cursor $cursor) { @@ -454,7 +489,15 @@ protected function style(string $style, string $value, array $attr = []): string return $s.$endCchr; }, $value, -1, $cchrCount); - if ($this->colors) { + if (!($attr['binary'] ?? false)) { + $value = preg_replace_callback(static::$unicodeCharsRx, function ($c) use (&$cchrCount, $startCchr, $endCchr) { + ++$cchrCount; + + return $startCchr.'\u{'.strtoupper(dechex(mb_ord($c[0]))).'}'.$endCchr; + }, $value); + } + + if ($this->colors && '' !== $value) { if ($cchrCount && "\033" === $value[0]) { $value = substr($value, \strlen($startCchr)); } else { @@ -477,10 +520,15 @@ protected function style(string $style, string $value, array $attr = []): string } } if (isset($attr['href'])) { + if ('label' === $style) { + $value .= '^'; + } $value = "\033]8;;{$attr['href']}\033\\{$value}\033]8;;\033\\"; } - } elseif ($attr['if_links'] ?? false) { - return ''; + } + + if ('label' === $style && '' !== $value) { + $value .= ' '; } return $value; @@ -491,7 +539,7 @@ protected function supportsColors(): bool if ($this->outputStream !== static::$defaultOutput) { return $this->hasColorSupport($this->outputStream); } - if (null !== static::$defaultColors) { + if (isset(static::$defaultColors)) { return static::$defaultColors; } if (isset($_SERVER['argv'][1])) { @@ -525,14 +573,24 @@ protected function supportsColors(): bool return static::$defaultColors = $this->hasColorSupport($h); } + /** + * @return void + */ protected function dumpLine(int $depth, bool $endOfValue = false) { + if (null === $this->colors) { + $this->colors = $this->supportsColors(); + } + if ($this->colors) { $this->line = sprintf("\033[%sm%s\033[m", $this->styles['default'], $this->line); } parent::dumpLine($depth); } + /** + * @return void + */ protected function endValue(Cursor $cursor) { if (-1 === $cursor->hashType) { @@ -563,23 +621,34 @@ private function hasColorSupport(mixed $stream): bool } // Follow https://no-color.org/ - if (isset($_SERVER['NO_COLOR']) || false !== getenv('NO_COLOR')) { + if ('' !== (($_SERVER['NO_COLOR'] ?? getenv('NO_COLOR'))[0] ?? '')) { return false; } - if ('Hyper' === getenv('TERM_PROGRAM')) { + // Detect msysgit/mingw and assume this is a tty because detection + // does not work correctly, see https://github.com/composer/composer/issues/9690 + if (!@stream_isatty($stream) && !\in_array(strtoupper((string) getenv('MSYSTEM')), ['MINGW32', 'MINGW64'], true)) { + return false; + } + + if ('\\' === \DIRECTORY_SEPARATOR && @sapi_windows_vt100_support($stream)) { return true; } - if (\DIRECTORY_SEPARATOR === '\\') { - return (\function_exists('sapi_windows_vt100_support') - && @sapi_windows_vt100_support($stream)) - || false !== getenv('ANSICON') - || 'ON' === getenv('ConEmuANSI') - || 'xterm' === getenv('TERM'); + if ('Hyper' === getenv('TERM_PROGRAM') + || false !== getenv('COLORTERM') + || false !== getenv('ANSICON') + || 'ON' === getenv('ConEmuANSI') + ) { + return true; + } + + if ('dumb' === $term = (string) getenv('TERM')) { + return false; } - return stream_isatty($stream); + // See https://github.com/chalk/supports-color/blob/d4f413efaf8da045c5ab440ed418ef02dbb28bf1/index.js#L157 + return preg_match('/^((screen|xterm|vt100|vt220|putty|rxvt|ansi|cygwin|linux).*)|(.*-256(color)?(-bce)?)$/', $term); } /** @@ -609,7 +678,7 @@ private function isWindowsTrueColor(): bool return $result; } - private function getSourceLink(string $file, int $line) + private function getSourceLink(string $file, int $line): string|false { if ($fmt = $this->displayOptions['fileLinkFormat']) { return \is_string($fmt) ? strtr($fmt, ['%f' => $file, '%l' => $line]) : ($fmt->format($file, $line) ?: 'file://'.$file.'#L'.$line); diff --git a/Dumper/ContextProvider/SourceContextProvider.php b/Dumper/ContextProvider/SourceContextProvider.php index d36e8bcf..cadddfac 100644 --- a/Dumper/ContextProvider/SourceContextProvider.php +++ b/Dumper/ContextProvider/SourceContextProvider.php @@ -11,7 +11,8 @@ namespace Symfony\Component\VarDumper\Dumper\ContextProvider; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; +use Symfony\Component\HttpKernel\Debug\FileLinkFormatter as LegacyFileLinkFormatter; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\VarDumper; @@ -28,9 +29,9 @@ final class SourceContextProvider implements ContextProviderInterface private int $limit; private ?string $charset; private ?string $projectDir; - private ?FileLinkFormatter $fileLinkFormatter; + private FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter; - public function __construct(string $charset = null, string $projectDir = null, FileLinkFormatter $fileLinkFormatter = null, int $limit = 9) + public function __construct(?string $charset = null, ?string $projectDir = null, FileLinkFormatter|LegacyFileLinkFormatter|null $fileLinkFormatter = null, int $limit = 9) { $this->charset = $charset; $this->projectDir = $projectDir; @@ -44,7 +45,7 @@ public function getContext(): ?array $file = $trace[1]['file']; $line = $trace[1]['line']; - $name = false; + $name = '-' === $file || 'Standard input code' === $file ? 'Standard input code' : false; $fileExcerpt = false; for ($i = 2; $i < $this->limit; ++$i) { diff --git a/Dumper/ContextualizedDumper.php b/Dumper/ContextualizedDumper.php index 1ba803d8..84cfb425 100644 --- a/Dumper/ContextualizedDumper.php +++ b/Dumper/ContextualizedDumper.php @@ -31,13 +31,16 @@ public function __construct(DataDumperInterface $wrappedDumper, array $contextPr $this->contextProviders = $contextProviders; } + /** + * @return string|null + */ public function dump(Data $data) { - $context = []; + $context = $data->getContext(); foreach ($this->contextProviders as $contextProvider) { $context[$contextProvider::class] = $contextProvider->getContext(); } - $this->wrappedDumper->dump($data->withContext($context)); + return $this->wrappedDumper->dump($data->withContext($context)); } } diff --git a/Dumper/DataDumperInterface.php b/Dumper/DataDumperInterface.php index b173bccf..df05b6af 100644 --- a/Dumper/DataDumperInterface.php +++ b/Dumper/DataDumperInterface.php @@ -20,5 +20,8 @@ */ interface DataDumperInterface { + /** + * @return string|null + */ public function dump(Data $data); } diff --git a/Dumper/HtmlDumper.php b/Dumper/HtmlDumper.php index c818c919..ea09e681 100644 --- a/Dumper/HtmlDumper.php +++ b/Dumper/HtmlDumper.php @@ -21,6 +21,7 @@ */ class HtmlDumper extends CliDumper { + /** @var callable|resource|string|null */ public static $defaultOutput = 'php://output'; protected static $themes = [ @@ -74,7 +75,7 @@ class HtmlDumper extends CliDumper ]; private array $extraDisplayOptions = []; - public function __construct($output = null, string $charset = null, int $flags = 0) + public function __construct($output = null, ?string $charset = null, int $flags = 0) { AbstractDumper::__construct($output, $charset, $flags); $this->dumpId = 'sf-dump-'.mt_rand(); @@ -82,12 +83,18 @@ public function __construct($output = null, string $charset = null, int $flags = $this->styles = static::$themes['dark'] ?? self::$themes['dark']; } + /** + * @return void + */ public function setStyles(array $styles) { $this->headerIsDumped = false; $this->styles = $styles + $this->styles; } + /** + * @return void + */ public function setTheme(string $themeName) { if (!isset(static::$themes[$themeName])) { @@ -101,6 +108,8 @@ public function setTheme(string $themeName) * Configures display options. * * @param array $displayOptions A map of display options to customize the behavior + * + * @return void */ public function setDisplayOptions(array $displayOptions) { @@ -110,6 +119,8 @@ public function setDisplayOptions(array $displayOptions) /** * Sets an HTML header that will be dumped once in the output stream. + * + * @return void */ public function setDumpHeader(?string $header) { @@ -118,6 +129,8 @@ public function setDumpHeader(?string $header) /** * Sets an HTML prefix and suffix that will encapse every single dump. + * + * @return void */ public function setDumpBoundaries(string $prefix, string $suffix) { @@ -136,6 +149,8 @@ public function dump(Data $data, $output = null, array $extraDisplayOptions = [] /** * Dumps the HTML header. + * + * @return string */ protected function getDumpHeader() { @@ -149,19 +164,15 @@ protected function getDumpHeader() '.$this->dumpHeader; } + /** + * @return void + */ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) { if ('' === $str && isset($cursor->attr['img-data'], $cursor->attr['content-type'])) { @@ -788,6 +803,9 @@ public function dumpString(Cursor $cursor, string $str, bool $bin, int $cut) } } + /** + * @return void + */ public function enterHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild) { if (Cursor::HASH_OBJECT === $type) { @@ -816,6 +834,9 @@ public function enterHash(Cursor $cursor, int $type, string|int|null $class, boo } } + /** + * @return void + */ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, bool $hasChild, int $cut) { $this->dumpEllipsis($cursor, $hasChild, $cut); @@ -827,7 +848,7 @@ public function leaveHash(Cursor $cursor, int $type, string|int|null $class, boo protected function style(string $style, string $value, array $attr = []): string { - if ('' === $value) { + if ('' === $value && ('label' !== $style || !isset($attr['file']) && !isset($attr['href']))) { return ''; } @@ -862,7 +883,6 @@ protected function style(string $style, string $value, array $attr = []): string } elseif ('private' === $style) { $style .= sprintf(' title="Private property defined in class: `%s`"', esc($this->utf8Encode($attr['class']))); } - $map = static::$controlCharsMap; if (isset($attr['ellipsis'])) { $class = 'sf-dump-ellipsis'; @@ -881,6 +901,7 @@ protected function style(string $style, string $value, array $attr = []): string } } + $map = static::$controlCharsMap; $v = "".preg_replace_callback(static::$controlCharsRx, function ($c) use ($map) { $s = $b = '%s', esc($this->utf8Encode($attr['href'])), $target, $v); } if (isset($attr['lang'])) { $v = sprintf('%s', esc($attr['lang']), $v); } + if ('label' === $style) { + $v .= ' '; + } return $v; } + /** + * @return void + */ protected function dumpLine(int $depth, bool $endOfValue = false) { if (-1 === $this->lastDepth) { @@ -944,7 +980,7 @@ protected function dumpLine(int $depth, bool $endOfValue = false) AbstractDumper::dumpLine($depth); } - private function getSourceLink(string $file, int $line) + private function getSourceLink(string $file, int $line): string|false { $options = $this->extraDisplayOptions + $this->displayOptions; @@ -956,7 +992,7 @@ private function getSourceLink(string $file, int $line) } } -function esc(string $str) +function esc(string $str): string { return htmlspecialchars($str, \ENT_QUOTES, 'UTF-8'); } diff --git a/Dumper/ServerDumper.php b/Dumper/ServerDumper.php index f2c891b6..60fdd7ac 100644 --- a/Dumper/ServerDumper.php +++ b/Dumper/ServerDumper.php @@ -30,7 +30,7 @@ class ServerDumper implements DataDumperInterface * @param DataDumperInterface|null $wrappedDumper A wrapped instance used whenever we failed contacting the server * @param ContextProviderInterface[] $contextProviders Context providers indexed by context name */ - public function __construct(string $host, DataDumperInterface $wrappedDumper = null, array $contextProviders = []) + public function __construct(string $host, ?DataDumperInterface $wrappedDumper = null, array $contextProviders = []) { $this->connection = new Connection($host, $contextProviders); $this->wrappedDumper = $wrappedDumper; @@ -41,10 +41,15 @@ public function getContextProviders(): array return $this->connection->getContextProviders(); } + /** + * @return string|null + */ public function dump(Data $data) { if (!$this->connection->write($data) && $this->wrappedDumper) { - $this->wrappedDumper->dump($data); + return $this->wrappedDumper->dump($data); } + + return null; } } diff --git a/Resources/functions/dump.php b/Resources/functions/dump.php index 6221a4d1..f2ff74c0 100644 --- a/Resources/functions/dump.php +++ b/Resources/functions/dump.php @@ -9,40 +9,52 @@ * file that was distributed with this source code. */ +use Symfony\Component\VarDumper\Caster\ScalarStub; use Symfony\Component\VarDumper\VarDumper; if (!function_exists('dump')) { /** * @author Nicolas Grekas + * @author Alexandre Daubois */ - function dump(mixed $var, mixed ...$moreVars): mixed + function dump(mixed ...$vars): mixed { - VarDumper::dump($var); + if (!$vars) { + VarDumper::dump(new ScalarStub('🐛')); - foreach ($moreVars as $v) { - VarDumper::dump($v); + return null; } - if (1 < func_num_args()) { - return func_get_args(); + if (array_key_exists(0, $vars) && 1 === count($vars)) { + VarDumper::dump($vars[0]); + $k = 0; + } else { + foreach ($vars as $k => $v) { + VarDumper::dump($v, is_int($k) ? 1 + $k : $k); + } } - return $var; + if (1 < count($vars)) { + return $vars; + } + + return $vars[$k]; } } if (!function_exists('dd')) { - /** - * @return never - */ - function dd(...$vars): void + function dd(mixed ...$vars): never { - if (!in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && !headers_sent()) { + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) && !headers_sent()) { header('HTTP/1.1 500 Internal Server Error'); } - foreach ($vars as $v) { - VarDumper::dump($v); + if (array_key_exists(0, $vars) && 1 === count($vars)) { + VarDumper::dump($vars[0]); + } else { + foreach ($vars as $k => $v) { + VarDumper::dump($v, is_int($k) ? 1 + $k : $k); + } } exit(1); diff --git a/Server/Connection.php b/Server/Connection.php index c6840867..4383278c 100644 --- a/Server/Connection.php +++ b/Server/Connection.php @@ -62,7 +62,7 @@ public function write(Data $data): bool $context = array_filter($context); $encodedPayload = base64_encode(serialize([$data, $context]))."\n"; - set_error_handler([self::class, 'nullErrorHandler']); + set_error_handler(static fn () => null); try { if (-1 !== stream_socket_sendto($this->socket, $encodedPayload)) { return true; @@ -82,16 +82,14 @@ public function write(Data $data): bool return false; } - private static function nullErrorHandler(int $t, string $m) - { - // no-op - } - + /** + * @return resource|null + */ private function createSocket() { - set_error_handler([self::class, 'nullErrorHandler']); + set_error_handler(static fn () => null); try { - return stream_socket_client($this->host, $errno, $errstr, 3); + return stream_socket_client($this->host, $errno, $errstr, 3) ?: null; } finally { restore_error_handler(); } diff --git a/Server/DumpServer.php b/Server/DumpServer.php index 8df05a15..a9228a2e 100644 --- a/Server/DumpServer.php +++ b/Server/DumpServer.php @@ -32,7 +32,7 @@ class DumpServer */ private $socket; - public function __construct(string $host, LoggerInterface $logger = null) + public function __construct(string $host, ?LoggerInterface $logger = null) { if (!str_contains($host, '://')) { $host = 'tcp://'.$host; diff --git a/Test/VarDumperTestTrait.php b/Test/VarDumperTestTrait.php index a202185e..4475efd1 100644 --- a/Test/VarDumperTestTrait.php +++ b/Test/VarDumperTestTrait.php @@ -27,7 +27,7 @@ trait VarDumperTestTrait 'flags' => null, ]; - protected function setUpVarDumper(array $casters, int $flags = null): void + protected function setUpVarDumper(array $casters, ?int $flags = null): void { $this->varDumperConfig['casters'] = $casters; $this->varDumperConfig['flags'] = $flags; @@ -52,7 +52,7 @@ public function assertDumpMatchesFormat(mixed $expected, mixed $data, int $filte $this->assertStringMatchesFormat($this->prepareExpectation($expected, $filter), $this->getDump($data, null, $filter), $message); } - protected function getDump(mixed $data, string|int $key = null, int $filter = 0): ?string + protected function getDump(mixed $data, string|int|null $key = null, int $filter = 0): ?string { if (null === $flags = $this->varDumperConfig['flags']) { $flags = getenv('DUMP_LIGHT_ARRAY') ? CliDumper::DUMP_LIGHT_ARRAY : 0; diff --git a/Tests/Caster/CasterTest.php b/Tests/Caster/CasterTest.php index 55eb4040..dda37ea7 100644 --- a/Tests/Caster/CasterTest.php +++ b/Tests/Caster/CasterTest.php @@ -22,7 +22,7 @@ class CasterTest extends TestCase { use VarDumperTestTrait; - private static $referenceArray = [ + private static array $referenceArray = [ 'null' => null, 'empty' => false, 'public' => 'pub', @@ -185,4 +185,32 @@ public function __debugInfo(): array } }); } + + public function testClassHierarchy() + { + $this->assertDumpMatchesFormat(<<<'DUMP' + Symfony\Component\VarDumper\Tests\Caster\B { + +a: "a" + #b: "b" + -c: "c" + +d: "d" + #e: "e" + -f: "f" + } + DUMP, new B()); + } +} + +class A +{ + public $a = 'a'; + protected $b = 'b'; + private $c = 'c'; +} + +class B extends A +{ + public $d = 'd'; + protected $e = 'e'; + private $f = 'f'; } diff --git a/Tests/Caster/DateCasterTest.php b/Tests/Caster/DateCasterTest.php index fbbd901f..2ce71c55 100644 --- a/Tests/Caster/DateCasterTest.php +++ b/Tests/Caster/DateCasterTest.php @@ -25,6 +25,22 @@ class DateCasterTest extends TestCase { use VarDumperTestTrait; + private string $previousTimezone; + + protected function setUp(): void + { + parent::setUp(); + + $this->previousTimezone = date_default_timezone_get(); + } + + protected function tearDown(): void + { + parent::tearDown(); + + date_default_timezone_set($this->previousTimezone); + } + /** * @dataProvider provideDateTimes */ @@ -106,6 +122,54 @@ public static function provideDateTimes() ]; } + /** + * @dataProvider provideNoTimezoneDateTimes + */ + public function testCastDateTimeNoTimezone($time, $xDate, $xInfos) + { + date_default_timezone_set('UTC'); + + $stub = new Stub(); + $date = new NoTimezoneDate($time); + $cast = DateCaster::castDateTime($date, Caster::castObject($date, \DateTime::class), $stub, false, 0); + + $xDump = << $xDate +] +EODUMP; + + $this->assertDumpEquals($xDump, $cast); + + $xDump = <<assertDumpMatchesFormat($xDump, $cast["\0~\0date"]); + } + + public static function provideNoTimezoneDateTimes() + { + return [ + ['2017-04-30 00:00:00.000000', '2017-04-30 00:00:00.0 +00:00', 'Sunday, April 30, 2017'], + ['2017-04-30 00:00:00.100000', '2017-04-30 00:00:00.100 +00:00', 'Sunday, April 30, 2017'], + ['2017-04-30 00:00:00.120000', '2017-04-30 00:00:00.120 +00:00', 'Sunday, April 30, 2017'], + ['2017-04-30 00:00:00.123000', '2017-04-30 00:00:00.123 +00:00', 'Sunday, April 30, 2017'], + ['2017-04-30 00:00:00.123400', '2017-04-30 00:00:00.123400 +00:00', 'Sunday, April 30, 2017'], + ['2017-04-30 00:00:00.123450', '2017-04-30 00:00:00.123450 +00:00', 'Sunday, April 30, 2017'], + ['2017-04-30 00:00:00.123456', '2017-04-30 00:00:00.123456 +00:00', 'Sunday, April 30, 2017'], + ]; + } + public function testCastDateTimeWithAdditionalChildProperty() { $stub = new Stub(); @@ -423,3 +487,11 @@ private function createInterval($intervalSpec, $ms, $invert) return $interval; } } + +class NoTimezoneDate extends \DateTime +{ + public function getTimezone(): \DateTimeZone|false + { + return false; + } +} diff --git a/Tests/Caster/DoctrineCasterTest.php b/Tests/Caster/DoctrineCasterTest.php index 85f6293b..b0b0c90c 100644 --- a/Tests/Caster/DoctrineCasterTest.php +++ b/Tests/Caster/DoctrineCasterTest.php @@ -31,14 +31,28 @@ public function testCastPersistentCollection() $collection = new PersistentCollection($this->createMock(EntityManagerInterface::class), $classMetadata, new ArrayCollection(['test'])); - $expected = <<= 2 + $expected = <<assertDumpMatchesFormat($expected, $collection); } diff --git a/Tests/Caster/ExceptionCasterTest.php b/Tests/Caster/ExceptionCasterTest.php index cc3c8c74..f8fe43d8 100644 --- a/Tests/Caster/ExceptionCasterTest.php +++ b/Tests/Caster/ExceptionCasterTest.php @@ -12,6 +12,7 @@ namespace Symfony\Component\VarDumper\Tests\Caster; use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\Exception\FlattenException; use Symfony\Component\ErrorHandler\Exception\SilencedErrorContext; use Symfony\Component\VarDumper\Caster\Caster; use Symfony\Component\VarDumper\Caster\ExceptionCaster; @@ -340,8 +341,8 @@ public function testExcludeVerbosity() public function testAnonymous() { - $e = new \Exception(sprintf('Boo "%s" ba.', \get_class(new class('Foo') extends \Exception { - }))); + $e = new \Exception(sprintf('Boo "%s" ba.', (new class('Foo') extends \Exception { + })::class)); $expectedDump = <<<'EODUMP' Exception { @@ -354,4 +355,34 @@ public function testAnonymous() $this->assertDumpMatchesFormat($expectedDump, $e, Caster::EXCLUDE_VERBOSE); } + + /** + * @requires function \Symfony\Component\ErrorHandler\Exception\FlattenException::create + */ + public function testFlattenException() + { + $f = FlattenException::createFromThrowable(new \Exception('Hello')); + + $expectedDump = <<<'EODUMP' +array:1 [ + 0 => Symfony\Component\ErrorHandler\Exception\FlattenException { + -message: "Hello" + -code: 0 + -previous: null + -trace: array:%d %a + -traceAsString: ""…%d + -class: "Exception" + -statusCode: 500 + -statusText: "Internal Server Error" + -headers: [] + -file: "%sExceptionCasterTest.php" + -line: %d + -asString: null + -dataRepresentation: ? Symfony\Component\VarDumper\Cloner\Data + } +] +EODUMP; + + $this->assertDumpMatchesFormat($expectedDump, [$f], Caster::EXCLUDE_VERBOSE); + } } diff --git a/Tests/Caster/FFICasterTest.php b/Tests/Caster/FFICasterTest.php index 32fea5fc..362e0a2c 100644 --- a/Tests/Caster/FFICasterTest.php +++ b/Tests/Caster/FFICasterTest.php @@ -24,6 +24,11 @@ class FFICasterTest extends TestCase { use VarDumperTestTrait; + /** + * @see FFICaster::MAX_STRING_LENGTH + */ + private const MAX_STRING_LENGTH = 255; + protected function setUp(): void { if (\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && 'preload' === \ini_get('ffi.enable')) { @@ -40,7 +45,7 @@ public function testCastAnonymousStruct() FFI\CData> size 4 align 4 { uint32_t x: 0 } - PHP, \FFI::new('struct { uint32_t x; }')); + PHP, \FFI::cdef()->new('struct { uint32_t x; }')); } public function testCastNamedStruct() @@ -49,7 +54,7 @@ public function testCastNamedStruct() FFI\CData size 4 align 4 { uint32_t x: 0 } - PHP, \FFI::new('struct Example { uint32_t x; }')); + PHP, \FFI::cdef()->new('struct Example { uint32_t x; }')); } public function testCastAnonymousUnion() @@ -59,7 +64,7 @@ public function testCastAnonymousUnion() uint32_t x: 0 uint32_t y: 0 } - PHP, \FFI::new('union { uint32_t x; uint32_t y; }')); + PHP, \FFI::cdef()->new('union { uint32_t x; uint32_t y; }')); } public function testCastNamedUnion() @@ -69,7 +74,7 @@ public function testCastNamedUnion() uint32_t x: 0 uint32_t y: 0 } - PHP, \FFI::new('union Example { uint32_t x; uint32_t y; }')); + PHP, \FFI::cdef()->new('union Example { uint32_t x; uint32_t y; }')); } public function testCastAnonymousEnum() @@ -78,7 +83,7 @@ public function testCastAnonymousEnum() FFI\CData> size 4 align 4 { cdata: 0 } - PHP, \FFI::new('enum { a, b }')); + PHP, \FFI::cdef()->new('enum { a, b }')); } public function testCastNamedEnum() @@ -87,7 +92,7 @@ public function testCastNamedEnum() FFI\CData size 4 align 4 { cdata: 0 } - PHP, \FFI::new('enum Example { a, b }')); + PHP, \FFI::cdef()->new('enum Example { a, b }')); } public static function scalarsDataProvider(): array @@ -118,7 +123,7 @@ public function testCastScalar(string $type, string $value, int $size, int $alig FFI\CData<$type> size $size align $align { cdata: $value } - PHP, \FFI::new($type)); + PHP, \FFI::cdef()->new($type)); } public function testCastVoidFunction() @@ -129,7 +134,7 @@ public function testCastVoidFunction() $abi callable(): void { returnType: FFI\CType size 1 align 1 {} } - PHP, \FFI::new('void (*)(void)')); + PHP, \FFI::cdef()->new('void (*)(void)')); } public function testCastIntFunction() @@ -140,7 +145,7 @@ public function testCastIntFunction() $abi callable(): uint64_t { returnType: FFI\CType size 8 align 8 {} } - PHP, \FFI::new('unsigned long long (*)(void)')); + PHP, \FFI::cdef()->new('unsigned long long (*)(void)')); } public function testCastFunctionWithArguments() @@ -151,14 +156,14 @@ public function testCastFunctionWithArguments() $abi callable(int32_t, char*): void { returnType: FFI\CType size 1 align 1 {} } - PHP, \FFI::new('void (*)(int a, const char* b)')); + PHP, \FFI::cdef()->new('void (*)(int a, const char* b)')); } public function testCastNonCuttedPointerToChar() { $actualMessage = "Hello World!\0"; - $string = \FFI::new('char[100]'); + $string = \FFI::cdef()->new('char[100]'); $pointer = \FFI::addr($string[0]); \FFI::memcpy($pointer, $actualMessage, \strlen($actualMessage)); @@ -173,17 +178,24 @@ public function testCastCuttedPointerToChar() { $actualMessage = str_repeat('Hello World!', 30)."\0"; $actualLength = \strlen($actualMessage); + $expectedMessage = substr($actualMessage, 0, self::MAX_STRING_LENGTH); - $expectedMessage = 'Hello World!Hello World!Hello World!Hello World!' - .'Hello World!Hello World!Hello World!Hello World!Hello World!Hel' - .'lo World!Hello World!Hello World!Hello World!Hello World!Hello ' - .'World!Hello World!Hello World!Hello World!Hello World!Hello Wor' - .'ld!Hello World!Hel'; - - $string = \FFI::new('char['.$actualLength.']'); + $string = \FFI::cdef()->new('char['.$actualLength.']'); $pointer = \FFI::addr($string[0]); \FFI::memcpy($pointer, $actualMessage, $actualLength); + // the max length is platform-dependent and can be less than 255, + // so we need to cut the expected message to the maximum length + // allowed by pages size of the current system + $ffi = \FFI::cdef(<<zend_get_page_size(); + $start = $ffi->cast('uintptr_t', $ffi->cast('char*', $pointer))->cdata; + $max = min(self::MAX_STRING_LENGTH, ($start | ($pageSize - 1)) - $start); + $expectedMessage = substr($expectedMessage, 0, $max); + $this->assertDumpEquals(<< size 8 align 8 { cdata: "$expectedMessage"… @@ -191,34 +203,21 @@ public function testCastCuttedPointerToChar() PHP, $pointer); } - /** - * It is worth noting that such a test can cause SIGSEGV, as it breaks - * into "foreign" memory. However, this is only theoretical, since - * memory is allocated within the PHP process and almost always "garbage - * data" will be read from the PHP process itself. - * - * If this test fails for some reason, please report it: We may have to - * disable the dumping of strings ("char*") feature in VarDumper. - * - * @see FFICaster::castFFIStringValue() - */ public function testCastNonTrailingCharPointer() { $actualMessage = 'Hello World!'; $actualLength = \strlen($actualMessage); - $string = \FFI::new('char['.$actualLength.']'); + $string = \FFI::cdef()->new('char['.($actualLength + 1).']'); $pointer = \FFI::addr($string[0]); - \FFI::memcpy($pointer, $actualMessage, $actualLength); - // Remove automatically addition of the trailing "\0" and remove trailing "\0" - $pointer = \FFI::cast('char*', \FFI::cast('void*', $pointer)); + $pointer = \FFI::cdef()->cast('char*', \FFI::cdef()->cast('void*', $pointer)); $pointer[$actualLength] = "\x01"; $this->assertDumpMatchesFormat(<< size 8 align 8 { - cdata: "$actualMessage%s" + cdata: %A"$actualMessage%s" } PHP, $pointer); } @@ -328,24 +327,24 @@ public function testCastPointerToNonEmptyScalars() CPP); // Create values - $int = \FFI::new('int64_t'); + $int = \FFI::cdef()->new('int64_t'); $int->cdata = 42; - $float = \FFI::new('float'); + $float = \FFI::cdef()->new('float'); $float->cdata = 42.0; - $double = \FFI::new('double'); + $double = \FFI::cdef()->new('double'); $double->cdata = 42.2; - $bool = \FFI::new('bool'); + $bool = \FFI::cdef()->new('bool'); $bool->cdata = true; // Fill struct $struct = $ffi->new('Example'); - $struct->a = \FFI::addr(\FFI::cast('int8_t', $int)); - $struct->b = \FFI::addr(\FFI::cast('uint8_t', $int)); - $struct->c = \FFI::addr(\FFI::cast('int64_t', $int)); - $struct->d = \FFI::addr(\FFI::cast('uint64_t', $int)); - $struct->e = \FFI::addr(\FFI::cast('float', $float)); - $struct->f = \FFI::addr(\FFI::cast('double', $double)); - $struct->g = \FFI::addr(\FFI::cast('bool', $bool)); + $struct->a = \FFI::addr(\FFI::cdef()->cast('int8_t', $int)); + $struct->b = \FFI::addr(\FFI::cdef()->cast('uint8_t', $int)); + $struct->c = \FFI::addr(\FFI::cdef()->cast('int64_t', $int)); + $struct->d = \FFI::addr(\FFI::cdef()->cast('uint64_t', $int)); + $struct->e = \FFI::addr(\FFI::cdef()->cast('float', $float)); + $struct->f = \FFI::addr(\FFI::cdef()->cast('double', $double)); + $struct->g = \FFI::addr(\FFI::cdef()->cast('bool', $bool)); $this->assertDumpEquals(<<<'OUTPUT' FFI\CData> size 56 align 8 { @@ -441,7 +440,7 @@ public function testCastComplexType() $var->func = (static fn (object $p) => 42); $abi = \PHP_OS_FAMILY === 'Windows' ? '[cdecl]' : '[fastcall]'; - $longSize = \FFI::type('long')->getSize(); + $longSize = \FFI::cdef()->type('long')->getSize(); $longType = 8 === $longSize ? 'int64_t' : 'int32_t'; $structSize = 56 + $longSize * 2; diff --git a/Tests/Caster/FiberCasterTest.php b/Tests/Caster/FiberCasterTest.php index 41ba099d..9e0275fb 100644 --- a/Tests/Caster/FiberCasterTest.php +++ b/Tests/Caster/FiberCasterTest.php @@ -20,7 +20,7 @@ class FiberCasterTest extends TestCase public function testCastFiberNotStarted() { - $fiber = new \Fiber(static fn() => true); + $fiber = new \Fiber(static fn () => true); $expected = <<getLocale(); - $expectedPattern = $var->getPattern(); + $expectedPattern = $this->normalizeNarrowNoBreakSpaceCharacter($var->getPattern()); $expectedCalendar = $var->getCalendar(); $expectedTimeZoneId = $var->getTimeZoneId(); $expectedTimeType = $var->getTimeType(); @@ -294,4 +294,9 @@ public function testCastDateFormatter() EOTXT; $this->assertDumpEquals($expected, $var); } + + private function normalizeNarrowNoBreakSpaceCharacter(string $input): string + { + return str_replace("\u{202F}", '\\u{202F}', $input); + } } diff --git a/Tests/Caster/MysqliCasterTest.php b/Tests/Caster/MysqliCasterTest.php index 983f541a..4eba406e 100644 --- a/Tests/Caster/MysqliCasterTest.php +++ b/Tests/Caster/MysqliCasterTest.php @@ -30,7 +30,6 @@ public function testNotConnected() $xCast = <<assertSame('NATURAL', $attr['CASE']->class); $this->assertSame('BOTH', $attr['DEFAULT_FETCH_MODE']->class); - $xDump = <<<'EODUMP' + if (\PHP_VERSION_ID >= 80215 && \PHP_VERSION_ID < 80300 || \PHP_VERSION_ID >= 80302) { + $xDump = <<<'EODUMP' +array:2 [ + "\x00~\x00inTransaction" => false + "\x00~\x00attributes" => array:10 [ + "CASE" => NATURAL + "ERRMODE" => EXCEPTION + "PERSISTENT" => false + "DRIVER_NAME" => "sqlite" + "ORACLE_NULLS" => NATURAL + "CLIENT_VERSION" => "%s" + "SERVER_VERSION" => "%s" + "STATEMENT_CLASS" => array:%d [ + 0 => "PDOStatement"%A + ] + "STRINGIFY_FETCHES" => false + "DEFAULT_FETCH_MODE" => BOTH + ] +] +EODUMP; + } else { + $xDump = <<<'EODUMP' array:2 [ "\x00~\x00inTransaction" => false "\x00~\x00attributes" => array:9 [ @@ -61,6 +82,7 @@ public function testCastPdo() ] ] EODUMP; + } $this->assertDumpMatchesFormat($xDump, $cast); } diff --git a/Tests/Caster/RdKafkaCasterTest.php b/Tests/Caster/RdKafkaCasterTest.php index 65e8ec3b..78b78ddc 100644 --- a/Tests/Caster/RdKafkaCasterTest.php +++ b/Tests/Caster/RdKafkaCasterTest.php @@ -30,8 +30,8 @@ class RdKafkaCasterTest extends TestCase private const TOPIC = 'test-topic'; private const GROUP_ID = 'test-group-id'; - private $hasBroker = false; - private $broker; + private bool $hasBroker = false; + private string $broker; protected function setUp(): void { diff --git a/Tests/Caster/RedisCasterTest.php b/Tests/Caster/RedisCasterTest.php index 566de12a..60203c46 100644 --- a/Tests/Caster/RedisCasterTest.php +++ b/Tests/Caster/RedisCasterTest.php @@ -17,14 +17,15 @@ /** * @author Nicolas Grekas * - * @requires extension redis - * * @group integration */ class RedisCasterTest extends TestCase { use VarDumperTestTrait; + /** + * @requires extension redis + */ public function testNotConnected() { $redis = new \Redis(); @@ -38,10 +39,18 @@ public function testNotConnected() $this->assertDumpMatchesFormat($xCast, $redis); } - public function testConnected() + /** + * @testWith ["Redis"] + * ["Relay\\Relay"] + */ + public function testConnected(string $class) { + if (!class_exists($class)) { + self::markTestSkipped(sprintf('"%s" class required', $class)); + } + $redisHost = explode(':', getenv('REDIS_HOST')) + [1 => 6379]; - $redis = new \Redis(); + $redis = new $class(); try { $redis->connect(...$redisHost); } catch (\Exception $e) { @@ -49,7 +58,7 @@ public function testConnected() } $xCast = <<assertDumpMatchesFormat( + <<foo(...) + ); + } + public function testUnionReturnType() { $f = function (): int|float {}; @@ -442,7 +463,10 @@ class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest" ); } - public function testGenerator() + /** + * @requires PHP < 8.4 + */ + public function testGeneratorPriorTo84() { if (\extension_loaded('xdebug')) { $this->markTestSkipped('xdebug is active'); @@ -514,6 +538,87 @@ public function testGenerator() $this->assertDumpMatchesFormat($expectedDump, $generator); } + /** + * @requires PHP 8.4 + */ + public function testGenerator() + { + if (\extension_loaded('xdebug')) { + $this->markTestSkipped('xdebug is active'); + } + + $generator = new GeneratorDemo(); + $generator = $generator->baz(); + + $expectedDump = <<<'EODUMP' +Generator { + function: "Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::baz" + this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} + %s: { + %sGeneratorDemo.php:12 { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() + › + › public function baz() + › { + } + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo->baz() {} +%A} + closed: false +} +EODUMP; + + $this->assertDumpMatchesFormat($expectedDump, $generator); + + foreach ($generator as $v) { + break; + } + + $expectedDump = <<<'EODUMP' +array:2 [ + 0 => ReflectionGenerator { + this: Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo { …} + %s: { + %s%eTests%eFixtures%eGeneratorDemo.php:%d { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() + › { + › yield 1; + › } +%A } + %s%eTests%eFixtures%eGeneratorDemo.php:20 { …} + %s%eTests%eFixtures%eGeneratorDemo.php:14 { …} +%A } + closed: false + } + 1 => Generator { + function: "Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo" + %s: { + %s%eTests%eFixtures%eGeneratorDemo.php:%d { + Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::foo() + › { + › yield 1; + › } + } +%A } + closed: false + } +] +EODUMP; + + $r = new \ReflectionGenerator($generator); + $this->assertDumpMatchesFormat($expectedDump, [$r, $r->getExecutingGenerator()]); + + foreach ($generator as $v) { + } + + $expectedDump = <<<'EODUMP' +Generator { + function: "Symfony\Component\VarDumper\Tests\Fixtures\GeneratorDemo::baz" + closed: true +} +EODUMP; + $this->assertDumpMatchesFormat($expectedDump, $generator); + } + public function testNewInInitializer() { $f = function ($a = new \stdClass()) {}; @@ -534,13 +639,14 @@ class: "Symfony\Component\VarDumper\Tests\Caster\ReflectionCasterTest" public function testReflectionClassWithAttribute() { $var = new \ReflectionClass(LotsOfAttributes::class); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: [] } ] @@ -553,14 +659,15 @@ public function testReflectionClassWithAttribute() public function testReflectionMethodWithAttribute() { $var = new \ReflectionMethod(LotsOfAttributes::class, 'someMethod'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:1 [ 0 => "two" ] @@ -575,14 +682,15 @@ public function testReflectionMethodWithAttribute() public function testReflectionPropertyWithAttribute() { $var = new \ReflectionProperty(LotsOfAttributes::class, 'someProperty'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:2 [ 0 => "one" "extra" => "hello" @@ -597,8 +705,9 @@ public function testReflectionPropertyWithAttribute() public function testReflectionClassConstantWithAttribute() { $var = new \ReflectionClassConstant(LotsOfAttributes::class, 'SOME_CONSTANT'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" arguments: array:1 [ 0 => "one" ] } 1 => ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\RepeatableAttribute" arguments: array:1 [ 0 => "two" ] @@ -626,14 +735,15 @@ public function testReflectionClassConstantWithAttribute() public function testReflectionParameterWithAttribute() { $var = new \ReflectionParameter([LotsOfAttributes::class, 'someMethod'], 'someParameter'); + $dumpedAttributeNameProperty = (\PHP_VERSION_ID < 80400 ? '' : '+').'name'; - $this->assertDumpMatchesFormat(<<< 'EOTXT' + $this->assertDumpMatchesFormat(<< ReflectionAttribute { - name: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" + $dumpedAttributeNameProperty: "Symfony\Component\VarDumper\Tests\Fixtures\MyAttribute" arguments: array:1 [ 0 => "three" ] @@ -650,6 +760,6 @@ public static function stub(): void } } -function reflectionParameterFixture(NotLoadableClass $arg1 = null, $arg2) +function reflectionParameterFixture(?NotLoadableClass $arg1, $arg2) { } diff --git a/Tests/Caster/SplCasterTest.php b/Tests/Caster/SplCasterTest.php index 743b357c..d2326596 100644 --- a/Tests/Caster/SplCasterTest.php +++ b/Tests/Caster/SplCasterTest.php @@ -104,7 +104,7 @@ public function testCastFileObject() flags: DROP_NEW_LINE|SKIP_EMPTY maxLineLen: 0 fstat: array:26 [ - "dev" => %d + "dev" => %i "ino" => %i "nlink" => %d "rdev" => 0 @@ -174,7 +174,7 @@ class([123]) extends \ArrayObject {}; $expected = << 123 ] flag::STD_PROP_LIST: false @@ -192,7 +192,7 @@ public function testArrayIterator() $expected = << 234 ] flag::STD_PROP_LIST: false @@ -213,6 +213,26 @@ public function testBadSplFileInfo() EOTXT; $this->assertDumpEquals($expected, $var); } + + public function testWeakMap() + { + $var = new \WeakMap(); + $obj = new \stdClass(); + $var[$obj] = 123; + + $expected = << { + object: {} + data: 123 + } + ] + } + EOTXT; + + $this->assertDumpEquals($expected, $var); + } } class MyArrayIterator extends \ArrayIterator diff --git a/Tests/Caster/StubCasterTest.php b/Tests/Caster/StubCasterTest.php index cd6876cd..cf0bc733 100644 --- a/Tests/Caster/StubCasterTest.php +++ b/Tests/Caster/StubCasterTest.php @@ -15,6 +15,7 @@ use Symfony\Component\VarDumper\Caster\ArgsStub; use Symfony\Component\VarDumper\Caster\ClassStub; use Symfony\Component\VarDumper\Caster\LinkStub; +use Symfony\Component\VarDumper\Caster\ScalarStub; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\HtmlDumper; use Symfony\Component\VarDumper\Test\VarDumperTestTrait; @@ -87,6 +88,19 @@ public function testArgsStubWithClosure() $this->assertDumpMatchesFormat($expectedDump, $args); } + public function testEmptyStub() + { + $args = [new ScalarStub('🐛')]; + + $expectedDump = <<<'EODUMP' +array:1 [ + 0 => 🐛 +] +EODUMP; + + $this->assertDumpMatchesFormat($expectedDump, $args); + } + public function testLinkStub() { $var = [new LinkStub(__CLASS__, 0, __FILE__)]; @@ -141,7 +155,7 @@ public function testClassStub() $expectedDump = <<<'EODUMP' array:1 [ - 0 => "hello(?stdClass $a, stdClass $b = null)" + 0 => "hello(?stdClass $a, ?stdClass $b = null)" ] EODUMP; @@ -192,8 +206,8 @@ public function testClassStubWithNotExistingMethod() public function testClassStubWithAnonymousClass() { - $var = [new ClassStub(\get_class(new class() extends \Exception { - }))]; + $var = [new ClassStub((new class() extends \Exception { + })::class)]; $cloner = new VarCloner(); $dumper = new HtmlDumper(); @@ -203,7 +217,7 @@ public function testClassStubWithAnonymousClass() $expectedDump = <<<'EODUMP' array:1 [ - 0 => "Exception@anonymous" + 0 => "Exception@anonymous" ] EODUMP; diff --git a/Tests/Caster/XmlReaderCasterTest.php b/Tests/Caster/XmlReaderCasterTest.php index 78416f30..635c4c4e 100644 --- a/Tests/Caster/XmlReaderCasterTest.php +++ b/Tests/Caster/XmlReaderCasterTest.php @@ -21,8 +21,7 @@ class XmlReaderCasterTest extends TestCase { use VarDumperTestTrait; - /** @var \XmlReader */ - private $reader; + private \XMLReader $reader; protected function setUp(): void { diff --git a/Tests/Cloner/StubTest.php b/Tests/Cloner/StubTest.php new file mode 100644 index 00000000..dd44aba3 --- /dev/null +++ b/Tests/Cloner/StubTest.php @@ -0,0 +1,53 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests\Cloner; + +use PHPUnit\Framework\TestCase; +use Symfony\Component\VarDumper\Cloner\Stub; + +final class StubTest extends TestCase +{ + public function testUnserializeNullValue() + { + $stub = new Stub(); + $stub->value = null; + + $stub = unserialize(serialize($stub)); + + self::assertNull($stub->value); + } + + public function testUnserializeNullInTypedProperty() + { + $stub = new MyStub(); + $stub->myProp = null; + + $stub = unserialize(serialize($stub)); + + self::assertNull($stub->myProp); + } + + public function testUninitializedStubPropertiesAreLeftUninitialized() + { + $stub = new MyStub(); + + $stub = unserialize(serialize($stub)); + + $r = new \ReflectionProperty(MyStub::class, 'myProp'); + self::assertFalse($r->isInitialized($stub)); + } +} + +final class MyStub extends Stub +{ + public mixed $myProp; +} diff --git a/Tests/Cloner/VarClonerTest.php b/Tests/Cloner/VarClonerTest.php index 36639ca9..ad179699 100644 --- a/Tests/Cloner/VarClonerTest.php +++ b/Tests/Cloner/VarClonerTest.php @@ -399,9 +399,7 @@ public function testJsonCast() public function testCaster() { $cloner = new VarCloner([ - '*' => function ($obj, $array) { - return ['foo' => 123]; - }, + '*' => fn ($obj, $array) => ['foo' => 123], __CLASS__ => function ($obj, $array) { ++$array['foo']; diff --git a/Tests/Command/Descriptor/CliDescriptorTest.php b/Tests/Command/Descriptor/CliDescriptorTest.php index 5941508a..5a24f1c1 100644 --- a/Tests/Command/Descriptor/CliDescriptorTest.php +++ b/Tests/Command/Descriptor/CliDescriptorTest.php @@ -12,7 +12,6 @@ namespace Symfony\Component\VarDumper\Tests\Command\Descriptor; use PHPUnit\Framework\TestCase; -use Symfony\Component\Console\Formatter\OutputFormatterStyle; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\VarDumper\Cloner\Data; use Symfony\Component\VarDumper\Command\Descriptor\CliDescriptor; @@ -20,8 +19,8 @@ class CliDescriptorTest extends TestCase { - private static $timezone; - private static $prevTerminalEmulator; + private static string $timezone; + private static string|false $prevTerminalEmulator; public static function setUpBeforeClass(): void { @@ -45,9 +44,7 @@ public function testDescribe(array $context, string $expectedOutput, bool $decor { $output = new BufferedOutput(); $output->setDecorated($decorated); - $descriptor = new CliDescriptor(new CliDumper(function ($s) { - return $s; - })); + $descriptor = new CliDescriptor(new CliDumper(fn ($s) => $s)); $descriptor->describe($output, new Data([[123]]), $context + ['timestamp' => 1544804268.3668], 1); @@ -86,20 +83,7 @@ public static function provideContext() 'file_link' => 'phpstorm://open?file=/Users/ogi/symfony/src/Symfony/Component/VarDumper/Tests/Command/Descriptor/CliDescriptorTest.php&line=30', ], ], - method_exists(OutputFormatterStyle::class, 'setHref') ? - << [ - [ - 'source' => [ - 'name' => 'CliDescriptorTest.php', - 'line' => 30, - 'file_relative' => 'src/Symfony/Component/VarDumper/Tests/Command/Descriptor/CliDescriptorTest.php', - 'file_link' => 'phpstorm://open?file=/Users/ogi/symfony/src/Symfony/Component/VarDumper/Tests/Command/Descriptor/CliDescriptorTest.php&line=30', - ], + yield 'source with hyperlink' => [ + [ + 'source' => [ + 'name' => 'CliDescriptorTest.php', + 'line' => 30, + 'file_relative' => 'src/Symfony/Component/VarDumper/Tests/Command/Descriptor/CliDescriptorTest.php', + 'file_link' => 'phpstorm://open?file=/Users/ogi/symfony/src/Symfony/Component/VarDumper/Tests/Command/Descriptor/CliDescriptorTest.php&line=30', ], - << [ [ diff --git a/Tests/Command/Descriptor/HtmlDescriptorTest.php b/Tests/Command/Descriptor/HtmlDescriptorTest.php index 09acf149..bdf6a86c 100644 --- a/Tests/Command/Descriptor/HtmlDescriptorTest.php +++ b/Tests/Command/Descriptor/HtmlDescriptorTest.php @@ -19,7 +19,7 @@ class HtmlDescriptorTest extends TestCase { - private static $timezone; + private static string $timezone; public static function setUpBeforeClass(): void { @@ -45,7 +45,7 @@ public function testItOutputsStylesAndScriptsOnFirstDescribeCall() $descriptor->describe($output, new Data([[123]]), ['timestamp' => 1544804268.3668], 1); - $this->assertStringNotMatchesFormat('%A', $output->fetch(), 'styles & scripts are output only once'); + $this->assertDoesNotMatchRegularExpression('#(.*)#', $output->fetch(), 'styles & scripts are output only once'); } /** diff --git a/Tests/Dumper/CliDumperTest.php b/Tests/Dumper/CliDumperTest.php index 25e9b103..6e55bc4c 100644 --- a/Tests/Dumper/CliDumperTest.php +++ b/Tests/Dumper/CliDumperTest.php @@ -12,7 +12,11 @@ namespace Symfony\Component\VarDumper\Tests\Dumper; use PHPUnit\Framework\TestCase; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; +use Symfony\Component\VarDumper\Caster\ClassStub; use Symfony\Component\VarDumper\Caster\CutStub; +use Symfony\Component\VarDumper\Cloner\Data; +use Symfony\Component\VarDumper\Cloner\Stub; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\AbstractDumper; use Symfony\Component\VarDumper\Dumper\CliDumper; @@ -57,7 +61,7 @@ public function testGet() $this->assertStringMatchesFormat( << 1 0 => &1 null "const" => 1.1 @@ -72,6 +76,7 @@ public function testGet() é\\x01test\\t\\n ing """ + "bo\\u{FEFF}m" => "te\\u{FEFF}st" "[]" => [] "res" => stream resource {@{$res} %A wrapper_type: "plainfile" @@ -85,7 +90,7 @@ public function testGet() +foo: ""…3 +"bar": "bar" } - "closure" => Closure(\$a, PDO &\$b = null) {#%d + "closure" => Closure(\$a, ?PDO &\$b = null) {#%d class: "Symfony\Component\VarDumper\Tests\Dumper\CliDumperTest" this: Symfony\Component\VarDumper\Tests\Dumper\CliDumperTest {#%d …} file: "%s%eTests%eFixtures%edumb-var.php" @@ -338,14 +343,12 @@ public function testThrowingCaster() #message: "Unexpected Exception thrown from a caster: Foobar" trace: { %sTwig.php:2 { - __TwigTemplate_VarDumperFixture_u75a09->doDisplay(array \$context, array \$blocks = []) + __TwigTemplate_VarDumperFixture_u75a09->doDisplay(array \$context, array \$blocks = []): array › foo bar › twig source › } - %s%eTemplate.php:%d { …} - %s%eTemplate.php:%d { …} - %s%eTemplate.php:%d { …} + %A%eTemplate.php:%d { …} %s%eTests%eDumper%eCliDumperTest.php:%d { …} %A } } @@ -455,21 +458,75 @@ public function testDumpArrayWithColor($value, $flags, $expectedOut) $this->assertSame($expectedOut, $out); } - private function getSpecialVars() + public function testCollapse() { - foreach (array_keys($GLOBALS) as $var) { - if ('GLOBALS' !== $var) { - unset($GLOBALS[$var]); - } + if ('\\' === \DIRECTORY_SEPARATOR) { + $this->markTestSkipped('This test cannot be run on Windows.'); } - $var = function &() { - $var = []; - $var[] = &$var; + $stub = new Stub(); + $stub->type = Stub::TYPE_OBJECT; + $stub->class = 'stdClass'; + $stub->position = 1; + + $data = new Data([ + [ + $stub, + ], + [ + "\0~collapse=1\0foo" => 123, + "\0+\0bar" => [1 => 2], + ], + [ + 'bar' => 123, + ], + ]); + + $dumper = new CliDumper(); + $dump = $dumper->dump($data, true); - return $var; - }; + $this->assertSame( + <<<'EOTXT' +{ + foo: 123 + +"bar": array:1 [ + "bar" => 123 + ] +} - return eval('return [$var(), $GLOBALS, &$GLOBALS];'); +EOTXT + , + $dump + ); + } + + public function testFileLinkFormat() + { + if (!class_exists(FileLinkFormatter::class)) { + $this->markTestSkipped(sprintf('Class "%s" is required to run this test.', FileLinkFormatter::class)); + } + + $data = new Data([ + [ + new ClassStub(self::class), + ], + ]); + + $ide = $_ENV['SYMFONY_IDE'] ?? null; + $_ENV['SYMFONY_IDE'] = 'vscode'; + + try { + $dumper = new CliDumper(); + $dumper->setColors(true); + $dump = $dumper->dump($data, true); + + $this->assertStringMatchesFormat('%svscode:%sCliDumperTest%s', $dump); + } finally { + if (null === $ide) { + unset($_ENV['SYMFONY_IDE']); + } else { + $_ENV['SYMFONY_IDE'] = $ide; + } + } } } diff --git a/Tests/Dumper/ContextualizedDumperTest.php b/Tests/Dumper/ContextualizedDumperTest.php index ba717940..25fd1ec7 100644 --- a/Tests/Dumper/ContextualizedDumperTest.php +++ b/Tests/Dumper/ContextualizedDumperTest.php @@ -37,7 +37,7 @@ public function testContextualizedCliDumper() $dumper->dump($data); $out = ob_get_clean(); - $this->assertStringContainsString("\e]8;;{$href}\e\\\e[", $out); + $this->assertStringContainsString("\e]8;;{$href}\e\\^\e]", $out); $this->assertStringContainsString("m{$var}\e[", $out); } } diff --git a/Tests/Dumper/FunctionsTest.php b/Tests/Dumper/FunctionsTest.php index 7444d4bf..d158d7eb 100644 --- a/Tests/Dumper/FunctionsTest.php +++ b/Tests/Dumper/FunctionsTest.php @@ -18,6 +18,17 @@ class FunctionsTest extends TestCase { + public function testDumpWithoutArg() + { + $this->setupVarDumper(); + + ob_start(); + $return = dump(); + ob_end_clean(); + + $this->assertNull($return); + } + public function testDumpReturnsFirstArg() { $this->setupVarDumper(); @@ -28,7 +39,20 @@ public function testDumpReturnsFirstArg() $return = dump($var1); ob_end_clean(); - $this->assertEquals($var1, $return); + $this->assertSame($var1, $return); + } + + public function testDumpReturnsFirstNamedArgWithoutSectionName() + { + $this->setupVarDumper(); + + $var1 = 'a'; + + ob_start(); + $return = dump(first: $var1); + ob_end_clean(); + + $this->assertSame($var1, $return); } public function testDumpReturnsAllArgsInArray() @@ -43,7 +67,22 @@ public function testDumpReturnsAllArgsInArray() $return = dump($var1, $var2, $var3); ob_end_clean(); - $this->assertEquals([$var1, $var2, $var3], $return); + $this->assertSame([$var1, $var2, $var3], $return); + } + + public function testDumpReturnsAllNamedArgsInArray() + { + $this->setupVarDumper(); + + $var1 = 'a'; + $var2 = 'b'; + $var3 = 'c'; + + ob_start(); + $return = dump($var1, second: $var2, third: $var3); + ob_end_clean(); + + $this->assertSame([$var1, 'second' => $var2, 'third' => $var3], $return); } protected function setupVarDumper() diff --git a/Tests/Dumper/HtmlDumperTest.php b/Tests/Dumper/HtmlDumperTest.php index 8c9592e4..9b914ad6 100644 --- a/Tests/Dumper/HtmlDumperTest.php +++ b/Tests/Dumper/HtmlDumperTest.php @@ -54,7 +54,7 @@ public function testGet() $this->assertStringMatchesFormat( <<array:24 [ +array:25 [ "number" => 1 0 => &1 null "const" => 1.1 @@ -69,6 +69,7 @@ public function testGet() é\\x01test\\t\\n ing """ + "bo\\u{FEFF}m" => "te\\u{FEFF}st" "[]" => [] "res" => stream resource @{$res} %A wrapper_type: "plainfile" @@ -83,7 +84,7 @@ public function testGet() +foo: "foo" +"bar": "bar" } - "closure" => Closure(\$a, PDO &\$b = null) {#%d + "closure" => Closure(\$a, ?PDO &\$b = null) {#%d class: "Symfony\Component\VarDumper\Tests\Dumper\HtmlDumperTest" this: cloneVar($var); + if (null !== $label) { + $var = $var->withContext(['label' => $label]); + } + + $dumper->dump($var); + }; + VarDumper::setHandler($handler); + $handler($var, $label); +}); + +$arrayObject = new \ArrayObject(); +dump($arrayObject); +$arrayObject['X'] = 'A'; +$arrayObject['Y'] = new \ArrayObject(['type' => 'object']); +$arrayObject['Y']['Z'] = 'B'; + +$arrayIterator = new \ArrayIterator(); +dump($arrayIterator); +$arrayIterator['X'] = 'A'; +$arrayIterator['Y'] = new \ArrayIterator(['type' => 'object']); +$arrayIterator['Y']['Z'] = 'B'; + +$recursiveArrayIterator = new \RecursiveArrayIterator(); +dump($recursiveArrayIterator); +$recursiveArrayIterator['X'] = 'A'; +$recursiveArrayIterator['Y'] = new \RecursiveArrayIterator(['type' => 'object']); +$recursiveArrayIterator['Y']['Z'] = 'B'; + +--EXPECTF-- +%s on line %d: +ArrayObject {#%d + storage: [] + flag::STD_PROP_LIST: false + flag::ARRAY_AS_PROPS: false + iteratorClass: "ArrayIterator" +} +%s on line %d: +ArrayIterator {#%d + storage: [] + flag::STD_PROP_LIST: false + flag::ARRAY_AS_PROPS: false +} +%s on line %d: +RecursiveArrayIterator {#%d + storage: [] + flag::STD_PROP_LIST: false + flag::ARRAY_AS_PROPS: false +} diff --git a/Tests/Dumper/functions/dump_with_multiple_args.phpt b/Tests/Dumper/functions/dump_with_multiple_args.phpt new file mode 100644 index 00000000..ea65e705 --- /dev/null +++ b/Tests/Dumper/functions/dump_with_multiple_args.phpt @@ -0,0 +1,18 @@ +--TEST-- +Test dump() with multiple args shows line number +--FILE-- + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Symfony\Component\VarDumper\Tests\Fixtures; + +class Php82NullStandaloneReturnType +{ + public function foo(null $bar): null + { + return null; + } +} diff --git a/Tests/Fixtures/Twig.php b/Tests/Fixtures/Twig.php index 5d1a73d4..e26a3925 100644 --- a/Tests/Fixtures/Twig.php +++ b/Tests/Fixtures/Twig.php @@ -18,7 +18,7 @@ class __TwigTemplate_VarDumperFixture_u75a09 extends AbstractTwigTemplate { private $path; - public function __construct(Twig\Environment $env = null, $path = null) + public function __construct(?Twig\Environment $env = null, $path = null) { if (null !== $env) { parent::__construct($env); @@ -28,23 +28,23 @@ public function __construct(Twig\Environment $env = null, $path = null) $this->path = $path; } - protected function doDisplay(array $context, array $blocks = []) + protected function doDisplay(array $context, array $blocks = []): array { // line 2 throw new \Exception('Foobar'); } - public function getTemplateName() + public function getTemplateName(): string { return 'foo.twig'; } - public function getDebugInfo() + public function getDebugInfo(): array { return [33 => 1, 34 => 2]; } - public function getSourceContext() + public function getSourceContext(): Twig\Source { return new Twig\Source(" foo bar\n twig source\n\n", 'foo.twig', $this->path ?: __FILE__); } diff --git a/Tests/Fixtures/dumb-var.php b/Tests/Fixtures/dumb-var.php index fc48012f..07d0973e 100644 --- a/Tests/Fixtures/dumb-var.php +++ b/Tests/Fixtures/dumb-var.php @@ -1,5 +1,14 @@ + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + namespace Symfony\Component\VarDumper\Tests\Fixture; if (!class_exists(\Symfony\Component\VarDumper\Tests\Fixture\DumbFoo::class)) { @@ -17,12 +26,12 @@ class DumbFoo $var = [ 'number' => 1, null, - 'const' => 1.1, true, false, NAN, INF, -INF, PHP_INT_MAX, - 'str' => "déjà\n", "\xE9\x01test\t\ning", + 'const' => 1.1, true, false, \NAN, \INF, -\INF, \PHP_INT_MAX, + 'str' => "déjà\n", "\xE9\x01test\t\ning", "bo\u{feff}m" => "te\u{feff}st", '[]' => [], 'res' => $g, 'obj' => $foo, - 'closure' => function ($a, \PDO &$b = null) {}, + 'closure' => function ($a, ?\PDO &$b = null) {}, 'line' => __LINE__ - 1, 'nobj' => [(object) []], ]; diff --git a/VarDumper.php b/VarDumper.php index 840bfd64..e1400f15 100644 --- a/VarDumper.php +++ b/VarDumper.php @@ -11,9 +11,9 @@ namespace Symfony\Component\VarDumper; +use Symfony\Component\ErrorHandler\ErrorRenderer\FileLinkFormatter; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\RequestStack; -use Symfony\Component\HttpKernel\Debug\FileLinkFormatter; use Symfony\Component\VarDumper\Caster\ReflectionCaster; use Symfony\Component\VarDumper\Cloner\VarCloner; use Symfony\Component\VarDumper\Dumper\CliDumper; @@ -37,16 +37,22 @@ class VarDumper */ private static $handler; - public static function dump(mixed $var) + /** + * @param string|null $label + * + * @return mixed + */ + public static function dump(mixed $var/* , string $label = null */) { + $label = 2 <= \func_num_args() ? func_get_arg(1) : null; if (null === self::$handler) { self::register(); } - return (self::$handler)($var); + return (self::$handler)($var, $label); } - public static function setHandler(callable $callable = null): ?callable + public static function setHandler(?callable $callable = null): ?callable { if (1 > \func_num_args()) { trigger_deprecation('symfony/var-dumper', '6.2', 'Calling "%s()" without any arguments is deprecated, pass null explicitly instead.', __METHOD__); @@ -79,19 +85,25 @@ private static function register(): void case 'server' === $format: case $format && 'tcp' === parse_url($format, \PHP_URL_SCHEME): $host = 'server' === $format ? $_SERVER['VAR_DUMPER_SERVER'] ?? '127.0.0.1:9912' : $format; - $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliDumper() : new HtmlDumper(); + $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliDumper() : new HtmlDumper(); $dumper = new ServerDumper($host, $dumper, self::getDefaultContextProviders()); break; default: - $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) ? new CliDumper() : new HtmlDumper(); + $dumper = \in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) ? new CliDumper() : new HtmlDumper(); } if (!$dumper instanceof ServerDumper) { $dumper = new ContextualizedDumper($dumper, [new SourceContextProvider()]); } - self::$handler = function ($var) use ($cloner, $dumper) { - $dumper->dump($cloner->cloneVar($var)); + self::$handler = function ($var, ?string $label = null) use ($cloner, $dumper) { + $var = $cloner->cloneVar($var); + + if (null !== $label) { + $var = $var->withContext(['label' => $label]); + } + + $dumper->dump($var); }; } @@ -99,7 +111,7 @@ private static function getDefaultContextProviders(): array { $contextProviders = []; - if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg'], true) && class_exists(Request::class)) { + if (!\in_array(\PHP_SAPI, ['cli', 'phpdbg', 'embed'], true) && class_exists(Request::class)) { $requestStack = new RequestStack(); $requestStack->push(Request::createFromGlobals()); $contextProviders['request'] = new RequestContextProvider($requestStack); diff --git a/composer.json b/composer.json index 71ec64c0..e6166f86 100644 --- a/composer.json +++ b/composer.json @@ -17,24 +17,21 @@ ], "require": { "php": ">=8.1", + "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0" }, "require-dev": { "ext-iconv": "*", - "symfony/console": "^5.4|^6.0", - "symfony/process": "^5.4|^6.0", - "symfony/uid": "^5.4|^6.0", + "symfony/console": "^5.4|^6.0|^7.0", + "symfony/error-handler": "^6.3|^7.0", + "symfony/http-kernel": "^5.4|^6.0|^7.0", + "symfony/process": "^5.4|^6.0|^7.0", + "symfony/uid": "^5.4|^6.0|^7.0", "twig/twig": "^2.13|^3.0.4" }, "conflict": { - "phpunit/phpunit": "<5.4.3", "symfony/console": "<5.4" }, - "suggest": { - "ext-iconv": "To convert non-UTF-8 strings to UTF-8 (or symfony/polyfill-iconv in case ext-iconv cannot be used).", - "ext-intl": "To show region name in time zone dump", - "symfony/console": "To use the ServerDumpCommand and/or the bin/var-dump-server script" - }, "autoload": { "files": [ "Resources/functions/dump.php" ], "psr-4": { "Symfony\\Component\\VarDumper\\": "" }, diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 61727f16..a629967b 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -17,6 +17,7 @@ ./Tests/ + ./Tests/Dumper/functions/