In the previous post, we looked at how to turn Messy TWIG PHP to something useful in general.
Today we'll look at how to change TWIG helper functions to their original object form.
Have you joined our "STAMP" series just in this post? We're trying to convert the TWIG file. In the TWIG template, we use the single variable meal
, an App\Meal
type object.
{{ meal.title }}
What do we want as an output? A PHP code that PHPStan can understand and analyze. All steps must happen automatically, without any manual effort, on a typical PHPStan run:
vendor/bin/phpstan
In the previous post, we managed to narrow the TWIG mess to a single proper method, doDisplay()
:
use Twig\Template;
/* templates/meal.twig */
class __TwigTemplate_8a9d1381e8329967... extends Template
{
protected function doDisplay(array $context, array $blocks = [])
{
// line 1
echo twig_escape_filter(
$this->env,
twig_get_attribute(
$this->env,
($context["meal"] ?? null),
"title",
"any",
false,
false,
false,
1
),
"html",
null,
true
);
}
}
Would it be enough to run PHPStan on an analyze App\Meal
object? There is no single mention about App\Meal
type, not even an object - we can see: functions, $this->env
property, and some $context
array.
This looks like a desperate situation in a new job when they tell you, "we have strict clean code standards".
Time to quit? Not so fast.
Let's get back to basics, our TWIG template:
{{ meal.title }}
Now forget everything we know about TWIG PHP and complex php-parser
.
What are the known axioms we can work with?
meal
is an object of App\Meal
type.title
is TWIG magic syntax
getTitle()
method call->title
property fetch in case of public propertyApp\Meal
class looks likenamespace App;
final class Meal
{
public function getTitle(): string
{
return 'Potato Salad and Schnitzel';
}
}
Knowing only this, how could we write this code in pure PHP?
$meal->title
Probably not, there is no public property $title
, so it would fail on undefined pubic property.
$meal->getTitle()
Better, but what does this method do? void
. It does not show anything. How can we improve it?
echo $meal->getTitle()
Wow, almost like the original!
But seeing only this line of code, the $meal
could be just an empty string for PHPStan. What about that?
/** @var \App\Meal $meal */
echo $meal->getTitle()
Great! It would be better to define the type in parameter type of some function, but we don't see any function there. The important one is that PHPStan now sees an object of a specific type and method call of a specific name on it.
twig_get_attribute()
It's important to know what one wants. Now we can modify the original echo
statement in doDisplay()
method. First, we can drop the twig_escape_filter()
function call, which is TWIG internal, unrelated to our template code.
// line 1
-echo twig_escape_filter(
- $this->env,
- twig_get_attribute(
+echo twig_get_attribute(
$this->env,
($context["meal"] ?? null),
"title",
"any",
false,
false,
false,
1
- ),
+ );
- "html",
- null,
- true
-);
We now have just the twig_get_attribute()
function call:
// line 1
echo twig_get_attribute(
$this->env,
($context["meal"] ?? null),
"title",
"any",
false,
false,
false,
1
);
Let's take it further. What does twig_get_attribute()
probably do?
meal
is defined.getTitle()
or fetch title
property on it.Now, we could drop always-present arguments unrelated to our original TWIG template:
// line 1
echo twig_get_attribute(
- $this->env,
($context["meal"] ?? null),
"title",
- "any",
- false,
- false,
- false,
- 1
);
What do we already know about our new code snippet?
// line 1
echo twig_get_attribute(
($context["meal"] ?? null),
"title",
);
$meal
variable always exists$meal
is an object of App\Meal
typegetTitle()
is the existing public methodLet's apply this knowledge:
// line 1
echo twig_get_attribute(
- ($context["meal"] ?? null),
+ /** @var \App\Meal $meal */
+ $meal,
- "title",
+ "getTitle",
);
↓
What do we know about this code snippet?
// line 1
echo twig_get_attribute(
/** @var \App\Meal $meal */
$meal,
"getTitle",
);
twig_get_attribute
is not needed, as its internal TWIG function"getTitle"
will be called directly on $meal
// line 1
-echo twig_get_attribute(
- /** @var \App\Meal $meal */
- $meal,
- "getTitle",
- );
+/** @var \App\Meal $meal */
+echo $meal->getTitle();
Great! It's a method call we've been waiting for:
/** @var \App\Meal $meal */
echo $meal->getTitle();
Now PHPStan can check the file and tell us if the getTitle
method exists on App\Meal
or not.
PHPStan now knows that $meal->getTitle()
returns a string
, and can report type errors:
10 * {{ meal.title }}
You've probably noticed we kept // line 1
comment in every snippet. What is it for?
Every proper PHPStan error has:
Here we analyze a PHP file that is much bigger than the original TWIG file. Our 1 line in TWIG template was compiled to ~80 lines of PHP.
So why is the // line 1
important? The metadata from the native TWIG compiler tells us that code under this comment belongs to line X
in the TWIG template.
How can we use line mapping? Let's say we change the method name in our template:
-{{ meal.title }}
+{{ meal.name }}
PHPStan then reports non-existing getName()
method error. Without mapping, it would report line that does not even exist:
Error in /templates/meal.twig file on line 54:
With mapping, we get correct TWIG line:
Error in /templates/meal.twig file on line 1:
All right, we have removed clutter from the compiled PHP template file and used NodeVisitor
from php-parser
to convert magical TWIG functions to explicit PHP code.
We're now ready use PHPStan for analysis:
vendor/bin/phpstan analyze /temp/twig/__TwigTemplate_8a9d1381e8329967...php
Or is there a better way to run all PHPStan rules on a single file? 🤔
You'll find the answer in the next post.
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!