STAMP #5: How do we Know Types of Template Variables

In the previous post, we finished the conversion of TWIG to PHP, run PHPStan on temporary PHP file and got list of found errors. We've done a full circle, and PHPStan analyses our TWIG templates.

I've shared the intro post on Reddit, that sparked many exciting questions.
Today we'll answer one of them.

<iframe id="reddit-embed" src="https://www.redditmedia.com/r/PHP/comments/qbwudj/stamp_0_static_analysis_of_templates/hhcrdlr/?depth=1&showmore=false&embed=true&showmedia=false" sandbox="allow-scripts allow-same-origin allow-popups" style="border: none;" height="309" width="860" scrolling="no"></iframe>

This is a great question! We skipped this piece to complete the TWIG rendering itself first and keep your cognitive load focused. Now we have time and space to bring the answer.


Context First

First, let's define what PHP and TWIG files we work with. We render a template with 1 parameter:

use App\Meal;

return $this->render('meal.twig', [
    'meal' => new Meal()
]);

The TWIG template contains one method call:

{{ meal.title }}

Using TWIG and php-parser we compile TWIG into nice and clean PHP:

echo $meal->getTitle();

Is this clear? Now we can level up.

Where does the Variable $meal come From?

Looking at the code, the $meal looks like coming out of nowhere. Where is it defined? In TWIG, these variables are created from a $context array, passed as a parameter of the main doDisplay() method in the compiled template class.

This $context is an array made mostly of 2nd argument in $this->render() method.


In our compiled PHP, we get the $meal variable from the $context array:

public function doDisplay(array $context)
{
    $meal = $context['meal'];
    echo $meal->getTitle();
}

And that's how TWIG variables are born in compiled PHP.

What is the Type of $meal Variable?

Now PHPStan and we know the variable $meal exists and comes from some $context array. But the type is still mixed. How do we know $meal is App\Meal?


Let's back up a little bit and look into first place the type appeared - in our controller:

use App\Meal;

return $this->render('meal.twig', [
    'meal' => new Meal()
]);

Here we see that the meal string is the type of App\Meal. Could we add this type somehow to meal.twig?


Not so fast! The templates are reusable so that another developer can render them in the following way:

use App\Meal;

return $this->render('meal.twig', [
    'meal' => 'Dinner'
]);

Here the $meal variable is a type of string and the template {{ meal.title }} will crash.


So to answer the question - the type depends on the place it's rendered in PHP. The same way PHPStan does not analyze traits standalone, but only in the context of specific class.

Template Context and Variables Types

We see that rendering the same template in different places can result in different types. Also, we should not forget, the PHPStan sees only the final compiled PHP file - it has no idea about the controller the template is rendered in.


Now that we know the rules of the game, the plan is pretty straightforward:

  1. collect known variable types in the exact place in PHP
  2. compile the TWIG to PHP
  3. decorate the compiled PHP with variable types

Let's take step by step in our practical example.

1. Collect Variable Types

We collect types at the exact moment we see the $twig->render() method in PHP code.

use App\Meal;

return $this->render('meal.twig', [
    'meal' => new Meal()
]);

The 2nd argument is an array, and we can traverse that array and detect the type with PHPStan's $scope.


At the end of this step, we'll have an array with a variable name and its type:

2. Compile the TWIG to PHP

We've already covered this process in previous parts:

3. Decorate the Compiled PHP

The TWIG is now compiled to PHP, and we know the types of variables. But the PHPStan has still no idea about types:

public function doDisplay(array $context)
{
    $meal = $context['meal'];
    echo $meal->getTitle();
}

How do we help PHPStan in standard PHP code if we know about some types that are not clear from the code?

Like this:

 public function doDisplay(array $context)
 {
+    /** @var \App\Meal $meal */
     $meal = $context['meal'];
     echo $meal->getTitle();
 }

We'll automate this process again with php-parser. Now the PHPStan can analyze the code with all the known types.


Happy coding!




Do you learn from my contents or use open-souce packages like Rector every day?
Consider supporting it on GitHub Sponsors. I'd really appreciate it!