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
    • Utils
  • none
  • Tracy
    • Bridges
      • Nette

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