diff --git a/Makefile b/Makefile index 8aa6111..aa8d318 100644 --- a/Makefile +++ b/Makefile @@ -15,7 +15,7 @@ csf: vendor vendor/bin/codefixer src tests phpstan: vendor - vendor/bin/phpstan analyse -l 7 -c phpstan.neon src + vendor/bin/phpstan analyse -l 8 -c phpstan.neon src tests: vendor vendor/bin/phpunit diff --git a/src/BootstrapRenderer.php b/src/BootstrapRenderer.php index 9dd6253..b9f3d9b 100644 --- a/src/BootstrapRenderer.php +++ b/src/BootstrapRenderer.php @@ -75,6 +75,7 @@ public function attachForm(BootstrapForm $form): void * * @param string[]|string $config top-level config key * @param Html|null $el elem to config. + * @return ($el is null ? Html|null : Html) */ public function configElem($config, ?Html $el = null): ?Html { @@ -142,12 +143,15 @@ public function configElem($config, ?Html $el = null): ?Html // el may be null, but maybe it has a container defined if (isset($config[Cnf::CONTAINER])) { $container = $this->configElem($config[Cnf::CONTAINER], null); - if ($container !== null && $el !== null) { - $elClone = clone $el; - $container->setHtml($elClone); - } + // a container config which yields no element of its own must not swallow the element it wraps + if ($container !== null) { + if ($el !== null) { + $elClone = clone $el; + $container->setHtml($elClone); + } - $el = $container; + $el = $container; + } } return $el; @@ -525,7 +529,7 @@ public function renderLabel(BaseControl $control): Html if ($controlLabel === null) { if (method_exists($control, 'allignWithInputControls') && $control->allignWithInputControls()) { - return $this->configElem(Cnf::LABEL, null); + return $this->getElem(Cnf::LABEL); } return Html::el(); @@ -545,7 +549,7 @@ public function renderLabel(BaseControl $control): Html */ public function renderPair(BaseControl $control): string { - $pairHtml = $this->configElem(Cnf::PAIR); + $pairHtml = $this->getElem(Cnf::PAIR); $pairHtml->id = $control->getOption(RendererOptions::ID); @@ -573,9 +577,7 @@ public function renderPair(BaseControl $control): string //endregion - if (!empty($nonLabel)) { - $pairHtml->addHtml($nonLabel); - } + $pairHtml->addHtml($nonLabel); return $pairHtml->render(0); } @@ -627,7 +629,7 @@ protected function fetchConfig(string $key): array * * @param string $additionalKeys config will be overridden in this order */ - protected function getElem(string $key, ...$additionalKeys): ?Html + protected function getElem(string $key, ...$additionalKeys): Html { $el = $this->configElem($key, Html::el()); diff --git a/src/Grid/BootstrapCell.php b/src/Grid/BootstrapCell.php index d1a21d9..ae8c77f 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()->getForm()->getRenderer(); + $renderer = $this->row->getContainer()->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 1c5c065..0a6ad30 100644 --- a/src/Grid/BootstrapRow.php +++ b/src/Grid/BootstrapRow.php @@ -11,6 +11,7 @@ use Nette\Forms\Container; use Nette\Forms\Control; use Nette\InvalidArgumentException; +use Nette\InvalidStateException; use Nette\SmartObject; use Nette\Utils\Html; @@ -56,9 +57,9 @@ class BootstrapRow implements IComponent, Control private $columnsOccupied = 0; /** - * Form or container this belong to + * Form or container this belong to. Null while the row is detached. * - * @var Container + * @var Container|null */ private $container; @@ -119,8 +120,13 @@ public function addCell(?int $numOfColumns = BootstrapCell::COLUMNS_NONE): Boots */ public function addComponent(IComponent $component, ?string $name = null, ?string $insertBefore = null): void { - $this->container->addComponent($component, $name, $insertBefore); - $this->ownedNames[] = $name; + $this->getContainer()->addComponent($component, $name, $insertBefore); + + // a successful add always leaves the component named, even when no name was passed here + $ownedName = $name ?? $component->getName(); + if ($ownedName !== null) { + $this->ownedNames[] = $ownedName; + } } /** @@ -172,23 +178,41 @@ public function getName(): ?string } /** - * Returns the container + * Returns the container, or null if the row is not attached to one + */ + public function getParent(): ?IContainer + { + return $this->container; + } + + /** + * Returns the container this row belongs to * - * @return Container + * @throws InvalidStateException if the row is not attached to a container */ - public function getParent(): IContainer + public function getContainer(): Container { + if ($this->container === null) { + throw new InvalidStateException('The row is not attached to a container.'); + } + return $this->container; } /** * Sets the container * - * @param Container|NULL $parent + * @param Container|null $parent * @param null $name ignored */ public function setParent(?IContainer $parent = null, ?string $name = null): static { + if ($parent !== null && !$parent instanceof Container) { + throw new InvalidArgumentException( + sprintf('%s can only be attached to a %s, %s given.', self::class, Container::class, $parent::class) + ); + } + $this->container = $parent; return $this; @@ -210,7 +234,7 @@ public function getOption(string $option) public function render(): Html { /** @var BootstrapRenderer $renderer */ - $renderer = $this->container->getForm()->getRenderer(); + $renderer = $this->getContainer()->getForm()->getRenderer(); $element = $renderer->configElem(RendererConfig::GRID_ROW, $this->elementPrototype); foreach ($this->cells as $cell) { diff --git a/tests/Grid/BootstrapRowTest.php b/tests/Grid/BootstrapRowTest.php index cfce79c..576e76f 100644 --- a/tests/Grid/BootstrapRowTest.php +++ b/tests/Grid/BootstrapRowTest.php @@ -6,6 +6,8 @@ use Contributte\FormsBootstrap\Grid\BootstrapCell; use Contributte\FormsBootstrap\Grid\BootstrapRow; use Nette\Application\UI\Presenter; +use Nette\InvalidArgumentException; +use Nette\InvalidStateException; use Nette\NotImplementedException; use Tests\BaseTestCase; @@ -113,6 +115,29 @@ public function testControlsAddedThroughCellStayReachableOnForm(): void $this->assertSame('name', $this->form['name']->getName()); } + public function testDetachedRowReportsNoParent(): void + { + $this->row->setParent(null); + + $this->assertNull($this->row->getParent()); + } + + public function testDetachedRowCannotBeRendered(): void + { + $this->row->setParent(null); + + $this->expectException(InvalidStateException::class); + + $this->row->getContainer(); + } + + public function testRowRefusesParentThatIsNotFormContainer(): void + { + $this->expectException(InvalidArgumentException::class); + + $this->row->setParent($this->createStub(Presenter::class)); + } + protected function setUp(): void { $this->form = new BootstrapForm();