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

nette / forms / 28608035798

02 Jul 2026 04:53PM UTC coverage: 93.28% (-0.2%) from 93.512%
28608035798

push

github

dg
Form: submission detection redesigned around SubmissionSource objects

A form no longer inherits its way of receiving submitted data, it is
given a source:

- interface SubmissionSource { receiveData(Form): ?array; prepare(Form) }
- Sources\HttpSource - reads the form's own HTTP request: method match,
  Sec-Fetch origin policy (?FetchSite $allowedOrigin, null disables the
  check), POST+FILES/GET merge and the '_form_' tracker of a named form,
  which it installs itself in the prepare() render hook and validates
  against the form name at receive time
- Sources\NullSource - never submitted; used by Repeater item templates
  and Blueprint dummy forms instead of the former anonymous-class trick,
  so introspective code gets false from isSubmitted() instead of hacks

Form gains setSubmissionSource() and a protected createDefaultSource()
seam: the default HttpSource materializes lazily at the first submission
query or render, so a bare `new Form` keeps its behavior. Descendants
anchored elsewhere (Nette\Application\UI\Form) return null from
createDefaultSource() and set their source later; the write-once setter
runs a catch-up loadHttpData() pass so that controls attached before the
source load their values. The presenter signal field of UI\Form is the
same kind of marker as the tracker and moves to the prepare() hook of
its source on the application side.

Behavior notes: the tracker is installed at render time, so it renders
after user fields and $form[TrackerId] exists only after
fireRenderEvents(); Form::$httpRequest was replaced by injecting
`new HttpSource($request)`; the cross-origin flag lives in the source,
allowCrossOrigin() delegates to it.

284 of 326 new or added lines in 5 files covered. (87.12%)

58 existing lines in 9 files now uncovered.

2443 of 2619 relevant lines covered (93.28%)

0.93 hits per line

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

93.02
/src/Forms/Controls/UploadControl.php
1
<?php declare(strict_types=1);
2

3
/**
4
 * This file is part of the Nette Framework (https://nette.org)
5
 * Copyright (c) 2004 David Grudl (https://davidgrudl.com)
6
 */
7

8
namespace Nette\Forms\Controls;
9

10
use Nette;
11
use Nette\Forms;
12
use Nette\Forms\Form;
13
use Nette\Http\FileUpload;
14
use Nette\Utils\Arrays;
15
use Stringable;
16
use function implode, ini_get, is_array;
17

18

19
/**
20
 * Text box and browse button that allow users to select a file to upload to the server.
21
 */
22
class UploadControl extends BaseControl
23
{
24
        /** validation rule */
25
        public const Valid = ':uploadControlValid';
26

27
        #[\Deprecated('use UploadControl::Valid')]
28
        public const VALID = self::Valid;
29

30
        private bool $nullable = false;
31

32

33
        public function __construct(string|Stringable|null $label = null, bool $multiple = false)
1✔
34
        {
35
                parent::__construct($label);
1✔
36
                $this->control->type = 'file';
1✔
37
                $this->control->multiple = $multiple;
1✔
38
                $this->setOption('type', 'file');
1✔
39
                $this->addCondition(true) // not to block the export of rules to JS
1✔
40
                        ->addRule($this->isOk(...), Forms\Validator::$messages[self::Valid]);
1✔
41
                $this->addRule(Form::MaxFileSize, null, Forms\Helpers::iniGetSize('upload_max_filesize'));
1✔
42
                if ($multiple) {
1✔
43
                        $this->addRule(Form::MaxLength, 'The maximum allowed number of uploaded files is %d', (int) ini_get('max_file_uploads'));
1✔
44
                }
45

46
                $this->monitor(Form::class, function (Form $form): void {
1✔
47
                        if (!$form->isMethod('post')) {
1✔
48
                                throw new Nette\InvalidStateException('File upload requires method POST.');
49
                        }
50

51
                        $form->getElementPrototype()->enctype = 'multipart/form-data';
1✔
52
                });
1✔
53
        }
1✔
54

55

56
        public function loadHttpData(): void
57
        {
58
                $this->value = $this->getSubmittedValue($this->control->multiple ? Form::DataList | Form::DataFile : Form::DataFile);
1✔
59
        }
1✔
60

61

62
        public function getHtmlName(): string
63
        {
64
                return parent::getHtmlName() . ($this->control->multiple ? '[]' : '');
1✔
65
        }
66

67

68
        /** @internal */
69
        public function setValue(mixed $value): static
1✔
70
        {
71
                return $this;
1✔
72
        }
73

74

75
        /**
76
         * Returns the uploaded file(s), a dummy FileUpload(null) when nothing was uploaded, or null when nullable is set.
77
         * @return FileUpload|FileUpload[]|null
78
         */
79
        public function getValue(): FileUpload|array|null
80
        {
81
                return $this->value ?? ($this->nullable ? null : new FileUpload(null));
1✔
82
        }
83

84

85
        /**
86
         * Has been any file uploaded?
87
         */
88
        public function isFilled(): bool
89
        {
90
                return (bool) $this->value;
1✔
91
        }
92

93

94
        /**
95
         * Sets whether getValue() returns null instead of FileUpload with error UPLOAD_ERR_NO_FILE.
96
         */
97
        public function setNullable(bool $value = true): static
1✔
98
        {
99
                $this->nullable = $value;
1✔
100
                return $this;
1✔
101
        }
102

103

104
        public function isNullable(): bool
105
        {
UNCOV
106
                return $this->nullable;
×
107
        }
108

109

110
        /**
111
         * Have been all files successfully uploaded?
112
         * @internal
113
         */
114
        public function isOk(): bool
115
        {
116
                return match (true) {
117
                        !$this->value => false,
1✔
118
                        is_array($this->value) => Arrays::every($this->value, fn(FileUpload $upload): bool => $upload->isOk()),
1✔
119
                        default => $this->value->isOk(),
1✔
120
                };
121
        }
122

123

124
        /** @param  (callable(Nette\Forms\Control, mixed): bool)|string  $validator */
125
        public function addRule(
1✔
126
                callable|string $validator,
127
                string|Stringable|null $errorMessage = null,
128
                mixed $arg = null,
129
        ): static
130
        {
131
                if ($validator === Form::Image) {
1✔
132
                        $this->control->accept = implode(', ', Forms\Helpers::getSupportedImages());
1✔
133

134
                } elseif ($validator === Form::MimeType) {
1✔
135
                        $this->control->accept = implode(', ', (array) $arg);
1✔
136

137
                } elseif ($validator === Form::MaxFileSize) {
1✔
138
                        if ($arg > ($ini = Forms\Helpers::iniGetSize('upload_max_filesize'))) {
1✔
UNCOV
139
                                trigger_error("Value of MaxFileSize ($arg) is greater than value of directive upload_max_filesize ($ini).", E_USER_WARNING);
×
140
                        }
141
                        $this->getRules()->removeRule($validator);
1✔
142

143
                } elseif ($validator === Form::MaxLength) {
1✔
144
                        if ($arg > ($ini = ini_get('max_file_uploads'))) {
1✔
UNCOV
145
                                trigger_error("Value of MaxLength ($arg) is greater than value of directive max_file_uploads ($ini).", E_USER_WARNING);
×
146
                        }
147
                        $this->getRules()->removeRule($validator);
1✔
148
                }
149

150
                return parent::addRule($validator, $errorMessage, $arg);
1✔
151
        }
152
}
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

© 2026 Coveralls, Inc