├── .gitignore ├── Classes ├── TransparentViewHelpers │ ├── Debug │ │ └── BreakViewHelper.php │ └── DebugViewHelper.php └── ActiveViewHelpers │ ├── Debug │ └── BreakViewHelper.php │ └── DebugViewHelper.php ├── composer.json ├── ext_localconf.php ├── ext_emconf.php └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | /.idea 2 | -------------------------------------------------------------------------------- /Classes/TransparentViewHelpers/Debug/BreakViewHelper.php: -------------------------------------------------------------------------------- 1 | registerBreakAliasArguments(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /Classes/ActiveViewHelpers/Debug/BreakViewHelper.php: -------------------------------------------------------------------------------- 1 | registerBreakAliasArguments(); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "namelesscoder/typo3-cms-fluid-debug", 3 | "type": "typo3-cms-extension", 4 | "license": "GPL-2.0-or-later", 5 | "preferred-install": "dist", 6 | "require": { 7 | "typo3/cms-fluid": "^8|^9" 8 | }, 9 | "replace": { 10 | "fluid_debug": "self.version", 11 | "typo3-ter/fluid_debug": "self.version" 12 | }, 13 | "autoload": { 14 | "psr-4": { 15 | "NamelessCoder\\CmsFluidDebug\\": "Classes/" 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /ext_localconf.php: -------------------------------------------------------------------------------- 1 | isTesting(): 8 | case $context->isDevelopment(): 9 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['f'][] = 'NamelessCoder\\CmsFluidDebug\\ActiveViewHelpers'; 10 | break; 11 | default: 12 | case $context->isProduction(): 13 | $GLOBALS['TYPO3_CONF_VARS']['SYS']['fluid']['namespaces']['f'][] = 'NamelessCoder\\CmsFluidDebug\\TransparentViewHelpers'; 14 | break; 15 | } 16 | })(); 17 | -------------------------------------------------------------------------------- /ext_emconf.php: -------------------------------------------------------------------------------- 1 | 'Fluid Debugging Utilities', 4 | 'description' => 'Utilities to assist with debugging in Fluid templates', 5 | 'state' => 'beta', 6 | 'uploadfolder' => 0, 7 | 'createDirs' => '', 8 | 'clearCacheOnLoad' => 1, 9 | 'author' => 'Claus Due', 10 | 'author_email' => 'claus@namelesscoder.net', 11 | 'author_company' => '', 12 | 'version' => '1.1.1', 13 | 'constraints' => [ 14 | 'depends' => [ 15 | 'php' => '7.0.0-7.3.99', 16 | 'typo3' => '8.7.10-9.5.99', 17 | ], 18 | 'conflicts' => [], 19 | 'suggests' => [], 20 | ], 21 | ]; 22 | -------------------------------------------------------------------------------- /Classes/TransparentViewHelpers/DebugViewHelper.php: -------------------------------------------------------------------------------- 1 | arguments, $this->buildRenderChildrenClosure(), $this->renderingContext); 17 | } 18 | 19 | public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) 20 | { 21 | $value = $renderChildrenClosure(); 22 | if ($arguments['pass']) { 23 | return $value; 24 | } 25 | return null; 26 | } 27 | 28 | public function compile( 29 | $argumentsName, 30 | $closureName, 31 | &$initializationPhpCode, 32 | ViewHelperNode $node, 33 | TemplateCompiler $compiler 34 | ) { 35 | return sprintf( 36 | '%s::renderStatic(%s, %s, $renderingContext)', 37 | get_class($this), 38 | $argumentsName, 39 | $closureName 40 | ); 41 | } 42 | 43 | protected static function breakPoint(bool $break, RenderingContextInterface $renderingContext, string $event, array $pointers, $value = null) 44 | { 45 | // void 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TYPO3 CMS Fluid Debugging Assistant 2 | =================================== 3 | 4 | This package contains overrides and additions to the standard Fluid debugging utility in TYPO3 CMS. It replaces the 5 | normal debugging ViewHelper with an improved version that solves several known problems. In addition to dumping 6 | variables it also allows you to insert xdebug break points in Fluid templates via a ViewHelper. 7 | 8 | 9 | The problems it solves 10 | ---------------------- 11 | 12 | 1. The normal Fluid ViewHelper generates output either where it is used, or at the very top of the document before the 13 | doctype declaration. Either method has tremendous risk to destroy CSS rendering or be unreadable if used in a clipped 14 | or very tiny component. 15 | 2. When you debug properties of an object in Fluid, the Extbase property accessor is used. This means two things: first, 16 | the data you look at is *not* the actual data, it is the public gettable data which has properties associated with 17 | it. And two, it will only show you data that has a property, i.e. no virtual properties which only have getters. 18 | 3. Debugging a Fluid template means accessing a variable or ViewHelper expression twice, because the built-in debug 19 | ViewHelper _will not return the variable it debugs_. 20 | 4. You do not want debug statements in your production code, but sometimes you would like to leave them in templates and 21 | make assertions on the output they debug - but you can't, because the debug ViewHelper produces output. 22 | 23 | These problems are well known when debugging in Fluid. And this package solves all of them. 24 | 25 | 26 | Installation 27 | ------------ 28 | 29 | Install with Composer: 30 | 31 | ```sh 32 | composer require namelesscoder/typo3-cms-fluid-debug 33 | ``` 34 | 35 | And if necessary, activate the `fluid_debug` extension in Extension Manager. 36 | 37 | No configuration is needed, but the package only has functionality when the site is not in Production context. Note that 38 | it is not enough to switch the context in the TYPO3 backend if you also defined `TYPO3_APPLICATION_CONTEXT` in your 39 | virtual host - the one defined in your virtual host takes priority, so make sure it is set to Development or Testing. 40 | 41 | 42 | The strategy 43 | ------------ 44 | 45 | Instead of outputting content, the overridden debug ViewHelper instead delegates the output of variables to the JS 46 | console in your browser, by using PageRenderer to insert a call to a `console.log()` or other method, which receives a 47 | JSON object representation of the data you dump. In TYPO3 backend the ViewHelper dumps to the debugging console (which 48 | you can disable) and in CLI mode a regular `var_dump` is triggered (and xdebug break point if so configured in use). 49 | 50 | This means the ViewHelper is safe to use anywhere as it will never produce debug output inside the DOM body. 51 | 52 | An additional strategy is to allow the debugging ViewHelper to return the raw value it was asked to debug. This means 53 | you can use it as part of any inline expression to debug the value at that exact point. Consider the following example: 54 | 55 | ```xml 56 | Long expression with debug in specific point: 57 | {myVariable -> f:debug() -> f:format.striptags() -> f:debug() -> f:format.nl2br()} 58 | 59 | Usage in argument value and arrays: 60 | 61 | 62 | Debugging an array costructed in Fluid while also passing that array: 63 | 64 | ``` 65 | 66 | Because the ViewHelper simply passes the value through, it can be left in place without causing rendering problems. This 67 | is a major difference compared to the native debugging ViewHelper which cannot be used this way without causing errors. 68 | 69 | The FE/BE/CLI sensitivity also means you get the least intrusive output possible; except on CLI where the standard 70 | var_dump is used to produce markup-free dumps. 71 | 72 | 73 | Dumping strategy 74 | ---------------- 75 | 76 | Contrary to the native TYPO3 CMS debug ViewHelper, the override dumps objects based primarily on the presence of getter 77 | methods which require no arguments - as opposed to basing it on the properties of a reflected object. 78 | 79 | Why this? 80 | 81 | The answer is that while Extbase dumping (which is what the native debug ViewHelper uses) is exceptionally good at 82 | dumping domain objects, it has shortcomings when the getter method you want to access doesn't have an associated 83 | property. Most prevalent example would be dumping a `File` resource which does not reveal all methods, including some of 84 | the most important API methods that are very useful in Fluid (example: metadata properties). 85 | 86 | So by dumping based not just on properties but by the presence of getter methods, with either the `get` or `has` or `is` 87 | prefixes, this dump reveals every property *that you can use in Fluid*, rather than just those that makes sense in an 88 | Extbase persistence context. 89 | 90 | 91 | Auto-suppressed in Production 92 | ----------------------------- 93 | 94 | The package silences itself when the TYPO3 application context is set to Production. 95 | 96 | There are two main reasons for this: 97 | 98 | 1. By auto-suppressing, it means it is safe to deploy templates which contain debug statements. 99 | 2. Because debug statements output to `console.log()` or other, if you use acceptance testing with browser integration 100 | you can make assertions on variables passing through Fluid as part of your acceptance testing; variables which you 101 | don't see in the template output but are used to render it. 102 | 103 | So rather than as you normally would, remove debug statements or suppress them with `f:comment`, you can simply leave 104 | them in there - they cause no output in DOM body, they pass the debugged value through, and in Production content they 105 | are replaced with completely transparent versions of themselves. 106 | 107 | 108 | Usage details 109 | ------------- 110 | 111 | The package currently contains two ViewHelpers: 112 | 113 | * An override for `f:debug` which is semi-compatible (also uses tag content to read dump value) 114 | * A specialised alias with reduced arguments, `f:debug.break`, which instead of outputting to console will create a 115 | dynamic breakpoint for xdebug so you can inspect the state in your IDE. 116 | 117 | The `f:debug` override has the following arguments: 118 | 119 | * `value` which can be specified as argument value or is otherwise taken from tag content / child node 120 | * `title` which is a string you can use to identify the debug output - if not specified, the current template source 121 | code chunk and line/character number is shown if the template is not compiled (flush system cache to cause compiling). 122 | * `level` which is a string containing `log`, `warn` etc. - method name on the `console` object to be called. 123 | * `maxDepth` which is in integer, maximum number of levels to allow when traversing arrays/objects (note that infinite 124 | recursion is automatically prevented). 125 | * `silent` which is a boolean you can set to `1` if you want to suppress the output in console altogether. 126 | * `pass` which is a boolean you can set to `0` to not pass the dumped variable, useful if you for example have a 127 | separate `{object}` that would otherwise cause string conversion problems. 128 | * `break` which is a boolean you can set to `1` to cause an xdebug break point. Only happens if xdebug is installed. 129 | * `compile` which is a boolean you can set to `0` to disable compiling, letting you debug and break on the behavior 130 | the template has during parsing and compiling without having to flush caches repeatedly. 131 | 132 | And the reduced alias `f:debug.break` has the following arguments: 133 | 134 | * `value` exactly like above 135 | * `pass` exactly like above 136 | * `silent` like above, but with default set to `1` to suppress output 137 | * `break` like above, but with default set to `1` to always break 138 | * `compile` exactly like above 139 | 140 | In other words, `f:debug` is the main utility and `f:debug.break` is a customised alias which uses different default 141 | argument values, making it an ideal "insert breakpoint here" ViewHelper. 142 | 143 | 144 | Note about using breakpoints 145 | ---------------------------- 146 | 147 | When you use break points with `f:debug.break` you don't just get the option of inspecting the variable you dump when 148 | the ViewHelper gets rendered - when break points are enabled, they trigger on the following events: 149 | 150 | * When the ViewHelper is initialized (when template is parsed, when ViewHelperNode is built in syntax tree) 151 | * When the ViewHelper is compiled to a PHP class (when you can dump for example the compiler's state) 152 | * When the ViewHelper is rendered (when you can inspect the actual value you want to dump, as well as other variables) 153 | 154 | A handful of key variables are extracted for easier reading in your IDE. These include the template source chunk, the 155 | line/character number, all current template variables, whether template is compiled, and so on. 156 | 157 | Note that you can also set `break="1"` on `f:debug` to cause an xdebug break point from that ViewHelper as well. 158 | 159 | **Important! Not all objects are possible to debug - when `f:debug` fails, `f:debug.break` and xdebug always works!** 160 | -------------------------------------------------------------------------------- /Classes/ActiveViewHelpers/DebugViewHelper.php: -------------------------------------------------------------------------------- 1 | [ 32 | 'getContents' => true 33 | ], 34 | 'getFileContents' => true 35 | ]; 36 | 37 | public function initializeArguments() 38 | { 39 | $this->registerArgument('value', 'mixed', 'Value to be dumped'); 40 | $this->registerArgument('title', 'string', 'Optional title for console output line', false); 41 | $this->registerArgument('level', 'string', 'Level - or method name - to use on "console" object, e.g. "log" to call "console.log()"', false, 'log'); 42 | $this->registerArgument('maxDepth', 'integer', 'Maximum depth for recursion', false, 8); 43 | $this->registerArgument('silent', 'boolean', 'If true, no output is generated at all. Combines well with "break" to debug in IDE', false, false); 44 | $this->registerArgument('pass', 'boolean', 'If true, passes through the child content or value of "value" argument. Defaults to "true"', false, true); 45 | $this->registerArgument('break', 'boolean', 'If true, and if xdebug is installed, creates a dynamic breakpoint in parsing, rendering and compiling stages of this ViewHelper', false, false); 46 | $this->registerArgument('compile', 'boolean', 'If switched to "false" this the ViewHelper will no longer allow the template to be compiled, thus always showing line number and triggering parse events', false, true); 47 | } 48 | 49 | protected function registerBreakAliasArguments() 50 | { 51 | $this->registerArgument('value', 'mixed', 'Value to be dumped'); 52 | $this->registerArgument('pass', 'boolean', 'If true, passes through the child content or value of "value" argument. Defaults to "true"', false, true); 53 | $this->registerArgument('silent', 'boolean', 'If true, no output is generated at all. Combines well with "break" to debug in IDE', false, true); 54 | $this->registerArgument('break', 'boolean', 'If true, and if xdebug is installed, creates a dynamic breakpoint in parsing, rendering and compiling stages of this ViewHelper', false, true); 55 | $this->registerArgument('compile', 'boolean', 'If switched to "false" this the ViewHelper will no longer allow the template to be compiled, thus always showing line number and triggering parse events', false, true); 56 | } 57 | 58 | public function setRenderingContext(RenderingContextInterface $renderingContext) 59 | { 60 | if (empty($this->templateParsingPointers)) { 61 | $this->templateParsingPointers = $renderingContext->getTemplateParser()->getCurrentParsingPointers(); 62 | } 63 | static::breakPoint($this->arguments['break'] ?? $this->prepareArguments()['break']->getDefaultValue(), $renderingContext, 'parse', $this->templateParsingPointers); 64 | parent::setRenderingContext($renderingContext); 65 | } 66 | 67 | public function compile( 68 | $argumentsName, 69 | $closureName, 70 | &$initializationPhpCode, 71 | ViewHelperNode $node, 72 | TemplateCompiler $compiler 73 | ) { 74 | if (!$this->arguments['compile']) { 75 | $compiler->disable(); 76 | } 77 | $renderingContext = $compiler->getRenderingContext(); 78 | $arguments = $node->getArguments(); 79 | foreach ($this->prepareArguments() as $name => $argumentDefinition) { 80 | if (isset($arguments[$name])) { 81 | $arguments[$name] = $arguments[$name]->evaluate($renderingContext); 82 | } else { 83 | $arguments[$name] = $argumentDefinition->getDefaultValue(); 84 | } 85 | } 86 | static::breakPoint($arguments['break'], $renderingContext, 'compile', $this->templateParsingPointers); 87 | return parent::compile($argumentsName, $closureName, $initializationPhpCode, $node, $compiler); 88 | } 89 | 90 | public function render() 91 | { 92 | $this->renderingContext->getViewHelperVariableContainer()->addOrUpdate(static::class, 'pointers', $this->templateParsingPointers); 93 | return static::renderStatic($this->arguments, $this->buildRenderChildrenClosure(), $this->renderingContext); 94 | } 95 | 96 | public static function renderStatic(array $arguments, \Closure $renderChildrenClosure, RenderingContextInterface $renderingContext) 97 | { 98 | $pointers = $renderingContext->getViewHelperVariableContainer()->get(static::class, 'pointers'); 99 | $value = $renderChildrenClosure(); 100 | static::breakPoint($arguments['break'], $renderingContext, 'render', $pointers ?? [], $value); 101 | if (!$arguments['silent']) { 102 | $converted = []; 103 | if (!empty($arguments['title'])) { 104 | $title = $arguments['title']; 105 | } elseif (!empty($pointers)) { 106 | $title = sprintf( 107 | 'Line %d, character %d: %s', 108 | $pointers[0], 109 | $pointers[1], 110 | trim($pointers[2]) 111 | ); 112 | } 113 | if (TYPO3_MODE === 'FE') { 114 | $representation = static::convertAnything($value, (int) $arguments['maxDepth'], $converted); 115 | $json = json_encode($representation, JSON_HEX_QUOT); 116 | $pageRenderer = GeneralUtility::makeInstance(PageRenderer::class); 117 | $pageRenderer->addJsFooterInlineCode( 118 | 'Dump ' . sha1(microtime()), 119 | sprintf( 120 | '%sconsole.%s(%s);', 121 | !empty($title) ? 'console.info(\'' . addslashes($title) . '\');' . PHP_EOL : '', 122 | $arguments['level'], 123 | $json 124 | ) 125 | ); 126 | } elseif (php_sapi_name() !== "cli") { 127 | DebugUtility::debug(is_array($value) ? DebugUtility::viewArray($value) : $representation, $title, 'Fluid'); 128 | } else { 129 | var_dump($value); 130 | } 131 | } 132 | if ($arguments['pass']) { 133 | return $value; 134 | } 135 | return null; 136 | } 137 | 138 | protected static function convertAnything($anything, int $depth, array &$convertedIds = []) 139 | { 140 | if ($depth < 0) { 141 | return ['MAX DEPTH REACHED' => 'Maximum depth reached']; 142 | } 143 | if (is_array($anything)) { 144 | $representation = static::convertArray($anything, $depth, $convertedIds); 145 | } elseif ($anything instanceof \Closure) { 146 | $representation = '(closure)'; 147 | } elseif (!is_object($anything)) { 148 | $representation = $anything; 149 | } else { 150 | $representation = static::convertObject($anything, $depth, $convertedIds); 151 | } 152 | return $representation; 153 | } 154 | 155 | protected static function convertArray(array $array, int $depth, array &$convertedIds = []): array 156 | { 157 | if ($depth < 0) { 158 | return ['MAX DEPTH REACHED' => 'Maximum depth reached']; 159 | } 160 | // Arrays are recursively travelled to convert any objects along the way 161 | $converted = []; 162 | foreach ($array as $key => $value) { 163 | $converted[$key] = static::convertAnything($value, $depth - 1, $convertedIds); 164 | } 165 | return $converted; 166 | } 167 | 168 | protected static function convertObject($object, int $depth, array &$convertedIds = []): array 169 | { 170 | if ($depth < 0) { 171 | return ['MAX DEPTH REACHED' => 'Maximum depth reached']; 172 | } 173 | if ($object instanceof \ArrayAccess) { 174 | return static::convertArray((array) $object, $depth - 1, $convertedIds); 175 | } elseif ($object instanceof \Iterator) { 176 | return static::convertArray(iterator_to_array($object), $depth - 1, $convertedIds); 177 | } 178 | // Objects are dumped as Extbase usually would. This gives us a starting point where child properties 179 | // are still objects. We recurse later on to solve those. 180 | if ($object instanceof DomainObjectInterface) { 181 | $objectId = get_class($object) . ':' . $object->getUid(); 182 | } elseif ($object instanceof ResourceInterface) { 183 | $objectId = $object->getHashedIdentifier(); 184 | } else { 185 | $objectId = spl_object_hash($object); 186 | } 187 | if (in_array($objectId, $convertedIds)) { 188 | return ['RECURSION' => 'Recursion to object ' . $objectId . ' which was already dumped above']; 189 | } 190 | $convertedIds[] = $objectId; 191 | $gettables = []; 192 | $objectReflection = new \ReflectionClass($object); 193 | foreach ($objectReflection->getMethods(\ReflectionMethod::IS_PUBLIC) as $publicMethod) { 194 | if ($publicMethod->getNumberOfRequiredParameters() > 0) { 195 | continue; 196 | } 197 | $methodName = $publicMethod->getName(); 198 | if (isset(static::$blacklistedMethods[$methodName]) || isset(static::$blacklistedMethods[get_class($object)][$methodName])) { 199 | $gettables[$methodName] = '(method blacklisted, not called)'; 200 | continue; 201 | } 202 | foreach (static::$gettableMethodPrefixes as $prefix) { 203 | if (strpos($methodName, $prefix) === 0) { 204 | $virtualPropertyName = lcfirst(substr($methodName, strlen($prefix))); 205 | if (!isset($gettables[$virtualPropertyName])) { 206 | $gettables[$virtualPropertyName] = $publicMethod->invoke($object); 207 | } 208 | } 209 | } 210 | } 211 | 212 | return static::convertArray($gettables, $depth - 1, $convertedIds); 213 | } 214 | 215 | protected static function breakPoint(bool $break, RenderingContextInterface $renderingContext, string $event, array $pointers, $value = null) 216 | { 217 | // Special break wrapper which extracts some key variables before breaking, allowing direct inspection of those. 218 | // These variables appear to be unused, but are defined for reading in the debugging IDE. 219 | if ($break && function_exists('xdebug_break')) { 220 | $variables = $renderingContext->getVariableProvider()->getAll(); 221 | $compiled = false; 222 | if (!empty($pointers)) { 223 | list ($line, $character, $templateCode) = $pointers; 224 | } else { 225 | $compiled = true; 226 | } 227 | xdebug_break(); 228 | } 229 | } 230 | } 231 | --------------------------------------------------------------------------------