From 5e0e432eda8316486d2b55306edf633c6451f6c1 Mon Sep 17 00:00:00 2001 From: Dalibor Korpar Date: Fri, 14 Aug 2026 17:34:10 +0200 Subject: [PATCH 1/2] chore: support nette/forms 3.3.0 nette/forms 3.3.0 changes things this library builds on top of: - Container::addSubmit() grew a third `?Closure $onSubmit` argument, which made BootstrapContainerTrait's override an incompatible declaration and crashed the whole library with a fatal error on load. The override takes the argument and wires it onto the button's onClick, as the parent does. - BaseControl::$caption and Form::$renderer became @property-deprecated, so the five places reading them magically now call getCaption() and getRenderer() instead. The test run is clean of deprecations again. Closes #118 Co-Authored-By: Claude Opus 5 --- composer.json | 2 +- src/BootstrapRenderer.php | 2 +- src/Grid/BootstrapCell.php | 2 +- src/Grid/BootstrapRow.php | 2 +- src/Inputs/ButtonInput.php | 2 +- src/Inputs/CheckboxInput.php | 2 +- src/Traits/BootstrapContainerTrait.php | 8 ++++++- tests/E2E/FormSubmissionTest.php | 24 ++++++++++++++++++++ tests/Traits/BootstrapContainerTraitTest.php | 23 +++++++++++++++++++ 9 files changed, 60 insertions(+), 7 deletions(-) diff --git a/composer.json b/composer.json index 05764ce..c49e0bd 100644 --- a/composer.json +++ b/composer.json @@ -18,7 +18,7 @@ ], "require": { "php": ">=8.2", - "nette/forms": "3.2.9", + "nette/forms": "3.3.0", "nette/application": "^3.0" }, "require-dev": { diff --git a/src/BootstrapRenderer.php b/src/BootstrapRenderer.php index ded98f7..9dd6253 100644 --- a/src/BootstrapRenderer.php +++ b/src/BootstrapRenderer.php @@ -509,7 +509,7 @@ public function renderEnd(): string */ public function renderLabel(BaseControl $control): Html { - if ($control->caption === null) { + if ($control->getCaption() === null) { return Html::el(); } diff --git a/src/Grid/BootstrapCell.php b/src/Grid/BootstrapCell.php index a252c03..d1a21d9 100644 --- a/src/Grid/BootstrapCell.php +++ b/src/Grid/BootstrapCell.php @@ -91,7 +91,7 @@ public function render(): Html $element = $this->elementPrototype; /** @var BootstrapRenderer $renderer */ - $renderer = $this->row->getParent()->form->renderer; + $renderer = $this->row->getParent()->getForm()->getRenderer(); $element = $renderer->configElem(RendererConfig::GRID_CELL, $element); $element->class[] = $this->createClass(); diff --git a/src/Grid/BootstrapRow.php b/src/Grid/BootstrapRow.php index 306b2a7..1c5c065 100644 --- a/src/Grid/BootstrapRow.php +++ b/src/Grid/BootstrapRow.php @@ -210,7 +210,7 @@ public function getOption(string $option) public function render(): Html { /** @var BootstrapRenderer $renderer */ - $renderer = $this->container->form->renderer; + $renderer = $this->container->getForm()->getRenderer(); $element = $renderer->configElem(RendererConfig::GRID_ROW, $this->elementPrototype); foreach ($this->cells as $cell) { diff --git a/src/Inputs/ButtonInput.php b/src/Inputs/ButtonInput.php index e47801f..866b2d5 100644 --- a/src/Inputs/ButtonInput.php +++ b/src/Inputs/ButtonInput.php @@ -35,7 +35,7 @@ public function getControl($content = null): Html $btn = parent::getControl($content); $btn->setName('button'); $this->addBtnClass($btn); - $btn->setHtml($content ?? (string) $this->caption); + $btn->setHtml($content ?? (string) $this->getCaption()); $btn->removeAttribute('value'); return $btn; diff --git a/src/Inputs/CheckboxInput.php b/src/Inputs/CheckboxInput.php index f8c86cb..290f17a 100644 --- a/src/Inputs/CheckboxInput.php +++ b/src/Inputs/CheckboxInput.php @@ -95,7 +95,7 @@ public function getControl(): Html return self::makeCheckbox( $this->getHtmlName(), $this->getHtmlId(), - $this->translate($this->caption), + $this->translate($this->getCaption()), $this->value, false, $this->required, diff --git a/src/Traits/BootstrapContainerTrait.php b/src/Traits/BootstrapContainerTrait.php index 9fece17..f433318 100644 --- a/src/Traits/BootstrapContainerTrait.php +++ b/src/Traits/BootstrapContainerTrait.php @@ -2,6 +2,7 @@ namespace Contributte\FormsBootstrap\Traits; +use Closure; use Contributte\FormsBootstrap\BootstrapContainer; use Contributte\FormsBootstrap\BootstrapForm; use Contributte\FormsBootstrap\Inputs\ButtonInput; @@ -294,14 +295,19 @@ public function addSelect(string $name, $label = null, ?array $items = null, ?in /** * @param string|Html|null $caption + * @param Closure|null $onSubmit handler bound to the button's onClick, added in nette/forms 3.3 * @return SubmitButtonInput */ - public function addSubmit(string $name, $caption = null): SubmitButton + public function addSubmit(string $name, $caption = null, ?Closure $onSubmit = null): SubmitButton { $comp = new SubmitButtonInput($caption); $comp->setBtnClass('btn-primary'); $this->addComponent($comp, $name); + if ($onSubmit !== null) { + $comp->onClick[] = $onSubmit; + } + return $comp; } diff --git a/tests/E2E/FormSubmissionTest.php b/tests/E2E/FormSubmissionTest.php index 7e9317c..5e025c2 100644 --- a/tests/E2E/FormSubmissionTest.php +++ b/tests/E2E/FormSubmissionTest.php @@ -2,7 +2,9 @@ namespace Tests\E2E; +use ArrayObject; use Contributte\FormsBootstrap\BootstrapForm; +use Nette\Forms\Controls\SubmitButton; use Nette\Http\FileUpload; /** @@ -41,6 +43,28 @@ public function testOnSuccessHandlerReceivesSubmittedValues(): void $this->assertFalse($this->presenter->errored); } + public function testAddSubmitHandlerIsCalledWhenThatButtonSubmitsTheForm(): void + { + // an object, so the closures below can share it without capturing by reference + $seen = new ArrayObject(); + + $this->submit( + function () use ($seen): BootstrapForm { + $form = new BootstrapForm(); + $form->setAction('/'); + $form->addText('name', 'Name'); + $form->addSubmit('send', 'Send', function (SubmitButton $button) use ($seen): void { + $seen['values'] = $button->getForm()->getValues('array'); + }); + + return $form; + }, + ['name' => 'Dalibor', 'send' => 'Send'] + ); + + $this->assertSame(['name' => 'Dalibor'], $seen['values'] ?? null); + } + public function testMissingRequiredValueFailsValidation(): void { $form = $this->submit( diff --git a/tests/Traits/BootstrapContainerTraitTest.php b/tests/Traits/BootstrapContainerTraitTest.php index ec38d0e..5c57675 100644 --- a/tests/Traits/BootstrapContainerTraitTest.php +++ b/tests/Traits/BootstrapContainerTraitTest.php @@ -122,6 +122,29 @@ public function testAddButtonIsNotSubmitter(): void $this->assertStringNotContainsString('type="submit"', $html); } + public function testAddSubmitWithoutHandlerRegistersNoClickListener(): void + { + $form = new BootstrapForm(); + $button = $form->addSubmit('send', 'Send'); + + $this->assertStringContainsString('type="submit"', (string) $button->getControl()); + $this->assertSame([], $button->onClick); + } + + /** + * nette/forms 3.3 grew a third addSubmit() argument that wires a handler + * straight onto the button's onClick. + */ + public function testAddSubmitTakesAnOnSubmitHandler(): void + { + $form = new BootstrapForm(); + $handler = function (): void { + }; + $button = $form->addSubmit('send', 'Send', $handler); + + $this->assertSame([$handler], $button->onClick); + } + public function testFactoriesAlsoWorkInsideContainer(): void { $form = new BootstrapForm(); From e616b8706dfaa68d684239cfd0e74dd445b89542 Mon Sep 17 00:00:00 2001 From: Dalibor Korpar Date: Fri, 14 Aug 2026 18:04:04 +0200 Subject: [PATCH 2/2] chore: require PHP 8.3, nette/forms ^3.3 and phpunit ^12.5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit nette/forms 3.3.0 (and nette/application 3.3.0, nette/component-model 4.0.1) only support PHP 8.3 - 8.5, so PHP 8.2 can no longer be supported. Raise the minimum to 8.3 and relax the exact 3.3.0 pin to ^3.3.0 so patch releases are picked up. With 8.2 gone, phpunit ^12.5 (php >=8.3) becomes installable — upgrade it, point phpunit.xml at the 12.5 schema and swap the presenter createMock() calls for createStub(), which is what PHPUnit 12 expects for a test double with no configured expectations. CI drops the 8.2 jobs (and the --ignore-platform-reqs workaround that only existed to force forms 3.3 onto an unsupported PHP); qa, static analysis, coverage and --prefer-lowest now run on 8.3. Co-Authored-By: Claude Opus 5 --- .github/workflows/coverage.yml | 2 +- .github/workflows/main.yaml | 11 ++++------- README.md | 2 +- composer.json | 6 +++--- phpunit.xml | 2 +- tests/Grid/BootstrapCellTest.php | 2 +- tests/Grid/BootstrapGroupRowRenderingTest.php | 2 +- tests/Grid/BootstrapRowTest.php | 2 +- tests/Inputs/ColorPickerTest.php | 2 +- tests/Inputs/DateTimeControlTest.php | 2 +- tests/Rendering/GroupRenderingTest.php | 2 +- tests/Rendering/RendererConfigTest.php | 4 ++-- tests/Rendering/SideBySideTest.php | 2 +- tests/Rendering/VerticalTest.php | 2 +- 14 files changed, 20 insertions(+), 23 deletions(-) diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 7cc4365..7cd4e45 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -19,4 +19,4 @@ jobs: uses: contributte/.github/.github/workflows/nette-tester-coverage-v2.yml@master secrets: inherit with: - php: "8.2" + php: "8.3" diff --git a/.github/workflows/main.yaml b/.github/workflows/main.yaml index 5363e10..8aff47e 100644 --- a/.github/workflows/main.yaml +++ b/.github/workflows/main.yaml @@ -23,7 +23,7 @@ jobs: strategy: matrix: - php-version: [ "8.2" ] + php-version: [ "8.3" ] operating-system: [ "ubuntu-latest" ] fail-fast: false @@ -82,7 +82,7 @@ jobs: strategy: matrix: - php-version: [ "8.2" ] + php-version: [ "8.3" ] operating-system: [ "ubuntu-latest" ] fail-fast: false @@ -138,16 +138,13 @@ jobs: strategy: matrix: - php-version: ["8.2", "8.3", "8.4","8.5"] + php-version: ["8.3", "8.4", "8.5"] operating-system: [ "ubuntu-latest" ] composer-args: [ "" ] include: - - php-version: "8.2" - operating-system: "ubuntu-latest" - composer-args: "--prefer-lowest" - php-version: "8.3" operating-system: "ubuntu-latest" - composer-args: "--ignore-platform-reqs" + composer-args: "--prefer-lowest" fail-fast: false steps: diff --git a/README.md b/README.md index 57bbe71..323411a 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ Nette extension for Bootstrap forms. | State | Version | Branch | Nette | PHP | Bootstrap | |-------------|---------------|----------|-------|---------|-------------| -| dev | `^0.9` | `master` | 3.3+ | `^8.2` | `4.x` `5.x` | +| dev | `^0.9` | `master` | 3.3+ | `^8.3` | `4.x` `5.x` | | stable | `^0.8` | `master` | 3.0+ | `^8.1` | `4.x` `5.x` | | stable | `^0.7` | `master` | 3.0+ | `^8.1` | `4.x` `5.x` | | stable | `^0.6` | `master` | 3.0+ | `^8.1` | `4.x` `5.x` | diff --git a/composer.json b/composer.json index c49e0bd..52f35ac 100644 --- a/composer.json +++ b/composer.json @@ -17,13 +17,13 @@ } ], "require": { - "php": ">=8.2", - "nette/forms": "3.3.0", + "php": ">=8.3", + "nette/forms": "^3.3.0", "nette/application": "^3.0" }, "require-dev": { "contributte/qa": "^v0.4", - "phpunit/phpunit": "^11.5", + "phpunit/phpunit": "^12.5", "phpstan/phpstan": "^2.2", "phpstan/phpstan-deprecation-rules": "^2.0", "phpstan/phpstan-nette": "^2.0", diff --git a/phpunit.xml b/phpunit.xml index 91a879c..1f94308 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,5 +1,5 @@ - + tests diff --git a/tests/Grid/BootstrapCellTest.php b/tests/Grid/BootstrapCellTest.php index bd818b6..454605f 100644 --- a/tests/Grid/BootstrapCellTest.php +++ b/tests/Grid/BootstrapCellTest.php @@ -56,7 +56,7 @@ protected function setUp(): void $this->form = new BootstrapForm(); $this->row = $this->form->addRow(); $this->cell = $this->row->addCell(12); - $this->form->setParent($this->createMock(Presenter::class)); + $this->form->setParent($this->createStub(Presenter::class)); // A real (non-empty) action makes Nette inject the "_do" signal field, // mirroring production where the form is attached to a routed presenter. $this->form->setAction('/'); diff --git a/tests/Grid/BootstrapGroupRowRenderingTest.php b/tests/Grid/BootstrapGroupRowRenderingTest.php index 76c2c3a..bc65a08 100644 --- a/tests/Grid/BootstrapGroupRowRenderingTest.php +++ b/tests/Grid/BootstrapGroupRowRenderingTest.php @@ -35,7 +35,7 @@ public function testGroupRowRendering(): void $form->addGroup('Group 1', false) ->add([$row1]); $form->setRenderer(new BootstrapRenderer(RenderMode::SIDE_BY_SIDE_MODE)); - $form->setParent($this->createMock(Presenter::class)); + $form->setParent($this->createStub(Presenter::class)); // A real (non-empty) action makes Nette inject the "_do" signal field, // mirroring production where the form is attached to a routed presenter. $form->setAction('/'); diff --git a/tests/Grid/BootstrapRowTest.php b/tests/Grid/BootstrapRowTest.php index 685eb88..cfce79c 100644 --- a/tests/Grid/BootstrapRowTest.php +++ b/tests/Grid/BootstrapRowTest.php @@ -117,7 +117,7 @@ protected function setUp(): void { $this->form = new BootstrapForm(); $this->row = $this->form->addRow(); - $this->form->setParent($this->createMock(Presenter::class)); + $this->form->setParent($this->createStub(Presenter::class)); // A real (non-empty) action makes Nette inject the "_do" signal field, // mirroring production where the form is attached to a routed presenter. $this->form->setAction('/'); diff --git a/tests/Inputs/ColorPickerTest.php b/tests/Inputs/ColorPickerTest.php index d4503b7..5d4df92 100644 --- a/tests/Inputs/ColorPickerTest.php +++ b/tests/Inputs/ColorPickerTest.php @@ -22,7 +22,7 @@ public function testShowsValidationState(): void { $form = new BootstrapForm(); // Rendering a form requires a presenter with a non-empty action; see BaseTestCase users. - $form->setParent($this->createMock(Presenter::class)); + $form->setParent($this->createStub(Presenter::class)); $form->setAction('/'); $input = $form->addColor('color', 'Choose color'); diff --git a/tests/Inputs/DateTimeControlTest.php b/tests/Inputs/DateTimeControlTest.php index 4f1d31e..27f9b01 100644 --- a/tests/Inputs/DateTimeControlTest.php +++ b/tests/Inputs/DateTimeControlTest.php @@ -21,7 +21,7 @@ public function testShowsValidationState(): void { $form = new BootstrapForm(); // Rendering a form requires a presenter with a non-empty action; see BaseTestCase users. - $form->setParent($this->createMock(Presenter::class)); + $form->setParent($this->createStub(Presenter::class)); $form->setAction('/'); $dt = $form->addDate('date', 'Date'); diff --git a/tests/Rendering/GroupRenderingTest.php b/tests/Rendering/GroupRenderingTest.php index ab2b346..093e4dd 100644 --- a/tests/Rendering/GroupRenderingTest.php +++ b/tests/Rendering/GroupRenderingTest.php @@ -125,7 +125,7 @@ public function testUngroupedControlsAreRenderedBeforeGroups(): void protected function setUp(): void { $this->form = new BootstrapForm(); - $this->form->setParent($this->createMock(Presenter::class)); + $this->form->setParent($this->createStub(Presenter::class)); // A real (non-empty) action makes Nette inject the "_do" signal field, // mirroring production where the form is attached to a routed presenter. $this->form->setAction('/'); diff --git a/tests/Rendering/RendererConfigTest.php b/tests/Rendering/RendererConfigTest.php index 03c21b8..963bd9a 100644 --- a/tests/Rendering/RendererConfigTest.php +++ b/tests/Rendering/RendererConfigTest.php @@ -149,7 +149,7 @@ public function testGroupHiddenAccessors(): void public function testHiddenFieldsStayInPlaceWhenGroupingIsOff(): void { $form = new BootstrapForm(); - $form->setParent($this->createMock(Presenter::class)); + $form->setParent($this->createStub(Presenter::class)); $form->setAction('/'); $form->getRenderer()->setGroupHidden(false); $form->addHidden('secret', 'v'); @@ -189,7 +189,7 @@ private function sideBySideForm(): BootstrapForm { $form = new BootstrapForm(); $form->setRenderer(new BootstrapRenderer(RenderMode::SIDE_BY_SIDE_MODE)); - $form->setParent($this->createMock(Presenter::class)); + $form->setParent($this->createStub(Presenter::class)); $form->setAction('/'); return $form; diff --git a/tests/Rendering/SideBySideTest.php b/tests/Rendering/SideBySideTest.php index 2d049e3..cce28d2 100644 --- a/tests/Rendering/SideBySideTest.php +++ b/tests/Rendering/SideBySideTest.php @@ -92,7 +92,7 @@ protected function setUp(): void { $this->form = new BootstrapForm(); $this->form->setRenderer(new BootstrapRenderer(RenderMode::SIDE_BY_SIDE_MODE)); - $this->form->setParent($this->createMock(Presenter::class)); + $this->form->setParent($this->createStub(Presenter::class)); // A real (non-empty) action makes Nette inject the "_do" signal field, // mirroring production where the form is attached to a routed presenter. $this->form->setAction('/'); diff --git a/tests/Rendering/VerticalTest.php b/tests/Rendering/VerticalTest.php index ca9575e..f4485ed 100644 --- a/tests/Rendering/VerticalTest.php +++ b/tests/Rendering/VerticalTest.php @@ -114,7 +114,7 @@ protected function setUp(): void { $this->form = new BootstrapForm(); $this->form->setRenderer(new BootstrapRenderer(RenderMode::VERTICAL_MODE)); - $this->form->setParent($this->createMock(Presenter::class)); + $this->form->setParent($this->createStub(Presenter::class)); // A real (non-empty) action makes Nette inject the "_do" signal field, // mirroring production where the form is attached to a routed presenter. $this->form->setAction('/');