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
      • Reflection
      • 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
  • 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:  * @property-read array $errors
 17:  * @property-read Nette\Utils\Html $elementPrototype
 18:  */
 19: class Form extends Container implements Nette\Utils\IHtmlString
 20: {
 21:     /** validator */
 22:     const EQUAL = ':equal',
 23:         IS_IN = self::EQUAL,
 24:         NOT_EQUAL = ':notEqual',
 25:         IS_NOT_IN = self::NOT_EQUAL,
 26:         FILLED = ':filled',
 27:         BLANK = ':blank',
 28:         REQUIRED = self::FILLED,
 29:         VALID = ':valid';
 30: 
 31:     /** @deprecated CSRF protection */
 32:     const PROTECTION = Controls\CsrfProtection::PROTECTION;
 33: 
 34:     // button
 35:     const SUBMITTED = ':submitted';
 36: 
 37:     // text
 38:     const MIN_LENGTH = ':minLength',
 39:         MAX_LENGTH = ':maxLength',
 40:         LENGTH = ':length',
 41:         EMAIL = ':email',
 42:         URL = ':url',
 43:         PATTERN = ':pattern',
 44:         INTEGER = ':integer',
 45:         NUMERIC = ':integer',
 46:         FLOAT = ':float',
 47:         MIN = ':min',
 48:         MAX = ':max',
 49:         RANGE = ':range';
 50: 
 51:     // multiselect
 52:     const COUNT = self::LENGTH;
 53: 
 54:     // file upload
 55:     const MAX_FILE_SIZE = ':fileSize',
 56:         MIME_TYPE = ':mimeType',
 57:         IMAGE = ':image',
 58:         MAX_POST_SIZE = ':maxPostSize';
 59: 
 60:     /** method */
 61:     const GET = 'get',
 62:         POST = 'post';
 63: 
 64:     /** submitted data types */
 65:     const DATA_TEXT = 1;
 66:     const DATA_LINE = 2;
 67:     const DATA_FILE = 3;
 68:     const DATA_KEYS = 8;
 69: 
 70:     /** @internal tracker ID */
 71:     const TRACKER_ID = '_form_';
 72: 
 73:     /** @internal protection token ID */
 74:     const PROTECTOR_ID = '_token_';
 75: 
 76:     /** @var callable[]  function (Form $sender); Occurs when the form is submitted and successfully validated */
 77:     public $onSuccess;
 78: 
 79:     /** @var callable[]  function (Form $sender); Occurs when the form is submitted and is not valid */
 80:     public $onError;
 81: 
 82:     /** @var callable[]  function (Form $sender); Occurs when the form is submitted */
 83:     public $onSubmit;
 84: 
 85:     /** @var mixed or NULL meaning: not detected yet */
 86:     private $submittedBy;
 87: 
 88:     /** @var array */
 89:     private $httpData;
 90: 
 91:     /** @var Nette\Utils\Html  <form> element */
 92:     private $element;
 93: 
 94:     /** @var IFormRenderer */
 95:     private $renderer;
 96: 
 97:     /** @var Nette\Localization\ITranslator */
 98:     private $translator;
 99: 
100:     /** @var ControlGroup[] */
101:     private $groups = array();
102: 
103:     /** @var array */
104:     private $errors = array();
105: 
106:     /** @var Nette\Http\IRequest  used only by standalone form */
107:     public $httpRequest;
108: 
109: 
110:     /**
111:      * Form constructor.
112:      * @param  string
113:      */
114:     public function __construct($name = NULL)
115:     {
116:         parent::__construct();
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:             $this->setParent(NULL, $name);
123:         }
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 static
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 static
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 static
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 static
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 static
353:      * @internal
354:      */
355:     public function setSubmittedBy(ISubmitterControl $by = NULL)
356:     {
357:         $this->submittedBy = $by === NULL ? FALSE : $by;
358:         return $this;
359:     }
360: 
361: 
362:     /**
363:      * Returns submitted HTTP data.
364:      * @return mixed
365:      */
366:     public function getHttpData($type = NULL, $htmlName = NULL)
367:     {
368:         if ($this->httpData === NULL) {
369:             if (!$this->isAnchored()) {
370:                 throw new Nette\InvalidStateException('Form is not anchored and therefore can not determine whether it was submitted.');
371:             }
372:             $data = $this->receiveHttpData();
373:             $this->httpData = (array) $data;
374:             $this->submittedBy = is_array($data);
375:         }
376:         if ($htmlName === NULL) {
377:             return $this->httpData;
378:         }
379:         return Helpers::extractHttpData($this->httpData, $htmlName, $type);
380:     }
381: 
382: 
383:     /**
384:      * Fires submit/click events.
385:      * @return void
386:      */
387:     public function fireEvents()
388:     {
389:         if (!$this->isSubmitted()) {
390:             return;
391: 
392:         } elseif (!$this->getErrors()) {
393:             $this->validate();
394:         }
395: 
396:         if ($this->submittedBy instanceof ISubmitterControl) {
397:             if ($this->isValid()) {
398:                 $this->submittedBy->onClick($this->submittedBy);
399:             } else {
400:                 $this->submittedBy->onInvalidClick($this->submittedBy);
401:             }
402:         }
403: 
404:         if (!$this->isValid()) {
405:             $this->onError($this);
406: 
407:         } elseif ($this->onSuccess !== NULL) {
408:             if (!is_array($this->onSuccess) && !$this->onSuccess instanceof \Traversable) {
409:                 throw new Nette\UnexpectedValueException('Property Form::$onSuccess must be array or Traversable, ' . gettype($this->onSuccess) . ' given.');
410:             }
411:             foreach ($this->onSuccess as $handler) {
412:                 $params = Nette\Utils\Callback::toReflection($handler)->getParameters();
413:                 $values = isset($params[1]) ? $this->getValues($params[1]->isArray()) : NULL;
414:                 Nette\Utils\Callback::invoke($handler, $this, $values);
415:                 if (!$this->isValid()) {
416:                     $this->onError($this);
417:                     break;
418:                 }
419:             }
420:         }
421: 
422:         $this->onSubmit($this);
423:     }
424: 
425: 
426:     /**
427:      * Internal: returns submitted HTTP data or NULL when form was not submitted.
428:      * @return array|NULL
429:      */
430:     protected function receiveHttpData()
431:     {
432:         $httpRequest = $this->getHttpRequest();
433:         if (strcasecmp($this->getMethod(), $httpRequest->getMethod())) {
434:             return;
435:         }
436: 
437:         if ($httpRequest->isMethod('post')) {
438:             $data = Nette\Utils\Arrays::mergeTree($httpRequest->getPost(), $httpRequest->getFiles());
439:         } else {
440:             $data = $httpRequest->getQuery();
441:             if (!$data) {
442:                 return;
443:             }
444:         }
445: 
446:         if ($tracker = $this->getComponent(self::TRACKER_ID, FALSE)) {
447:             if (!isset($data[self::TRACKER_ID]) || $data[self::TRACKER_ID] !== $tracker->getValue()) {
448:                 return;
449:             }
450:         }
451: 
452:         return $data;
453:     }
454: 
455: 
456:     /********************* validation ****************d*g**/
457: 
458: 
459:     public function validate(array $controls = NULL)
460:     {
461:         $this->cleanErrors();
462:         if ($controls === NULL && $this->submittedBy instanceof ISubmitterControl) {
463:             $controls = $this->submittedBy->getValidationScope();
464:         }
465:         $this->validateMaxPostSize();
466:         parent::validate($controls);
467:     }
468: 
469: 
470:     /** @internal */
471:     public function validateMaxPostSize()
472:     {
473:         if (!$this->submittedBy || strcasecmp($this->getMethod(), 'POST') || empty($_SERVER['CONTENT_LENGTH'])) {
474:             return;
475:         }
476:         $maxSize = ini_get('post_max_size');
477:         $units = array('k' => 10, 'm' => 20, 'g' => 30);
478:         if (isset($units[$ch = strtolower(substr($maxSize, -1))])) {
479:             $maxSize = (int) $maxSize << $units[$ch];
480:         }
481:         if ($maxSize > 0 && $maxSize < $_SERVER['CONTENT_LENGTH']) {
482:             $this->addError(sprintf(Validator::$messages[self::MAX_FILE_SIZE], $maxSize));
483:         }
484:     }
485: 
486: 
487:     /**
488:      * Adds global error message.
489:      * @param  string  error message
490:      * @return void
491:      */
492:     public function addError($message)
493:     {
494:         $this->errors[] = $message;
495:     }
496: 
497: 
498:     /**
499:      * Returns global validation errors.
500:      * @return array
501:      */
502:     public function getErrors()
503:     {
504:         return array_unique(array_merge($this->errors, parent::getErrors()));
505:     }
506: 
507: 
508:     /**
509:      * @return bool
510:      */
511:     public function hasErrors()
512:     {
513:         return (bool) $this->getErrors();
514:     }
515: 
516: 
517:     /**
518:      * @return void
519:      */
520:     public function cleanErrors()
521:     {
522:         $this->errors = array();
523:     }
524: 
525: 
526:     /**
527:      * Returns form's validation errors.
528:      * @return array
529:      */
530:     public function getOwnErrors()
531:     {
532:         return array_unique($this->errors);
533:     }
534: 
535: 
536:     /********************* rendering ****************d*g**/
537: 
538: 
539:     /**
540:      * Returns form's HTML element template.
541:      * @return Nette\Utils\Html
542:      */
543:     public function getElementPrototype()
544:     {
545:         if (!$this->element) {
546:             $this->element = Nette\Utils\Html::el('form');
547:             $this->element->action = ''; // RFC 1808 -> empty uri means 'this'
548:             $this->element->method = self::POST;
549:         }
550:         return $this->element;
551:     }
552: 
553: 
554:     /**
555:      * Sets form renderer.
556:      * @return static
557:      */
558:     public function setRenderer(IFormRenderer $renderer = NULL)
559:     {
560:         $this->renderer = $renderer;
561:         return $this;
562:     }
563: 
564: 
565:     /**
566:      * Returns form renderer.
567:      * @return IFormRenderer
568:      */
569:     public function getRenderer()
570:     {
571:         if ($this->renderer === NULL) {
572:             $this->renderer = new Rendering\DefaultFormRenderer;
573:         }
574:         return $this->renderer;
575:     }
576: 
577: 
578:     /**
579:      * Renders form.
580:      * @return void
581:      */
582:     public function render()
583:     {
584:         $args = func_get_args();
585:         array_unshift($args, $this);
586:         echo call_user_func_array(array($this->getRenderer(), 'render'), $args);
587:     }
588: 
589: 
590:     /**
591:      * Renders form to string.
592:      * @param can throw exceptions? (hidden parameter)
593:      * @return string
594:      */
595:     public function __toString()
596:     {
597:         try {
598:             return $this->getRenderer()->render($this);
599: 
600:         } catch (\Throwable $e) {
601:         } catch (\Exception $e) {
602:         }
603:         if (isset($e)) {
604:             if (func_num_args()) {
605:                 throw $e;
606:             }
607:             trigger_error("Exception in " . __METHOD__ . "(): {$e->getMessage()} in {$e->getFile()}:{$e->getLine()}", E_USER_ERROR);
608:         }
609:     }
610: 
611: 
612:     /********************* backend ****************d*g**/
613: 
614: 
615:     /**
616:      * @return Nette\Http\IRequest
617:      */
618:     private function getHttpRequest()
619:     {
620:         if (!$this->httpRequest) {
621:             $factory = new Nette\Http\RequestFactory;
622:             $this->httpRequest = $factory->createHttpRequest();
623:         }
624:         return $this->httpRequest;
625:     }
626: 
627: 
628:     /**
629:      * @return array
630:      */
631:     public function getToggles()
632:     {
633:         $toggles = array();
634:         foreach ($this->getComponents(TRUE, 'Nette\Forms\Controls\BaseControl') as $control) {
635:             $toggles = $control->getRules()->getToggleStates($toggles);
636:         }
637:         return $toggles;
638:     }
639: 
640: }
641: 
Nette 2.3-20161221 API API documentation generated by ApiGen 2.8.0