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