Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added support for form fields in containers #380

Draft
wants to merge 2 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## [Unreleased][unreleased]

### Added
- Support for form fields in containers

### Fixed
- Report errors when numeric container names are used in latte

## [0.13.0] - 2023-06-05
### Changed
- Separated collection of Form Containers
Expand Down
93 changes: 89 additions & 4 deletions docs/how_it_works.md
Original file line number Diff line number Diff line change
Expand Up @@ -125,10 +125,95 @@ It is important to check the context first (text after path of latte file - rend

Now the type of `$baz` will be `'bar'|null` and isset() in condition will be valid.

<!-- TODO

## Components
-->

<!-- TODO
Components are collected from PHP classes (e.g. Presenters or Controls) when using one of these ways:

1) class method createComponent()
```php
protected function createComponentSomething(): SomeControl
{
return new SomeControl();
}
```

2) calling method addComponent()
```php
public function actionDefault(): void
{
$this->addComponent('something', new SomeControl());
}
```

3) assign to $this:
```php
public function actionDefault(): void
{
$this['something'] = new SomeControl();
}
```

Subcomponents of components are also collected, so it is possible to use this:
```latte
{control someControl}
{control someControl-header}
{control someControl-body}
```

### Common errors

#### Component with name "xxx" probably doesn't exist.
First of all, check if your component is registered in Presenter / Control using one of way described above and if the name fits.


## Forms
-->

Forms are collected from PHP classes (e.g. Presenters or Controls) when they are registered as components via `createComponent*` or `createComponent` method if this method returns instance of `Nette\Forms\Form`.
Form fields and form containers are also collected and can be then analysed.

**IMPORTANT NOTE**: Container fields are currently assigned to containers if the name of container is the same as the name of variable which adds some field to this container.

### Common errors

#### Form control with name "xxx" probably does not exist.
Let's say you register form like this:

