Skip to content
Open
5 changes: 5 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,10 +34,15 @@
"php-parallel-lint/php-console-highlighter": "^1.0",
"php-parallel-lint/php-parallel-lint": "^1.4",
"phpstan/phpstan": "^2.1",
"phpstan/phpstan-deprecation-rules": "^2.0",
"phpstan/phpstan-nette": "^2.0",
"rector/rector": "^2.0",
"symplify/easy-coding-standard": "^12.5",
"nette/neon": "^3.4.4"
},
"conflict": {
"nette/component-model": "<3.1.0"
},
"autoload": {
"psr-4": {
"Kdyby\\Replicator\\": "src/Replicator/"
Expand Down
14 changes: 14 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
parameters:
ignoreErrors:
# https://github.com/phpstan/phpstan-nette/issues/141
-
message: '#^Parameter \#1 \$array of function array_filter expects array, Iterator\<int\|string, Nette\\ComponentModel\\IComponent\> given\.$#'
identifier: argument.type
count: 3
path: src/Replicator/Container.php

-
message: '#^Parameter \#1 \$array of function array_filter expects array, Iterator\<int\|string, Nette\\ComponentModel\\IComponent\>\|list\<Nette\\ComponentModel\\IComponent\> given\.$#'
identifier: argument.type
count: 2
path: src/Replicator/Container.php
9 changes: 8 additions & 1 deletion phpstan.dist.neon
Original file line number Diff line number Diff line change
@@ -1,6 +1,13 @@
# https://phpstan.org/config-reference

includes:
- vendor/phpstan/phpstan-deprecation-rules/rules.neon
- vendor/phpstan/phpstan-nette/extension.neon
- vendor/phpstan/phpstan-nette/rules.neon
- phpstan-baseline.neon

parameters:
level: 1
level: max
paths:
- src/
treatPhpDocTypesAsCertain: false
145 changes: 92 additions & 53 deletions src/Replicator/Container.php
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@
use Closure;
use Nette;
use ReflectionClass;
use SplObjectStorage;
use Traversable;
use WeakMap;

/**
* @author Filip Procházka <filip@prochazka.su>
Expand All @@ -38,7 +37,7 @@ class Container extends Nette\Forms\Container
public $createDefault;

/**
* @var string
* @var class-string<Nette\Forms\Container>
*/
public $containerClass = Nette\Forms\Container::class;

Expand All @@ -53,12 +52,12 @@ class Container extends Nette\Forms\Container
private $submittedBy = FALSE;

/**
* @var array
* @var array<string, Nette\Forms\Container>
*/
private $created = [];

/**
* @var array
* @var ?array<string, array<string, mixed>>
*/
private $httpPost;

Expand Down Expand Up @@ -110,19 +109,25 @@ protected function attached(Nette\ComponentModel\IComponent $obj): void
}

/**
* @return iterable<Nette\Forms\Container>
* @return array<Nette\Forms\Container>
*/
public function getContainers(bool $recursive = FALSE): iterable
public function getContainers(bool $recursive = FALSE): array
{
return $this->getComponents($recursive, \Nette\Forms\Container::class);
return array_filter(
$recursive ? $this->getComponentTree() : $this->getComponents(),
fn ($component): bool => $component instanceof \Nette\Forms\Container,
);
}

/**
* @return iterable<Nette\Forms\ISubmitterControl>
* @return array<Nette\Forms\Controls\SubmitButton>
*/
public function getButtons(bool $recursive = FALSE): iterable
public function getButtons(bool $recursive = FALSE): array
{
return $this->getComponents($recursive, Nette\Forms\ISubmitterControl::class);
return array_filter(
$recursive ? $this->getComponentTree() : $this->getComponents(),
fn ($component): bool => $component instanceof Nette\Forms\Controls\SubmitButton,
);
}

/**
Expand All @@ -143,10 +148,13 @@ protected function createComponent(string $name): ?Nette\ComponentModel\ICompone

private function getFirstControlName(): ?string
{
$controls = iterator_to_array($this->getComponents(FALSE, Nette\Forms\IControl::class));
$controls = array_filter(
$this->getComponents(),
fn ($component): bool => $component instanceof Nette\Forms\Control,
);
$firstControl = reset($controls);

return $firstControl ? $firstControl->name : NULL;
return $firstControl ? $firstControl->getName() : NULL;
}

protected function createContainer(): Nette\Forms\Container
Expand Down Expand Up @@ -186,17 +194,19 @@ public function createOne(?string $name = NULL): Nette\Forms\Container
throw new Nette\InvalidArgumentException("Container with name '{$name}' already exists.");
}

return $this[$name];
// ComponentModel\ArrayAccess will call createComponent and attach
// the returned to the tree, if a component with such name does not exists.
/** @var Nette\Forms\Container */
$newContainer = $this[$name];
return $newContainer;
}

