Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationLatte
      • ApplicationTracy
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsLatte
      • Framework
      • HttpTracy
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Latte
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
    • Reflection
    • Security
    • Templating
    • Utils
  • NetteModule
  • none
  • Tracy

Classes

  • Container
  • ControlGroup
  • Form
  • Helpers
  • Rule
  • Rules

Interfaces

  • IControl
  • IFormRenderer
  • ISubmitterControl
  • Overview
  • Namespace
  • Class
  • Tree
  • Deprecated
  • Other releases
  • Nette homepage
  1: <?php
  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;
  9: 
 10: use Nette;
 11: 
 12: 
 13: /**
 14:  * Creates, validates and renders HTML forms.
 15:  *
 16:  * @author     David Grudl
 17:  *
 18:  * @property-read array $errors
 19:  * @property-read Nette\Utils\Html $elementPrototype
 20:  */
 21: class Form extends Container implements Nette\Utils\IHtmlString
 22: {
 23:     /** validator */
 24:     const EQUAL = ':equal',
 25:         IS_IN = self::EQUAL,
 26:         NOT_EQUAL = ':notEqual',
 27:         FILLED = ':filled',
 28:         BLANK = ':blank',
 29:         REQUIRED = self::FILLED,
 30:         VALID = ':valid';
 31: 
 32:     /** @deprecated CSRF protection */
 33:     const PROTECTION = Controls\CsrfProtection::PROTECTION;
 34: 
 35:     // button
 36:     const SUBMITTED = ':submitted';
 37: 
 38:     // text
 39:     const MIN_LENGTH = ':minLength',
 40:         MAX_LENGTH = ':maxLength',
 41:         LENGTH = ':length',
 42:         EMAIL = ':email',
 43:         URL = ':url',
 44:         PATTERN = ':pattern',
 45:         INTEGER = ':integer',
 46:         NUMERIC = ':integer',
 47:         FLOAT = ':float',
 48:         MIN = ':min',
 49:         MAX = ':max',
 50:         RANGE = ':range';
 51: 
 52:     // multiselect
 53:     const COUNT = self::LENGTH;
 54: 
 55:     // file upload
 56:     const MAX_FILE_SIZE = ':fileSize',
 57:         MIME_TYPE = ':mimeType',
 58:         IMAGE = ':image',
 59:         MAX_POST_SIZE = ':maxPostSize';
 60: 
 61:     /** method */
 62:     const GET = 'get',
 63:         POST = 'post';
 64: 
 65:     /** submitted data types */
 66:     const DATA_TEXT = 1;
 67:     const DATA_LINE = 2;
 68:     const DATA_FILE = 3;
 69:     const DATA_KEYS = 8;
 70: 
 71:     /** @internal tracker ID */
 72:     const TRACKER_ID = '_form_';
 73: 
 74:     /** @internal protection token ID */
 75:     const PROTECTOR_ID = '_token_';
 76: 
 77:     /** @var callable[]  function (Form $sender); Occurs when the form is submitted and successfully validated */
 78:     public $onSuccess;
 79: 
 80:     /** @var callable[]  function (Form $sender); Occurs when the form is submitted and is not valid */
 81:     public $onError;
 82: 
 83:     /** @var callable[]  function (Form $sender); Occurs when the form is submitted */
 84:     public $onSubmit;
 85: 
 86:     /** @var mixed or NULL meaning: not detected yet */
 87:     private $submittedBy;
 88: 
 89:     /** @var array */
 90:     private $httpData;
 91: 
 92:     /** @var Nette\Utils\Html  <form> element */
 93:     private $element;
 94: 
 95:     /** @var IFormRenderer */
 96:     private $renderer;
 97: 
 98:     /** @var Nette\Localization\ITranslator */
 99:     private $translator;
100: 
101:     /** @var ControlGroup[] */
102:     private $groups = array();
103: 
104:     /** @var array */
105:     private $errors = array();
106: 
107:     /** @var Nette\Http\IRequest  used only by standalone form */
108:     public $httpRequest;
109: 
110: 
111:     /**
112:      * Form constructor.
113:      * @param  string
114:      */
115:     public function __construct($name = NULL)
116:     {
117:         if ($name !== NULL) {
118:             $this->getElementPrototype()->id = 'frm-' . $name;
119:             $tracker = new Controls\HiddenField($name);
120:             $tracker->setOmitted();
121:             $this[self::TRACKER_ID] = $tracker;
122:         }
123:         parent::__construct(NULL, $name);
124:     }
125: 
126: 
127:     /**
128:      * @return void
129:      */
130:     protected function validateParent(Nette\ComponentModel\IContainer $parent)
131:     {
132:         parent::validateParent($parent);
133:         $this->monitor(__CLASS__);
134:     }
135: 
136: 
137:     /**
138:      * This method will be called when the component (or component's parent)
139:      * becomes attached to a monitored object. Do not call this method yourself.
140:      * @param  Nette\ComponentModel\IComponent
141:      * @return void
142:      */
143:     protected function attached($obj)
144:     {
145:         if ($obj instanceof self) {
146:             throw new Nette\InvalidStateException('Nested forms are forbidden.');
147:         }
148:     }
149: 
150: 
151:     /**
152:      * Returns self.
153:      * @return Form
154:      */
155:     public function getForm($need = TRUE)
156:     {
157:         return $this;
158:     }
159: 
160: 
161:     /**
162:      * Sets form's action.
163:      * @param  mixed URI
164:      * @return self
165:      */
166:     public function setAction($url)
167:     {
168:         $this->getElementPrototype()->action = $url;
169:         return $this;
170:     }
171: 
172: 
173:     /**
174:      * Returns form's action.
175:      * @return mixed URI
176:      */
177:     public function getAction()
178:     {
179:         return $this->getElementPrototype()->action;
180:     }
181: 
182: 
183:     /**
184:      * Sets form's method.
185:      * @param  string get | post
186:      * @return self
187:      */
188:     public function setMethod($method)
189:     {
190:         if ($this->httpData !== NULL) {
191:             throw new Nette\InvalidStateException(__METHOD__ . '() must be called until the form is empty.');
192:         }
193:         $this->getElementPrototype()->method = strtolower($method);
194:         return $this;
195:     }
196: 
197: 
198:     /**
199:      * Returns form's method.
200:      * @return string get | post
201:      */
202:     public function getMethod()
203:     {
204:         return $this->getElementPrototype()->method;
205:     }
206: 
207: 
208:     /**
209:      * Cross-Site Request Forgery (CSRF) form protection.
210:      * @param  string
211:      * @return Controls\CsrfProtection
212:      */
213:     public function addProtection($message = NULL)
214:     {
215:         return $this[self::PROTECTOR_ID] = new Controls\CsrfProtection($message);
216:     }
217: 
218: 
219:     /**
220:      * Adds fieldset group to the form.
221:      * @param  string  caption
222:      * @param  bool    set this group as current
223:      * @return ControlGroup
224:      */
225:     public function addGroup($caption = NULL, $setAsCurrent = TRUE)
226:     {
227:         $group = new ControlGroup;
228:         $group->setOption('label', $caption);
229:         $group->setOption('visual', TRUE);
230: 
231:         if ($setAsCurrent) {
232:             $this->setCurrentGroup($group);
233:         }
234: 
235:         if (!is_scalar($caption) || isset($this->groups[$caption])) {
236:             return $this->groups[] = $group;
237:         } else {
238:             return $this->groups[$caption] = $group;
239:         }
240:     }
241: 
242: 
243:     /**
244:      * Removes fieldset group from form.
245:      * @param  string|ControlGroup
246:      * @return void
247:      */
248:     public function removeGroup($name)
249:     {
250:         if (is_string($name) && isset($this->groups[$name])) {
251:             $group = $this->groups[$name];
252: 
253:         } elseif ($name instanceof ControlGroup && in_array($name, $this->groups, TRUE)) {
254:             $group = $name;
255:             $name = array_search($group, $this->groups, TRUE);
256: 
257:         } else {
258:             throw new Nette\InvalidArgumentException("Group not found in form '$this->name'");
259:         }
260: 
261:         foreach ($group->getControls() as $control) {
262:             $control->getParent()->removeComponent($control);
263:         }
264: 
265:         unset($this->groups[$name]);
266:     }
267: 
268: 
269:     /**
270:      * Returns all defined groups.
271:      * @return ControlGroup[]
272:      */
273:     public function getGroups()
274:     {
275:         return $this->groups;
276:     }
277: 
278: 
279:     /**
280:      * Returns the specified group.
281:      * @param  string  name
282:      * @return ControlGroup
283:      */
284:     public function getGroup($name)
285:     {
286:         return isset($this->groups[$name]) ? $this->groups[$name] : NULL;
287:     }
288: 
289: 
290:     /********************* translator ****************d*g**/
291: 
292: 
293:     /**
294:      * Sets translate adapter.
295:      * @return self
296:      */
297:     public function setTranslator(Nette\Localization\ITranslator $translator = NULL)
298:     {
299:         $this->translator = $translator;
300:         return $this;
301:     }
302: 
303: 
304:     /**
305:      * Returns translate adapter.
306:      * @return Nette\Localization\ITranslator|NULL
307:      */
308:     public function getTranslator()
309:     {
310:         return $this->translator;
311:     }
312: 
313: 
314:     /********************* submission ****************d*g**/
315: 
316: 
317:     /**
318:      * Tells if the form is anchored.
319:      * @return bool
320:      */
321:     public function isAnchored()
322:     {
323:         return TRUE;
324:     }
325: 
326: 
327:     /**
328:      * Tells if the form was submitted.
329:      * @return ISubmitterControl|FALSE  submittor control
330:      */
331:     public function isSubmitted()
332:     {
333:         if ($this->submittedBy === NULL) {
334:             $this->getHttpData();
335:         }
336:         return $this->submittedBy;
337:     }
338: 
339: 
340:     /**
341:      * Tells if the form was submitted and successfully validated.
342:      * @return bool
343:      */
344:     public function isSuccess()
345:     {
346:         return $this->isSubmitted() && $this->isValid();
347:     }
348: 
349: 
350:     /**
351:      * Sets the submittor control.
352:      * @return self
353:      */
354:     public function setSubmittedBy(ISubmitterControl $by = NULL)
355:     {
356:         $this->submittedBy = $by === NULL ? FALSE : $by;
357:         return $this;
358:     }
359: 
360: 
361:     /**
362:      * Returns submitted HTTP data.
363:      * @return mixed
364:      */
365:     public function getHttpData($type = NULL, $htmlName = NULL)
366:     {
367:         if ($this->httpData === NULL) {
368:             if (!$this->isAnchored()) {
369:                 throw new Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
370:             }
371:             $data = $this->receiveHttpData();
372:             $this->httpData = (array) $data;
373:             $this->submittedBy = is_array($data);
374:         }
375:         if ($htmlName === NULL) {
376:             return $this->httpData;
377:         }
378:         return Helpers::extractHttpData($this->httpData, $htmlName, $type);
379:     }
380: 
381: 
382:     /**
383:      * Fires submit/click events.
384:      * @return void
385:      */
386:     public function fireEvents()
387:     {
388:         if (!$this->isSubmitted()) {
389:             return;
390: 
391:         } elseif (!$this->getErrors()) {
392:             $this->validate();
393:         }
394: 
395:         if ($this->submittedBy instanceof ISubmitterControl) {
396:             if ($this->isValid()) {
397:                 $this->submittedBy->onClick($this->submittedBy);
398:             } else {
399:                 $this->submittedBy->onInvalidClick($this->submittedBy);
400:             }
401:         }
402: 
403:         if (!$this->isValid()) {
404:             $this->onError($this);
405:         } elseif ($this->onSuccess) {
406:             foreach ($this->onSuccess as $handler) {
407:                 $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
408:                 $values = isset($params[1]) ? $this->getValues($params[1]->isArray()) : NULL;
409:                 Nette\Utils\Callback::invoke($handler, $this, $values);
410:                 if (!$this->isValid()) {
411:                     $this->onError($this);
412:                     break;
413:                 }
414:             }
415:         }
416: 
417:         $this->onSubmit($this);
418:     }
419: 
420: 
421:     /**
422:      * Internal: returns submitted HTTP data or NULL when form was not submitted.
423:      * @return array|NULL
424:      */
425:     protected function receiveHttpData()
426:     {
427:         $httpRequest = $this->getHttpRequest();
428:         if (strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
429:             return;
430:         }
431: 
432:         if ($httpRequest->isMethod('post')) {
433:             $data = Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
434:         } else {
435:             $data = $httpRequest->getQuery();
436:             if (!$data) {
437:                 return;
438:             }
439:         }
440: 
441:         if ($tracker = $this->getComponent(self::TRACKER_ID, FALSE)) {
442:             if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) {
443:                 return;
444:             }
445:         }
446: 
447:         return $data;
448:     }
449: 
450: 
451:     /********************* validation ****************d*g**/
452: 
453: 
454:     public function validate(array $controls = NULL)
455:     {
456:         $this->cleanErrors();
457:         if ($controls === NULL && $this->submittedBy instanceof ISubmitterControl) {
458:             $controls = $this->submittedBy->getValidationScope();
459:         }
460:         $this->validateMaxPostSize();
461:         parent::validate($controls);
462:     }
463: 
464: 
465:     /** @internal */
466:     public function validateMaxPostSize()
467:     {
468:         if (!$this->submittedBy || strcasecmp($this->getMethod(), 'POST') || empty($_SERVER['CONTENT_LENGTH'])) {
469:             return;
470:         }
471:         $maxSize = ini_get('post_max_size');
472:         $units = array('k' => 10, 'm' => 20, 'g' => 30);
473:         if (isset($units[$ch = strtolower(substr($maxSize, -1))])) {
474:             $maxSize <<= $units[$ch];
475:         }
476:         if ($maxSize > 0 && $maxSize < $_SERVER['CONTENT_LENGTH']) {
477:             $this->addError(sprintf(Rules::$defaultMessages[self::MAX_FILE_SIZE], $maxSize));
478:         }
479:     }
480: 
481: 
482:     /**
483:      * Adds global error message.
484:      * @param  string  error message
485:      * @return void
486:      */
487:     public function addError($message)
488:     {
489:         $this->errors[] = $message;
490:     }
491: 
492: 
493:     /**
494:      * Returns global validation errors.
495:      * @return array
496:      */
497:     public function getErrors()
498:     {
499:         return array_unique(array_merge($this->errors, parent::getErrors()));
500:     }
501: 
502: 
503:     /**
504:      * @return bool
505:      */
506:     public function hasErrors()
507:     {
508:         return (bool) $this->getErrors();
509:     }
510: 
511: 
512:     /**
513:      * @return void
514:      */
515:     public function cleanErrors()
516:     {
517:         $this->errors = array();
518:     }
519: 
520: 
521:     /**
522:      * Returns form's validation errors.
523:      * @return array
524:      */
525:     public function getOwnErrors()
526:     {
527:         return array_unique($this->errors);
528:     }
529: 
530: 
531:     /** @deprecated */
532:     public function getAllErrors()
533:     {
534:         trigger_error(__METHOD__ . '() is deprecated; use getErrors() instead.', E_USER_DEPRECATED);
535:         return $this->getErrors();
536:     }
537: 
538: 
539:     /********************* rendering ****************d*g**/
540: 
541: 
542:     /**
543:      * Returns form's HTML element template.
544:      * @return Nette\Utils\Html
545:      */
546:     public function getElementPrototype()
547:     {
548:         if (!$this->element) {
549:             $this->element = Nette\Utils\Html::el('form');
550:             $this->element->action = ''; // RFC 1808 -> empty uri means 'this'
551:             $this->element->method = self::POST;
552:         }
553:         return $this->element;
554:     }
555: 
556: 
557:     /**
558:      * Sets form renderer.
559:      * @return self
560:      */
561:     public function setRenderer(IFormRenderer $renderer = NULL)
562:     {
563:         $this->renderer = $renderer;
564:         return $this;
565:     }
566: 
567: 
568:     /**
569:      * Returns form renderer.
570:      * @return IFormRenderer
571:      */
572:     public function getRenderer()
573:     {
574:         if ($this->renderer === NULL) {
575:             $this->renderer = new Rendering\DefaultFormRenderer;
576:         }
577:         return $this->renderer;
578:     }
579: 
580: 
581:     /**
582:      * Renders form.
583:      * @return void
584:      */
585:     public function render()
586:     {
587:         $args = func_get_args();
588:         array_unshift($args, $this);
589:         echo call_user_func_array(array($this->getRenderer(), 'render'), $args);
590:     }
591: 
592: 
593:     /**
594:      * Renders form to string.
595:      * @param can throw exceptions? (hidden parameter)
596:      * @return string
597:      */
598:     public function __toString()
599:     {
600:         try {
601:             return $this->getRenderer()->render($this);
602: 
603:         } catch (\Throwable $e) {
604:         } catch (\Exception $e) {
605:         }
606:         if (isset($e)) {
607:             if (func_num_args()) {
608:                 throw $e;
609:             }
610:             trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
611:         }
612:     }
613: 
614: 
615:     /********************* backend ****************d*g**/
616: 
617: 
618:     /**
619:      * @return Nette\Http\IRequest
620:      */
621:     private function getHttpRequest()
622:     {
623:         if (!$this->httpRequest) {
624:             $factory = new Nette\Http\RequestFactory;
625:             $this->httpRequest = $factory->createHttpRequest();
626:         }
627:         return $this->httpRequest;
628:     }
629: 
630: 
631:     /**
632:      * @return array
633:      */
634:     public function getToggles()
635:     {
636:         $toggles = array();
637:         foreach ($this->getControls() as $control) {
638:             $toggles = $control->getRules()->getToggleStates($toggles);
639:         }
640:         return $toggles;
641:     }
642: 
643: }
644: 
Nette 2.2 API documentation generated by ApiGen 2.8.0