```php

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:
```latte
{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}
```
37 changes: 27 additions & 10 deletions src/Compiler/NodeVisitor/AddFormClassesNodeVisitor.php
Original file line number Diff line number Diff line change
Expand Up @@ -134,11 +134,8 @@ public function enterNode(Node $node): ?Node
$itemArgument = $node->getArgs()[0] ?? null;
$itemArgumentValue = $itemArgument ? $itemArgument->value : null;

if ($itemArgumentValue instanceof String_) {
$controlName = $itemArgumentValue->value;
// TODO remove when container are supported
$controlNameParts = explode('-', $controlName);
$controlName = end($controlNameParts);
if ($itemArgumentValue instanceof String_ || $itemArgumentValue instanceof LNumber) {
$controlName = (string)$itemArgumentValue->value;
$formControl = $this->actualForm->getControl($controlName);
if ($formControl === null) {
$this->errorControlNodes[] = [
Expand Down Expand Up @@ -178,11 +175,8 @@ public function enterNode(Node $node): ?Node
return null;
}

if ($node->dim instanceof String_) {
$controlName = $node->dim->value;
// TODO remove when container are supported
$controlNameParts = explode('-', $controlName);
$controlName = end($controlNameParts);
if ($node->dim instanceof String_ || $node->dim instanceof LNumber) {
$controlName = (string)$node->dim->value;
$formControl = $this->actualForm->getControl($controlName);
if ($formControl === null) {
$this->errorControlNodes[] = [
Expand All @@ -196,6 +190,29 @@ public function enterNode(Node $node): ?Node
if ($formControlType instanceof ObjectType && ($formControlType->isInstanceOf('Nette\Forms\Controls\CheckboxList')->yes() || $formControlType->isInstanceOf('Nette\Forms\Controls\RadioList')->yes())) {
$this->possibleAlwaysTrueLabels[] = $this->findParentStmt($node);
}

/**
* Replace:
* <code>
* $form['foo-bar']->getControl();
* </code>
*
* With:
* <code>
* $form['foo']['bar']->getControl();
* <code>
*
* if foobar exists in actual form
*/
if (str_contains($controlName, '-')) {
$controlNameParts = explode('-', $controlName);
$tmpDim = new ArrayDimFetch(new Variable('form'), new String_(array_shift($controlNameParts)));
foreach ($controlNameParts as $controlNamePart) {
$tmpDim = new ArrayDimFetch($tmpDim, new String_($controlNamePart));
}
$tmpDim->setAttributes($node->getAttributes());
return $tmpDim;
}
} elseif ($node->dim instanceof Variable) {
// dynamic control
} else {
Expand Down
10 changes: 9 additions & 1 deletion src/LatteContext/CollectedData/Form/CollectedFormControl.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,17 @@ final class CollectedFormControl extends CollectedLatteContextObject

private ControlInterface $formControl;

private string $parentName;

/**
* @param class-string $className
*/
public function __construct(string $className, string $methodName, ControlInterface $formControl)
public function __construct(string $className, string $methodName, ControlInterface $formControl, string $parentName)
{
$this->className = $className;
$this->methodName = $methodName;
$this->formControl = $formControl;
$this->parentName = $parentName;
}

public function getClassName(): string
Expand All @@ -40,4 +43,9 @@ public function getFormControl(): ControlInterface
{
return $this->formControl;
}

public function getParentName(): string
{
return $this->parentName;
}
}
4 changes: 3 additions & 1 deletion src/LatteContext/Collector/FormControlCollector.php
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ public function collectData(Node $node, Scope $scope): ?array
return null;
}

$parentName = $this->nameResolver->resolve($node->var) ?: 'form';
$formControls = [];
foreach ($controlNames as $controlName) {
$controlName = (string)$controlName;
Expand All @@ -141,7 +142,8 @@ public function collectData(Node $node, Scope $scope): ?array
$formControls[] = new CollectedFormControl(
$classReflection->getName(),
$methodName,
$formControl
$formControl,
$parentName
);
}
return $formControls;
Expand Down
49 changes: 46 additions & 3 deletions src/LatteContext/Finder/FormControlFinder.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,9 @@

use Efabrica\PHPStanLatte\Analyser\LatteContextData;
use Efabrica\PHPStanLatte\LatteContext\CollectedData\Form\CollectedFormControl;
use Efabrica\PHPStanLatte\Template\Form\Container;
use Efabrica\PHPStanLatte\Template\Form\ControlInterface;
use Efabrica\PHPStanLatte\Template\Form\Field;
use Efabrica\PHPStanLatte\Template\ItemCombinator;
use PHPStan\Reflection\ReflectionProvider;

Expand All @@ -28,14 +30,55 @@ public function __construct(LatteContextData $latteContext, ReflectionProvider $

$collectedFormControls = $latteContext->getCollectedData(CollectedFormControl::class);

/** @var array<string, array<string, array<string, Container>>> $containers */
$containers = [];

/** @var array<string, array<string, array<string, Field[]>>> $controls */
$controls = [];

/** @var CollectedFormControl $collectedFormControl */
foreach ($collectedFormControls as $collectedFormControl) {
$className = $collectedFormControl->getClassName();
$methodName = $collectedFormControl->getMethodName();
if (!isset($this->assignedFormControls[$className][$methodName])) {
$this->assignedFormControls[$className][$methodName] = [];
$formControl = $collectedFormControl->getFormControl();
if (!$formControl instanceof Container) {
$parentName = $collectedFormControl->getParentName();
if (!isset($controls[$className][$methodName][$parentName])) {
$controls[$className][$methodName][$parentName] = [];
}
$controls[$className][$methodName][$parentName][] = $formControl;
continue;
}

$containerName = $formControl->getName();
$containers[$className][$methodName][$containerName] = $formControl;
}

foreach ($containers as $className => $classContainers) {
foreach ($classContainers as $methodName => $methodContainers) {
foreach ($methodContainers as $containerName => $container) {
$containerControls = $controls[$className][$methodName][$containerName] ?? [];
$container->addControls($containerControls);
unset($controls[$className][$methodName][$containerName]);

if (!isset($this->assignedFormControls[$className][$methodName])) {
$this->assignedFormControls[$className][$methodName] = [];
}

$this->assignedFormControls[$className][$methodName][] = $container;
}
}
}

foreach ($controls as $className => $classControls) {
foreach ($classControls as $methodName => $methodControls) {
foreach ($methodControls as $parentControls) {
if (!isset($this->assignedFormControls[$className][$methodName])) {
$this->assignedFormControls[$className][$methodName] = [];
}
$this->assignedFormControls[$className][$methodName] = array_merge($this->assignedFormControls[$className][$methodName], $parentControls);
}
}
$this->assignedFormControls[$className][$methodName][] = $collectedFormControl->getFormControl();
}
}

Expand Down
11 changes: 9 additions & 2 deletions src/Template/Form/Behavior/ControlHolderBehavior.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

namespace Efabrica\PHPStanLatte\Template\Form\Behavior;

use Efabrica\PHPStanLatte\Template\Form\ControlHolderInterface;
use Efabrica\PHPStanLatte\Template\Form\ControlInterface;

trait ControlHolderBehavior
Expand All @@ -21,13 +22,19 @@ public function getControls(): array

public function getControl(string $name): ?ControlInterface
{
return $this->controls[$name] ?? null;
$nameParts = explode('-', $name);
$controlName = array_shift($nameParts);
$control = $this->controls[$controlName] ?? null;
if ($control instanceof ControlHolderInterface && $nameParts !== []) {
return $control->getControl(implode('-', $nameParts));
}
return $control;
}

/**
* @param ControlInterface[] $controls
*/
private function addControls(array $controls): void
public function addControls(array $controls): void
{
foreach ($controls as $control) {
$this->controls[$control->getName()] = $control;
Expand Down
2 changes: 2 additions & 0 deletions src/Template/Form/ControlHolderInterface.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,6 @@ interface ControlHolderInterface
* @return ControlInterface[]
*/
public function getControls(): array;

public function getControl(string $name): ?ControlInterface;
}
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
{form containerForm}
{$form[part1][text1]->getHtmlId()}
{input part1-text1}
{input part1-text2}
{input part1-submit1}

{input part2-text2}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,29 @@ public function testForms(): void
48,
'default.latte',
],
[
'Form control with name "part1-text2" probably does not exist.',
61,
'default.latte',
],
[
'Form control with name "5" probably does not exist.',
87,
'default.latte',
],
[
'Form control with name "5" probably does not exist.',
90,
91,
'default.latte',
],
[
'Form control with name "1" probably does not exist.',
105,
'default.latte',
],
[
'Form control with name "1" probably does not exist.',
108,
109,
'default.latte',
],
]);
Expand Down