Namespaces

  • Nette
    • Application
      • Diagnostics
      • Responses
      • Routers
      • UI
    • Caching
      • Storages
    • ComponentModel
    • Database
      • Diagnostics
      • Drivers
      • Reflection
      • Table
    • DI
      • Config
        • Adapters
      • Diagnostics
      • Extensions
    • Diagnostics
    • Forms
      • Controls
      • Rendering
    • Http
      • Diagnostics
    • Iterators
    • Latte
      • Macros
    • Loaders
    • Localization
    • Mail
    • PhpGenerator
    • Reflection
    • Security
      • Diagnostics
    • Templating
    • Utils
  • NetteModule
  • none

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   mixed $action
 19:  * @property   string $method
 20:  * @property-read array $groups
 21:  * @property   Nette\Localization\ITranslator|NULL $translator
 22:  * @property-read bool $anchored
 23:  * @property-read ISubmitterControl|FALSE $submitted
 24:  * @property-read bool $success
 25:  * @property-read array $httpData
 26:  * @property-read array $errors
 27:  * @property-read Nette\Utils\Html $elementPrototype
 28:  * @property   IFormRenderer $renderer
 29:  */
 30: class Form extends Container
 31: {
 32:     /** validator */
 33:     const EQUAL = ':equal',
 34:         IS_IN = self::EQUAL,
 35:         NOT_EQUAL = ':notEqual',
 36:         FILLED = ':filled',
 37:         BLANK = ':blank',
 38:         REQUIRED = self::FILLED,
 39:         VALID = ':valid';
 40: 
 41:     /** @deprecated CSRF protection */
 42:     const PROTECTION = Controls\CsrfProtection::PROTECTION;
 43: 
 44:     // button
 45:     const SUBMITTED = ':submitted';
 46: 
 47:     // text
 48:     const MIN_LENGTH = ':minLength',
 49:         MAX_LENGTH = ':maxLength',
 50:         LENGTH = ':length',
 51:         EMAIL = ':email',
 52:         URL = ':url',
 53:         REGEXP = ':regexp',
 54:         PATTERN = ':pattern',
 55:         INTEGER = ':integer',
 56:         NUMERIC = ':integer',
 57:         FLOAT = ':float',
 58:         RANGE = ':range';
 59: 
 60:     // multiselect
 61:     const COUNT = self::LENGTH;
 62: 
 63:     // file upload
 64:     const MAX_FILE_SIZE = ':fileSize',
 65:         MIME_TYPE = ':mimeType',
 66:         IMAGE = ':image',
 67:         MAX_POST_SIZE = ':maxPostSize';
 68: 
 69:     /** method */
 70:     const GET = 'get',
 71:         POST = 'post';
 72: 
 73:     /** submitted data types */
 74:     const DATA_TEXT = 1;
 75:     const DATA_LINE = 2;
 76:     const DATA_FILE = 3;
 77:     const 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 array of function (Form $sender); Occurs when the form is submitted and successfully validated */
 86:     public $onSuccess;
 87: 
 88:     /** @var array of function (Form $sender); Occurs when the form is submitted and is not valid */
 89:     public $onError;
 90: 
 91:     /** @var array of function (Form $sender); Occurs when the form is submitted */
 92:     public $onSubmit;
 93: 
 94:     /** @var mixed or NULL meaning: not detected yet */
 95:     private $submittedBy;
 96: 
 97:     /** @var array */
 98:     private $httpData;
 99: 
100:     /** @var Nette\Utils\Html  <form> element */
101:     private $element;
102: 
103:     /** @var IFormRenderer */
104:     private $renderer;
105: 
106:     /** @var Nette\Localization\ITranslator */
107:     private $translator;
108: 
109:     /** @var ControlGroup[] */
110:     private $groups = array();
111: 
112:     /** @var array */
113:     private $errors = array();
114: 
115:     /** @var Nette\Http\IRequest  used only by standalone form */
116:     public $httpRequest;
117: 
118: 
119:     /**
120:      * Form constructor.
121:      * @param  string
122:      */
123:     public function __construct($name = NULL)
124:     {
125:         $this->element = Nette\Utils\Html::el('form');
126:         $this->element->action = ''; // RFC 1808 -> empty uri means 'this'
127:         $this->element->method = self::POST;
128:         $this->element->id = $name === NULL ? NULL : 'frm-' . $name;
129: 
130:         $this->monitor(__CLASS__);
131:         if ($name !== NULL) {
132:             $tracker = new Controls\HiddenField($name);
133:             $tracker->setOmitted();
134:             $this[self::TRACKER_ID] = $tracker;
135:         }
136:         parent::__construct(NULL, $name);
137:     }
138: 
139: 
140:     /**
141:      * This method will be called when the component (or component's parent)
142:      * becomes attached to a monitored object. Do not call this method yourself.
143:      * @param  Nette\ComponentModel\IComponent
144:      * @return void
145:      */
146:     protected function attached($obj)
147:     {
148:         if ($obj instanceof self) {
149:             throw new Nette\InvalidStateException('Nested forms are forbidden.');
150:         }
151:     }
152: 
153: 
154:     /**
155:      * Returns self.
156:      * @return Form
157:      */
158:     public function getForm($need = TRUE)
159:     {
160:         return $this;
161:     }
162: 
163: 
164:     /**
165:      * Sets form's action.
166:      * @param  mixed URI
167:      * @return self
168:      */
169:     public function setAction($url)
170:     {
171:         $this->element->action = $url;
172:         return $this;
173:     }
174: 
175: 
176:     /**
177:      * Returns form's action.
178:      * @return mixed URI
179:      */
180:     public function getAction()
181:     {
182:         return $this->element->action;
183:     }
184: 
185: 
186:     /**
187:      * Sets form's method.
188:      * @param  string get | post
189:      * @return self
190:      */
191:     public function setMethod($method)
192:     {
193:         if ($this->httpData !== NULL) {
194:             throw new Nette\InvalidStateException(__METHOD__ . '() must be called until the form is empty.');
195:         }
196:         $this->element->method = strtolower($method);
197:         return $this;
198:     }
199: 
200: 
201:     /**
202:      * Returns form's method.
203:      * @return string get | post
204:      */
205:     public function getMethod()
206:     {
207:         return $this->element->method;
208:     }
209: 
210: 
211:     /**
212:      * Cross-Site Request Forgery (CSRF) form protection.
213:      * @param  string
214:      * @return Controls\CsrfProtection
215:      */
216:     public function addProtection($message = NULL)
217:     {
218:         return $this[self::PROTECTOR_ID] = new Controls\CsrfProtection($message);
219:     }
220: 
221: 
222:     /**
223:      * Adds fieldset group to the form.
224:      * @param  string  caption
225:      * @param  bool    set this group as current
226:      * @return ControlGroup
227:      */
228:     public function addGroup($caption = NULL, $setAsCurrent = TRUE)
229:     {
230:         $group = new ControlGroup;
231:         $group->setOption('label', $caption);
232:         $group->setOption('visual', TRUE);
233: 
234:         if ($setAsCurrent) {
235:             $this->setCurrentGroup($group);
236:         }
237: 
238:         if (!is_scalar($caption) || isset($this->groups[$caption])) {
239:             return $this->groups[] = $group;
240:         } else {
241:             return $this->groups[$caption] = $group;
242:         }
243:     }
244: 
245: 
246:     /**
247:      * Removes fieldset group from form.
248:      * @param  string|ControlGroup
249:      * @return void
250:      */
251:     public function removeGroup($name)
252:     {
253:         if (is_string($name) && isset($this->groups[$name])) {
254:             $group = $this->groups[$name];
255: 
256:         } elseif ($name instanceof ControlGroup && in_array($name, $this->groups, TRUE)) {
257:             $group = $name;
258:             $name = array_search($group, $this->groups, TRUE);
259: 
260:         } else {
261:             throw new Nette\InvalidArgumentException("Group not found in form '$this->name'");
262:         }
263: 
264:         foreach ($group->getControls() as $control) {
265:             $control->getParent()->removeComponent($control);
266:         }
267: 
268:         unset($this->groups[$name]);
269:     }
270: 
271: 
272:     /**
273:      * Returns all defined groups.
274:      * @return ControlGroup[]
275:      */
276:     public function getGroups()
277:     {
278:         return $this->groups;
279:     }
280: 
281: 
282:     /**
283:      * Returns the specified group.
284:      * @param  string  name
285:      * @return ControlGroup
286:      */
287:     public function getGroup($name)
288:     {
289:         return isset($this->groups[$name]) ? $this->groups[$name] : NULL;
290:     }
291: 
292: 
293:     /********************* translator ****************d*g**/
294: 
295: 
296:     /**
297:      * Sets translate adapter.
298:      * @return self
299:      */
300:     public function setTranslator(Nette\Localization\ITranslator $translator = NULL)
301:     {
302:         $this->translator = $translator;
303:         return $this;
304:     }
305: 
306: 
307:     /**
308:      * Returns translate adapter.
309:      * @return Nette\Localization\ITranslator|NULL
310:      */
311:     public function getTranslator()
312:     {
313:         return $this->translator;
314:     }
315: 
316: 
317:     /********************* submission ****************d*g**/
318: 
319: 
320:     /**
321:      * Tells if the form is anchored.
322:      * @return bool
323:      */
324:     public function isAnchored()
325:     {
326:         return TRUE;
327:     }
328: 
329: 
330:     /**
331:      * Tells if the form was submitted.
332:      * @return ISubmitterControl|FALSE  submittor control
333:      */
334:     public function isSubmitted()
335:     {
336:         if ($this->submittedBy === NULL) {
337:             $this->getHttpData();
338:         }
339:         return $this->submittedBy;
340:     }
341: 
342: 
343:     /**
344:      * Tells if the form was submitted and successfully validated.
345:      * @return bool
346:      */
347:     public function isSuccess()
348:     {
349:         return $this->isSubmitted() && $this->isValid();
350:     }
351: 
352: 
353:     /**
354:      * Sets the submittor control.
355:      * @return self
356:      */
357:     public function setSubmittedBy(ISubmitterControl $by = NULL)
358:     {
359:         $this->submittedBy = $by === NULL ? FALSE : $by;
360:         return $this;
361:     }
362: 
363: 
364:     /**
365:      * Returns submitted HTTP data.
366:      * @return mixed
367:      */
368:     public function getHttpData($type = NULL, $htmlName = NULL)
369:     {
370:         if ($this->httpData === NULL) {
371:             if (!$this->isAnchored()) {
372:                 throw new Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
373:             }
374:             $data = $this->receiveHttpData();
375:             $this->httpData = (array) $data;
376:             $this->submittedBy = is_array($data);
377:         }
378:         if ($htmlName === NULL) {
379:             return $this->httpData;
380:         }
381:         return Helpers::extractHttpData($this->httpData, $htmlName, $type);
382:     }
383: 
384: 
385:     /**
386:      * Fires submit/click events.
387:      * @return void
388:      */
389:     public function fireEvents()
390:     {
391:         if (!$this->isSubmitted()) {
392:             return;
393: 
394:         } elseif (!$this->getErrors()) {
395:             $this->validate();
396:         }
397: 
398:         if ($this->submittedBy instanceof ISubmitterControl) {
399:             if ($this->isValid()) {
400:                 $this->submittedBy->onClick($this->submittedBy);
401:             } else {
402:                 $this->submittedBy->onInvalidClick($this->submittedBy);
403:             }
404:         }
405: 
406:         if ($this->onSuccess) {
407:             foreach ($this->onSuccess as $handler) {
408:                 if (!$this->isValid()) {
409:                     $this->onError($this);
410:                     break;
411:                 }
412:                 Nette\Utils\Callback::invoke($handler, $this);
413:             }
414:         } elseif (!$this->isValid()) {
415:             $this->onError($this);
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:         return $this->element;
549:     }
550: 
551: 
552:     /**
553:      * Sets form renderer.
554:      * @return self
555:      */
556:     public function setRenderer(IFormRenderer $renderer = NULL)
557:     {
558:         $this->renderer = $renderer;
559:         return $this;
560:     }
561: 
562: 
563:     /**
564:      * Returns form renderer.
565:      * @return IFormRenderer
566:      */
567:     public function getRenderer()
568:     {
569:         if ($this->renderer === NULL) {
570:             $this->renderer = new Rendering\DefaultFormRenderer;
571:         }
572:         return $this->renderer;
573:     }
574: 
575: 
576:     /**
577:      * Renders form.
578:      * @return void
579:      */
580:     public function render()
581:     {
582:         $args = func_get_args();
583:         array_unshift($args, $this);
584:         echo call_user_func_array(array($this->getRenderer(), 'render'), $args);
585:     }
586: 
587: 
588:     /**
589:      * Renders form to string.
590:      * @return can throw exceptions? (hidden parameter)
591:      * @return string
592:      */
593:     public function __toString()
594:     {
595:         try {
596:             return $this->getRenderer()->render($this);
597: 
598:         } catch (\Throwable $e) {
599:         } catch (\Exception $e) {
600:         }
601:         if (isset($e)) {
602:             if (func_num_args()) {
603:                 throw $e;
604:             }
605:             trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
606:         }
607:     }
608: 
609: 
610:     /********************* backend ****************d*g**/
611: 
612: 
613:     /**
614:      * @return Nette\Http\IRequest
615:      */
616:     private function getHttpRequest()
617:     {
618:         if (!$this->httpRequest) {
619:             $factory = new Nette\Http\RequestFactory;
620:             $this->httpRequest = $factory->createHttpRequest();
621:         }
622:         return $this->httpRequest;
623:     }
624: 
625: 
626:     /**
627:      * @return array
628:      */
629:     public function getToggles()
630:     {
631:         $toggles = array();
632:         foreach ($this->getControls() as $control) {
633:             $toggles = $control->getRules()->getToggleStates($toggles);
634:         }
635:         return $toggles;
636:     }
637: 
638: }
639: 
Nette 2.1 API documentation generated by ApiGen 2.8.0