Autodoc
  • Namespace
  • Class
  • Tree

Namespaces

  • BlueTihi
    • Context
  • Brickrouge
    • Element
      • Nodes
    • Renderer
    • Widget
  • ICanBoogie
    • ActiveRecord
    • AutoConfig
    • CLDR
    • Composer
    • Core
    • Event
    • Exception
    • HTTP
      • Dispatcher
      • Request
    • I18n
      • Translator
    • Mailer
    • Modules
      • Taxonomy
        • Support
      • Thumbnailer
        • Versions
    • Object
    • Operation
      • Dispatcher
    • Prototype
    • Routes
    • Routing
      • Dispatcher
    • Session
  • Icybee
    • ActiveRecord
      • Model
    • ConfigOperation
    • Document
    • EditBlock
    • Element
      • ActionbarContextual
      • ActionbarSearch
      • ActionbarToolbar
    • FormBlock
    • Installer
    • ManageBlock
    • Modules
      • Articles
      • Cache
        • Collection
        • ManageBlock
      • Comments
        • ManageBlock
      • Contents
        • ManageBlock
      • Dashboard
      • Editor
        • Collection
      • Files
        • File
        • ManageBlock
      • Forms
        • Form
        • ManageBlock
      • I18n
      • Images
        • ManageBlock
      • Members
      • Modules
        • ManageBlock
      • Nodes
        • ManageBlock
        • Module
      • Pages
        • BreadcrumbElement
        • LanguagesElement
        • ManageBlock
        • NavigationBranchElement
        • NavigationElement
        • Page
        • PageController
      • Registry
      • Search
      • Seo
      • Sites
        • ManageBlock
      • Taxonomy
        • Terms
          • ManageBlock
        • Vocabulary
          • ManageBlock
      • Users
        • ManageBlock
        • NonceLogin
        • Roles
      • Views
        • ActiveRecordProvider
        • Collection
        • View
    • Operation
      • ActiveRecord
      • Constructor
      • Module
      • Widget
    • Rendering
  • None
  • Patron
  • PHP

Classes

  • A
  • Actions
  • Alert
  • AlterCSSClassNamesEvent
  • AssetsCollector
  • Button
  • CSSCollector
  • Dataset
  • Date
  • DateRange
  • DateTime
  • Decorator
  • Document
  • DropdownMenu
  • Element
  • File
  • Form
  • Group
  • Helpers
  • HTMLString
  • Iterator
  • JSCollector
  • ListView
  • ListViewColumn
  • Modal
  • Pager
  • Popover
  • PopoverWidget
  • Ranger
  • RecursiveIterator
  • Salutation
  • Searchbox
  • Section
  • SplitButton
  • Text
  • Widget

Interfaces

  • CSSClassNames
  • DecoratorInterface
  • HTMLStringInterface
  • Validator

Traits

  • CSSClassNamesProperty

Exceptions

  • ElementIsEmpty

