Skip to content

Commit

Permalink
Added feature: Transform dynamic form controls to "dynamic" string
Browse files Browse the repository at this point in the history
  • Loading branch information
lulco committed Jul 28, 2023
1 parent 8d2d4a8 commit 647918a
Show file tree
Hide file tree
Showing 11 changed files with 231 additions and 39 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
- Collecting options for checkbox list and radio list and report if some non-existing option is used
- Tip for error message "Latte template xxx.latte was not analysed"
- Tip for standalone templates
- Feature Transform dynamic form controls to "dynamic" string (control with name $record->id will be transformed to "$record->id") (Turn this feature with parameter `latte.features.transformDynamicFormControlNamesToString: true`)

### Fixed
- `If condition is always true` for CheckboxList::getLabelPart(), CheckboxList::getControlPart(), RadioList::getLabelPart() and RadioList::getControlPart()
Expand Down
28 changes: 28 additions & 0 deletions docs/how_it_works.md
Original file line number Diff line number Diff line change
Expand Up @@ -242,3 +242,31 @@ Then you can access all registered fields in latte this way:
{input xxx} <-- this field is not registered in createComponent method therefore it is marked as non-existing
{/form}
```

### Features
By default, controls with dynamic names which can't be resolved as constant string or integer are not collected.

Example:
```php
$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:
```neon
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:
```latte
{input text1}
{input text2}
{input $text3}
```
5 changes: 5 additions & 0 deletions extension.neon
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ parameters:
resolveAllPossiblePaths: false
reportUnanalysedTemplates: false
collectedPaths: []
features:
transformDynamicFormControlNamesToString: false
ignoreErrors:
-
messages:
Expand All @@ -55,6 +57,7 @@ parametersSchema:
resolveAllPossiblePaths: bool()
reportUnanalysedTemplates: bool()
collectedPaths: arrayOf(string())
features: arrayOf(bool(), string())
])

rules:
Expand Down Expand Up @@ -121,6 +124,7 @@ services:

# Name resolvers
- Efabrica\PHPStanLatte\Resolver\NameResolver\NameResolver
- Efabrica\PHPStanLatte\Resolver\NameResolver\FormControlNameResolver(%latte.features.transformDynamicFormControlNamesToString%)

# Method resolvers
- Efabrica\PHPStanLatte\Resolver\CallResolver\CalledClassResolver
Expand Down Expand Up @@ -156,6 +160,7 @@ services:
- addNodeVisitor(100, Efabrica\PHPStanLatte\Compiler\NodeVisitor\RemoveEmptyStringFromLabelAndControlPartNodeVisitor())
- addNodeVisitor(100, Efabrica\PHPStanLatte\Compiler\NodeVisitor\RemoveTernaryConditionWithDynamicFormFieldsNodeVisitor())
- addNodeVisitor(200, Efabrica\PHPStanLatte\Compiler\NodeVisitor\TransformFormStackToGetFormNodeVisitor())
- addNodeVisitor(200, Efabrica\PHPStanLatte\Compiler\NodeVisitor\TransformDynamicFormControlsToDynamicStringNodeVisitor(%latte.features.transformDynamicFormControlNamesToString%))

# Link processors
- Efabrica\PHPStanLatte\LinkProcessor\LinkProcessorFactory
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
<?php

declare(strict_types=1);

namespace Efabrica\PHPStanLatte\Compiler\NodeVisitor;

use Efabrica\PHPStanLatte\Compiler\NodeVisitor\Behavior\ExprTypeNodeVisitorBehavior;
use Efabrica\PHPStanLatte\Compiler\NodeVisitor\Behavior\ExprTypeNodeVisitorInterface;
use Efabrica\PHPStanLatte\Resolver\NameResolver\NameResolver;
use PhpParser\Node;
use PhpParser\Node\Expr\ArrayDimFetch;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PhpParser\Node\Scalar\String_;
use PhpParser\NodeVisitorAbstract;

/**
* changed output from:
* <code>
* $form[$dynamicInput];
* </code>
*
* to:
* <code>
* $form["$dynamicInput"];
* </code>
*
* if feature flag is turned on and type of $dynamicInput cannot be resolved as constant string / int
*/
final class TransformDynamicFormControlsToDynamicStringNodeVisitor extends NodeVisitorAbstract implements ExprTypeNodeVisitorInterface
{
use ExprTypeNodeVisitorBehavior;

private bool $featureTransformDynamicFormControlNamesToString;

private NameResolver $nameResolver;

public function __construct(
bool $featureTransformDynamicFormControlNamesToString,
NameResolver $nameResolver
) {
$this->featureTransformDynamicFormControlNamesToString = $featureTransformDynamicFormControlNamesToString;
$this->nameResolver = $nameResolver;
}

public function enterNode(Node $node): ?Node
{
if ($this->featureTransformDynamicFormControlNamesToString === false) {
return null;
}

if (!$node instanceof ArrayDimFetch) {
return null;
}

if ($this->nameResolver->resolve($node->var) !== 'form') {
return null;
}

if ($node->dim === null) {
return null;
}

$type = $this->getType($node->dim);
if ($type !== null && $type->getConstantScalarValues() !== []) {
return null;
}

if ($node->dim instanceof Variable) {
$node->dim = new String_('$' . $this->nameResolver->resolve($node->dim));
}

if ($node->dim instanceof PropertyFetch) {
$varName = $this->nameResolver->resolve($node->dim->var);
$propertyName = $this->nameResolver->resolve($node->dim->name);
if ($varName === null || $propertyName === null) {
return null;
}
$node->dim = new String_('$' . $varName . '->' . $propertyName);
}

return $node;
}
}
10 changes: 5 additions & 5 deletions src/LatteContext/Collector/FormControlCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@

use Efabrica\PHPStanLatte\LatteContext\CollectedData\Form\CollectedFormControl;
use Efabrica\PHPStanLatte\PhpDoc\LattePhpDocResolver;
use Efabrica\PHPStanLatte\Resolver\NameResolver\FormControlNameResolver;
use Efabrica\PHPStanLatte\Resolver\NameResolver\NameResolver;
use Efabrica\PHPStanLatte\Resolver\ValueResolver\ValueResolver;
use Efabrica\PHPStanLatte\Template\Form\Container;
use Efabrica\PHPStanLatte\Template\Form\Field;
use PhpParser\Node;
Expand All @@ -22,18 +22,18 @@
*/
final class FormControlCollector extends AbstractLatteContextCollector
{
private ValueResolver $valueResolver;
private FormControlNameResolver $formControlNameResolver;

private LattePhpDocResolver $lattePhpDocResolver;

public function __construct(
NameResolver $nameResolver,
ReflectionProvider $reflectionProvider,
ValueResolver $valueResolver,
FormControlNameResolver $formControlNameResolver,
LattePhpDocResolver $lattePhpDocResolver
) {
parent::__construct($nameResolver, $reflectionProvider);
$this->valueResolver = $valueResolver;
$this->formControlNameResolver = $formControlNameResolver;
$this->lattePhpDocResolver = $lattePhpDocResolver;
}

Expand Down Expand Up @@ -120,7 +120,7 @@ public function collectData(Node $node, Scope $scope): ?array
}

if ($controlNameArg !== null) {
$controlNames = $this->valueResolver->resolveStringsOrInts($controlNameArg->value, $scope);
$controlNames = $this->formControlNameResolver->resolve($controlNameArg->value, $scope);
if ($controlNames === null) {
return null;
}
Expand Down
76 changes: 76 additions & 0 deletions src/Resolver/NameResolver/FormControlNameResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
<?php

declare(strict_types=1);

namespace Efabrica\PHPStanLatte\Resolver\NameResolver;

use Efabrica\PHPStanLatte\Resolver\ValueResolver\ValueResolver;
use PhpParser\Node\Expr;
use PhpParser\Node\Expr\PropertyFetch;
use PhpParser\Node\Expr\Variable;
use PHPStan\Analyser\Scope;
use PHPStan\Type\IntegerRangeType;

final class FormControlNameResolver
{
private bool $featureTransformDynamicFormControlNamesToString;

private ValueResolver $valueResolver;

private NameResolver $nameResolver;

public function __construct(
bool $featureTransformDynamicFormControlNamesToString,
ValueResolver $valueResolver,
NameResolver $nameResolver
) {
$this->featureTransformDynamicFormControlNamesToString = $featureTransformDynamicFormControlNamesToString;
$this->valueResolver = $valueResolver;
$this->nameResolver = $nameResolver;
}

/**
* @return array<int|string>|null
*/
public function resolve(Expr $expr, Scope $scope): ?array
{
$type = $scope->getType($expr);
if ($type instanceof IntegerRangeType) {
$min = $type->getMin() !== null ? $type->getMin() : $type->getMax();
$max = $type->getMax() !== null ? $type->getMax() : $type->getMin();

if ($min === null || $max === null) {
return null;
}

$names = [];
for ($i = $min; $i <= $max; $i++) {
$names[] = $i;
}
} else {
$names = $this->valueResolver->resolve($expr, $scope);
}

if ($this->featureTransformDynamicFormControlNamesToString && $names === null) {
if ($expr instanceof Variable) {
$variableName = $this->nameResolver->resolve($expr);
$names = ['$' . $variableName];
} elseif ($expr instanceof PropertyFetch) {
$varName = $this->nameResolver->resolve($expr->var);
$propertyName = $this->nameResolver->resolve($expr->name);
if ($varName === null || $propertyName === null) {
return null;
}
$names = ['$' . $varName . '->' . $propertyName];
}
}

if ($names === null) {
return null;
}

return array_filter($names, function ($value) {
return is_string($value) || is_int($value);
});
}
}
32 changes: 0 additions & 32 deletions src/Resolver/ValueResolver/ValueResolver.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
use PhpParser\Node\Scalar\MagicConst\Dir;
use PhpParser\Node\Scalar\MagicConst\File;
use PHPStan\Analyser\Scope;
use PHPStan\Type\IntegerRangeType;
use PHPStan\Type\UnionType;

final class ValueResolver
Expand Down Expand Up @@ -139,35 +138,4 @@ public function resolveStrings(Expr $expr, Scope $scope): ?array

return array_filter($values, 'is_string');
}

/**
* @return array<int|string>|null
*/
public function resolveStringsOrInts(Expr $expr, Scope $scope): ?array
{
$type = $scope->getType($expr);
if ($type instanceof IntegerRangeType) {
$min = $type->getMin() !== null ? $type->getMin() : $type->getMax();
$max = $type->getMax() !== null ? $type->getMax() : $type->getMin();

if ($min === null || $max === null) {
return null;
}

$values = [];
for ($i = $min; $i <= $max; $i++) {
$values[] = $i;
}
} else {
$values = $this->resolve($expr, $scope);
}

if ($values === null) {
return null;
}

return array_filter($values, function ($value) {
return is_string($value) || is_int($value);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,16 @@
use Nette\Application\UI\Form;
use Nette\Forms\Container;
use Nette\Forms\Controls\SubmitButton;
use stdClass;

final class FormsPresenter extends ParentPresenter
{
public function actionDefault(): void
{
$this->template->dynamicVariable = $this->name;
$this->template->dynamicPropertyFetch = new stdClass();
}

protected function createComponentFirstForm(): Form
{
$form = new Form();
Expand Down Expand Up @@ -49,8 +56,15 @@ protected function createComponentSecondForm(): Form
->addRule(Form::EMAIL);
$form->addPassword('password', 'Passowrd')
->setRequired();
$dynamicVariable = $this->name;
$form->addText($dynamicVariable, 'Dynamic name (variable)');

$dynamicPropertyFetch = new stdClass();
$form->addPassword($dynamicPropertyFetch->name, 'Dynamic name (property fetch)');

$submit = new SubmitButton();
$form->addComponent($submit, 'submit');
$submitName = 'submit';
$form->addComponent($submit, $submitName);

$form->onSuccess[] = function (Form $form, array $values): void {
};
Expand All @@ -60,7 +74,7 @@ protected function createComponentSecondForm(): Form

protected function createComponentCustomForm(): CustomForm
{
$form = new CustomForm();
$form = new CustomForm('custom parameter');
$form->addGroup('General');
$form->addCustomText('custom_text', 'Custom text')
->setRequired();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,5 +139,9 @@
{input radio_list:2}{label radio_list:2 /}
{input radio_list:3}{label radio_list:3 /}
{input radio_list:4}{label radio_list:4 /}
{php \PHPStan\dumpType($form[$dynamicVariable])}
{input $dynamicVariable}
{php \PHPStan\dumpType($form[$dynamicPropertyFetch->name])}
{input $dynamicPropertyFetch->name}
{input submit, 'class' => 'btn'}
{/form}
Original file line number Diff line number Diff line change
Expand Up @@ -519,6 +519,16 @@ public function testForms(): void
141,
'default.latte',
],
[
'Dumped type: Nette\Forms\Controls\TextInput',
142,
'default.latte',
],
[
'Dumped type: Nette\Forms\Controls\TextInput',
144,
'default.latte',
],
]);
}

Expand Down
2 changes: 2 additions & 0 deletions tests/config.neon
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ parameters:
latte:
tmpDir: %rootDir%/../../../tmp/phpstan-latte
resolveAllPossiblePaths: true
features:
transformDynamicFormControlNamesToString: true
checkExplicitMixed: true
checkImplicitMixed: true

Expand Down

0 comments on commit 647918a

Please sign in to comment.