1 <?php
2
3 4 5 6 7 8 9 10
11
12 namespace Brickrouge;
13
14 use ICanBoogie\Errors;
15
16 17 18
19 class Form extends Element implements Validator
20 {
21 22 23 24 25
26 const DISABLED = '#form-disabled';
27
28 29 30 31 32 33
34 const HIDDENS = '#form-hiddens';
35
36 37 38 39 40 41 42
43 const LABEL = '#form-label';
44
45 46 47 48 49
50 const LABEL_COMPLEMENT = '#form-label-complement';
51
52 53 54 55 56
57 const NO_LOG = '#form-no-log';
58
59 60 61 62 63 64
65 const VALUES = '#form-values';
66
67 68 69 70 71 72
73 const RENDERER = '#form-renderer';
74
75 76 77 78 79 80 81
82 const ACTIONS = '#form-actions';
83
84 const ERRORS = '#form-errors';
85
86 87 88 89 90
91 static protected function get_auto_name()
92 {
93 return 'form-autoname-' . self::$auto_name_index++;
94 }
95
96 static protected $auto_name_index = 1;
97
98 99 100 101 102
103 public $hiddens = array();
104
105 106 107 108 109
110 public $name;
111
112 113 114 115 116 117 118 119 120 121 122 123 124 125
126 public function __construct(array $attributes=array())
127 {
128 $attributes += array
129 (
130 'action' => isset($attributes['id']) ? '#' . $attributes['id'] : '',
131 'method' => 'POST',
132 'enctype' => 'multipart/form-data',
133 'name' => isset($attributes['id']) ? $attributes['id'] : self::get_auto_name()
134 );
135
136 if (strtoupper($attributes['method']) != 'POST')
137 {
138 unset($attributes['enctype']);
139 }
140
141 $this->name = $attributes['name'] ?: self::get_auto_name();
142
143 parent::__construct('form', $attributes);
144 }
145
146 147 148 149 150 151 152 153
154 public function __toString()
155 {
156 $values = $this[self::VALUES];
157 $disabled = $this[self::DISABLED];
158
159 $name = $this->name;
160 $errors = null;
161
162 if ($name)
163 {
164 $errors = retrieve_form_errors($name);
165 }
166
167 if ($values || $disabled || $errors)
168 {
169 if ($values)
170 {
171 $values = array_flatten($values);
172 }
173
174 if (!$errors)
175 {
176 $errors = new Errors();
177 }
178
179 $this->alter_elements($values, $disabled, $errors);
180 }
181
182 return parent::__toString();
183 }
184
185 186 187 188 189
190 public function offsetSet($offset, $value)
191 {
192 parent::offsetSet($offset, $value);
193
194 if ($offset == self::HIDDENS)
195 {
196 $this->hiddens = $value;
197 }
198 }
199
200 201 202 203 204 205 206 207 208 209 210
211 public function getIterator()
212 {
213 return new \RecursiveIteratorIterator(new RecursiveIterator($this), \RecursiveIteratorIterator::SELF_FIRST);
214 }
215
216 217 218
219 protected $required = array();
220
221 222 223
224 protected $booleans = array();
225
226 227 228
229 protected $validators = array();
230
231 232 233
234 protected $validator;
235
236 237 238 239 240 241 242 243
244 public function __sleep()
245 {
246 $required = array();
247 $booleans = array();
248 $validators = array();
249
250 foreach ($this as $element)
251 {
252 $name = $element['name'];
253
254 if (!$name)
255 {
256 continue;
257 }
258
259 if ($element[Element::REQUIRED])
260 {
261 $required[$name] = self::select_element_label($element);
262 }
263
264 if ($element->tag_name == 'input')
265 {
266 if ($element['type'] == 'checkbox')
267 {
268 $booleans[$name] = true;
269 }
270 }
271 else if ($element->type == Element::TYPE_CHECKBOX_GROUP)
272 {
273 foreach ($element[self::OPTIONS] as $option_name => $dummy)
274 {
275 $booleans[$name . '[' . $option_name . ']'] = true;
276 }
277 }
278
279 if ($element[self::VALIDATOR] || $element[self::VALIDATOR_OPTIONS] || $element instanceof Validator)
280 {
281 $validators[$name] = $element;
282 }
283 }
284
285 $this->required = $required;
286 $this->booleans = $booleans;
287 $this->validators = $validators;
288 $this->validator = $this[self::VALIDATOR];
289
290
291
292
293
294
295 return array('name', 'required', 'booleans', 'validators', 'validator');
296 }
297
298 299 300 301 302 303 304
305 protected function render_children(array $children)
306 {
307 $renderer = $this[self::RENDERER];
308
309 if ($renderer)
310 {
311 if (is_string($renderer))
312 {
313 $class = $renderer;
314
315 if (!class_exists($class))
316 {
317 $class = 'Brickrouge\Renderer\\' . $class;
318 }
319
320 $renderer = new $class();
321 }
322
323 return $renderer($this);
324 }
325
326 return parent::render_children($children);
327 }
328
329 330 331 332 333 334
335 protected function render_inner_html()
336 {
337 $inner_html = parent::render_inner_html();
338 $hiddens = $this->render_hiddens($this->hiddens);
339
340 if (!$inner_html)
341 {
342 $this->add_class('has-no-content');
343 }
344 else
345 {
346 $this->remove_class('has-no-content');
347 }
348
349
350
351
352
353 $alert = null;
354
355 if (!$this[self::NO_LOG])
356 {
357 $name = $this->name;
358 $errors = retrieve_form_errors($name);
359
360 if ($errors)
361 {
362 $alert = $this->render_errors($errors);
363
364 store_form_errors($name, array());
365 }
366 }
367
368
369
370
371
372 $actions = $this[self::ACTIONS];
373
374 if ($actions)
375 {
376 $this->add_class('has-actions');
377
378 $actions = $this->render_actions($actions);
379 }
380 else
381 {
382 $this->remove_class('has-actions');
383 }
384
385 if (!$inner_html && !$actions)
386 {
387 throw new ElementIsEmpty();
388 }
389
390 return $hiddens . $alert . $inner_html . $actions;
391 }
392
393 394 395 396 397 398 399 400 401
402 protected function render_errors($errors)
403 {
404 return (string) new Alert($errors);
405 }
406
407 408 409 410 411 412 413
414 protected function render_actions($actions)
415 {
416 return (string) new Actions($actions, array('class' => 'form-actions'));
417 }
418
419 420 421 422 423 424 425
426 protected function render_hiddens(array $hiddens)
427 {
428 $html = '';
429
430 foreach ($hiddens as $name => $value)
431 {
432
433
434
435
436 if ($value === null)
437 {
438 continue;
439 }
440
441 $html .= '<input type="hidden" name="' . escape($name) . '" value="' . escape($value) . '" />';
442 }
443
444 return $html;
445 }
446
447 448 449 450 451 452 453
454 protected function alter_elements($values, $disabled, $errors)
455 {
456 foreach ($this as $element)
457 {
458
459
460
461
462 if ($disabled)
463 {
464 $element['disabled'] = true;
465 }
466
467 $name = $element['name'];
468
469 if (!$name)
470 {
471 continue;
472 }
473
474
475
476
477
478 if (isset($errors[$name]))
479 {
480 $element[Element::STATE] = 'error';
481 }
482
483
484
485
486
487 if ($values && array_key_exists($name, $values))
488 {
489 $type = $element['type'];
490 $value = $values[$name];
491
492
493
494
495
496 if ($type == 'checkbox')
497 {
498 if ($element['checked'] === null)
499 {
500 $element['checked'] = !empty($value);
501 }
502 }
503 else if ($type == 'radio')
504 {
505 if ($element['checked'] === null)
506 {
507 $element_value = $element['value'];
508 $element['checked'] = $element_value == $value;
509 }
510 }
511 else if ($element['value'] === null)
512 {
513 $element['value'] = $value;
514 }
515 }
516 }
517 }
518
519 520 521
522
523 const STORED_KEY_NAME = '_brickrouge_form_key';
524
525 526 527 528 529
530 public function save()
531 {
532 $key = store_form($this);
533
534 $this->hiddens[self::STORED_KEY_NAME] = $key;
535
536 return $this;
537 }
538
539 540 541 542 543 544 545 546 547 548
549 static public function load($key)
550 {
551 if (is_array($key))
552 {
553 if (empty($key[self::STORED_KEY_NAME]))
554 {
555 throw new \Exception('The key to retrieve the form is missing.');
556 }
557
558 $key = $key[self::STORED_KEY_NAME];
559 }
560
561 $form = retrieve_form($key);
562
563 if (!$form)
564 {
565 throw new \Exception('The form has expired.');
566 }
567
568 $form[self::VALIDATOR] = $form->validator;
569
570 return $form;
571 }
572
573 574 575 576 577 578 579
580 static public function exists($key)
581 {
582 check_session();
583
584 return !empty($_SESSION['brickrouge.saved_forms'][$key]);
585 }
586
587 static public function select_element_label($element)
588 {
589 $label = $element[self::LABEL_MISSING];
590
591 if (!$label)
592 {
593 $label = $element[Form::LABEL];
594 }
595
596 if (!$label)
597 {
598 $label = $element[Element::LABEL];
599 }
600
601 if (!$label)
602 {
603 $label = $element[self::LEGEND] ?: $label;
604 }
605
606
607
608
609
610 $label = t($label, array(), array('scope' => 'element.label'));
611 $label = strip_tags($label);
612
613 return $label;
614 }
615
616 617 618 619 620
621 public function validate($values, Errors $errors)
622 {
623
624
625
626
627 if (empty($values[self::STORED_KEY_NAME]))
628 {
629 $this->__sleep();
630 }
631
632
633
634
635
636
637 $values = array_flatten($values);
638
639 $this->values = $values;
640
641
642
643
644
645 $validators = $this->validators;
646
647 foreach ($validators as $identifier => $element)
648 {
649 $element->form = $this;
650 $element->name = $identifier;
651 $element->label = self::select_element_label($element);
652 }
653
654
655
656
657
658 $this->validate_required_elements($this->required, $validators, $values, $errors);
659
660
661
662
663
664
665
666
667 foreach ($validators as $name => $element)
668 {
669 $value = isset($values[$name]) ? $values[$name] : null;
670
671 if (($value === null || $value === '') && empty($this->required[$name]))
672 {
673 continue;
674 }
675
676 $element->validate($value, $errors);
677 }
678
679
680
681 store_form_errors($this->name, $errors);
682
683 if (count($errors))
684 {
685 return;
686 }
687
688 return parent::validate($values, $errors);
689 }
690
691 protected function validate_required_elements(array $required, array &$validators, array $values, Errors $errors)
692 {
693 $missing = array();
694
695 foreach ($required as $name => $label)
696 {
697 if (!isset($values[$name]) || (isset($values[$name]) && is_string($values[$name]) && !strlen(trim($values[$name]))))
698 {
699 $missing[$name] = t($label);
700
701
702
703
704
705
706
707 unset($validators[$name]);
708 }
709 }
710
711 if ($missing)
712 {
713 if (count($missing) == 1)
714 {
715 $errors[key($missing)] = t('The field %field is required!', array('%field' => t(current($missing))));
716 }
717 else
718 {
719 foreach ($missing as $name => $label)
720 {
721 $errors[$name] = true;
722 }
723
724 $last = array_pop($missing);
725
726 $errors[] = t('The fields %list and %last are required!', array('%list' => implode(', ', $missing), '%last' => $last));
727 }
728 }
729 }
730
731 732 733
734
735 static public function validate_email(Errors $errors, $element, $value)
736 {
737 if (filter_var($value, FILTER_VALIDATE_EMAIL))
738 {
739 return true;
740 }
741
742 $errors[$element->name] = t('Invalid email address %value for the %label element.', array('value' => $value, 'label' => $element->label));
743
744 return false;
745 }
746
747 static public function validate_url(Errors $errors, $element, $value)
748 {
749 if (filter_var($value, FILTER_VALIDATE_URL))
750 {
751 return true;
752 }
753
754 $errors[$element->name] = t('Invalid URL %value for the %label element.', array('value' => $value, 'label' => $element->label));
755
756 return false;
757 }
758
759 static public function validate_string(Errors $errors, $element, $value, $rules)
760 {
761 $messages = array();
762 $args = array();
763
764 foreach ($rules as $rule => $params)
765 {
766 switch ($rule)
767 {
768 case 'length-min':
769 {
770 if (strlen($value) < $params)
771 {
772 $messages[] = t
773 (
774 'The string %string is too short (minimum size is :size characters)', array
775 (
776 '%string' => $value,
777 ':size' => $params
778 )
779 );
780 }
781 }
782 break;
783
784 case 'length-max':
785 {
786 if (strlen($value) > $params)
787 {
788 $messages[] = t
789 (
790 'The string %string is too long (maximum size is :size characters)', array
791 (
792 '%string' => shorten($value, 32, 1),
793 ':size' => $params
794 )
795 );
796 }
797 }
798 break;
799
800 case 'regex':
801 {
802 if (!preg_match($params, $value))
803 {
804 $messages[] = t('Invalid format of value %value', array('%value' => $value));
805 }
806 }
807 break;
808 }
809 }
810
811 if ($messages)
812 {
813 $message = implode('. ', $messages);
814
815 $message .= t(' for the %label input element.', array('%label' => $element->label));
816
817 $errors[$element->name] = t($message, $args);
818 }
819
820 return empty($messages);
821 }
822
823 static public function validate_range(Errors $errors, $element, $value, $rules)
824 {
825 list($min, $max) = $rules;
826
827 $rc = ($value >= $min && $value <= $max);
828
829 if (!$rc)
830 {
831 $errors[$element->name] = t
832 (
833 '@wdform.errors.range', array
834 (
835 '%label' => $element->label,
836 ':min' => $min,
837 ':max' => $max
838 )
839 );
840 }
841
842 return $rc;
843 }
844 }