Namespaces

  • Latte
    • Loaders
    • Macros
    • Runtime
  • Nette
    • Application
      • Responses
      • Routers
      • UI
    • Bridges
      • ApplicationDI
      • ApplicationLatte
      • ApplicationTracy
      • CacheDI
      • CacheLatte
      • DatabaseDI
      • DatabaseTracy
      • DITracy
      • FormsDI
      • FormsLatte
      • Framework
      • HttpDI
      • HttpTracy
      • MailDI
      • ReflectionDI
      • SecurityDI
      • SecurityTracy
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Conventions
      • Drivers
      • Table
    • DI
      • Config
        • Adapters
      • Extensions
    • Forms
      • Controls
      • Rendering
    • Http
    • Iterators
    • Loaders
    • Localization
    • Mail
    • Neon
    • PhpGenerator
      • Traits
    • Reflection
    • Security
    • Tokenizer
    • Utils
  • Tracy
    • Bridges
      • Nette
  • none

Classes

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

Interfaces

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