• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

contributte / forms-multiplier / 5574364510

pending completion
5574364510

push

github

f3l1x
Composer: update dev deps

239 of 321 relevant lines covered (74.45%)

10.95 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

86.84
/src/Multiplier.php
1
<?php declare(strict_types = 1);
2

3
namespace Contributte\FormMultiplier;
4

5
use Contributte\FormMultiplier\Buttons\CreateButton;
6
use Contributte\FormMultiplier\Buttons\RemoveButton;
7
use Iterator;
8
use Nette\ComponentModel\IComponent;
9
use Nette\Forms\Container;
10
use Nette\Forms\Control;
11
use Nette\Forms\Controls\BaseControl;
12
use Nette\Forms\Controls\SubmitButton;
13
use Nette\Forms\Form;
14
use Nette\Utils\ArrayHash;
15
use Nette\Utils\Arrays;
16
use Traversable;
17

18
class Multiplier extends Container
19
{
20

21
        public const SUBMIT_CREATE_NAME = 'multiplier_creator';
22

23
        public const SUBMIT_REMOVE_NAME = 'multiplier_remover';
24

25
        /** @var callable[] */
26
        public array $onCreate = [];
27

28
        /** @var callable[] */
29
        public array $onRemove = [];
30

31
        /** @var callable[] */
32
        public array $onCreateComponents = [];
33

34
        /** @var callable */
35
        protected $factory;
36

37
        protected int $copyNumber;
38

39
        protected int $number = 0;
40

41
        protected bool $created = false;
42

43
        /** @var mixed[] */
44
        protected array $values = [];
45

46
        protected bool $erase = false;
47

48
        /** @var CreateButton[] */
49
        protected array $createButtons = [];
50

51
        protected ?RemoveButton $removeButton = null;
52

53
        /** @var mixed[] */
54
        protected array $httpData = [];
55

56
        protected ?int $maxCopies = null;
57

58
        protected int $totalCopies = 0;
59

60
        protected int $minCopies = 1;
61

62
        protected bool $resetKeys = true;
63

64
        /** @var Container[] */
65
        protected array $noValidate = [];
66

67
        private ?Form $form = null;
68

69
        private bool $attachedCalled = false;
70

71
        public function __construct(callable $factory, int $copyNumber = 1, ?int $maxCopies = null)
72
        {
73
                $this->factory = $factory;
36✔
74
                $this->minCopies = $this->copyNumber = $copyNumber;
36✔
75
                $this->maxCopies = $maxCopies;
36✔
76

77
                $this->monitor(Form::class, function (Form $form): void {
36✔
78
                        $this->form = $form;
79

80
                        if ($this->getCurrentGroup() === null) {
81
                                $this->setCurrentGroup($form->getCurrentGroup());
82
                        }
83

84
                        if ($form instanceof \Nette\Application\UI\Form) {
85
                                if ($form->isAnchored()) {
86
                                        $this->whenAttached();
87
                                } else {
88
                                        $form->onAnchor[] = function (): void {
89
                                                $this->whenAttached();
90
                                        };
91
                                }
92
                        }
93

94
                        $form->onRender[] = function (): void {
95
                                $this->whenAttached();
96
                        };
97
                });
36✔
98
                $this->monitor(self::class, [$this, 'whenAttached']);
36✔
99
        }
100

101
        public static function register(string $name = 'addMultiplier'): void
102
        {
103
                Container::extensionMethod($name, function (Container $form, $name, $factory, $copyNumber = 1, $maxCopies = null) {
2✔
104
                        $multiplier = new Multiplier($factory, $copyNumber, $maxCopies);
105
                        $multiplier->setCurrentGroup($form->getCurrentGroup());
106

107
                        return $form[$name] = $multiplier;
108
                });
2✔
109
        }
110

111
        public function getForm(bool $throw = true): ?Form
112
        {
113
                if ($this->form) {
34✔
114
                        return $this->form;
34✔
115
                }
116

117
                return parent::getForm($throw);
×
118
        }
119

120
        public function setResetKeys(bool $reset = true): self
121
        {
122
                $this->resetKeys = $reset;
×
123

124
                return $this;
×
125
        }
126

127
        public function setMinCopies(int $minCopies): self
128
        {
129
                $this->minCopies = $minCopies;
14✔
130

131
                return $this;
14✔
132
        }
133

134
        public function setFactory(callable $factory): self
135
        {
136
                $this->factory = $factory;
×
137

138
                return $this;
×
139
        }
140

141
        public function getMaxCopies(): ?int
142
        {
143
                return $this->maxCopies;
×
144
        }
145

146
        public function getMinCopies(): ?int
147
        {
148
                return $this->minCopies;
×
149
        }
150

151
        public function getCopyNumber(): int
152
        {
153
                return $this->copyNumber;
×
154
        }
155

156
        public function addRemoveButton(?string $caption = null): RemoveButton
157
        {
158
                return $this->removeButton = new RemoveButton($caption);
16✔
159
        }
160

161
        public function addCreateButton(?string $caption = null, int $copyCount = 1): CreateButton
162
        {
163
                return $this->createButtons[$copyCount] = new CreateButton($caption, $copyCount);
21✔
164
        }
165

166
        /**
167
         * @param Control[]|null $controls
168
         */
169
        public function validate(?array $controls = null): void
170
        {
171
                /** @var Control[] $components */
172
                $components = $controls ?? iterator_to_array($this->getComponents());
13✔
173

174
                foreach ($components as $index => $control) {
13✔
175
                        foreach ($this->noValidate as $item) {
13✔
176
                                if ($control === $item) {
6✔
177
                                        unset($components[$index]);
6✔
178
                                }
179
                        }
180
                }
181

182
                parent::validate($components);
13✔
183
        }
184

185
        /**
186
         * @param mixed[]|object $defaults
187
         */
188
        public function addCopy(?int $number = null, array|object $defaults = []): Container
189
        {
190
                if (!is_numeric($number)) {
32✔
191
                        $number = $this->createNumber();
15✔
192
                } else {
193
                        /** @var Container|null $component */
194
                        $component = $this->getComponent((string) $number, false);
24✔
195
                        if ($component !== null) {
24✔
196
                                return $component;
×
197
                        }
198
                }
199

200
                $container = $this->createContainer();
32✔
201
                if ($defaults) {
32✔
202
                        $container->setDefaults($defaults, $this->erase);
7✔
203
                }
204

205
                $this->attachContainer($container, (string) $number);
32✔
206
                $this->attachRemoveButton($container);
32✔
207

208
                $this->totalCopies++;
32✔
209

210
                return $container;
32✔
211
        }
212

213
        public function createCopies(): void
214
        {
215
                if ($this->created === true) {
34✔
216
                        return;
13✔
217
                }
218

219
                $this->created = true;
34✔
220

221
                $resolver = new ComponentResolver($this->httpData, $this->values, $this->maxCopies, $this->minCopies);
34✔
222

223
                $this->attachCreateButtons();
34✔
224
                $this->createComponents($resolver);
34✔
225
                $this->detachCreateButtons();
34✔
226

227
                if ($this->maxCopies === null || $this->totalCopies < $this->maxCopies) {
34✔
228
                        $this->attachCreateButtons();
31✔
229
                }
230

231
                if ($this->form !== null && $resolver->isRemoveAction() && $this->totalCopies >= $this->minCopies && !$resolver->reachedMinLimit()) {
34✔
232
                        /** @var RemoveButton $removeButton */
233
                        $removeButton = $this->removeButton;
4✔
234
                        $this->form->setSubmittedBy($removeButton->create($this));
4✔
235

236
                        $this->resetFormEvents();
4✔
237

238
                        $this->onRemoveEvent();
4✔
239
                }
240

241
                // onCreateEvent
242
                $this->onCreateEvent();
34✔
243
        }
244

245
        /**
246
         * @return Submitter[]
247
         */
248
        public function getCreateButtons(): array
249
        {
250
                if ($this->maxCopies !== null && $this->totalCopies >= $this->maxCopies) {
1✔
251
                        return [];
×
252
                }
253

254
                $buttons = [];
1✔
255
                foreach ($this->createButtons as $button) {
1✔
256
                        $buttons[$button->getCopyCount()] = $this->getComponent($button->getComponentName());
1✔
257
                }
258

259
                return $buttons;
1✔
260
        }
261

262
        /**
263
         * @internal
264
         */
265
        public function resetFormEvents(): void
266
        {
267
                if ($this->form === null) {
10✔
268
                        return;
×
269
                }
270

271
                $this->form->onSuccess = $this->form->onError = $this->form->onSubmit = [];
10✔
272
        }
273

274
        /**
275
         * @param  Control[]|null  $controls
276
         * @return object|mixed[]
277
         * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingAnyTypeHint
278
         */
279
        public function getValues($returnType = null, ?array $controls = null): object|array
280
        {
281
                if (!$this->resetKeys) {
×
282
                        return parent::getValues($returnType, $controls);
×
283
                }
284

285
                /** @var mixed[] $values */
286
                $values = parent::getValues('array', $controls);
×
287
                $values = array_values($values);
×
288

289
                $returnType = $returnType === true ? 'array' : $returnType; // @phpstan-ignore-line nette backwards compatibility
×
290

291
                return $returnType === 'array' ? $values : ArrayHash::from($values);
×
292
        }
293

294
        /**
295
         * @return Iterator|Control[]
296
         */
297
        public function getControls(): Iterator
298
        {
299
                $this->createCopies();
×
300

301
                return parent::getControls();
×
302
        }
303

304
        /**
305
         * @return Iterator<int|string,Container>
306
         */
307
        public function getContainers(): Iterator
308
        {
309
                $this->createCopies();
13✔
310

311
                /** @var Iterator<int|string,Container> $containers */
312
                $containers = $this->getComponents(false, Container::class);
13✔
313

314
                return $containers;
13✔
315
        }
316

317
        /**
318
         * @param mixed[]|object $values
319
         * @phpcsSuppress SlevomatCodingStandard.TypeHints.ParameterTypeHint.MissingNativeTypeHint
320
         */
321
        public function setValues($values, bool $erase = false): self
322
        {
323
                $values = $values instanceof Traversable ? iterator_to_array($values) : (array) $values;
10✔
324

325
                $this->values = $values;
10✔
326
                $this->erase = $erase;
10✔
327

328
                if ($this->created) {
10✔
329
                        foreach ($this->getContainers() as $container) {
1✔
330
                                $this->removeComponent($container);
×
331
                        }
332

333
                        $this->created = false;
1✔
334
                        $this->detachCreateButtons();
1✔
335
                        $this->createCopies();
1✔
336
                }
337

338
                return $this;
10✔
339
        }
340

341
        protected function whenAttached(): void
342
        {
343
                if ($this->attachedCalled) {
34✔
344
                        return;
34✔
345
                }
346

347
                $this->loadHttpData();
34✔
348
                $this->createCopies();
34✔
349

350
                $this->attachedCalled = true;
34✔
351
        }
352

353
        protected function onCreateEvent(): void
354
        {
355
                foreach ($this->onCreate as $callback) {
34✔
356
                        foreach ($this->getContainers() as $container) {
13✔
357
                                $callback($container);
13✔
358
                        }
359
                }
360
        }
361

362
        protected function onRemoveEvent(): void
363
        {
364
                foreach ($this->onRemove as $callback) {
4✔
365
                        $callback($this);
1✔
366
                }
367
        }
368

369
        protected function isValidMaxCopies(): bool
370
        {
371
                return $this->maxCopies === null || $this->totalCopies < $this->maxCopies;
16✔
372
        }
373

374
        protected function isFormSubmitted(): bool
375
        {
376
                return $this->getForm() !== null && $this->getForm()->isAnchored() && $this->getForm()->isSubmitted();
34✔
377
        }
378

379
        protected function loadHttpData(): void
380
        {
381
                if ($this->form !== null && $this->isFormSubmitted()) {
34✔
382
                        $this->httpData = (array) Arrays::get($this->form->getHttpData(), $this->getHtmlName(), []);
18✔
383
                }
384
        }
385

386
        protected function createNumber(): int
387
        {
388
                $count = iterator_count($this->getComponents(false, Form::class));
15✔
389
                while ($this->getComponent((string) $count, false)) {
15✔
390
                        $count++;
9✔
391
                }
392

393
                return $count;
15✔
394
        }
395

396
        protected function fillContainer(Container $container): void
397
        {
398
                call_user_func($this->factory, $container, $this->getForm());
32✔
399
        }
400

401
        /**
402
         * @return string[]
403
         */
404
        protected function getHtmlName(): array
405
        {
406
                return explode('-', $this->lookupPath(Form::class) ?? '');
18✔
407
        }
408

409
        protected function createContainer(): Container
410
        {
411
                $control = new Container();
32✔
412
                $control->currentGroup = $this->currentGroup;
32✔
413
                $this->fillContainer($control);
32✔
414

415
                return $control;
32✔
416
        }
417

418
        /**
419
         * Return name of first submit button
420
         */
421
        protected function getFirstSubmit(): ?string
422
        {
423
                $submits = iterator_to_array($this->getComponents(false, SubmitButton::class));
32✔
424
                if ($submits) {
32✔
425
                        return reset($submits)->getName();
20✔
426
                }
427

428
                return null;
14✔
429
        }
430

431
        protected function attachContainer(Container $container, ?string $name): void
432
        {
433
                $this->addComponent($container, $name, $this->getFirstSubmit());
32✔
434
        }
435

436
        protected function removeComponentProperly(IComponent $component): void
437
        {
438
                if ($this->getCurrentGroup() !== null && $component instanceof Control) {
21✔
439
                        $this->getCurrentGroup()->remove($component);
4✔
440
                }
441

442
                $this->removeComponent($component);
21✔
443
        }
444

445
        private function createComponents(ComponentResolver $resolver): void
446
        {
447
                $containers = [];
34✔
448

449
                // Components from httpData
450
                if ($this->isFormSubmitted()) {
34✔
451
                        foreach ($resolver->getValues() as $number => $_) {
18✔
452
                                $containers[] = $container = $this->addCopy($number);
16✔
453

454
                                /** @var BaseControl $control */
455
                                foreach ($container->getControls() as $control) {
16✔
456
                                        $control->loadHttpData();
16✔
457
                                }
458
                        }
459
                } else { // Components from default values
460
                        foreach ($resolver->getDefaults() as $number => $values) {
16✔
461
                                $containers[] = $this->addCopy($number, $values);
7✔
462
                        }
463
                }
464

465
                // Default number of copies
466
                if (!$this->isFormSubmitted() && !$this->values) {
34✔
467
                        $copyNumber = $this->copyNumber;
9✔
468
                        while ($copyNumber > 0 && $this->isValidMaxCopies()) {
9✔
469
                                $containers[] = $container = $this->addCopy();
9✔
470
                                $copyNumber--;
9✔
471
                        }
472
                }
473

474
                // Dynamic
475
                foreach ($this->onCreateComponents as $callback) {
34✔
476
                        $callback($this);
1✔
477
                }
478

479
                // New containers, if create button hitted
480
                if ($this->form !== null && $resolver->isCreateAction() && $this->form->isValid()) {
34✔
481
                        $count = $resolver->getCreateNum();
7✔
482
                        while ($count > 0 && $this->isValidMaxCopies()) {
7✔
483
                                $this->noValidate[] = $containers[] = $container = $this->addCopy();
6✔
484
                                $container->setValues($this->createContainer()->getValues('array'));
6✔
485
                                $count--;
6✔
486
                        }
487
                }
488

489
                if ($this->removeButton && $this->totalCopies <= $this->minCopies) {
34✔
490
                        foreach ($containers as $container) {
7✔
491
                                $this->detachRemoveButton($container);
5✔
492
                        }
493
                }
494
        }
495

496
        private function detachCreateButtons(): void
497
        {
498
                foreach ($this->createButtons as $button) {
34✔
499
                        $this->removeComponentProperly($this->getComponent($button->getComponentName()));
21✔
500
                }
501
        }
502

503
        private function attachCreateButtons(): void
504
        {
505
                foreach ($this->createButtons as $button) {
34✔
506
                        $this->addComponent($button->create($this), $button->getComponentName());
21✔
507
                }
508
        }
509

510
        private function detachRemoveButton(Container $container): void
511
        {
512
                $button = $container->getComponent(self::SUBMIT_REMOVE_NAME);
5✔
513
                if ($this->getCurrentGroup() !== null) {
5✔
514
                        $this->getCurrentGroup()->remove($button);
1✔
515
                }
516

517
                $container->removeComponent($button);
5✔
518
        }
519

520
        private function attachRemoveButton(Container $container): void
521
        {
522
                if (!$this->removeButton) {
32✔
523
                        return;
18✔
524
                }
525

526
                $container->addComponent($this->removeButton->create($this), self::SUBMIT_REMOVE_NAME);
14✔
527
        }
528

529
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc