diff --git a/composer.json b/composer.json index a40da6c..16addb8 100644 --- a/composer.json +++ b/composer.json @@ -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/" diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon new file mode 100644 index 0000000..13cd09b --- /dev/null +++ b/phpstan-baseline.neon @@ -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\ given\.$#' + identifier: argument.type + count: 3 + path: src/Replicator/Container.php + + - + message: '#^Parameter \#1 \$array of function array_filter expects array, Iterator\\|list\ given\.$#' + identifier: argument.type + count: 2 + path: src/Replicator/Container.php diff --git a/phpstan.dist.neon b/phpstan.dist.neon index 1721a26..c12f8df 100644 --- a/phpstan.dist.neon +++ b/phpstan.dist.neon @@ -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 diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php index 7470491..3cfd5e7 100644 --- a/src/Replicator/Container.php +++ b/src/Replicator/Container.php @@ -13,8 +13,7 @@ use Closure; use Nette; use ReflectionClass; -use SplObjectStorage; -use Traversable; +use WeakMap; /** * @author Filip Procházka @@ -38,7 +37,7 @@ class Container extends Nette\Forms\Container public $createDefault; /** - * @var string + * @var class-string */ public $containerClass = Nette\Forms\Container::class; @@ -53,12 +52,12 @@ class Container extends Nette\Forms\Container private $submittedBy = FALSE; /** - * @var array + * @var array */ private $created = []; /** - * @var array + * @var ?array> */ private $httpPost; @@ -110,19 +109,25 @@ protected function attached(Nette\ComponentModel\IComponent $obj): void } /** - * @return iterable + * @return array */ - 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 + * @return array */ - 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, + ); } /** @@ -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 @@ -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> $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); @@ -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) { @@ -245,13 +255,18 @@ protected function createDefault(): void } /** - * @return mixed|null + * @return ?array> */ - 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 */ // See https://github.com/nette/forms/pull/333 + $httpData = $this->getForm() + ->getHttpData(); + /** @var ?array> */ + $httpPost = Nette\Utils\Arrays::get($httpData, $path, NULL); + $this->httpPost = $httpPost; } return $this->httpPost; @@ -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; @@ -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 @@ -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 $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; @@ -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]); } @@ -321,6 +345,9 @@ public function remove(Nette\ComponentModel\Container $container, bool $cleanUpG /** * Counts filled values, filtered by given names + * + * @param array $components + * @param array $subComponents */ public function countFilledWithout(array $components = [], array $subComponents = []): int { @@ -333,12 +360,16 @@ 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; } @@ -346,18 +377,31 @@ public function countFilledWithout(array $components = [], array $subComponents return count(array_filter($rows)); } + /** + * @param array $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; + } } } @@ -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 @@ -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(); }); @@ -404,7 +445,7 @@ function (Nette\Forms\Container $_this, string $name, callable $factory, int $cr } ); - if (self::$registered) { + if (self::$registered !== null) { return; } @@ -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; @@ -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); } } diff --git a/src/Replicator/DI/ReplicatorExtension.php b/src/Replicator/DI/ReplicatorExtension.php index 8647a6a..bbcab7b 100644 --- a/src/Replicator/DI/ReplicatorExtension.php +++ b/src/Replicator/DI/ReplicatorExtension.php @@ -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());