TwigStan is a static analyzer for Twig templates powered by PHPStan.
Caution
This is very experimental
TwigStan converts Twig templates into simplified PHP code, allowing PHPStan to analyze them. It then reports any errors back to the original template and line number.
The process consists of the following steps:
The TwigCompiler loads the template and converts it into a Twig AST (Abstract Syntax Tree). The AST is optimized by running several Twig NodeVisitors. The AST is then compiled into PHP using Twig's default compiler. The compiled PHP code is loaded and converted into a PHP AST. On the PHP AST, we run various PHP NodeVisitors. The goal is no longer to render the template but to analyze it. This means we can remove elements that are not relevant to us. The PHP AST is then dumped back into PHP code and saved to disk as a compilation result.
In the next steps, we will use these PHP files.
The next step is to flatten the Twig templates. Templates can extend other templates. The child template can choose to override blocks or not, and the parent template can also extend another template. Variables set in a parent template should be available in the child template.
The TwigFlattener processes all the compilation results. It reads the Twig metadata to identify the parent(s) and defined blocks. It takes the logic in the parent template (set variables, etc.) from the doDisplay
method and copies it into the child template's doDisplay
block.
The same is done for the block hierarchy. It understands which blocks are overridden. The child template will eventually have all blocks defined.
While flattening, the original filename and line numbers are preserved. This is important because later on, we want to trace errors back to their original location.
After the flattening process is finished, the PHP AST is again dumped to disk as a flattening result.
Now that we have a flat template, we don't know anything about the context the template receives or the modified context inside the template.
We use PHPStan to run the BlockContextCollector. This collector gathers the context before rendering every block or parent block call.
While running PHPStan, it's also a good time to search for places that render the template.
- ContextFromReturnedArrayWithTemplateAttributeCollector and ContextFromControllerRenderMethodCallCollector search for controllers that render a Twig template.
- ContextFromTwigRenderMethodCallCollector search for
Twig\Environment::render
calls.
Now that we know the context passed to a template, and the context before every block call in the template, we can inject this knowledge as PHPDocs into the flattened template.
Every template is now flattened and has defined context types.
We ask PHPStan to run the analysis on these files.
The AnalysisResultFromJsonReader processes the results from PHPStan. For every error in the flattened PHP code, it tries to find the original Twig file and line number. It filters out a few errors that are false positives. It also collapses errors that are already reported higher in the hierarchy. When an error is reported in a parent template, it should only be reported once, instead of every time it's flattened in a child template.
$ composer require --dev twigstan/twigstan:dev-main
Then run TwigStan and it will explain what to do next:
$ vendor/bin/twigstan
TwigStan supports the new {% types %}
tag that will be introduced in Twig 3.13.
If your types are not automatially resolved from where they are rendered, you manually type each and every variable like t
{% types { variableName: 'type' } %}
The type can be a valid PHPDoc expression. For example:
{% types { name: 'string|null' } %}
Next to using multiple {% types %}
tags, you can also define multiple types in a single line:
{% types {
name: 'string',
users: 'array<int, App\\User>',
} %}
If you want to indicate that a variable is optional, you can do it as follows:
{% types {
isEnabled?: 'bool',
} %}
Note
Starting from Twig version 4 you no longer have to escape backslashes in fully qualified class names.
You can dump the type of a variable by using:
{% dump_type variableName %}
When running TwigStan it will then output the type of the variable at that point.
For example:
{% types { authenticated: 'bool' } %}
This will print `bool`:
{% dump_type authenticated %}
{% if authenticated %}
This will print `true`:
{% dump_type authenticated %}
{% else %}
This will print `false`:
{% dump_type authenticated %}
{% endif %}
If you want to dump the types for the whole context (everything that's available), you can do:
{% dump_type %}
- Macros are not yet supported
- Horizontal reuse is not yet supported
- Dynamic inheritance is not supported
- Conditional inheritence is not yet supported
- Not all render points are detected (currently only supports Symfony controllers)
- Performance (PHPStan's cache misses all the time, should be fixed to speed things up significantly)
- Baseline is missing
- PHPStan extension installer is not yet supported
- Ondřej Mirtes for creating PHPStan and providing guidance to create TwigStan.
- Tomas Votruba for creating and blogging about Twig PHPStan Compiler; and for creating Bladestan.
- Jan Matošík for creating a phpstan-twig proof of concept.
- Jeroen Versteeg for creating TwigQI and discussing ideas.