/**
* @param array|Traversable $values
*
* @return Nette\Forms\Container|Container
* @param iterable<string, iterable<string, mixed>> $values
*/
public function setValues(array|object $values, bool $erase = FALSE, bool $onlyDisabled = FALSE): static
{
if (!$this->form->isAnchored() || !$this->form->isSubmitted()) {
if (!$this->form?->isAnchored() || !$this->form->isSubmitted()) {
foreach ($values as $name => $value) {
if ((is_iterable($value)) && !$this->getComponent($name, FALSE)) {
$this->createOne($name);
Expand Down Expand Up @@ -234,7 +244,7 @@ protected function createDefault(): void

if (!$this->getForm()->isSubmitted()) {
foreach (range(0, $this->createDefault - 1) as $key) {
$this->createOne($key);
$this->createOne((string) $key);
}

} elseif ($this->forceDefault) {
Expand All @@ -245,13 +255,18 @@ protected function createDefault(): void
}

/**
* @return mixed|null
* @return ?array<string, array<string, mixed>>
*/
private function getHttpData()
private function getHttpData(): ?array
{
if ($this->httpPost === NULL) {
$path = explode(self::NAME_SEPARATOR, $this->lookupPath(Nette\Forms\Form::class));
$this->httpPost = Nette\Utils\Arrays::get($this->getForm()->getHttpData(), $path, NULL);
$path = explode(self::NameSeparator, $this->lookupPath(Nette\Forms\Form::class));
/** @var array<string, mixed> */ // See https://github.com/nette/forms/pull/333
$httpData = $this->getForm()
->getHttpData();
/** @var ?array<string, array<string, mixed>> */
$httpPost = Nette\Utils\Arrays::get($httpData, $path, NULL);
$this->httpPost = $httpPost;
}

return $this->httpPost;
Expand All @@ -267,7 +282,11 @@ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpG
}

// to check if form was submitted by this one
foreach ($container->getComponents(TRUE, Nette\Forms\ISubmitterControl::class) as $button) {
$buttons = array_filter(
$container->getComponentTree(),
fn ($component): bool => $component instanceof Nette\Forms\SubmitterControl,
);
foreach ($buttons as $button) {
/** @var Nette\Forms\Controls\SubmitButton $button */
if ($button->isSubmittedBy()) {
$this->submittedBy = TRUE;
Expand All @@ -276,7 +295,7 @@ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpG
}

/** @var Nette\Forms\Controls\BaseControl[] $components */
$components = $container->getComponents(TRUE);
$components = $container->getComponentTree();
$this->removeComponent($container);

// reflection is required to hack form groups
Expand All @@ -287,12 +306,12 @@ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpG
// walk groups and clean then from removed components
$affected = [];
foreach ($this->getForm()->getGroups() as $group) {
/** @var SplObjectStorage $groupControls */
/** @var WeakMap<Nette\Forms\Control, null> $groupControls */
$groupControls = $controlsProperty->getValue($group);

foreach ($components as $control) {
if ($groupControls->contains($control)) {
$groupControls->detach($control);
if ($groupControls->offsetExists($control)) {
unset($groupControls[$control]);

if (!in_array($group, $affected, TRUE)) {
$affected[] = $group;
Expand All @@ -303,7 +322,12 @@ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpG

// remove affected & empty groups
if ($cleanUpGroups && $affected) {
foreach ($this->getForm()->getComponents(FALSE, Nette\Forms\Container::class) as $cont) {
$containers = array_filter(
$this->getForm()
->getComponents(),
fn ($component): bool => $component instanceof Nette\Forms\Container,
);
foreach ($containers as $cont) {
if ($index = array_search($cont->currentGroup, $affected, TRUE)) {
unset($affected[$index]);
}
Expand All @@ -321,6 +345,9 @@ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpG

/**
* Counts filled values, filtered by given names
*
* @param array<string> $components
* @param array<string> $subComponents
*/
public function countFilledWithout(array $components = [], array $subComponents = []): int
{
Expand All @@ -333,31 +360,48 @@ public function countFilledWithout(array $components = [], array $subComponents
$rows = [];
$subComponents = array_flip($subComponents);
foreach ($httpData as $item) {
$filter = function ($value) use (&$filter) {
$filter = function ($value) use (&$filter): bool {
if (is_array($value)) {
return count(array_filter($value, $filter)) > 0;
}

return strlen($value);
if (is_string($value)) {
return strlen($value) > 0;
}

return true;
};
$rows[] = array_filter(array_diff_key($item, $subComponents), $filter) ?: FALSE;
}

return count(array_filter($rows));
}

/**
* @param array<string> $exceptChildren
*/
public function isAllFilled(array $exceptChildren = []): bool
{
$components = [];
foreach ($this->getComponents(FALSE, Nette\Forms\IControl::class) as $control) {
/** @var Nette\Forms\Controls\BaseControl $control */
$components[] = $control->getName();
$controls = array_filter(
$this->getComponents(),
fn ($component): bool => $component instanceof Nette\Forms\Control,
);
foreach ($controls as $control) {
if (($name = $control->getName()) !== null) {
$components[] = $name;
}
}

foreach ($this->getContainers() as $container) {
foreach ($container->getComponents(TRUE, Nette\Forms\ISubmitterControl::class) as $button) {
/** @var Nette\Forms\Controls\SubmitButton $button */
$exceptChildren[] = $button->getName();
$buttons = array_filter(
$container->getComponentTree(),
fn ($component): bool => $component instanceof Nette\Forms\SubmitterControl,
);
foreach ($buttons as $button) {
if (($name = $button->getName()) !== null) {
$exceptChildren[] = $name;
}
}
}

Expand All @@ -366,9 +410,9 @@ public function isAllFilled(array $exceptChildren = []): bool
return $filled === iterator_count($this->getContainers());
}

public function addContainer($name): Nette\Forms\Container
public function addContainer(string|int $name): Nette\Forms\Container
{
return $this[$name] = new Nette\Forms\Container();
return $this[(string) $name] = new Nette\Forms\Container();
}

public function addComponent(Nette\ComponentModel\IComponent $component, ?string $name, ?string $insertBefore = NULL): static
Expand All @@ -381,14 +425,11 @@ public function addComponent(Nette\ComponentModel\IComponent $component, ?string
return $this;
}

/**
* @var bool
*/
private static $registered = FALSE;
private static ?string $registered = null;

public static function register(string $methodName = 'addDynamic'): void
{
if (self::$registered) {
if (self::$registered !== null) {
Nette\Forms\Container::extensionMethod(self::$registered, function () {
throw new Nette\MemberAccessException();
});
Expand All @@ -404,7 +445,7 @@ function (Nette\Forms\Container $_this, string $name, callable $factory, int $cr
}
);

if (self::$registered) {
if (self::$registered !== null) {
return;
}

Expand All @@ -415,13 +456,15 @@ function (Nette\Forms\Controls\SubmitButton $_this, ?callable $callback = NULL)
$_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($callback) {
/** @var self $replicator */
$replicator = $button->lookup(static::class);
$container = $button->parent;
\assert($container instanceof Nette\ComponentModel\Container);
if (is_callable($callback)) {
$callback($replicator, $button->parent);
$callback($replicator, $container);
}
if ($form = $button->getForm(FALSE)) {
$form->onSuccess = [];
}
$replicator->remove($button->parent);
$replicator->remove($container);
};

return $_this;
Expand All @@ -434,13 +477,9 @@ function (Nette\Forms\Controls\SubmitButton $_this, bool $allowEmpty = FALSE, ?c
$_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($allowEmpty, $callback) {
/** @var self $replicator */
$replicator = $button->lookup(static::class);
if (!is_bool($allowEmpty)) {
$callback = Closure::fromCallable($allowEmpty);
$allowEmpty = FALSE;
}
if ($allowEmpty === TRUE || $replicator->isAllFilled() === TRUE) {
if ($allowEmpty || $replicator->isAllFilled() === TRUE) {
$newContainer = $replicator->createOne();
if (is_callable($callback)) {
if ($callback !== NULL) {
$callback($replicator, $newContainer);
}
}
Expand Down
2 changes: 1 addition & 1 deletion src/Replicator/DI/ReplicatorExtension.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public function afterCompile(Nette\PhpGenerator\ClassType $class): void
$init->addBody(Container::class . '::register();');
}

public static function register(Nette\Configurator $configurator): void
public static function register(Nette\Bootstrap\Configurator $configurator): void
{
$configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) {
$compiler->addExtension('formsReplicator', new ReplicatorExtension());
Expand Down