TwigStan is a static analyzer for Twig templates powered by PHPStan.
Caution
This is very experimental
Note
This project requires a significant amount of my time and effort to develop. If you find it valuable, please consider sponsoring me to support its continued growth and give it a star! ⭐️ Your support is truly appreciated! 🙏
TwigStan uses Twig to compile templates to PHP code. It then optimizes the compiled PHP code slightly, allowing PHPStan to analyze it better. It then reports any errors back to the original template and line number.
The process consists of the following steps:
We use PHPStan to search the PHP codebase for places where a Twig template is rendered. We collect the context that is passed to the template.
- ContextFromReturnedArrayWithTemplateAttributeCollector and ContextFromControllerRenderMethodCallCollector search for controllers that render a Twig template.
- ContextFromTwigRenderMethodCallCollector search for
Twig\Environment::render
calls.
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. We inject the previously collected template context as PHPDocs into the template. 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 can analyze the context for every block.
We use PHPStan to run the BlockContextCollector. This collector gathers the context before rendering every block or parent block call.
Now that we know 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.
While this analysis is running, we also collect {% include %}
usage in the templates.
Since we know the context at this point, we collect it.
If we found included templates, we will repeat the full analysis process for these templates. This means that we go back to the Compilation step.
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:^0.1
Then run TwigStan and it will explain what to do next:
vendor/bin/twigstan
Make sure that you configure your phpPaths
to point to the PHP codebase that renders the Twig templates.
This will make sure that TwigStan can collect the template context from the PHP codebase.
Use the dump_type
tag to debug if types are properly resolved.
When you have a template that is not rendered directly, but is used as a parent template, you can mark it as abstract.
The benefit of this is that it will not be analyzed as a standalone template, but only as part of the template hierarchy.
You can mark a template as abstract by adding a comment at the top of the file:
{# @twigstan-abstract #}
When you try to render an abstract template, TwigStan will report an error.
You can dump the type of a variable by using:
{% dump_type variableName %}
If you want to dump the types for the whole context (everything that's available), you can do:
{% dump_type %}
TwigStan supports the new {% types %}
tag that was 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 this:
{% types { variableName: 'type' } %}
Note
Keep in mind that TwigStan tries to resolve all these types automatically by collecting the context from the PHP codebase.
Don't blindly start adding {% types %}
tags. First investigate if TwigStan can resolve the types automatically.
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.
- MicroSymfony (PR, GitHub action output)
- 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.
- Markus Staab for improving PHPStan for specific use cases in TwigStan.