Development

/branches/1.4/lib/widget/sfWidgetFormSchema.class.php

You must first sign up to be able to contribute.

root/branches/1.4/lib/widget/sfWidgetFormSchema.class.php

Revision 26870, 22.4 kB (checked in by fabien, 4 years ago)

[1.3, 1.4] fixed sfWidgetFormSchema::setPositions() which accepts duplication positions (closes #7992)

  • Property svn:mime-type set to text/x-php
  • Property svn:eol-style set to native
  • Property svn:keywords set to Id
Line 
1 <?php
2
3 /*
4  * This file is part of the symfony package.
5  * (c) Fabien Potencier <fabien.potencier@symfony-project.com>
6  *
7  * For the full copyright and license information, please view the LICENSE
8  * file that was distributed with this source code.
9  */
10
11 /**
12  * sfWidgetFormSchema represents an array of fields.
13  *
14  * A field is a named validator.
15  *
16  * @package    symfony
17  * @subpackage widget
18  * @author     Fabien Potencier <fabien.potencier@symfony-project.com>
19  * @version    SVN: $Id$
20  */
21 class sfWidgetFormSchema extends sfWidgetForm implements ArrayAccess
22 {
23   const
24     FIRST  = 'first',
25     LAST   = 'last',
26     BEFORE = 'before',
27     AFTER  = 'after';
28
29   protected static
30     $defaultFormatterName = 'table';
31
32   protected
33     $formFormatters = array(),
34     $fields         = array(),
35     $positions      = array(),
36     $helps          = array();
37
38   /**
39    * Constructor.
40    *
41    * The first argument can be:
42    *
43    *  * null
44    *  * an array of sfWidget instances
45    *
46    * Available options:
47    *
48    *  * name_format:    The sprintf pattern to use for input names
49    *  * form_formatter: The form formatter name (table and list are bundled)
50    *
51    * @param mixed $fields     Initial fields
52    * @param array $options    An array of options
53    * @param array $attributes An array of default HTML attributes
54    * @param array $labels     An array of HTML labels
55    * @param array $helps      An array of help texts
56    *
57    * @throws InvalidArgumentException when the passed fields not null or array
58    *
59    * @see sfWidgetForm
60    */
61   public function __construct($fields = null, $options = array(), $attributes = array(), $labels = array(), $helps = array())
62   {
63     $this->addOption('name_format', '%s');
64     $this->addOption('form_formatter', null);
65
66     parent::__construct($options, $attributes);
67
68     if (is_array($fields))
69     {
70       foreach ($fields as $name => $widget)
71       {
72         $this[$name] = $widget;
73       }
74     }
75     else if (null !== $fields)
76     {
77       throw new InvalidArgumentException('sfWidgetFormSchema constructor takes an array of sfWidget objects.');
78     }
79
80     $this->setLabels($labels);
81     $this->helps = $helps;
82   }
83
84   /**
85    * Sets the default value for a field.
86    *
87    * @param string $name  The field name
88    * @param string $value The default value (required - the default value is here because PHP do not allow signature changes with inheritance)
89    *
90    * @return sfWidget The current widget instance
91    */
92   public function setDefault($name, $value = null)
93   {
94     $this[$name]->setDefault($value);
95
96     return $this;
97   }
98
99   /**
100    * Gets the default value of a field.
101    *
102    * @param string $name The field name (required - the default value is here because PHP do not allow signature changes with inheritance)
103    *
104    * @return string The default value
105    */
106   public function getDefault($name = null)
107   {
108     return $this[$name]->getDefault();
109   }
110
111   /**
112    * Sets the default values for the widget.
113    *
114    * @param array $values The default values for the widget
115    *
116    * @return sfWidget The current widget instance
117    */
118   public function setDefaults(array $values)
119   {
120     foreach ($this->fields as $name => $widget)
121     {
122       if (array_key_exists($name, $values))
123       {
124         $widget->setDefault($values[$name]);
125       }
126     }
127
128     return $this;
129   }
130
131   /**
132    * Returns the defaults values for the widget schema.
133    *
134    * @return array An array of default values
135    */
136   public function getDefaults()
137   {
138     $defaults = array();
139
140     foreach ($this->fields as $name => $widget)
141     {
142       $defaults[$name] = $widget instanceof sfWidgetFormSchema ? $widget->getDefaults() : $widget->getDefault();
143     }
144
145     return $defaults;
146   }
147
148   /**
149    * Adds a form formatter.
150    *
151    * @param string                      $name      The formatter name
152    * @param sfWidgetFormSchemaFormatter $formatter An sfWidgetFormSchemaFormatter instance
153    *
154    * @return sfWidget The current widget instance
155    */
156   public function addFormFormatter($name, sfWidgetFormSchemaFormatter $formatter)
157   {
158     $this->formFormatters[$name] = $formatter;
159
160     return $this;
161   }
162
163   /**
164    * Returns all the form formats defined for this form schema.
165    *
166    * @return array An array of named form formats
167    */
168   public function getFormFormatters()
169   {
170     return $this->formFormatters;
171   }
172
173   /**
174    * Sets the generic default formatter name used by the class. If you want all
175    * of your forms to be generated with the <code>list</code> format, you can
176    * do it in a project or application configuration class:
177    *
178    * <pre>
179    * class ProjectConfiguration extends sfProjectConfiguration
180    * {
181    *   public function setup()
182    *   {
183    *     sfWidgetFormSchema::setDefaultFormFormatterName('list');
184    *   }
185    * }
186    * </pre>
187    *
188    * @param string $name New default formatter name
189    */
190   static public function setDefaultFormFormatterName($name)
191   {
192     self::$defaultFormatterName = $name;
193   }
194
195   /**
196    * Sets the form formatter name to use when rendering the widget schema.
197    *
198    * @param string $name The form formatter name
199    *
200    * @return sfWidget The current widget instance
201    */
202   public function setFormFormatterName($name)
203   {
204     $this->options['form_formatter'] = $name;
205
206     return $this;
207   }
208
209   /**
210    * Gets the form formatter name that will be used to render the widget schema.
211    *
212    * @return string The form formatter name
213    */
214   public function getFormFormatterName()
215   {
216     return null === $this->options['form_formatter'] ? self::$defaultFormatterName : $this->options['form_formatter'];
217   }
218
219   /**
220    * Returns the form formatter to use for widget schema rendering
221    *
222    * @return sfWidgetFormSchemaFormatter sfWidgetFormSchemaFormatter instance
223    *
224    * @throws InvalidArgumentException when the form formatter not exists
225    */
226   public function getFormFormatter()
227   {
228     $name = $this->getFormFormatterName();
229
230     if (!isset($this->formFormatters[$name]))
231     {
232       $class = 'sfWidgetFormSchemaFormatter'.ucfirst($name);
233
234       if (!class_exists($class))
235       {
236         throw new InvalidArgumentException(sprintf('The form formatter "%s" does not exist.', $name));
237       }
238
239       $this->formFormatters[$name] = new $class($this);
240     }
241
242     return $this->formFormatters[$name];
243   }
244
245   /**
246    * Sets the format string for the name HTML attribute.
247    *
248    * If you are using the form framework with symfony, do not use a reserved word in the
249    * name format.  If you do, symfony may act in an unexpected manner.
250    *
251    * For symfony 1.1+, the following words are reserved and must NOT be used as
252    * the name format:
253    *
254    *  * module    (example: module[%s])
255    *  * action    (example: action[%s])
256    *
257    * However, you CAN use other variations, such as actions[%s] (note the s).
258    *
259    * @param string $format The format string (must contain a %s for the name placeholder)
260    *
261    * @return sfWidget The current widget instance
262    *
263    * @throws InvalidArgumentException when no %s exists in the format
264    */
265   public function setNameFormat($format)
266   {
267     if (false !== $format && false === strpos($format, '%s'))
268     {
269       throw new InvalidArgumentException(sprintf('The name format must contain %%s ("%s" given)', $format));
270     }
271
272     $this->options['name_format'] = $format;
273
274     return $this;
275   }
276
277   /**
278    * Gets the format string for the name HTML attribute.
279    *
280    * @return string The format string
281    */
282   public function getNameFormat()
283   {
284     return $this->options['name_format'];
285   }
286
287   /**
288    * Sets the label names to render for each field.
289    *
290    * @param array $labels  An array of label names
291    *
292    * @return sfWidget The current widget instance
293    */
294   public function setLabels(array $labels)
295   {
296     foreach ($this->fields as $name => $widget)
297     {
298       if (array_key_exists($name, $labels))
299       {
300         $widget->setLabel($labels[$name]);
301       }
302     }
303
304     return $this;
305   }
306
307   /**
308    * Gets the labels.
309    *
310    * @return array An array of label names
311    */
312   public function getLabels()
313   {
314     $labels = array();
315
316     foreach ($this->fields as $name => $widget)
317     {
318       $labels[$name] = $widget->getLabel();
319     }
320
321     return $labels;
322   }
323
324   /**
325    * Sets a label.
326    *
327    * @param string $name  The field name
328    * @param string $value The label name (required - the default value is here because PHP do not allow signature changes with inheritance)
329    *
330    * @return sfWidget The current widget instance
331    *
332    * @throws InvalidArgumentException when you try to set a label on a none existing widget
333    */
334   public function setLabel($name, $value = null)
335   {
336     if (2 == func_num_args())
337     {
338       if (!isset($this->fields[$name]))
339       {
340         throw new InvalidArgumentException(sprintf('Unable to set the label on an unexistant widget ("%s").', $name));
341       }
342
343       $this->fields[$name]->setLabel($value);
344     }
345     else
346     {
347       // set the label for this widget schema
348       parent::setLabel($name);
349     }
350
351     return $this;
352   }
353
354   /**
355    * Gets a label by field name.
356    *
357    * @param  string $name  The field name (required - the default value is here because PHP do not allow signature changes with inheritance)
358    *
359    * @return string The label name or an empty string if it is not defined
360    *
361    * @throws InvalidArgumentException when you try to get a label for a none existing widget
362    */
363   public function getLabel($name = null)
364   {
365     if (1 == func_num_args())
366     {
367       if (!isset($this->fields[$name]))
368       {
369         throw new InvalidArgumentException(sprintf('Unable to get the label on an unexistant widget ("%s").', $name));
370       }
371
372       return $this->fields[$name]->getLabel();
373     }
374     else
375     {
376       // label for this widget schema
377       return parent::getLabel();
378     }
379   }
380
381   /**
382    * Sets the help texts to render for each field.
383    *
384    * @param array $helps An array of help texts
385    *
386    * @return sfWidget The current widget instance
387    */
388   public function setHelps(array $helps)
389   {
390     $this->helps = $helps;
391
392     return $this;
393   }
394
395   /**
396    * Sets the help texts.
397    *
398    * @return array An array of help texts
399    */
400   public function getHelps()
401   {
402     return $this->helps;
403   }
404
405   /**
406    * Sets a help text.
407    *
408    * @param string $name The field name
409    * @param string $help The help text
410    *
411    * @return sfWidget The current widget instance
412    */
413   public function setHelp($name, $help)
414   {
415     $this->helps[$name] = $help;
416
417     return $this;
418   }
419
420   /**
421    * Gets a text help by field name.
422    *
423    * @param string $name The field name
424    *
425    * @return string The help text or an empty string if it is not defined
426    */
427   public function getHelp($name)
428   {
429     return array_key_exists($name, $this->helps) ? $this->helps[$name] : '';
430   }
431
432   /**
433    * Gets the stylesheet paths associated with the widget.
434    *
435    * @return array An array of stylesheet paths
436    */
437   public function getStylesheets()
438   {
439     $stylesheets = array();
440
441     foreach ($this->fields as $field)
442     {
443       $stylesheets = array_merge($stylesheets, $field->getStylesheets());
444     }
445
446     return $stylesheets;
447   }
448
449   /**
450    * Gets the JavaScript paths associated with the widget.
451    *
452    * @return array An array of JavaScript paths
453    */
454   public function getJavaScripts()
455   {
456     $javascripts = array();
457
458     foreach ($this->fields as $field)
459     {
460       $javascripts = array_merge($javascripts, $field->getJavaScripts());
461     }
462
463     return array_unique($javascripts);
464   }
465
466   /**
467    * Returns true if the widget schema needs a multipart form.
468    *
469    * @return bool true if the widget schema needs a multipart form, false otherwise
470    */
471   public function needsMultipartForm()
472   {
473     foreach ($this->fields as $field)
474     {
475       if ($field->needsMultipartForm())
476       {
477         return true;
478       }
479     }
480
481     return false;
482   }
483
484   /**
485    * Renders a field by name.
486    *
487    * @param string $name       The field name
488    * @param string $value      The field value
489    * @param array  $attributes An array of HTML attributes to be merged with the current HTML attributes
490    * @param array  $errors     An array of errors for the field
491    *
492    * @return string An HTML string representing the rendered widget
493    *
494    * @throws InvalidArgumentException when the widget not exist
495    */
496   public function renderField($name, $value = null, $attributes = array(), $errors = array())
497   {
498     if (null === $widget = $this[$name])
499     {
500       throw new InvalidArgumentException(sprintf('The field named "%s" does not exist.', $name));
501     }
502
503     if ($widget instanceof sfWidgetFormSchema && $errors && !$errors instanceof sfValidatorErrorSchema)
504     {
505       $errors = new sfValidatorErrorSchema($errors->getValidator(), array($errors));
506     }
507
508     // we clone the widget because we want to change the id format temporarily
509     $clone = clone $widget;
510     $clone->setIdFormat($this->options['id_format']);
511
512     return $clone->render($this->generateName($name), $value, array_merge($clone->getAttributes(), $attributes), $errors);
513   }
514
515   /**
516    * Renders the widget.
517    *
518    * @param string $name       The name of the HTML widget
519    * @param mixed  $values     The values of the widget
520    * @param array  $attributes An array of HTML attributes
521    * @param array  $errors     An array of errors
522    *
523    * @return string An HTML representation of the widget
524    *
525    * @throws InvalidArgumentException when values type is not array|ArrayAccess
526    */
527   public function render($name, $values = array(), $attributes = array(), $errors = array())
528   {
529     if (null === $values)
530     {
531       $values = array();
532     }
533
534     if (!is_array($values) && !$values instanceof ArrayAccess)
535     {
536       throw new InvalidArgumentException('You must pass an array of values to render a widget schema');
537     }
538
539     $formFormat = $this->getFormFormatter();
540
541     $rows = array();
542     $hiddenRows = array();
543     $errorRows = array();
544
545     // render each field
546     foreach ($this->positions as $name)
547     {
548       $widget = $this[$name];
549       $value = isset($values[$name]) ? $values[$name] : null;
550       $error = isset($errors[$name]) ? $errors[$name] : array();
551       $widgetAttributes = isset($attributes[$name]) ? $attributes[$name] : array();
552
553       if ($widget instanceof sfWidgetForm && $widget->isHidden())
554       {
555         $hiddenRows[] = $this->renderField($name, $value, $widgetAttributes);
556       }
557       else
558       {
559         $field = $this->renderField($name, $value, $widgetAttributes, $error);
560
561         // don't add a label tag and errors if we embed a form schema
562         $label = $widget instanceof sfWidgetFormSchema ? $this->getFormFormatter()->generateLabelName($name) : $this->getFormFormatter()->generateLabel($name);
563         $error = $widget instanceof sfWidgetFormSchema ? array() : $error;
564
565         $rows[] = $formFormat->formatRow($label, $field, $error, $this->getHelp($name));
566       }
567     }
568
569     if ($rows)
570     {
571       // insert hidden fields in the last row
572       for ($i = 0, $max = count($rows); $i < $max; $i++)
573       {
574         $rows[$i] = strtr($rows[$i], array('%hidden_fields%' => $i == $max - 1 ? implode("\n", $hiddenRows) : ''));
575       }
576     }
577     else
578     {
579       // only hidden fields
580       $rows[0] = implode("\n", $hiddenRows);
581     }
582
583     return $this->getFormFormatter()->formatErrorRow($this->getGlobalErrors($errors)).implode('', $rows);
584   }
585
586   /**
587    * Gets errors that need to be included in global errors.
588    *
589    * @param array $errors An array of errors
590    *
591    * @return string An HTML representation of global errors for the widget
592    */
593   public function getGlobalErrors($errors)
594   {
595     $globalErrors = array();
596
597     // global errors and errors for non existent fields
598     if (null !== $errors)
599     {
600       foreach ($errors as $name => $error)
601       {
602         if (!isset($this->fields[$name]))
603         {
604           $globalErrors[] = $error;
605         }
606       }
607     }
608
609     // errors for hidden fields
610     foreach ($this->positions as $name)
611     {
612       if ($this[$name] instanceof sfWidgetForm && $this[$name]->isHidden())
613       {
614         if (isset($errors[$name]))
615         {
616           $globalErrors[$this->getFormFormatter()->generateLabelName($name)] = $errors[$name];
617         }
618       }
619     }
620
621     return $globalErrors;
622   }
623
624   /**
625    * Generates a name.
626    *
627    * @param string $name The name
628    *
629    * @return string The generated name
630    */
631   public function generateName($name)
632   {
633     $format = $this->getNameFormat();
634
635     if ('[%s]' == substr($format, -4) && preg_match('/^(.+?)\[(.+)\]$/', $name, $match))
636     {
637       $name = sprintf('%s[%s][%s]', substr($format, 0, -4), $match[1], $match[2]);
638     }
639     else if (false !== $format)
640     {
641       $name = sprintf($format, $name);
642     }
643
644     if ($parent = $this->getParent())
645     {
646       $name = $parent->generateName($name);
647     }
648
649     return $name;
650   }
651
652   /**
653    * Returns true if the schema has a field with the given name (implements the ArrayAccess interface).
654    *
655    * @param string $name The field name
656    *
657    * @return bool true if the schema has a field with the given name, false otherwise
658    */
659   public function offsetExists($name)
660   {
661     return isset($this->fields[$name]);
662   }
663
664   /**
665    * Gets the field associated with the given name (implements the ArrayAccess interface).
666    *
667    * @param string $name The field name
668    *
669    * @return sfWidget|null The sfWidget instance associated with the given name, null if it does not exist
670    */
671   public function offsetGet($name)
672   {
673     return isset($this->fields[$name]) ? $this->fields[$name] : null;
674   }
675
676   /**
677    * Sets a field (implements the ArrayAccess interface).
678    *
679    * @param string   $name   The field name
680    * @param sfWidget $widget An sfWidget instance
681    *
682    * @throws InvalidArgumentException when the field is not instance of sfWidget
683    */
684   public function offsetSet($name, $widget)
685   {
686     if (!$widget instanceof sfWidget)
687     {
688       throw new InvalidArgumentException('A field must be an instance of sfWidget.');
689     }
690
691     if (!isset($this->fields[$name]))
692     {
693       $this->positions[] = (string) $name;
694     }
695
696     $this->fields[$name] = clone $widget;
697     $this->fields[$name]->setParent($this);
698
699     if ($widget instanceof sfWidgetFormSchema)
700     {
701       $this->fields[$name]->setNameFormat($name.'[%s]');
702     }
703   }
704
705   /**
706    * Removes a field by name (implements the ArrayAccess interface).
707    *
708    * @param string $name field name
709    */
710   public function offsetUnset($name)
711   {
712     unset($this->fields[$name]);
713     if (false !== $position = array_search((string) $name, $this->positions))
714     {
715       unset($this->positions[$position]);
716
717       $this->positions = array_values($this->positions);
718     }
719   }
720
721   /**
722    * Returns an array of fields.
723    *
724    * @return sfWidget An array of sfWidget instance
725    */
726   public function getFields()
727   {
728     return $this->fields;
729   }
730
731   /**
732    * Gets the positions of the fields.
733    *
734    * The field positions are only used when rendering the schema with ->render().
735    *
736    * @return array An ordered array of field names
737    */
738   public function getPositions()
739   {
740     return $this->positions;
741   }
742
743   /**
744    * Sets the positions of the fields.
745    *
746    * @param array $positions An ordered array of field names
747    *
748    * @return sfWidget The current widget instance
749    *
750    * @throws InvalidArgumentException when not all fields set in $positions
751    *
752    * @see getPositions()
753    */
754   public function setPositions(array $positions)
755   {
756     $positions = array_unique(array_values($positions));
757     $current   = array_keys($this->fields);
758
759     if ($diff = array_diff($positions, $current))
760     {
761       throw new InvalidArgumentException('Widget schema does not include the following field(s): '.implode(', ', $diff));
762     }
763
764     if ($diff = array_diff($current, $positions))
765     {
766       throw new InvalidArgumentException('Positions array must include all fields. Missing: '.implode(', ', $diff));
767     }
768
769     foreach ($positions as &$position)
770     {
771       $position = (string) $position;
772     }
773
774     $this->positions = $positions;
775
776     return $this;
777   }
778
779   /**
780    * Moves a field in a given position
781    *
782    * Available actions are:
783    *
784    *  * sfWidgetFormSchema::BEFORE
785    *  * sfWidgetFormSchema::AFTER
786    *  * sfWidgetFormSchema::LAST
787    *  * sfWidgetFormSchema::FIRST
788    *
789    * @param string   $field  The field name to move
790    * @param constant $action The action (see above for all possible actions)
791    * @param string   $pivot  The field name used for AFTER and BEFORE actions
792    *
793    * @throws InvalidArgumentException when field not exist
794    * @throws InvalidArgumentException when relative field not exist
795    * @throws LogicException           when you try to move a field without a relative field
796    * @throws LogicException           when the $action not exist
797    */
798   public function moveField($field, $action, $pivot = null)
799   {
800     $field = (string) $field;
801     if (false === $fieldPosition = array_search($field, $this->positions))
802     {
803       throw new InvalidArgumentException(sprintf('Field "%s" does not exist.', $field));
804     }
805     unset($this->positions[$fieldPosition]);
806     $this->positions = array_values($this->positions);
807
808     if (null !== $pivot)
809     {
810       $pivot = (string) $pivot;
811       if (false === $pivotPosition = array_search($pivot, $this->positions))
812       {
813         throw new InvalidArgumentException(sprintf('Field "%s" does not exist.', $pivot));
814       }
815     }
816
817     switch ($action)
818     {
819       case sfWidgetFormSchema::FIRST:
820         array_unshift($this->positions, $field);
821         break;
822       case sfWidgetFormSchema::LAST:
823         array_push($this->positions, $field);
824         break;
825       case sfWidgetFormSchema::BEFORE:
826         if (null === $pivot)
827         {
828           throw new LogicException(sprintf('Unable to move field "%s" without a relative field.', $field));
829         }
830         $this->positions = array_merge(
831           array_slice($this->positions, 0, $pivotPosition),
832           array($field),
833           array_slice($this->positions, $pivotPosition)
834         );
835         break;
836       case sfWidgetFormSchema::AFTER:
837         if (null === $pivot)
838         {
839           throw new LogicException(sprintf('Unable to move field "%s" without a relative field.', $field));
840         }
841         $this->positions = array_merge(
842           array_slice($this->positions, 0, $pivotPosition + 1),
843           array($field),
844           array_slice($this->positions, $pivotPosition + 1)
845         );
846         break;
847       default:
848         throw new LogicException(sprintf('Unknown move operation for field "%s".', $field));
849     }
850   }
851
852   public function __clone()
853   {
854     foreach ($this->fields as $name => $field)
855     {
856       // offsetSet will clone the field and change the parent
857       $this[$name] = $field;
858     }
859
860     foreach ($this->formFormatters as &$formFormatter)
861     {
862       $formFormatter = clone $formFormatter;
863       $formFormatter->setWidgetSchema($this);
864     }
865   }
866 }
867
Note: See TracBrowser for help on using the browser.