diff --git a/.gitignore b/.gitignore old mode 100755 new mode 100644 diff --git a/.travis.yml b/.travis.yml index 3560e33..24cc2d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,47 +1,50 @@ language: php - sudo: false +php: + - 7.1 + - 7.2 + - 7.3 cache: directories: - $HOME/.composer/cache -env: - - NETTE=nette-2.4-dev - - NETTE=nette-2.4 - -php: - - 5.6 - - 7.0 - -matrix: - include: - - php: 5.6 - env: NETTE=nette-2.4 COMPOSER_EXTRA_ARGS="--prefer-lowest --prefer-stable" - - php: 7.0 - env: NETTE=nette-2.4 COVERAGE="--coverage ./coverage.xml --coverage-src ./src" TESTER_RUNTIME="phpdbg" - allow_failures: - - php: 7.0 - env: NETTE=nette-2.4 COVERAGE="--coverage ./coverage.xml --coverage-src ./src" TESTER_RUNTIME="phpdbg" - before_install: - - travis_retry composer self-update - - wget -O /tmp/composer-nette https://raw.githubusercontent.com/Kdyby/TesterExtras/master/bin/composer-nette.php - - php /tmp/composer-nette + - phpenv config-rm xdebug.ini || return 0 install: - - travis_retry composer update --no-interaction --prefer-dist $COMPOSER_EXTRA_ARGS + - travis_retry composer install --no-progress --prefer-dist - travis_retry composer create-project --no-interaction jakub-onderka/php-parallel-lint /tmp/php-parallel-lint - - travis_retry composer create-project --no-interaction kdyby/code-checker /tmp/code-checker - - travis_retry wget -O /tmp/coveralls.phar https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar script: - - vendor/bin/tester $COVERAGE -s -p ${TESTER_RUNTIME:-php} -c ./tests/php.ini-unix ./tests/KdybyTests/ - php /tmp/php-parallel-lint/parallel-lint.php -e php,phpt --exclude vendor . - - php /tmp/code-checker/src/code-checker.php --short-arrays - -after_script: - - if [ "$COVERAGE" != "" ]; then php /tmp/coveralls.phar --verbose --config tests/.coveralls.yml || true; fi + - ./vendor/bin/tester -s -p php --colors 1 -C ./tests/Replicator after_failure: - - 'for i in $(find ./tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done' + - for i in $(find tests -name \*.actual); do echo "--- $i"; cat $i; echo; echo; done + +jobs: + include: + - env: title="Lowest Dependencies 7.1" + php: 7.1 + install: + - travis_retry composer update --no-progress --prefer-dist --prefer-lowest + script: + - ./vendor/bin/tester -s -p php --colors 1 -C ./tests/Replicator + + - stage: Test Coverage + php: 7.1 + script: + - ./vendor/bin/tester -s -p phpdbg --colors 1 -C --coverage ./coverage.xml --coverage-src ./src ./tests/Replicator + after_script: + - travis_retry wget https://github.com/php-coveralls/php-coveralls/releases/download/v2.1.0/php-coveralls.phar + - php php-coveralls.phar --verbose --config tests/.coveralls.yml + + - stage: Outdated Dependencies + php: 7.1 + script: + - composer outdated --direct --strict + + allow_failures: + - stage: Test Coverage + - stage: Outdated Dependencies diff --git a/README.md b/README.md old mode 100755 new mode 100644 index fc2ba0b..413ad01 --- a/README.md +++ b/README.md @@ -1,47 +1,55 @@ -Kdyby/FormsReplicator -====== +# Kdyby/FormsReplicator -[![Build Status](https://travis-ci.org/Kdyby/FormsReplicator.svg?branch=master)](https://travis-ci.org/Kdyby/FormsReplicator) -[![Downloads this Month](https://img.shields.io/packagist/dm/kdyby/forms-replicator.svg)](https://packagist.org/packages/kdyby/forms-replicator) -[![Latest stable](https://img.shields.io/packagist/v/kdyby/forms-replicator.svg)](https://packagist.org/packages/kdyby/forms-replicator) -[![Coverage Status](https://coveralls.io/repos/github/Kdyby/FormsReplicator/badge.svg?branch=master)](https://coveralls.io/github/Kdyby/FormsReplicator?branch=master) -[![Join the chat at https://gitter.im/Kdyby/Help](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/Kdyby/Help) - - -Save me please! ---------------- - -The maintainer of this project has no more time to maintain it. It may even contain unfixed bugs :( - -If you need something like this and you're willing to join in, you're welcome to take over this project. +----- -![help](https://cdn.kdyby.org/keyboard-help.png) +[![Build Status](https://img.shields.io/travis/Kdyby/FormsReplicator.svg?style=flat-square)](https://travis-ci.org/Kdyby/FormsReplicator) +[![Code coverage](https://img.shields.io/coveralls/Kdyby/FormsReplicator.svg?style=flat-square)](https://coveralls.io/github/Kdyby/FormsReplicator) +[![Licence](https://img.shields.io/packagist/l/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator) +[![Downloads this Month](https://img.shields.io/packagist/dm/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator) +[![Downloads total](https://img.shields.io/packagist/dt/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator) +[![Latest stable](https://img.shields.io/packagist/v/Kdyby/FormsReplicator.svg?style=flat-square)](https://packagist.org/packages/kdyby/forms-replicator) +## Discussion / Help -Requirements ------------- +[![Join the chat](https://img.shields.io/gitter/room/Kdyby/Help.svg?style=flat-square)](https://gitter.im/Kdyby/Help) -Kdyby/FormsReplicator requires PHP 5.5 or higher. +## Install -- [Nette Framework](https://github.com/nette/nette) +```sh +composer require kdyby/forms-replicator +``` +## Versions -Installation ------------- +| State | Version | Branch | PHP | +|-------------|--------------|----------|----------| +| rc | `^2.0.0@rc` | `master` | `>= 7.1` | +| stable | `^1.3.0` | `master` | `>= 5.6` | -The best way to install Kdyby/FormsReplicator is using [Composer](http://getcomposer.org/): +## Overview -```sh -$ composer require kdyby/forms-replicator:~1.1 -``` +- [Learn more in the documentation](https://github.com/Kdyby/FormsReplicator/blob/master/docs/en/index.md) +## Maintainers -Documentation ------------- + + + + + + +
+ + + +
+ David Šolc +
-Learn more in the [documentation](https://github.com/Kdyby/FormsReplicator/blob/master/docs/en/index.md). +----- +Thank you for testing, reporting and contributing. ----- -Homepage [http://www.kdyby.org](http://www.kdyby.org) and repository [http://github.com/kdyby/FormsReplicator](http://github.com/kdyby/FormsReplicator). +Homepage [https://www.kdyby.org](https://www.kdyby.org) and repository [https://github.com/Kdyby/FormsReplicator](https://github.com/Kdyby/FormsReplicator). diff --git a/composer.json b/composer.json index 819bbea..85551a8 100644 --- a/composer.json +++ b/composer.json @@ -10,49 +10,44 @@ "name": "Filip Procházka", "homepage": "http://filip-prochazka.com", "email": "filip@prochazka.su" + }, + { + "name": "David Šolc", + "homepage": "https://solc.dev", + "email": "solcik@gmail.com" } ], "support": { - "email": "filip@prochazka.su", - "issues": "https://github.com/kdyby/replicator/issues" + "issues": "https://github.com/Kdyby/FormsReplicator/issues" }, "require": { - "nette/forms": "~2.4@dev" + "php": ">=7.1", + "nette/forms": "^3.0", + "nette/utils": "^3.0" }, "require-dev": { - "php": ">=5.6", - "nette/application": "~2.4@dev", - "nette/bootstrap": "~2.4@dev", - "nette/caching": "~2.4@dev", - "nette/component-model": "~2.2@dev", - "nette/database": "~2.4@dev", - "nette/deprecated": "~2.4@dev", - "nette/di": "~2.4@dev", - "nette/finder": "~2.4@dev", - "nette/forms": "~2.4@dev", - "nette/http": "~2.4@dev", - "nette/mail": "~2.4@dev", - "nette/neon": "~2.4@dev", - "nette/php-generator": "~2.4@dev", - "nette/reflection": "~2.4@dev", - "nette/robot-loader": "~2.4@dev", - "nette/safe-stream": "~2.3@dev", - "nette/security": "~2.4@dev", - "nette/tokenizer": "~2.2@dev", - "nette/utils": "~2.4@dev", - "latte/latte": "~2.4@dev", - "tracy/tracy": "~2.4@dev", - - "nette/tester": "~1.7" + "nette/application": "^3.0@rc", + "nette/bootstrap": "^3.0@rc", + "nette/di": "^3.0@rc", + "tracy/tracy": "^2.6", + "nette/tester": "^2.2" }, "autoload": { - "psr-0": { - "Kdyby\\Replicator\\": "src/" + "psr-4": { + "Kdyby\\Replicator\\": "src/Replicator/" + } + }, + "autoload-dev": { + "psr-4": { + "KdybyTests\\Replicator\\": "tests/Replicator/" } }, + "suggest": { + "nette/di": "to use ReplicatorExtension[CompilerExtension]" + }, "extra": { "branch-alias": { - "dev-master": "1.2-dev" + "dev-master": "2.0-dev" } } } diff --git a/src/Kdyby/Replicator/Container.php b/src/Kdyby/Replicator/Container.php deleted file mode 100644 index 93ab02e..0000000 --- a/src/Kdyby/Replicator/Container.php +++ /dev/null @@ -1,562 +0,0 @@ - - * @author Jan Tvrdík - * - * @method \Nette\Application\UI\Form getForm() - * @property \Nette\Forms\Container $parent - */ -class Container extends Nette\Forms\Container -{ - - /** @var bool */ - public $forceDefault; - - /** @var int */ - public $createDefault; - - /** @var string */ - public $containerClass = 'Nette\Forms\Container'; - - /** @var callable */ - protected $factoryCallback; - - /** @var boolean */ - private $submittedBy = FALSE; - - /** @var array */ - private $created = []; - - /** @var \Nette\Http\IRequest */ - private $httpRequest; - - /** @var array */ - private $httpPost; - - - - /** - * @param callable $factory - * @param int $createDefault - * @param bool $forceDefault - * - * @throws \Nette\InvalidArgumentException - */ - public function __construct($factory, $createDefault = 0, $forceDefault = FALSE) - { - parent::__construct(); - $this->monitor('Nette\Application\UI\Presenter'); - $this->monitor('Nette\Forms\Form'); - - try { - $this->factoryCallback = Callback::closure($factory); - } catch (Nette\InvalidArgumentException $e) { - $type = is_object($factory) ? 'instanceof ' . get_class($factory) : gettype($factory); - throw new Nette\InvalidArgumentException( - 'Replicator requires callable factory, ' . $type . ' given.', 0, $e - ); - } - - $this->createDefault = (int)$createDefault; - $this->forceDefault = $forceDefault; - } - - - - /** - * @param callable $factory - */ - public function setFactory($factory) - { - $this->factoryCallback = Callback::closure($factory); - } - - - - /** - * Magical component factory - * - * @param \Nette\ComponentModel\IContainer - */ - protected function attached($obj) - { - parent::attached($obj); - - if ( - !$obj instanceof Nette\Application\UI\Presenter - && - $this->form instanceof Nette\Application\UI\Form - ) { - return; - } - - $this->loadHttpData(); - $this->createDefault(); - } - - - - /** - * @param boolean $recursive - * @return \ArrayIterator|\Nette\Forms\Container[] - */ - public function getContainers($recursive = FALSE) - { - return $this->getComponents($recursive, 'Nette\Forms\Container'); - } - - - - /** - * @param boolean $recursive - * @return \ArrayIterator|Nette\Forms\Controls\SubmitButton[] - */ - public function getButtons($recursive = FALSE) - { - return $this->getComponents($recursive, 'Nette\Forms\ISubmitterControl'); - } - - - - /** - * Magical component factory - * - * @param string $name - * @return \Nette\Forms\Container - */ - protected function createComponent($name) - { - $container = $this->createContainer($name); - $container->currentGroup = $this->currentGroup; - $this->addComponent($container, $name, $this->getFirstControlName()); - - Callback::invoke($this->factoryCallback, $container); - - return $this->created[$container->name] = $container; - } - - - - /** - * @return string - */ - private function getFirstControlName() - { - $controls = iterator_to_array($this->getComponents(FALSE, 'Nette\Forms\IControl')); - $firstControl = reset($controls); - return $firstControl ? $firstControl->name : NULL; - } - - - - /** - * @param string $name - * - * @return \Nette\Forms\Container - */ - protected function createContainer($name) - { - $class = $this->containerClass; - return new $class(); - } - - - - /** - * @return boolean - */ - public function isSubmittedBy() - { - if ($this->submittedBy) { - return TRUE; - } - - foreach ($this->getButtons(TRUE) as $button) { - if ($button->isSubmittedBy()) { - return $this->submittedBy = TRUE; - } - } - - return FALSE; - } - - - - /** - * Create new container - * - * @param string|int $name - * - * @throws \Nette\InvalidArgumentException - * @return \Nette\Forms\Container - */ - public function createOne($name = NULL) - { - if ($name === NULL) { - $names = array_keys(iterator_to_array($this->getContainers())); - $name = $names ? max($names) + 1 : 0; - } - - // Container is overriden, therefore every request for getComponent($name, FALSE) would return container - if (isset($this->created[$name])) { - throw new Nette\InvalidArgumentException("Container with name '$name' already exists."); - } - - return $this[$name]; - } - - - - /** - * @param array|\Traversable $values - * @param bool $erase - * @param bool $onlyDisabled - * @return \Nette\Forms\Container|Container - */ - public function setValues($values, $erase = FALSE, $onlyDisabled = FALSE) - { - if (!$this->form->isAnchored() || !$this->form->isSubmitted()) { - foreach ($values as $name => $value) { - if ((is_array($value) || $value instanceof \Traversable) && !$this->getComponent($name, FALSE)) { - $this->createOne($name); - } - } - } - - return parent::setValues($values, $erase, $onlyDisabled); - } - - - - /** - * Loads data received from POST - * @internal - */ - protected function loadHttpData() - { - if (!$this->getForm()->isSubmitted()) { - return; - } - - foreach ((array) $this->getHttpData() as $name => $value) { - if ((is_array($value) || $value instanceof \Traversable) && !$this->getComponent($name, FALSE)) { - $this->createOne($name); - } - } - } - - - - /** - * Creates default containers - * @internal - */ - protected function createDefault() - { - if (!$this->createDefault) { - return; - } - - if (!$this->getForm()->isSubmitted()) { - foreach (range(0, $this->createDefault - 1) as $key) { - $this->createOne($key); - } - - } elseif ($this->forceDefault) { - while (iterator_count($this->getContainers()) < $this->createDefault) { - $this->createOne(); - } - } - } - - - - /** - * @param string $name - * @return array|null - */ - protected function getContainerValues($name) - { - $post = $this->getHttpData(); - return isset($post[$name]) ? $post[$name] : NULL; - } - - - - /** - * @return mixed|NULL - */ - private function getHttpData() - { - if ($this->httpPost === NULL) { - $path = explode(self::NAME_SEPARATOR, $this->lookupPath('Nette\Forms\Form')); - $this->httpPost = Nette\Utils\Arrays::get($this->getForm()->getHttpData(), $path, NULL); - } - - return $this->httpPost; - } - - - - /** - * @internal - * @param \Nette\Application\Request $request - * @return Container - */ - public function setRequest(Nette\Application\Request $request) - { - $this->httpRequest = $request; - return $this; - } - - - - /** - * @return \Nette\Application\Request - */ - private function getRequest() - { - if ($this->httpRequest !== NULL) { - return $this->httpRequest; - } - - return $this->httpRequest = $this->getForm()->getPresenter()->getRequest(); - } - - - - /** - * @param \Nette\Forms\Container $container - * @param boolean $cleanUpGroups - * - * @throws \Nette\InvalidArgumentException - * @return void - */ - public function remove(Nette\Forms\Container $container, $cleanUpGroups = FALSE) - { - if ($container->parent !== $this) { - throw new Nette\InvalidArgumentException('Given component ' . $container->name . ' is not children of ' . $this->name . '.'); - } - - // to check if form was submitted by this one - foreach ($container->getComponents(TRUE, 'Nette\Forms\ISubmitterControl') as $button) { - /** @var \Nette\Forms\Controls\SubmitButton $button */ - if ($button->isSubmittedBy()) { - $this->submittedBy = TRUE; - break; - } - } - - /** @var \Nette\Forms\Controls\BaseControl[] $components */ - $components = $container->getComponents(TRUE); - $this->removeComponent($container); - - // reflection is required to hack form groups - $groupRefl = Nette\Reflection\ClassType::from('Nette\Forms\ControlGroup'); - $controlsProperty = $groupRefl->getProperty('controls'); - $controlsProperty->setAccessible(TRUE); - - // walk groups and clean then from removed components - $affected = []; - foreach ($this->getForm()->getGroups() as $group) { - /** @var \SplObjectStorage $groupControls */ - $groupControls = $controlsProperty->getValue($group); - - foreach ($components as $control) { - if ($groupControls->contains($control)) { - $groupControls->detach($control); - - if (!in_array($group, $affected, TRUE)) { - $affected[] = $group; - } - } - } - } - - // remove affected & empty groups - if ($cleanUpGroups && $affected) { - foreach ($this->getForm()->getComponents(FALSE, 'Nette\Forms\Container') as $container) { - if ($index = array_search($container->currentGroup, $affected, TRUE)) { - unset($affected[$index]); - } - } - - /** @var \Nette\Forms\ControlGroup[] $affected */ - foreach ($affected as $group) { - if (!$group->getControls() && in_array($group, $this->getForm()->getGroups(), TRUE)) { - $this->getForm()->removeGroup($group); - } - } - } - } - - - - /** - * Counts filled values, filtered by given names - * - * @param array $components - * @param array $subComponents - * @return int - */ - public function countFilledWithout(array $components = [], array $subComponents = []) - { - $httpData = array_diff_key((array)$this->getHttpData(), array_flip($components)); - - if (!$httpData) { - return 0; - } - - $rows = []; - $subComponents = array_flip($subComponents); - foreach ($httpData as $item) { - $filter = function ($value) use (&$filter) { - if (is_array($value)) { - return count(array_filter($value, $filter)) > 0; - } - return strlen($value); - }; - $rows[] = array_filter(array_diff_key($item, $subComponents), $filter) ?: FALSE; - } - - return count(array_filter($rows)); - } - - - - /** - * @param array $exceptChildren - * @return bool - */ - public function isAllFilled(array $exceptChildren = []) - { - $components = []; - foreach ($this->getComponents(FALSE, 'Nette\Forms\IControl') as $control) { - /** @var \Nette\Forms\Controls\BaseControl $control */ - $components[] = $control->getName(); - } - - foreach ($this->getContainers() as $container) { - foreach ($container->getComponents(TRUE, 'Nette\Forms\ISubmitterControl') as $button) { - /** @var \Nette\Forms\Controls\SubmitButton $button */ - $exceptChildren[] = $button->getName(); - } - } - - $filled = $this->countFilledWithout($components, array_unique($exceptChildren)); - return $filled === iterator_count($this->getContainers()); - } - - - - /** - * @param $name - * @return \Nette\Forms\Container - */ - public function addContainer($name) - { - return $this[$name] = new Nette\Forms\Container(); - } - - - - /** - * @param \Nette\ComponentModel\IComponent $component - * @param $name - * @param null $insertBefore - * @return \Nette\ComponentModel\Container|\Nette\Forms\Container - */ - public function addComponent(Nette\ComponentModel\IComponent $component, $name, $insertBefore = NULL) - { - $group = $this->currentGroup; - $this->currentGroup = NULL; - parent::addComponent($component, $name, $insertBefore); - $this->currentGroup = $group; - return $this; - } - - - - /** - * @var bool - */ - private static $registered = FALSE; - - /** - * @param string $methodName - * @return void - */ - public static function register($methodName = 'addDynamic') - { - if (self::$registered) { - Nette\Utils\ObjectMixin::setExtensionMethod(Nette\Forms\Container::class, self::$registered, function () { - throw new Nette\MemberAccessException; - }); - } - - Nette\Utils\ObjectMixin::setExtensionMethod(Nette\Forms\Container::class, $methodName, function (Nette\Forms\Container $_this, $name, $factory, $createDefault = 0, $forceDefault = FALSE) { - $control = new Container($factory, $createDefault, $forceDefault); - $control->currentGroup = $_this->currentGroup; - return $_this[$name] = $control; - }); - - if (self::$registered) { - return; - } - - Nette\Utils\ObjectMixin::setExtensionMethod(SubmitButton::class, 'addRemoveOnClick', function (SubmitButton $_this, $callback = NULL) { - $_this->setValidationScope(FALSE); - $_this->onClick[] = function (SubmitButton $button) use ($callback) { - $replicator = $button->lookup(__NAMESPACE__ . '\Container'); - /** @var Container $replicator */ - if (is_callable($callback)) { - Callback::invoke($callback, $replicator, $button->parent); - } - if ($form = $button->getForm(FALSE)) { - $form->onSuccess = []; - } - $replicator->remove($button->parent); - }; - return $_this; - }); - - Nette\Utils\ObjectMixin::setExtensionMethod(SubmitButton::class, 'addCreateOnClick', function (SubmitButton $_this, $allowEmpty = FALSE, $callback = NULL) { - $_this->onClick[] = function (SubmitButton $button) use ($allowEmpty, $callback) { - $replicator = $button->lookup(__NAMESPACE__ . '\Container'); - /** @var Container $replicator */ - if (!is_bool($allowEmpty)) { - $callback = Callback::closure($allowEmpty); - $allowEmpty = FALSE; - } - if ($allowEmpty === TRUE || $replicator->isAllFilled() === TRUE) { - $newContainer = $replicator->createOne(); - if (is_callable($callback)) { - Callback::invoke($callback, $replicator, $newContainer); - } - } - $button->getForm()->onSuccess = []; - }; - return $_this; - }); - - self::$registered = $methodName; - } - -} diff --git a/src/Replicator/Container.php b/src/Replicator/Container.php new file mode 100644 index 0000000..ac46c78 --- /dev/null +++ b/src/Replicator/Container.php @@ -0,0 +1,462 @@ + + * @author Jan Tvrdík + * + * @method Nette\Application\UI\Form getForm() + * @property Nette\Forms\Container $parent + */ +class Container extends Nette\Forms\Container +{ + + /** @var bool */ + public $forceDefault; + + /** @var int */ + public $createDefault; + + /** @var string */ + public $containerClass = Nette\Forms\Container::class; + + /** @var callable */ + protected $factoryCallback; + + /** @var boolean */ + private $submittedBy = FALSE; + + /** @var array */ + private $created = []; + + /** @var array */ + private $httpPost; + + + /** + * @throws Nette\InvalidArgumentException + */ + public function __construct(callable $factory, int $createDefault = 0, bool $forceDefault = FALSE) + { + $this->monitor(Nette\Application\UI\Presenter::class); + $this->monitor(Nette\Forms\Form::class); + + try { + $this->factoryCallback = Closure::fromCallable($factory); + } catch (Nette\InvalidArgumentException $e) { + $type = is_object($factory) ? 'instanceof ' . get_class($factory) : gettype($factory); + throw new Nette\InvalidArgumentException( + 'Replicator requires callable factory, ' . $type . ' given.', 0, $e + ); + } + + $this->createDefault = $createDefault; + $this->forceDefault = $forceDefault; + } + + + public function setFactory(callable $factory): void + { + $this->factoryCallback = Closure::fromCallable($factory); + } + + + /** + * Magical component factory + */ + protected function attached(Nette\ComponentModel\IComponent $obj): void + { + parent::attached($obj); + + if ( + !$obj instanceof Nette\Application\UI\Presenter + && + $this->form instanceof Nette\Application\UI\Form + ) { + return; + } + + $this->loadHttpData(); + $this->createDefault(); + } + + + /** + * @return Iterator|Nette\Forms\Container[] + */ + public function getContainers(bool $recursive = FALSE): Iterator + { + return $this->getComponents($recursive, \Nette\Forms\Container::class); + } + + + /** + * @return Iterator|Nette\Forms\Controls\SubmitButton[] + */ + public function getButtons(bool $recursive = FALSE): Iterator + { + return $this->getComponents($recursive, Nette\Forms\ISubmitterControl::class); + } + + + /** + * Magical component factory + * + * @return Nette\Forms\Container + */ + protected function createComponent(string $name): ?Nette\ComponentModel\IComponent + { + $container = $this->createContainer(); + $container->currentGroup = $this->currentGroup; + $this->addComponent($container, $name, $this->getFirstControlName()); + + ($this->factoryCallback)($container); + + return $this->created[$container->name] = $container; + } + + + private function getFirstControlName(): ?string + { + $controls = iterator_to_array($this->getComponents(FALSE, Nette\Forms\IControl::class)); + $firstControl = reset($controls); + + return $firstControl ? $firstControl->name : NULL; + } + + + protected function createContainer(): Nette\Forms\Container + { + $class = $this->containerClass; + + return new $class(); + } + + + public function isSubmittedBy(): bool + { + if ($this->submittedBy) { + return TRUE; + } + + foreach ($this->getButtons(TRUE) as $button) { + if ($button->isSubmittedBy()) { + return $this->submittedBy = TRUE; + } + } + + return FALSE; + } + + + /** + * @throws Nette\InvalidArgumentException + */ + public function createOne(?string $name = NULL): Nette\Forms\Container + { + if ($name === NULL) { + $names = array_keys(iterator_to_array($this->getContainers())); + $name = $names ? max($names) + 1 : 0; + } + + // Container is overriden, therefore every request for getComponent($name, FALSE) would return container + if (isset($this->created[$name])) { + throw new Nette\InvalidArgumentException("Container with name '$name' already exists."); + } + + return $this[$name]; + } + + + /** + * @param array|Traversable $values + * + * @return Nette\Forms\Container|Container + */ + public function setValues($values, bool $erase = FALSE, bool $onlyDisabled = FALSE) + { + if (!$this->form->isAnchored() || !$this->form->isSubmitted()) { + foreach ($values as $name => $value) { + if ((is_array($value) || $value instanceof Traversable) && !$this->getComponent($name, FALSE)) { + $this->createOne($name); + } + } + } + + return parent::setValues($values, $erase, $onlyDisabled); + } + + + /** + * @internal + */ + protected function loadHttpData(): void + { + if (!$this->getForm()->isSubmitted()) { + return; + } + + foreach ((array) $this->getHttpData() as $name => $value) { + if ((is_array($value) || $value instanceof Traversable) && !$this->getComponent($name, FALSE)) { + $this->createOne($name); + } + } + } + + + /** + * @internal + */ + protected function createDefault(): void + { + if (!$this->createDefault) { + return; + } + + if (!$this->getForm()->isSubmitted()) { + foreach (range(0, $this->createDefault - 1) as $key) { + $this->createOne($key); + } + + } elseif ($this->forceDefault) { + while (iterator_count($this->getContainers()) < $this->createDefault) { + $this->createOne(); + } + } + } + + + /** + * @return mixed|NULL + */ + private function getHttpData() + { + 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); + } + + return $this->httpPost; + } + + + /** + * @throws Nette\InvalidArgumentException + */ + public function remove(Nette\ComponentModel\Container $container, bool $cleanUpGroups = FALSE): void + { + if ($container->parent !== $this) { + throw new Nette\InvalidArgumentException('Given component ' . $container->name . ' is not children of ' . $this->name . '.'); + } + + // to check if form was submitted by this one + foreach ($container->getComponents(TRUE, Nette\Forms\ISubmitterControl::class) as $button) { + /** @var Nette\Forms\Controls\SubmitButton $button */ + if ($button->isSubmittedBy()) { + $this->submittedBy = TRUE; + break; + } + } + + /** @var Nette\Forms\Controls\BaseControl[] $components */ + $components = $container->getComponents(TRUE); + $this->removeComponent($container); + + // reflection is required to hack form groups + $groupRefl = new ReflectionClass(Nette\Forms\ControlGroup::class); + $controlsProperty = $groupRefl->getProperty('controls'); + $controlsProperty->setAccessible(TRUE); + + // walk groups and clean then from removed components + $affected = []; + foreach ($this->getForm()->getGroups() as $group) { + /** @var SplObjectStorage $groupControls */ + $groupControls = $controlsProperty->getValue($group); + + foreach ($components as $control) { + if ($groupControls->contains($control)) { + $groupControls->detach($control); + + if (!in_array($group, $affected, TRUE)) { + $affected[] = $group; + } + } + } + } + + // remove affected & empty groups + if ($cleanUpGroups && $affected) { + foreach ($this->getForm()->getComponents(FALSE, Nette\Forms\Container::class) as $cont) { + if ($index = array_search($cont->currentGroup, $affected, TRUE)) { + unset($affected[$index]); + } + } + + /** @var Nette\Forms\ControlGroup[] $affected */ + foreach ($affected as $group) { + if (!$group->getControls() && in_array($group, $this->getForm()->getGroups(), TRUE)) { + $this->getForm()->removeGroup($group); + } + } + } + } + + + /** + * Counts filled values, filtered by given names + */ + public function countFilledWithout(array $components = [], array $subComponents = []): int + { + $httpData = array_diff_key((array) $this->getHttpData(), array_flip($components)); + + if (!$httpData) { + return 0; + } + + $rows = []; + $subComponents = array_flip($subComponents); + foreach ($httpData as $item) { + $filter = function ($value) use (&$filter) { + if (is_array($value)) { + return count(array_filter($value, $filter)) > 0; + } + + return strlen($value); + }; + $rows[] = array_filter(array_diff_key($item, $subComponents), $filter) ?: FALSE; + } + + return count(array_filter($rows)); + } + + + 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(); + } + + foreach ($this->getContainers() as $container) { + foreach ($container->getComponents(TRUE, Nette\Forms\ISubmitterControl::class) as $button) { + /** @var Nette\Forms\Controls\SubmitButton $button */ + $exceptChildren[] = $button->getName(); + } + } + + $filled = $this->countFilledWithout($components, array_unique($exceptChildren)); + + return $filled === iterator_count($this->getContainers()); + } + + + public function addContainer($name): Nette\Forms\Container + { + return $this[$name] = new Nette\Forms\Container(); + } + + + public function addComponent(Nette\ComponentModel\IComponent $component, ?string $name, ?string $insertBefore = NULL): Nette\ComponentModel\IContainer + { + $group = $this->currentGroup; + $this->currentGroup = NULL; + parent::addComponent($component, $name, $insertBefore); + $this->currentGroup = $group; + + return $this; + } + + + /** + * @var bool + */ + private static $registered = FALSE; + + + public static function register(string $methodName = 'addDynamic'): void + { + if (self::$registered) { + Nette\Forms\Container::extensionMethod(self::$registered, function () { + throw new Nette\MemberAccessException; + }); + } + + Nette\Forms\Container::extensionMethod( + $methodName, + function (Nette\Forms\Container $_this, string $name, callable $factory, int $createDefault = 0, bool $forceDefault = FALSE) { + $control = new Container($factory, $createDefault, $forceDefault); + $control->currentGroup = $_this->currentGroup; + + return $_this[$name] = $control; + } + ); + + if (self::$registered) { + return; + } + + Nette\Forms\Controls\SubmitButton::extensionMethod( + 'addRemoveOnClick', + function (Nette\Forms\Controls\SubmitButton $_this, ?callable $callback = NULL) { + $_this->setValidationScope([]); + $_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($callback) { + /** @var Container $replicator */ + $replicator = $button->lookup(Container::class); + if (is_callable($callback)) { + $callback($replicator, $button->parent); + } + if ($form = $button->getForm(FALSE)) { + $form->onSuccess = []; + } + $replicator->remove($button->parent); + }; + + return $_this; + } + ); + + Nette\Forms\Controls\SubmitButton::extensionMethod( + 'addCreateOnClick', + function (Nette\Forms\Controls\SubmitButton $_this, bool $allowEmpty = FALSE, ?callable $callback = NULL) { + $_this->onClick[] = function (Nette\Forms\Controls\SubmitButton $button) use ($allowEmpty, $callback) { + /** @var Container $replicator */ + $replicator = $button->lookup(Container::class); + if (!is_bool($allowEmpty)) { + $callback = Closure::fromCallable($allowEmpty); + $allowEmpty = FALSE; + } + if ($allowEmpty === TRUE || $replicator->isAllFilled() === TRUE) { + $newContainer = $replicator->createOne(); + if (is_callable($callback)) { + $callback($replicator, $newContainer); + } + } + $button->getForm()->onSuccess = []; + }; + + return $_this; + } + ); + + self::$registered = $methodName; + } + +} diff --git a/src/Kdyby/Replicator/DI/ReplicatorExtension.php b/src/Replicator/DI/ReplicatorExtension.php similarity index 77% rename from src/Kdyby/Replicator/DI/ReplicatorExtension.php rename to src/Replicator/DI/ReplicatorExtension.php index aaa951a..054d418 100644 --- a/src/Kdyby/Replicator/DI/ReplicatorExtension.php +++ b/src/Replicator/DI/ReplicatorExtension.php @@ -10,10 +10,8 @@ namespace Kdyby\Replicator\DI; -use Kdyby; +use Kdyby\Replicator\Container; use Nette; -use Nette\PhpGenerator as Code; - /** @@ -22,20 +20,16 @@ class ReplicatorExtension extends Nette\DI\CompilerExtension { - public function afterCompile(Code\ClassType $class) + public function afterCompile(Nette\PhpGenerator\ClassType $class): void { parent::afterCompile($class); $init = $class->getMethod('initialize'); - $init->addBody('Kdyby\Replicator\Container::register();'); + $init->addBody(Container::class . '::register();'); } - - /** - * @param \Nette\Configurator $configurator - */ - public static function register(Nette\Configurator $configurator) + public static function register(Nette\Configurator $configurator): void { $configurator->onCompile[] = function ($config, Nette\DI\Compiler $compiler) { $compiler->addExtension('formsReplicator', new ReplicatorExtension()); diff --git a/tests/KdybyTests/.gitignore b/tests/.gitignore old mode 100755 new mode 100644 similarity index 100% rename from tests/KdybyTests/.gitignore rename to tests/.gitignore diff --git a/tests/KdybyTests/Replicator/Container.phpt b/tests/KdybyTests/Replicator/Container.phpt deleted file mode 100644 index 7060961..0000000 --- a/tests/KdybyTests/Replicator/Container.phpt +++ /dev/null @@ -1,280 +0,0 @@ - - * @package Kdyby\Replicator - */ - -namespace KdybyTests\Replicator; - -use Kdyby; -use Kdyby\Replicator\Container; -use Nette; -use Nette\Application\Request; -use Nette\Application\UI; -use Nette\Forms\Controls; -use Tester; -use Tester\Assert; - -require_once __DIR__ . '/../bootstrap.php'; - - - -/** - * @author Filip Procházka - */ -class ContainerTest extends Tester\TestCase -{ - - public function testReplicating() - { - $replicator = new Container(function (Nette\Forms\Container $container) { - $container->addText('name', "Name"); - }); - - Assert::true($replicator[0]['name'] instanceof Controls\TextInput); - Assert::true($replicator[2]['name'] instanceof Controls\TextInput); - Assert::true($replicator[1000]['name'] instanceof Controls\TextInput); - } - - - - public function testRendering_attachAfterDefinition() - { - $form = new BaseForm(); - $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { - $user->addText('name'); - }, 1); - $users->addSubmit('add'); - - $this->connectForm($form); - - // container and submit button - Assert::same(2, iterator_count($users->getComponents())); - - // simulate rendering additional key - Assert::true($users[2]['name'] instanceof Controls\TextInput); - - // 2 containers and submit button - Assert::same(3, iterator_count($users->getComponents())); - - Assert::same(['users' => [ - 0 => ['name' => ''], - 2 => ['name' => ''], - ]], $form->getValues(TRUE)); - } - - - - public function testRendering_attachBeforeDefinition() - { - $form = new BaseForm(); - - $this->connectForm($form); - - $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { - $user->addText('name'); - }, 1); - $users->addSubmit('add'); - - // container and submit button - Assert::same(2, iterator_count($users->getComponents())); - - // simulate rendering additional key - Assert::true($users[2]['name'] instanceof Controls\TextInput); - - // 2 containers and submit button - Assert::same(3, iterator_count($users->getComponents())); - - Assert::same(['users' => [ - 0 => ['name' => ''], - 2 => ['name' => ''], - ]], $form->getValues(TRUE)); - } - - - - public function testSubmit_attachAfterDefinition() - { - $form = new BaseForm(); - $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { - $user->addText('name'); - }, 1); - $users->addSubmit('add'); - - $this->connectForm($form, [ - 'users' => [ - 0 => ['name' => 'David'], - 2 => ['name' => 'Holy'], - 3 => ['name' => 'Rimmer'], - ], - 'do' => 'form-submit' - ]); - - // container and submit button - Assert::same(4, iterator_count($users->getComponents())); - - Assert::same(['users' => [ - 0 => ['name' => 'David'], - 2 => ['name' => 'Holy'], - 3 => ['name' => 'Rimmer'], - ]], $form->getValues(TRUE)); - } - - - - public function testSubmit_attachBeforeDefinition() - { - $form = new BaseForm(); - - $this->connectForm($form, [ - 'users' => [ - 0 => ['name' => 'David'], - 2 => ['name' => 'Holy'], - 3 => ['name' => 'Rimmer'], - ], - 'do' => 'form-submit' - ]); - - $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { - $user->addText('name'); - }, 1); - $users->addSubmit('add'); - - // container and submit button - Assert::same(4, iterator_count($users->getComponents())); - - Assert::same(['users' => [ - 0 => ['name' => 'David'], - 2 => ['name' => 'Holy'], - 3 => ['name' => 'Rimmer'], - ]], $form->getValues(TRUE)); - } - - - - public function testSubmit_nestedReplicator_notFilled() - { - $form = new BaseForm(); - $this->connectForm($form, [ - 'users' => [0 => ['emails' => [0 => ['email' => '']]]], - 'do' => 'form-submit', - ]); - $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { - $user->addDynamic('emails', function (Nette\Forms\Container $email) { - $email->addText('email'); - $email->addText('note'); - }); - }); - $users->addSubmit('add')->addCreateOnClick(); - Assert::false($users->isAllFilled()); - } - - - - public function testSubmit_nestedReplicator_filled() - { - $form = new BaseForm(); - $this->connectForm($form, [ - 'users' => [0 => ['emails' => [0 => ['email' => 'foo']]]], - 'do' => 'form-submit', - ]); - $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { - $user->addDynamic('emails', function (Nette\Forms\Container $email) { - $email->addText('email'); - $email->addText('note'); - }); - }); - $users->addSubmit('add')->addCreateOnClick(); - Assert::true($users->isAllFilled()); - } - - - - protected function connectForm(UI\Form $form, array $post = []) - { - $container = $this->createContainer(); - - /** @var MockPresenter $presenter */ - $presenter = $container->createInstance('KdybyTests\Replicator\MockPresenter', ['form' => $form]); - $container->callInjects($presenter); - $presenter->run(new Request('Mock', $post ? 'POST' : 'GET', ['action' => 'default'], $post)); - - $presenter['form']; // connect form - - return $presenter; - } - - - - /** - * @return \SystemContainer|\Nette\DI\Container - */ - protected function createContainer() - { - $config = new Nette\Configurator(); - $config->setTempDirectory(TEMP_DIR); - Kdyby\Replicator\DI\ReplicatorExtension::register($config); - - return $config->createContainer(); - } - - - // TODO: add tests using standalone \Nette\Forms\Form and not the UI\Form. - // https://github.com/Kdyby/Replicator/issues/40 - // The Replicator can't be used with standalone \Nette\Forms\Form (so without the UI\Form). - // Problem is that attached is not triggered, so values from Request are not populated to the container. - -} - - - -class BaseForm extends UI\Form -{ - - /** - * @param string $name - * @param callable $factory - * @param int $createDefault - * @param bool $forceDefault - * @return Container - */ - public function addDynamic($name, $factory, $createDefault = 0, $forceDefault = FALSE) - { - $control = new Container($factory, $createDefault, $forceDefault); - $control->currentGroup = $this->currentGroup; - return $this[$name] = $control; - } - -} - -class MockPresenter extends Nette\Application\UI\Presenter -{ - - /** - * @var UI\Form - */ - private $form; - - public function __construct(UI\Form $form) - { - $this->form = $form; - } - - protected function beforeRender() - { - $this->terminate(); - } - - protected function createComponentForm() - { - return $this->form; - } - -} - - -\run(new ContainerTest()); diff --git a/tests/KdybyTests/Replicator/Extension.phpt b/tests/KdybyTests/Replicator/Extension.phpt deleted file mode 100644 index 9fc15b7..0000000 --- a/tests/KdybyTests/Replicator/Extension.phpt +++ /dev/null @@ -1,60 +0,0 @@ - - * @package Kdyby\Replicator - */ - -namespace KdybyTests\Replicator; - -use Kdyby; -use Nette; -use Tester; -use Tester\Assert; - -require_once __DIR__ . '/../bootstrap.php'; - - - -/** - * @author Filip Procházka - */ -class ExtensionTest extends Tester\TestCase -{ - - protected function setUp() - { - parent::setUp(); - Tester\Environment::$checkAssertions = FALSE; - } - - - - /** - * @return \SystemContainer|\Nette\DI\Container - */ - protected function createContainer() - { - $config = new Nette\Configurator(); - $config->setTempDirectory(TEMP_DIR); - Kdyby\Replicator\DI\ReplicatorExtension::register($config); - - return $config->createContainer(); - } - - - - public function testExtensionMethodIsRegistered() - { - $this->createContainer(); // initialize - - $form = new Nette\Forms\Form(); - $form->addDynamic('people', function () {}); - } - -} - -\run(new ExtensionTest()); diff --git a/tests/Replicator/ContainerTest.php b/tests/Replicator/ContainerTest.php new file mode 100644 index 0000000..fec4fff --- /dev/null +++ b/tests/Replicator/ContainerTest.php @@ -0,0 +1,326 @@ + + * @package Kdyby\Replicator + */ + +namespace KdybyTests\Replicator; + +use Kdyby\Replicator\Container; +use Nette; +use Tester\Assert; +use Tester\TestCase; + + +require_once __DIR__ . '/../bootstrap.php'; + + +/** + * @author Filip Procházka + */ +class ContainerTest extends TestCase +{ + + public function testReplicating() + { + $replicator = new Container(function (Nette\Forms\Container $container) { + $container->addText('name', 'Name'); + }); + + Assert::type(Nette\Forms\Controls\TextInput::class, $replicator[0]['name']); + Assert::type(Nette\Forms\Controls\TextInput::class, $replicator[2]['name']); + Assert::type(Nette\Forms\Controls\TextInput::class, $replicator[1000]['name']); + } + + + public function testRenderingAttachAfterDefinition(): void + { + $form = new BaseForm(); + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addText('name'); + }, 1); + $users->addSubmit('add'); + + $this->connectForm($form); + + // container and submit button + Assert::same(2, iterator_count($users->getComponents())); + + // simulate rendering additional key + Assert::type(Nette\Forms\Controls\TextInput::class, $users[2]['name']); + + // 2 containers and submit button + Assert::same(3, iterator_count($users->getComponents())); + + Assert::equal(Nette\Utils\ArrayHash::from([ + 'users' => [ + 0 => ['name' => ''], + 2 => ['name' => ''], + ], + ]), $form->getValues()); + } + + + public function testRenderingAttachBeforeDefinition(): void + { + $form = new BaseForm(); + + $this->connectForm($form); + + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addText('name'); + }, 1); + $users->addSubmit('add'); + + // container and submit button + Assert::same(2, iterator_count($users->getComponents())); + + // simulate rendering additional key + Assert::type(Nette\Forms\Controls\TextInput::class, $users[2]['name']); + + // 2 containers and submit button + Assert::same(3, iterator_count($users->getComponents())); + + Assert::equal(Nette\Utils\ArrayHash::from([ + 'users' => [ + '0' => ['name' => ''], + '2' => ['name' => ''], + ], + ]), $form->getValues()); + } + + + public function testSubmitAttachAfterDefinition(): void + { + $form = new BaseForm(); + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addText('name'); + }, 1); + $users->addSubmit('add'); + + $this->connectForm($form, [ + 'users' => [ + 0 => ['name' => 'David'], + 2 => ['name' => 'Holy'], + 3 => ['name' => 'Rimmer'], + ], + '_do' => 'form-submit', + ]); + + // container and submit button + Assert::same(4, iterator_count($users->getComponents())); + + Assert::equal(Nette\Utils\ArrayHash::from([ + 'users' => [ + 0 => ['name' => 'David'], + 2 => ['name' => 'Holy'], + 3 => ['name' => 'Rimmer'], + ], + ]), $form->getValues()); + } + + + public function testSubmitAttachBeforeDefinition(): void + { + $form = new BaseForm(); + + $this->connectForm($form, [ + 'users' => [ + 0 => ['name' => 'David'], + 2 => ['name' => 'Holy'], + 3 => ['name' => 'Rimmer'], + ], + '_do' => 'form-submit', + ]); + + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addText('name'); + }, 1); + $users->addSubmit('add'); + + // container and submit button + Assert::same(4, iterator_count($users->getComponents())); + + Assert::equal(Nette\Utils\ArrayHash::from([ + 'users' => [ + 0 => ['name' => 'David'], + 2 => ['name' => 'Holy'], + 3 => ['name' => 'Rimmer'], + ], + ]), $form->getValues()); + } + + + public function testSubmitNestedReplicatorNotFilled(): void + { + $form = new BaseForm(); + $this->connectForm($form, [ + 'users' => [ + 0 => ['emails' => [0 => ['email' => '']]], + ], + '_do' => 'form-submit', + ]); + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addDynamic('emails', function (Nette\Forms\Container $email) { + $email->addText('email'); + $email->addText('note'); + }); + }); + $users->addSubmit('add')->addCreateOnClick(); + Assert::false($users->isAllFilled()); + } + + + public function testSubmitNestedReplicatorFilled(): void + { + $form = new BaseForm(); + $this->connectForm($form, [ + 'users' => [ + 0 => ['emails' => [0 => ['email' => 'foo', 'note' => 'aa']]], + ], + '_do' => 'form-submit', + ]); + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addDynamic('emails', function (Nette\Forms\Container $email) { + $email->addText('email'); + $email->addText('note'); + }); + }); + $users->addSubmit('add')->addCreateOnClick(); + Assert::true($users->isAllFilled()); + } + + + public function testAddContainer(): void + { + $form = new BaseForm(); + $this->connectForm($form, [ + 'users' => [ + 0 => ['emails' => [0 => ['email' => 'foo']]], + 2 => ['emails' => [0 => ['email' => 'bar']]], + ], + '_do' => 'form-submit', + ]); + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addDynamic('emails', function (Nette\Forms\Container $email) { + $email->addText('email'); + }); + }); + /** @var Nette\Forms\Controls\SubmitButton $submit */ + $submit = $users->addSubmit('add')->addCreateOnClick(); + + Assert::same(2, iterator_count($users->getContainers())); + + $submit->click(); + + Assert::same(3, iterator_count($users->getContainers())); + } + + + public function testRemoveContainer(): void + { + $form = new BaseForm(); + $this->connectForm($form, [ + 'users' => [ + 0 => ['emails' => [0 => ['email' => 'foo']]], + 2 => ['emails' => [0 => ['email' => 'bar']]], + ], + '_do' => 'form-submit', + ]); + $users = $form->addDynamic('users', function (Nette\Forms\Container $user) { + $user->addDynamic('emails', function (Nette\Forms\Container $email) { + $email->addText('email'); + }); + $user->addSubmit('remove')->addRemoveOnClick(); + }); + + Assert::same(2, iterator_count($users->getContainers())); + + /** @var Container $section */ + $section = $users['2']; + $users->remove($section); + + Assert::same(1, iterator_count($users->getContainers())); + + /** @var Nette\Forms\Controls\SubmitButton $sectionRemoveButton */ + $sectionRemoveButton = $users['0']['remove']; + $sectionRemoveButton->click(); + + Assert::same(0, iterator_count($users->getContainers())); + } + + + private function connectForm(Nette\Application\UI\Form $form, array $post = []): MockPresenter + { + $container = Helper::createContainer(); + + /** @var MockPresenter $presenter */ + $presenter = $container->createInstance(MockPresenter::class, ['form' => $form]); + $container->callInjects($presenter); + $presenter->run(new Nette\Application\Request('Mock', $post ? 'POST' : 'GET', ['action' => 'default'], $post)); + + $presenter->getComponent('form'); // connect form + + return $presenter; + } + + // TODO: add tests using standalone \Nette\Forms\Form and not the UI\Form. + // https://github.com/Kdyby/Replicator/issues/40 + // The Replicator can't be used with standalone \Nette\Forms\Form (so without the UI\Form). + // Problem is that attached is not triggered, so values from Request are not populated to the container. + +} + + +class BaseForm extends Nette\Application\UI\Form +{ + + public function addDynamic(string $name, callable $factory, int $createDefault = 0, bool $forceDefault = FALSE): Container + { + $control = new Container($factory, $createDefault, $forceDefault); + $control->currentGroup = $this->currentGroup; + + return $this[$name] = $control; + } + +} + + +class MockPresenter extends Nette\Application\UI\Presenter +{ + + /** + * @var Nette\Application\UI\Form + */ + private $form; + + + public function __construct(Nette\Application\UI\Form $form) + { + parent::__construct(); + $this->form = $form; + } + + + /** + * @throws Nette\Application\AbortException + */ + protected function beforeRender(): void + { + $this->terminate(); + } + + + protected function createComponentForm(): Nette\Application\UI\Form + { + return $this->form; + } + +} + + +(new ContainerTest())->run(); diff --git a/tests/Replicator/ExtensionTest.php b/tests/Replicator/ExtensionTest.php new file mode 100644 index 0000000..98b5ad5 --- /dev/null +++ b/tests/Replicator/ExtensionTest.php @@ -0,0 +1,46 @@ + + * @package Kdyby\Replicator + */ + +namespace KdybyTests\Replicator; + +use Nette; +use Tester\Environment; +use Tester\TestCase; + + +require_once __DIR__ . '/../bootstrap.php'; + + +/** + * @author Filip Procházka + */ +class ExtensionTest extends TestCase +{ + + protected function setUp(): void + { + parent::setUp(); + Environment::$checkAssertions = FALSE; + } + + + public function testExtensionMethodIsRegistered(): void + { + Helper::createContainer(); + + $form = new Nette\Forms\Form(); + $form->addDynamic('people', function () { + }); + } + +} + + +(new ExtensionTest())->run(); diff --git a/tests/Replicator/Helper.php b/tests/Replicator/Helper.php new file mode 100644 index 0000000..c73dbf0 --- /dev/null +++ b/tests/Replicator/Helper.php @@ -0,0 +1,23 @@ +setTempDirectory(TEMP_DIR); + $config->addConfig(__DIR__ . '/config.neon'); + ReplicatorExtension::register($config); + + return $config->createContainer(); + } + +} diff --git a/tests/Replicator/config.neon b/tests/Replicator/config.neon new file mode 100644 index 0000000..e71372e --- /dev/null +++ b/tests/Replicator/config.neon @@ -0,0 +1,3 @@ +application: + scanDirs: false + scanComposer: false diff --git a/tests/KdybyTests/bootstrap.php b/tests/bootstrap.php old mode 100755 new mode 100644 similarity index 59% rename from tests/KdybyTests/bootstrap.php rename to tests/bootstrap.php index 405399a..092d811 --- a/tests/KdybyTests/bootstrap.php +++ b/tests/bootstrap.php @@ -8,30 +8,30 @@ * For the full copyright and license information, please view the file license.md that was distributed with this source code. */ -if (@!include __DIR__ . '/../../vendor/autoload.php') { +if (@!include __DIR__ . '/../vendor/autoload.php') { echo 'Install Nette Tester using `composer update --dev`'; exit(1); } // configure environment Tester\Environment::setup(); -class_alias('Tester\Assert', 'Assert'); date_default_timezone_set('Europe/Prague'); // create temporary directory -define('TEMP_DIR', __DIR__ . '/../tmp/' . (isset($_SERVER['argv']) ? md5(serialize($_SERVER['argv'])) : getmypid())); +define('TEMP_DIR', __DIR__ . '/tmp/' . (isset($_SERVER['argv']) ? md5(serialize($_SERVER['argv'])) : getmypid())); Tester\Helpers::purge(TEMP_DIR); $_SERVER = array_intersect_key($_SERVER, array_flip([ - 'PHP_SELF', 'SCRIPT_NAME', 'SERVER_ADDR', 'SERVER_SOFTWARE', 'HTTP_HOST', 'DOCUMENT_ROOT', 'OS', 'argc', 'argv'])); + 'PHP_SELF', + 'SCRIPT_NAME', + 'SERVER_ADDR', + 'SERVER_SOFTWARE', + 'HTTP_HOST', + 'DOCUMENT_ROOT', + 'OS', + 'argc', + 'argv', +])); $_SERVER['REQUEST_TIME'] = 1234567890; $_ENV = $_GET = $_POST = []; - -function id($val) { - return $val; -} - -function run(Tester\TestCase $testCase) { - $testCase->run(); -} diff --git a/tests/composer-nette-2.2.json b/tests/composer-nette-2.2.json deleted file mode 100644 index 8c52abf..0000000 --- a/tests/composer-nette-2.2.json +++ /dev/null @@ -1,34 +0,0 @@ -{ - "name": "kdyby/forms-replicator", - "type": "library", - "description": "Nette forms container replicator aka addDynamic", - "keywords": ["nette", "kdyby", "forms", "replicator", "addDynamic"], - "homepage": "http://kdyby.org", - "license": ["BSD-3-Clause", "GPL-2.0", "GPL-3.0"], - "authors": [ - { - "name": "Filip Procházka", - "homepage": "http://filip-prochazka.com", - "email": "filip@prochazka.su" - } - ], - "support": { - "email": "filip@prochazka.su", - "issues": "https://github.com/kdyby/replicator/issues" - }, - "require": { - "nette/forms": "2.2.*" - }, - "require-dev": { - "nette/nette": "2.2.*", - "nette/bootstrap": "2.2.*", - - "nette/tester": "@dev", - "jakub-onderka/php-parallel-lint": "~0.6" - }, - "autoload": { - "psr-0": { - "Kdyby\\Replicator\\": "src/" - } - } -} diff --git a/tests/conventions.txt b/tests/conventions.txt deleted file mode 100644 index bf471e6..0000000 --- a/tests/conventions.txt +++ /dev/null @@ -1,26 +0,0 @@ -Test case file name -=================== - -Nette\....phpt - -Nette\Debug.phpt - tests for a class's basic behaviour -Nette\Debug.fireLog().phpt - tests for a method's basic behaviour -Nette\Debug.fireLog().inc - common code for more test cases -Nette\Debug.fireLog().expect - expected raw output -Nette\Debug.fireLog().area.phpt - tests for a specified area of class/method - -- areas: basic, error, bug#123 -- numbers have three digits - - -Test case phpDoc -================ - -/** - * Test: some test name - * - * @author John Doe - * @phpVersion < 5.3 default operator is >= - * @skip some reason why test is skipped - * @phpIni short_open_tag=on - */ diff --git a/tests/php.ini-unix b/tests/php.ini-unix deleted file mode 100644 index e69de29..0000000 diff --git a/tests/prepare-composer.php b/tests/prepare-composer.php deleted file mode 100644 index ddc9590..0000000 --- a/tests/prepare-composer.php +++ /dev/null @@ -1,16 +0,0 @@ -