diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 2dca508..7d99d06 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: - php-version: [ '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + php-version: [ '8.1', '8.2', '8.3', '8.4', '8.5' ] steps: - name: Checkout diff --git a/composer.json b/composer.json index 83ac8ac..3f4d01f 100644 --- a/composer.json +++ b/composer.json @@ -13,15 +13,16 @@ "require": { "php": ">=8.0", "latte/latte": "^3.0", - "nette/forms": "^3.0", + "nette/forms": "^3.1", "nette/utils": "^3.0 || ^4.0" }, "require-dev": { "nette/application": "^3.0", - "nette/bootstrap": "^3.0", + "nette/bootstrap": "^3.1", "nette/di": "^3.0", - "phpstan/phpstan": "~1.8", - "phpstan/phpstan-nette": "~1.0", + "phpstan/phpstan": "^2.2", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-nette": "^2.0", "tracy/tracy": "^2.5" }, "extra": { @@ -42,6 +43,10 @@ "psr-4": { "NextrasDemos\\FormsRendering\\LatteMacros\\": "examples/lattemacros/", "NextrasDemos\\FormsRendering\\Renderers\\": "examples/renderers/" - } + }, + "exclude-from-classmap": [ + "examples/lattemacros/temp/", + "examples/renderers/temp/" + ] } } diff --git a/examples/.gitignore b/examples/.gitignore deleted file mode 100644 index c1e0f82..0000000 --- a/examples/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -log -temp diff --git a/examples/lattemacros/LatteMacrosPresenter.latte b/examples/lattemacros/LatteMacrosPresenter.latte index 1c38888..5dbb203 100644 --- a/examples/lattemacros/LatteMacrosPresenter.latte +++ b/examples/lattemacros/LatteMacrosPresenter.latte @@ -12,7 +12,7 @@

Form LatteMacros rendering


- {form form class => form-horizontal} + {form form, class => form-horizontal}
{bsLabel text class => "label-control col-sm-3" /}
{bsInput text}{inputError text}
diff --git a/examples/lattemacros/index.php b/examples/lattemacros/index.php index dadb9aa..1f90820 100644 --- a/examples/lattemacros/index.php +++ b/examples/lattemacros/index.php @@ -3,13 +3,13 @@ namespace NextrasDemos\FormsRendering\LatteMacros; use Nette\Application\Application; -use Nette\Configurator; +use Nette\Bootstrap\Configurator; require_once __DIR__ . '/../../vendor/autoload.php'; $configurator = new Configurator; -$configurator->enableDebugger(__DIR__ . '/log'); +$configurator->enableTracy(__DIR__ . '/log'); $configurator->setTempDirectory(__DIR__ . '/temp'); $configurator->addConfig(__DIR__ . '/config.neon'); diff --git a/examples/lattemacros/log/.gitignore b/examples/lattemacros/log/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/lattemacros/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/lattemacros/temp/.gitignore b/examples/lattemacros/temp/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/lattemacros/temp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/renderers/RenderersPresenter.php b/examples/renderers/RenderersPresenter.php index 8a03322..a062a3c 100644 --- a/examples/renderers/RenderersPresenter.php +++ b/examples/renderers/RenderersPresenter.php @@ -2,6 +2,7 @@ namespace NextrasDemos\FormsRendering\Renderers; +use Nette\Application\Attributes\Persistent; use Nette\Application\UI\Form; use Nette\Application\UI\Presenter; use Nextras\FormsRendering\Renderers\Bs3FormRenderer; @@ -14,14 +15,14 @@ class RenderersPresenter extends Presenter { /** * @var string - * @persistent */ + #[Persistent] public $renderer = 'bs3'; /** * @var bool - * @persistent */ + #[Persistent] public $showBulky = true; diff --git a/examples/renderers/index.php b/examples/renderers/index.php index 70c1d06..f748bc6 100644 --- a/examples/renderers/index.php +++ b/examples/renderers/index.php @@ -3,13 +3,13 @@ namespace NextrasDemos\FormsRendering\Renderers; use Nette\Application\Application; -use Nette\Configurator; +use Nette\Bootstrap\Configurator; require_once __DIR__ . '/../../vendor/autoload.php'; $configurator = new Configurator; -$configurator->enableDebugger(__DIR__ . '/log'); +$configurator->enableTracy(__DIR__ . '/log'); $configurator->setTempDirectory(__DIR__ . '/temp'); $configurator->addConfig(__DIR__ . '/config.neon'); diff --git a/examples/renderers/log/.gitignore b/examples/renderers/log/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/renderers/log/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/examples/renderers/temp/.gitignore b/examples/renderers/temp/.gitignore new file mode 100644 index 0000000..d6b7ef3 --- /dev/null +++ b/examples/renderers/temp/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore diff --git a/phpstan.dist.neon b/phpstan.dist.neon index ead69a6..f3f56a9 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -1,4 +1,5 @@ includes: + - vendor/phpstan/phpstan-deprecation-rules/rules.neon - vendor/phpstan/phpstan-nette/extension.neon - vendor/phpstan/phpstan-nette/rules.neon @@ -7,3 +8,14 @@ parameters: paths: - src - examples + + excludePaths: + - examples/**/log/* + - examples/**/temp/* + + ignoreErrors: + - + message: '#^Call to function method_exists\(\) with ''Nette\\\\Bridges\\\\FormsLatte\\\\Runtime'' and ''renderFormBegin'' will always evaluate to true\.$#' + identifier: function.alreadyNarrowedType + reportUnmatched: false + path: src/LatteTags/InputNode.php diff --git a/src/LatteTags/Bs3/Bs3InputNode.php b/src/LatteTags/Bs3/Bs3InputNode.php index 12dd16f..2627651 100644 --- a/src/LatteTags/Bs3/Bs3InputNode.php +++ b/src/LatteTags/Bs3/Bs3InputNode.php @@ -18,17 +18,19 @@ class Bs3InputNode extends BaseInputNode { + private const INPUT_CONTROLS = ['radio', 'checkbox', 'file', 'hidden', 'range', 'image', 'submit', 'reset']; + public static function input(Html $input, BaseControl $control, bool $isSubItem): Html { - static $inputControls = ['radio', 'checkbox', 'file', 'hidden', 'range', 'image', 'submit', 'reset']; $name = $input->getName(); if ( $name === 'select' || $name === 'textarea' || - ($name === 'input' && !in_array($input->type, $inputControls, true)) + ($name === 'input' && !in_array($input->type, self::INPUT_CONTROLS, true)) ) { $input->addClass('form-control'); - } elseif ($name === 'input' && ($input->type === 'submit' || $input->type === 'reset')) { + } elseif ($name === 'input' && ($input->type === 'submit' || $input->type === 'reset') && is_string($input->value)) { + // is_string above is needed by PHPStan since phpstan-nette claims all properties are mixed. $input->setName('button'); $input->addHtml($input->value); $input->addClass('btn'); diff --git a/src/LatteTags/InputNode.php b/src/LatteTags/InputNode.php index 93adbe7..3fa332c 100644 --- a/src/LatteTags/InputNode.php +++ b/src/LatteTags/InputNode.php @@ -9,7 +9,6 @@ namespace Nextras\FormsRendering\LatteTags; -use Latte\Compiler\Nodes\Php\Scalar\StringNode; use Latte\Compiler\PrintContext; use Nette\Bridges\FormsLatte\Nodes\InputNode as NetteInputNode; use Nette\Forms\Controls\BaseControl; @@ -21,8 +20,10 @@ abstract class InputNode extends NetteInputNode public function print(PrintContext $context): string { $class = static::class; + $hasForms33 = method_exists(\Nette\Bridges\FormsLatte\Runtime::class, 'renderFormBegin'); + $input = $hasForms33 ? 'Nette\Bridges\FormsLatte\Runtime::item(%node, $this->global)' : '$this->global->forms->get(%node);'; return $context->format( - '$ʟ_input = Nette\Bridges\FormsLatte\Runtime::item(%node, $this->global);' + '$ʟ_input = ' . $input . 'echo ' . $class . '::input($ʟ_input->' . ($this->part ? ('getControlPart(%node)') : 'getControl()') . ($this->attributes->items ? '->addAttributes(%2.node)' : '') diff --git a/src/LatteTags/LabelNode.php b/src/LatteTags/LabelNode.php index 33590b5..b8b3810 100644 --- a/src/LatteTags/LabelNode.php +++ b/src/LatteTags/LabelNode.php @@ -16,6 +16,13 @@ use Nette\Utils\Html; +/** + * These are fake properties to satisfy PHPStan, at least one of them + * should exist depending on the versions of installed dependencies. + * TODO: Drop these once we no longer support forms < 3.3. + * @property-read array $tagRanges + * @property-read mixed $endLine + */ abstract class LabelNode extends NetteLabelNode { public function print(PrintContext $context): string @@ -34,7 +41,7 @@ public function print(PrintContext $context): string $this->position, $this->content, // TODO: Drop the `endLine` support once we no longer support forms < 3.3. - property_exists($this, 'endLine') ? $this->endLine : end($this->tagRanges), + isset($this->endLine) ? $this->endLine : end($this->tagRanges), ); } diff --git a/src/Renderers/Bs3FormRenderer.php b/src/Renderers/Bs3FormRenderer.php index fb382b1..4bda0ba 100644 --- a/src/Renderers/Bs3FormRenderer.php +++ b/src/Renderers/Bs3FormRenderer.php @@ -137,7 +137,11 @@ private function controlsInit(): void if ($control instanceof Controls\Checkbox) { $control->getContainerPrototype()->setName('div')->appendAttribute('class', $control->getControlPrototype()->type); } else { - $control->getItemLabelPrototype()->addClass($control->getControlPrototype()->type . '-inline'); + $type = $control->getControlPrototype()->type; + // For PHPStan: Both control types initialize it to constructor in string. + \assert(is_string($type)); + + $control->getItemLabelPrototype()->addClass($type . '-inline'); } } } diff --git a/src/Renderers/Bs4FormRenderer.php b/src/Renderers/Bs4FormRenderer.php index 2397ed4..8afba02 100644 --- a/src/Renderers/Bs4FormRenderer.php +++ b/src/Renderers/Bs4FormRenderer.php @@ -148,7 +148,10 @@ private function controlsInit(): void foreach ($this->form->getControls() as $control) { if ($this->layout === FormLayout::INLINE && !$control instanceof Controls\Checkbox && method_exists($control, 'getLabelPrototype')) { - $control->getLabelPrototype()->addClass('my-1')->addClass('mr-2'); + $labelPrototype = $control->getLabelPrototype(); + \assert($labelPrototype instanceof Html); // For PHPStan + + $labelPrototype->addClass('my-1')->addClass('mr-2'); } if ($control instanceof Controls\Button) { @@ -167,7 +170,8 @@ private function controlsInit(): void } elseif ($control instanceof Controls\Checkbox || $control instanceof Controls\CheckboxList || $control instanceof Controls\RadioList) { $control->getControlPrototype()->addClass('form-check-input'); - $control->getSeparatorPrototype() + $wrapper = $control instanceof Controls\Checkbox ? $control->getContainerPrototype() : $control->getSeparatorPrototype(); + $wrapper ->setName('div') ->appendAttribute('class', 'form-check') ->appendAttribute('class', 'form-check-inline', $this->layout == FormLayout::INLINE); diff --git a/src/Renderers/Bs5FormRenderer.php b/src/Renderers/Bs5FormRenderer.php index c920aef..2a232a7 100644 --- a/src/Renderers/Bs5FormRenderer.php +++ b/src/Renderers/Bs5FormRenderer.php @@ -158,20 +158,26 @@ private function controlsInit(): void // The checkboxes are exception since they have their own inline class. if (!$control instanceof Controls\Checkbox && !$control instanceof Controls\CheckboxList && !$control instanceof Controls\RadioList && method_exists($control, 'getControlPrototype')) { - $control->getControlPrototype()->addClass('d-inline-block'); + $controlPrototype = $control->getControlPrototype(); + \assert($controlPrototype instanceof Html); // For PHPStan + + $controlPrototype->addClass('d-inline-block'); // But setting `display: inline-block` is not enough since the widgets will inherit // `width: 100%` from `.form-control` and end up wrapped anyway. // Let’s counter that using `width: auto`. - $control->getControlPrototype()->addClass('w-auto'); + $controlPrototype->addClass('w-auto'); if ($control instanceof Controls\TextBase && $control->control->type === 'color') { // `input[type=color]` is a special case since `width: auto` would make it squish. - $control->getControlPrototype()->addStyle('min-width', '3rem'); + $controlPrototype->addStyle('min-width', '3rem'); } } + $labelPrototype = $control->getLabelPrototype(); + \assert($labelPrototype instanceof Html); // For PHPStan + // Also, we need to add some spacing between the label and the control. - $control->getLabelPrototype()->addClass('me-2'); + $labelPrototype->addClass('me-2'); } if ($control instanceof Controls\Button) { @@ -204,7 +210,8 @@ private function controlsInit(): void $control->getControlPrototype()->addClass('form-check-input'); // They also need to be individually wrapped in `div.form-check`. - $control->getSeparatorPrototype() + $wrapper = $control instanceof Controls\Checkbox ? $control->getContainerPrototype() : $control->getSeparatorPrototype(); + $wrapper ->setName('div') ->appendAttribute('class', 'form-check') // They support being displayed inline with `.form-check-inline`.