In this section you can read how the PHPStan Latte extension works, how to solve several issues which are reported by this extension.
Variables are collected from PHP classes (e.g. Presenters or Controls) and they have to be sent to template via one of these ways:
1) $this->template->foo = 'bar';
2) $this->template->add('foo', 'bar');
3) $this->template->setParameters(['foo' => 'bar']);
4) [$this->template->foo, $this->template->bar] = ['bar', 'baz'];
5) list($this->template->foo, $this->template->bar) = ['bar', 'baz'];
6) $this->template->render('path_to_latte.latte', ['foo' => 'bar', 'bar' => 'baz']);
7) $this->template->render('path_to_latte.latte', new SomeControlTemplateType());
It has to be done in correspondent methods like actionFoo
, renderFoo
in Presenters or render
, renderSomething
in Controls. Assigning in called methods, parent methods and / or global methods like __construct
, startup
, beforeRender
are also accepted:
class SomeControlExtendsControl
{
public function render(): void
{
$this->assignToTemplate();
}
private function assignToTemplate(): void
{
$this->template->foo = 'bar'; // $foo will be available in the Latte template because this method is called from render
}
private function neverCalledMethod(): void
{
$this->template->baz = 'bar'; // $baz will not be available in the template because this method is never called
}
}
If you use some non-standard way of assigning of variables to template, you have to create your own VariableCollector.
This error can have several reasons. The PHPStan Latte extension checks Latte templates with some context. One Latte template can be used in several components or methods of Presenter. It is important to check the context first (text after path of Latte file - rendered from, included in etc.)
-
Missing assignment
------ ------------------------------------------------------------------------------------------ Line modules/Presenters/templates/Foo/bar.latte rendered from App\Presenters\FooPresenter::bar ------ ------------------------------------------------------------------------------------------ 5 Variable $baz might not be defined. ------ ------------------------------------------------------------------------------------------
Here we have to check FooPresenter and its method(s) actionBar and/or renderBar. If there is no
$baz
sent to template, you have to add it and this error will disappear. -
Missing action/render, but variable is used in template
------ ------------------------------------------------------------------------------------- Line modules/Presenters/templates/Foo/bar.latte rendered from App\Presenters\FooPresenter ------ ------------------------------------------------------------------------------------- 5 Variable $baz might not be defined. ------ -------------------------------------------------------------------------------------
Nette is sometimes tricky how it handles Latte templates. All Latte files in
templates
directory can be visited even without Presenter's action/render method (see more details here). In the example above we can see there is no::bar
action after FooPresenter so this is exactly the case whenbar.latte
exists butactionBar
neitherrenderBar
exists, so no variables are sent to this template inbar
context.
This is a similar case as in "Variable $baz might not be defined", a single template may be used in several places, like components or Presenter methods.
You may also see the same message when including the template with {include}
with parameters, for example: {include "list.latte", show: true}
in one place, {include "list.latte", show: 'false'}
in another.
The template may contain {if $foo}
and $foo
may be set in one of those places like $this->template->foo = true
, and $this->template->foo = false
in another.
The PHPStan extension sees a specific type true
/false
instead of bool
in those.
The solution is to make the PHPStan Latte extension see bool
instead. There may be several ways how to achieve that, you may for example set the variable in a method:
function setTemplateVars(bool $foo)
{
$this->template->foo = $foo;
}
Another option is to create a method that will return the value in the presenter:
$this->template->foo = $this->isFoo();
private function isFoo(): bool
{
return false;
}
You can also use a typed property:
private bool $foo;
$this->template->foo = $this->foo;
-
Variable is conditionally assigned
public function actionBar(): void { if ($someCondition) { $this->template->baz = 'bar'; } }
Then in the Latte template:
{ifset $baz} do something {/ifset}
This extension is using phpdoc for type hinting variables in compiled templates. So even if you assign variable in some condition, variable is always assigned, because PHP has no type like
undefined
. Compiled template looks as follows:public function main() : array { extract($this->params); /** @var 'bar' $baz */ /* line 1 */ if (isset($baz)) { echo 'do something'; } }
PHPStan will report error:
------ ------------------------------------------------------------------------------------------ Line modules/Presenters/templates/Foo/bar.latte rendered from App\Presenters\FooPresenter::bar ------ ------------------------------------------------------------------------------------------ 1 Variable $baz in isset() always exists and is not nullable. ------ ------------------------------------------------------------------------------------------
The easiest way how to fix this error is to assign
$baz
in all branches of your code. Type of variable will be union.public function actionBar(): void { $this->template->baz = null; if ($someCondition) { $this->template->baz = 'bar'; } }
Now the type of
$baz
will be'bar'|null
and isset() in condition will be valid.
Components are collected from PHP classes (e.g. Presenters or Controls) when using one of these ways:
- class method createComponent()
protected function createComponentSomething(): SomeControl
{
return new SomeControl();
}
- calling method addComponent()
public function actionDefault(): void
{
$this->addComponent('something', new SomeControl());
}
- assign to
$this
:
public function actionDefault(): void
{
$this['something'] = new SomeControl();
}
Subcomponents of components are also collected, so it is possible to use this:
{control someControl}
{control someControl-header}
{control someControl-body}
First of all, check if your component is registered in Presenter / Control using one of way described above and if the name fits.
Forms are collected from PHP classes (e.g. Presenters or Controls) when they are registered as components via createComponent*
or addComponent
method if this method returns instance of Nette\Forms\Form
.
Form fields, containers, groups and fields options are also collected and can be then analysed.
Let's say you register form like this:
use Nette\Application\UI\Form;
protected function createComponentContainerForm(): Form
{
$form = new Form();
$form->setMethod('get');
$form->addCheckbox('checkbox', 'Checkbox');
$part1 = $form->addContainer('part1');
$part1->addText('text1', 'Text 1');
$part1->addSubmit('submit1', 'Submit 1');
$part2 = $form->addContainer('part2');
$part2->addText('text2', 'Text 2');
$part2->addSubmit('submit2', 'Submit 2');
return $form;
}
Then you can access all registered fields in latte this way:
{form containerForm}
{$form[part1][text1]->getHtmlId()}
{input part1-text1}
{input part1-submit1}
{input part2-text2}
{input part2-submit2}
{input checkbox:}
{input xxx} <-- this field is not registered in createComponent method therefore it is marked as non-existing
{/form}
By default, controls with dynamic names which can't be resolved as constant string or integer are not collected.
Example:
$form->addText('text1', 'Text 1'); // <- this is collected
$text2 = 'text2';
$form->addText($text2, 'Text 2'); // <- this is collected
$text3 = $this->name; // some dynamic name
$form->addText($text3, 'Text 3'); // <- this is not collected
With feature flag transformDynamicFormControlNamesToString
it is collected. Try it:
parameters:
latte:
features:
transformDynamicFormControlNamesToString: true
Form field is collected and if it is used with the same name in latte, it will be identified as TextInput. For Form above use latte:
{input text1}
{input text2}
{input $text3}