* * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA * 02110-1301, USA. */ use ReflectionMethod; use TYPO3\CMS\Core\SingletonInterface; use TYPO3\CMS\Core\Utility\StringUtility; use TYPO3\CMS\Extbase\DomainObject\AbstractDomainObject; use TYPO3\CMS\Extbase\DomainObject\AbstractEntity; use TYPO3\CMS\Extbase\DomainObject\AbstractValueObject; use TYPO3\CMS\Extbase\DomainObject\DomainObjectInterface; use TYPO3\CMS\Extbase\Object\ObjectManager; use TYPO3\CMS\Extbase\Persistence\Generic\LazyLoadingProxy; use TYPO3\CMS\Extbase\Persistence\Generic\Mapper\DataMapper; use TYPO3\CMS\Extbase\Persistence\Generic\PersistenceManager; use TYPO3\CMS\Extbase\Persistence\Generic\Qom\QueryObjectModelFactory; use TYPO3\CMS\Extbase\Persistence\ObjectStorage; use TYPO3\CMS\Extbase\Reflection\ReflectionService; use TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer; /** * Copies from TYPO3 core, and modified to our needs. * * This class is a backport of the corresponding class of TYPO3 Flow. * All credits go to the TYPO3 Flow team. * * A debugging utility class */ class DebuggerUtility { const PLAINTEXT_INDENT = ' '; const HTML_INDENT = '   '; /** * @var \TYPO3\CMS\Extbase\Persistence\ObjectStorage */ protected static $renderedObjects; /** * Hardcoded list of Extbase class names (regex) which should not be displayed during debugging * * @var array */ protected static $blacklistedClassNames = [ 'PHPUnit_Framework_MockObject_InvocationMocker', ReflectionService::class, ObjectManager::class, DataMapper::class, PersistenceManager::class, QueryObjectModelFactory::class, ContentObjectRenderer::class ]; /** * Hardcoded list of property names (regex) which should not be displayed during debugging * * @var array */ protected static $blacklistedPropertyNames = ['warning']; /** * Is set to TRUE once the CSS file is included in the current page to prevent double inclusions of the CSS file. * * @var bool */ protected static $stylesheetEchoed = false; /** * Defines the max recursion depth of the dump, set to 8 due to common memory limits * * @var int */ protected static $maxDepth = 8; /** * Clear the state of the debugger */ protected static function clearState(): void { self::$renderedObjects = new ObjectStorage(); } /** * Renders a dump of the given value * * @param mixed $value * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string */ protected static function renderDump($value, int $level, bool $plainText, bool $ansiColors): string { $dump = ''; if (is_string($value)) { $croppedValue = strlen($value) > 2000 ? substr($value, 0, 2000) . '...' : $value; if ($plainText) { $dump = self::ansiEscapeWrap('"' . implode(PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level + 1), str_split($croppedValue, 76)) . '"', '33', $ansiColors) . ' (' . strlen($value) . ' chars)'; } else { $lines = str_split($croppedValue, 76); $lines = array_map(static function (string $line): string { return htmlspecialchars($line, ENT_COMPAT); }, $lines); $dump = sprintf('\'%s\' (%s chars)', implode('
' . str_repeat(self::HTML_INDENT, $level + 1), $lines), strlen($value)); } } elseif (is_numeric($value)) { $dump = sprintf('%s (%s)', self::ansiEscapeWrap((string)$value, '35', $ansiColors), gettype($value)); } elseif (is_bool($value)) { $dump = $value ? self::ansiEscapeWrap('TRUE', '32', $ansiColors) : self::ansiEscapeWrap('FALSE', '32', $ansiColors); } elseif ($value === null || is_resource($value)) { $dump = gettype($value); } elseif (is_array($value)) { $dump = self::renderArray($value, $level + 1, $plainText, $ansiColors); } elseif (is_object($value)) { if ($value instanceof \Closure) { $dump = self::renderClosure($value, $level + 1, $plainText, $ansiColors); } else { $dump = self::renderObject($value, $level + 1, $plainText, $ansiColors); } } return $dump; } /** * Renders a dump of the given array * * @param array $array * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string */ protected static function renderArray(array $array, int $level, bool $plainText = false, bool $ansiColors = false): string { $content = ''; $count = count($array); if ($plainText) { $header = self::ansiEscapeWrap('array', '36', $ansiColors); } else { $header = 'array'; } $header .= $count > 0 ? '(' . $count . ' item' . ($count > 1 ? 's' : '') . ')' : '(empty)'; if ($level >= self::$maxDepth) { if ($plainText) { $header .= ' ' . self::ansiEscapeWrap('max depth', '47;30', $ansiColors); } else { $header .= 'max depth'; } } else { $content = self::renderCollection($array, $level, $plainText, $ansiColors); if (!$plainText) { $header = ($level > 1 && $count > 0 ? '' : '') . $header . ''; } } if ($level > 1 && $count > 0 && !$plainText) { $dump = '' . $header . '' . $content . ''; } else { $dump = $header . $content; } return $dump; } /** * Renders a dump of the given object * * @param object $object * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string */ protected static function renderObject(object $object, int $level, bool $plainText = false, bool $ansiColors = false): string { if ($object instanceof LazyLoadingProxy) { $object = $object->_loadRealInstance(); if (!is_object($object)) { return gettype($object); } } $header = self::renderHeader($object, $level, $plainText, $ansiColors); if ($level < self::$maxDepth && !self::isBlacklisted($object) && !(self::isAlreadyRendered($object) && $plainText !== true)) { $content = self::renderContent($object, $level, $plainText, $ansiColors); } else { $content = ''; } if ($plainText) { return $header . $content; } return '' . $header . '' . $content . ''; } /** * Renders a dump of the given closure * * @param \Closure $object * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string */ protected static function renderClosure(\Closure $object, int $level, bool $plainText = false, bool $ansiColors = false): string { $header = self::renderHeader($object, $level, $plainText, $ansiColors); if ($level < self::$maxDepth && (!self::isAlreadyRendered($object) || $plainText)) { $content = self::renderContent($object, $level, $plainText, $ansiColors); } else { $content = ''; } if ($plainText) { return $header . $content; } return '' . $header . '' . $content . ''; } /** * Checks if a given object or property should be excluded/filtered * * @param object $value A ReflectionProperty or other Object * @return bool TRUE if the given object should be filtered */ protected static function isBlacklisted(object $value): bool { if ($value instanceof \ReflectionProperty) { $result = in_array($value->getName(), self::$blacklistedPropertyNames, true); } else { $result = in_array(get_class($value), self::$blacklistedClassNames, true); } return $result; } /** * Checks if a given object was already rendered. * * @param object $object * @return bool TRUE if the given object was already rendered */ protected static function isAlreadyRendered(object $object): bool { return self::$renderedObjects->contains($object); } /** * Renders the header of a given object/collection. It is usually the class name along with some flags. * * @param object $object * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string The rendered header with tags */ protected static function renderHeader(object $object, int $level, bool $plainText, bool $ansiColors): string { $dump = ''; $persistenceType = null; $className = get_class($object); $classReflection = new \ReflectionClass($className); if ($plainText) { $dump .= self::ansiEscapeWrap($className, '36', $ansiColors); } else { $dump .= '' . htmlspecialchars($className, ENT_COMPAT) . ''; } if (!$object instanceof \Closure) { if ($object instanceof SingletonInterface) { $scope = 'singleton'; } else { $scope = 'prototype'; } if ($plainText) { $dump .= ' ' . self::ansiEscapeWrap($scope, '44;37', $ansiColors); } else { $dump .= '' . $scope . ''; } if ($object instanceof AbstractDomainObject) { if ($object->_isDirty()) { $persistenceType = 'modified'; } elseif ($object->_isNew()) { $persistenceType = 'transient'; } else { $persistenceType = 'persistent'; } } if ($object instanceof ObjectStorage && $object->_isDirty()) { $persistenceType = 'modified'; } if ($object instanceof AbstractEntity) { $domainObjectType = 'entity'; } elseif ($object instanceof AbstractValueObject) { $domainObjectType = 'valueobject'; } else { $domainObjectType = 'object'; } $persistenceType = $persistenceType === null ? '' : $persistenceType . ' '; if ($plainText) { $dump .= ' ' . self::ansiEscapeWrap($persistenceType . $domainObjectType, '42;30', $ansiColors); } else { $dump .= '' . $persistenceType . $domainObjectType . ''; } } if (strpos(implode('|', self::$blacklistedClassNames), get_class($object)) > 0) { if ($plainText) { $dump .= ' ' . self::ansiEscapeWrap('filtered', '47;30', $ansiColors); } else { $dump .= 'filtered'; } } elseif (self::$renderedObjects->contains($object) && !$plainText) { $dump = '' . $dump . 'see above'; } elseif ($level >= self::$maxDepth && !$object instanceof \DateTimeInterface) { if ($plainText) { $dump .= ' ' . self::ansiEscapeWrap('max depth', '47;30', $ansiColors); } else { $dump .= 'max depth'; } } elseif ($level > 1 && !$object instanceof \DateTimeInterface && !$plainText) { if (($object instanceof \Countable && empty($object)) || empty($classReflection->getProperties())) { $dump = '' . $dump . ''; } else { $dump = '' . $dump . ''; } } if ($object instanceof \Countable) { $objectCount = count($object); $dump .= $objectCount > 0 ? ' (' . $objectCount . ' items)' : ' (empty)'; } if ($object instanceof \DateTimeInterface) { $dump .= ' (' . $object->format(\DateTimeInterface::RFC3339) . ', ' . $object->getTimestamp() . ')'; } if ($object instanceof DomainObjectInterface && !$object->_isNew()) { $dump .= ' (uid=' . $object->getUid() . ', pid=' . $object->getPid() . ')'; } return $dump; } /** * @param object $object * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string The rendered body content of the Object(Storage) */ protected static function renderContent(object $object, int $level, bool $plainText, bool $ansiColors): string { $dump = ''; if ($object instanceof \Iterator || $object instanceof \ArrayObject) { $dump .= self::renderCollection($object, $level, $plainText, $ansiColors); } else { self::$renderedObjects->attach($object); if (!$plainText) { $dump .= ''; } if ($object instanceof \Closure) { $dump .= PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level) . ($plainText ? '' : '') . self::ansiEscapeWrap('function (', '33', $ansiColors) . ($plainText ? '' : ''); $reflectionFunction = new \ReflectionFunction($object); $params = []; foreach ($reflectionFunction->getParameters() as $parameter) { $parameterDump = ''; if ($parameter->isArray()) { if ($plainText) { $parameterDump .= self::ansiEscapeWrap('array ', '36', $ansiColors); } else { $parameterDump .= 'array '; } } elseif ($parameter->getClass() instanceof \ReflectionClass) { if ($plainText) { $parameterDump .= self::ansiEscapeWrap($parameter->getClass()->name . ' ', '36', $ansiColors); } else { $parameterDump .= '' . htmlspecialchars($parameter->getClass()->name, ENT_COMPAT) . ''; } } if ($parameter->isPassedByReference()) { $parameterDump .= '&'; } if ($parameter->isVariadic()) { $parameterDump .= '...'; } if ($plainText) { $parameterDump .= self::ansiEscapeWrap('$' . $parameter->name, '37', $ansiColors); } else { $parameterDump .= '' . htmlspecialchars('$' . $parameter->name, ENT_COMPAT) . ''; } if ($parameter->isDefaultValueAvailable()) { $parameterDump .= ' = '; if ($plainText) { $parameterDump .= self::ansiEscapeWrap(var_export($parameter->getDefaultValue(), true), '33', $ansiColors); } else { $parameterDump .= '' . htmlspecialchars(var_export($parameter->getDefaultValue(), true), ENT_COMPAT) . ''; } } $params[] = $parameterDump; } $dump .= implode(', ', $params); if ($plainText) { $dump .= self::ansiEscapeWrap(') {' . PHP_EOL, '33', $ansiColors); } else { $dump .= ') {' . PHP_EOL . ''; } $lines = (array)file((string)$reflectionFunction->getFileName()); for ($l = (int)$reflectionFunction->getStartLine(); $l < (int)$reflectionFunction->getEndLine() - 1; ++$l) { $line = (string)($lines[$l] ?? ''); $dump .= $plainText ? $line : htmlspecialchars($line, ENT_COMPAT); } $dump .= str_repeat(self::PLAINTEXT_INDENT, $level); if ($plainText) { $dump .= self::ansiEscapeWrap('}' . PHP_EOL, '33', $ansiColors); } else { $dump .= '}'; } } else { if (get_class($object) === \stdClass::class) { $objReflection = new \ReflectionObject($object); $properties = $objReflection->getProperties(); $methods = []; } else { $classReflection = new \ReflectionClass(get_class($object)); $properties = $classReflection->getProperties(); $methods = $classReflection->getMethods(); } foreach ($properties as $property) { if (self::isBlacklisted($property) || $property->isPublic() !== true) { continue; } $dump .= PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level); if ($plainText) { $dump .= self::ansiEscapeWrap($property->getName(), '37', $ansiColors); } else { $dump .= '' . htmlspecialchars($property->getName(), ENT_COMPAT) . ''; } $dump .= ' => '; $property->setAccessible(true); $dump .= self::renderDump($property->getValue($object), $level, $plainText, $ansiColors); } foreach ($methods as $method) { if (self::exposeMethod($method) === false) { continue; } $dump .= PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level); if ($plainText) { $dump .= self::ansiEscapeWrap(self::shortenMethodName($method->getName()), '37', $ansiColors); } else { $dump .= '' . htmlspecialchars(self::shortenMethodName($method->getName()), ENT_COMPAT) . ''; } $dump .= ' => '; $dump .= self::renderDump($method->invoke($object), $level, $plainText, $ansiColors); } } } return $dump; } /** * @param iterable $collection * @param int $level * @param bool $plainText * @param bool $ansiColors * @return string */ protected static function renderCollection(iterable $collection, int $level, bool $plainText, bool $ansiColors): string { $dump = ''; foreach ($collection as $key => $value) { $key = (string)$key; $dump .= PHP_EOL . str_repeat(self::PLAINTEXT_INDENT, $level); if ($plainText) { $dump .= self::ansiEscapeWrap($key, '37', $ansiColors); } else { $dump .= '' . htmlspecialchars($key, ENT_COMPAT) . ''; } $dump .= ' => '; $dump .= self::renderDump($value, $level, $plainText, $ansiColors); } if ($collection instanceof \Iterator && !$collection instanceof \Generator) { $collection->rewind(); } return $dump; } /** * Wrap a string with the ANSI escape sequence for colorful output * * @param string $string The string to wrap * @param string $ansiColors The ansi color sequence (e.g. "1;37") * @param bool $enable If FALSE, the raw string will be returned * @return string The wrapped or raw string */ protected static function ansiEscapeWrap(string $string, string $ansiColors, bool $enable = true): string { if ($enable) { return '[' . $ansiColors . 'm' . $string . ''; } return $string; } /** * A var_dump function optimized for Extbase's object structures * * @param mixed $variable The value to dump * @param string $title optional custom title for the debug output * @param int $maxDepth Sets the max recursion depth of the dump. De- or increase the number according to your needs and memory limit. * @param bool $plainText If TRUE, the dump is in plain text, if FALSE the debug output is in HTML format. * @param bool $ansiColors If TRUE (default), ANSI color codes is added to the output, if FALSE the debug output not colored. * @param bool $return if TRUE, the dump is returned for custom post-processing (e.g. embed in custom HTML). If FALSE (default), the dump is directly displayed. * @param array $blacklistedClassNames An array of class names (RegEx) to be filtered. Default is an array of some common class names. * @param array $blacklistedPropertyNames An array of property names and/or array keys (RegEx) to be filtered. Default is an array of some common property names. * @return string if $return is TRUE, the dump is returned. By default, the dump is directly displayed, and nothing is returned. */ public static function var_dump( $variable, string $title = null, int $maxDepth = 8, bool $plainText = false, bool $ansiColors = true, bool $return = false, array $blacklistedClassNames = null, array $blacklistedPropertyNames = null ): string { self::$maxDepth = $maxDepth; if ($title === null) { $title = 'Extbase Variable Dump'; } $ansiColors = $plainText && $ansiColors; if ($ansiColors === true) { $title = '' . $title . ''; } $backupBlacklistedClassNames = self::$blacklistedClassNames; if (is_array($blacklistedClassNames)) { self::$blacklistedClassNames = $blacklistedClassNames; } $backupBlacklistedPropertyNames = self::$blacklistedPropertyNames; if (is_array($blacklistedPropertyNames)) { self::$blacklistedPropertyNames = $blacklistedPropertyNames; } self::clearState(); $css = ''; if (!$plainText && self::$stylesheetEchoed === false) { $css = ' '; self::$stylesheetEchoed = true; } if ($plainText) { $output = $title . PHP_EOL . self::renderDump($variable, 0, true, $ansiColors) . PHP_EOL . PHP_EOL; } else { $output = '
' . htmlspecialchars($title, ENT_COMPAT) . '
' . self::renderDump($variable, 0, false, false) . '
'; } self::$blacklistedClassNames = $backupBlacklistedClassNames; self::$blacklistedPropertyNames = $backupBlacklistedPropertyNames; if ($return === true) { return $css . $output; } echo $css . $output; return ''; } protected static function exposeMethod(ReflectionMethod $method): bool { $methodName = $method->getName(); $allowedPrefixes = ['get', 'has', 'is']; $allowedByName = false; foreach ($allowedPrefixes as $allowedPrefix) { if (StringUtility::beginsWith($methodName, $allowedPrefix)) { $allowedByName = true; break; } } return $allowedByName && $method->isPublic() && count($method->getParameters()) === 0 ; } protected static function shortenMethodName(string $methodName): string { $allowedPrefixes = ['get', 'has', 'is']; foreach ($allowedPrefixes as $allowedPrefix) { if (StringUtility::beginsWith($methodName, $allowedPrefix)) { return lcfirst(substr($methodName, strlen($allowedPrefix))); } } return $methodName; } }