├── .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 |
--------------------------------------------------------------------------------