Functions

  • _array_flatten_callback
  • array_flatten
  • array_insert
  • check_session
  • dump
  • escape
  • escape_all
  • format
  • format_size
  • get_accessible_file
  • get_document
  • normalize
  • render_css_class
  • render_exception
  • retrieve_form
  • retrieve_form_errors
  • shorten
  • stable_sort
  • store_form
  • store_form_errors
  • strip
  • t
  1 <?php
  2 
  3 /*
  4  * This file is part of the Brickrouge package.
  5  *
  6  * (c) Olivier Laviale <olivier.laviale@gmail.com>
  7  *
  8  * For the full copyright and license information, please view the LICENSE
  9  * file that was distributed with this source code.
 10  */
 11 
 12 namespace Brickrouge;
 13 
 14 use ICanBoogie\Errors;
 15 
 16 /**
 17  * A `<FORM>` element.
 18  */
 19 class Form extends Element implements Validator
 20 {
 21     /**
 22      * Set to true to disable all the elements of the form.
 23      *
 24      * @var boolean
 25      */
 26     const DISABLED = '#form-disabled';
 27 
 28     /**
 29      * Used to provide hidden values. Each key/value pair of the array is used to create
 30      * an hidden input element with key as `name` attribute and value as `value` attribute.
 31      *
 32      * @var array[string]mixed
 33      */
 34     const HIDDENS = '#form-hiddens';
 35 
 36     /**
 37      * Used by elements to define a form label, this is different from the
 38      * {@link Element::LABEL}, which wraps the element in a `<LABEL>` element, the form label is
 39      * associated with the element but its layout depend on the form renderer.
 40      *
 41      * @var string
 42      */
 43     const LABEL = '#form-label';
 44 
 45     /**
 46      * Complement to the {@link LABEL} tag. Its layout depends on the form renderer.
 47      *
 48      * @var string
 49      */
 50     const LABEL_COMPLEMENT = '#form-label-complement';
 51 
 52     /**
 53      * If true possible alert messages are not displayed.
 54      *
 55      * @var boolean
 56      */
 57     const NO_LOG = '#form-no-log';
 58 
 59     /**
 60      * Values for the elements of the form. The form recursively iterates through its
 61      * children to set their values, if their values it not already set (e.g. non null).
 62      *
 63      * @var array
 64      */
 65     const VALUES = '#form-values';
 66 
 67     /**
 68      * The class name of the renderer to use to render the children of the form. If no
 69      * renderer is defined, children are simply concatenated.
 70      *
 71      * @var string
 72      */
 73     const RENDERER = '#form-renderer';
 74 
 75     /**
 76      * Defines the actions of the form.
 77      *
 78      * @var bool|array|string
 79      *
 80      * @see render_actions()
 81      */
 82     const ACTIONS = '#form-actions';
 83 
 84     const ERRORS = '#form-errors';
 85 
 86     /**
 87      * Returns a unique form name.
 88      *
 89      * @return string
 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      * Hidden values, initialized with the {@link HIDDENS} tag.
100      *
101      * @var array[string]string
102      */
103     public $hiddens = array();
104 
105     /**
106      * Name of the form.
107      *
108      * @var string
109      */
110     public $name;
111 
112     /**
113      * Default attributes are added to those provided using a union:
114      *
115      * - `action`: If the `id` attribute is provided, `action` is set to "#<id>".
116      * - `method`: "POST"
117      * - `enctype`: "multipart/form-data"
118      * - `name`: The value of the `id` attribute or a name generated with the {@link get_auto_name()} method
119      *
120      * If `method` is different than "POST" then the `enctype` attribute is unset.
121      *
122      * @param array $attributes
123      *
124      * @see Element::__construct
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      * Renders the object into an HTML string.
148      *
149      * Before rendering the object form elements are altered according to the {@link VALUES} and
150      * {@link DISABLED} tags and previous validation errors.
151      *
152      * @see Element::__toString()
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      * Override the method to map the {@link HIDDENS} tag to the {@link $hiddens} property.
187      *
188      * @see Element::offsetSet()
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      * Returns a recursive iterator.
202      *
203      * A {@link RecursiveIterator} iterator is created to traverse the children of the form.
204      *
205      * The recursive iterator is created with the {@link SELF_FIRST} mode.
206      *
207      * @return \RecursiveIteratorIterator
208      *
209      * @see IteratorAggregate::getIterator()
210      */
211     public function getIterator()
212     {
213         return new \RecursiveIteratorIterator(new RecursiveIterator($this), \RecursiveIteratorIterator::SELF_FIRST);
214     }
215 
216     /**
217      * @var array[string]Element The required elements of the form.
218      */
219     protected $required = array();
220 
221     /**
222      * @var array[string] Booleans found in the form.
223      */
224     protected $booleans = array();
225 
226     /**
227      * @var array[string]Element Elements of the form with a validator.
228      */
229     protected $validators = array();
230 
231     /**
232      * @var callable Validator callback of the form.
233      */
234     protected $validator;
235 
236     /**
237      * The method alters the {@link $required}, {@link $validators} and {@link $validator}
238      * properties required for the serialization.
239      *
240      * The following properties are exported: name, required, validators and validator.
241      *
242      * @return array
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         # we return the variables to serialize, we only export variables needed for later
292         # validation.
293         #
294 
295         return array('name', 'required', 'booleans', 'validators', 'validator');
296     }
297 
298     /**
299      * If a rendered is defined it is used to render the children.
300      *
301      * The rendered is defined using the {@link RENDERER} attribute.
302      *
303      * @see Element::render_children()
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      * Add hidden input elements and log messages to the inner HTML of the element
331      * being converted to a string.
332      *
333      * @see Element::render_inner_html()
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         # alert message
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()); // reset form errors.
365             }
366         }
367 
368         #
369         # actions
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      * Renders errors as an HTML element.
395      *
396      * An {@link Alert} object is used to render the provided errors.
397      *
398      * @param string|\ICanBoogie\Errors $errors.
399      *
400      * @return string
401      */
402     protected function render_errors($errors)
403     {
404         return (string) new Alert($errors);
405     }
406 
407     /**
408      * Renders actions using an {@link Actions} element.
409      *
410      * @param mixed $actions
411      *
412      * @return string Return the actions block.
413      */
414     protected function render_actions($actions)
415     {
416         return (string) new Actions($actions, array('class' => 'form-actions'));
417     }
418 
419     /**
420      * Renders hidden values.
421      *
422      * @param array $hiddens
423      *
424      * @return string
425      */
426     protected function render_hiddens(array $hiddens)
427     {
428         $html = '';
429 
430         foreach ($hiddens as $name => $value)
431         {
432             #
433             # we skip undefined values
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      * Alters the elements according to the state of the form.
449      *
450      * @param array $values
451      * @param bool $disabled true if the form is disabled, false otherwise.
452      * @param array|\ICanBoogie\Errors $errors The validation errors.
453      */
454     protected function alter_elements($values, $disabled, $errors)
455     {
456         foreach ($this as $element)
457         {
458             #
459             # disable the element if the form is disabled.
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             # if the element is referenced in the errors, we set its state to 'error'
476             #
477 
478             if (isset($errors[$name]))
479             {
480                 $element[Element::STATE] = 'error';
481             }
482 
483             #
484             # set value
485             #
486 
487             if ($values && array_key_exists($name, $values))
488             {
489                 $type = $element['type'];
490                 $value = $values[$name];
491 
492                 #
493                 # we don't override the `value` or `checked` attributes if they are already defined
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      * Save and restore.
521      */
522 
523     const STORED_KEY_NAME = '_brickrouge_form_key';
524 
525     /**
526      * Save the form in the session for future validation.
527      *
528      * @return string The key used to identify the form save in the session.
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      * Loads a form previously saved in session.
541      *
542      * @param $string $key The key used to identify the form to load, or an array in which
543      * {@link STORED_KEY_NAME} defines the key.
544      *
545      * @return Form
546      *
547      * @throws \Exception if the form cannot be retrieved.
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      * Checks if a previously saved form exists for a given key.
575      *
576      * @param string $key The key used to identify the form.
577      *
578      * @return boolean Return `true` if the form exists.
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         # remove HTML markups from the label
608         #
609 
610         $label = t($label, array(), array('scope' => 'element.label'));
611         $label = strip_tags($label);
612 
613         return $label;
614     }
615 
616     /**
617      * Validates the form using the provided values.
618      *
619      * @see Element::validate()
620      */
621     public function validate($values, Errors $errors)
622     {
623         #
624         # validation without prior save
625         #
626 
627         if (empty($values[self::STORED_KEY_NAME]))
628         {
629             $this->__sleep();
630         }
631 
632         #
633         # we flatten the array so that we can easily get values
634         # for keys such as `cars[1][color]`
635         #
636 
637         $values = array_flatten($values);
638 
639         $this->values = $values;
640 
641         #
642         # process required values
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         # process required elements
656         #
657 
658         $this->validate_required_elements($this->required, $validators, $values, $errors);
659 
660         #
661         # process elements validators
662         #
663         # note: If the value for the element is `null` and the value is not required the element's
664         # validator is *not* called.
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         // FIXME-20111013: ICanBoogie won't save the errors in the session, so we have to do it ourselves for now.
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                 # The value for this required element is missing.
703                 # In order to avoid troubles, the element is removed
704                 # for the validators array.
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      * Validators
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 }
Autodoc API documentation generated by ApiGen 2.8.0