Development

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

You must first sign up to be able to contribute.

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

Revision 17587, 17.6 kB (checked in by Kris.Wallsmith, 6 years ago)

[1.1, 1.2, 1.3] fixed rare occassion when a widget schema is paired with a simple validator error rather than a validator error schema

  • 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     $parent         = null,
34     $formFormatters = array(),
35     $options        = array(),
36     $labels         = array(),
37     $fields         = array(),
38     $positions      = array(),
39     $helps          = array();
40
41   /**
42    * Constructor.
43    *
44    * The first argument can be:
45    *
46    *  * null
47    *  * an array of sfWidget instances
48    *
49    * Available options:
50    *
51    *  * name_format:    The sprintf pattern to use for input names
52    *  * form_formatter: The form formatter name (table and list are bundled)
53    *
54    * @param mixed $fields     Initial fields
55    * @param array $options    An array of options
56    * @param array $attributes An array of default HTML attributes
57    * @param array $labels     An array of HTML labels
58    * @param array $helps      An array of help texts
59    *
60    * @see sfWidgetForm
61    */
62   public function __construct($fields = null, $options = array(), $attributes = array(), $labels = array(), $helps = array())
63   {
64     $this->labels = $labels;
65     $this->helps  = $helps;
66
67     $this->addOption('name_format', '%s');
68     $this->addOption('form_formatter', null);
69
70     parent::__construct($options, $attributes);
71
72     if (is_array($fields))
73     {
74       foreach ($fields as $name => $widget)
75       {
76         $this[$name] = $widget;
77       }
78     }
79     else if (!is_null($fields))
80     {
81       throw new InvalidArgumentException('sfWidgetFormSchema constructor takes an array of sfWidget objects.');
82     }
83   }
84
85   /**
86    * Adds a form formatter.
87    *
88    * @param string                      $name       The formatter name
89    * @param sfWidgetFormSchemaFormatter $formatter  An sfWidgetFormSchemaFormatter instance
90    */
91   public function addFormFormatter($name, sfWidgetFormSchemaFormatter $formatter)
92   {
93     $this->formFormatters[$name] = $formatter;
94   }
95
96   /**
97    * Returns all the form formats defined for this form schema.
98    *
99    * @return array An array of named form formats
100    */
101   public function getFormFormatters()
102   {
103     return $this->formFormatters;
104   }
105
106   /**
107    * Sets the generic default formatter name used by the class. If you want all
108    * of your forms to be generated with the <code>list</code> format, you can
109    * do it in a project or application configuration class:
110    *
111    * <pre>
112    * class ProjectConfiguration extends sfProjectConfiguration
113    * {
114    *   public function setup()
115    *   {
116    *     sfWidgetFormSchema::setDefaultFormFormatterName('list');
117    *   }
118    * }
119    * </pre>
120    *
121    * @param string $name  New default formatter name
122    */
123   static public function setDefaultFormFormatterName($name)
124   {
125     self::$defaultFormatterName = $name;
126   }
127
128   /**
129    * Sets the form formatter name to use when rendering the widget schema.
130    *
131    * @param string $name  The form formatter name
132    */
133   public function setFormFormatterName($name)
134   {
135     $this->options['form_formatter'] = $name;
136   }
137
138   /**
139    * Gets the form formatter name that will be used to render the widget schema.
140    *
141    * @return string The form formatter name
142    */
143   public function getFormFormatterName()
144   {
145     return is_null($this->options['form_formatter']) ? self::$defaultFormatterName : $this->options['form_formatter'];
146   }
147
148   /**
149    * Returns the form formatter to use for widget schema rendering
150    *
151    * @return sfWidgetFormSchemaFormatter sfWidgetFormSchemaFormatter instance
152    *
153    * @throws InvalidArgumentException
154    */
155   public function getFormFormatter()
156   {
157     $name = $this->getFormFormatterName();
158
159     if (!isset($this->formFormatters[$name]))
160     {
161       $class = 'sfWidgetFormSchemaFormatter'.ucfirst($name);
162
163       if (!class_exists($class))
164       {
165         throw new InvalidArgumentException(sprintf('The form formatter "%s" does not exist.', $name));
166       }
167
168       $this->formFormatters[$name] = new $class($this);
169     }
170
171     return $this->formFormatters[$name];
172   }
173
174   /**
175    * Sets the format string for the name HTML attribute.
176    *
177    * If you are using the form framework with symfony, do not use a reserved word in the
178    * name format.  If you do, symfony may act in an unexpected manner.
179    *
180    * For symfony 1.1 and 1.2, the following words are reserved and must NOT be used as
181    * the name format:
182    *
183    *  * module    (example: module[%s])
184    *  * action    (example: action[%s])
185    *
186    * However, you CAN use other variations, such as actions[%s] (note the s).
187    *
188    * @param string $format  The format string (must contain a %s for the name placeholder)
189    */
190   public function setNameFormat($format)
191   {
192     if (false !== $format && false === strpos($format, '%s'))
193     {
194       throw new InvalidArgumentException(sprintf('The name format must contain %%s ("%s" given)', $format));
195     }
196
197     $this->options['name_format'] = $format;
198   }
199
200   /**
201    * Gets the format string for the name HTML attribute.
202    *
203    * @return string The format string
204    */
205   public function getNameFormat()
206   {
207     return $this->options['name_format'];
208   }
209
210   /**
211    * Sets the label names to render for each field.
212    *
213    * @param array $labels  An array of label names
214    */
215   public function setLabels($labels)
216   {
217     $this->labels = $labels;
218   }
219
220   /**
221    * Gets the labels.
222    *
223    * @return array An array of label names
224    */
225   public function getLabels()
226   {
227     return $this->labels;
228   }
229
230   /**
231    * Sets a label.
232    *
233    * @param string $name   The field name
234    * @param string $value  The label name
235    */
236   public function setLabel($name, $value)
237   {
238     $this->labels[$name] = $value;
239   }
240
241   /**
242    * Gets a label by field name.
243    *
244    * @param  string $name  The field name
245    *
246    * @return string The label name or an empty string if it is not defined
247    */
248   public function getLabel($name)
249   {
250     return array_key_exists($name, $this->labels) ? $this->labels[$name] : '';
251   }
252
253   /**
254    * Sets the help texts to render for each field.
255    *
256    * @param array $helps  An array of help texts
257    */
258   public function setHelps($helps)
259   {
260     $this->helps = $helps;
261   }
262
263   /**
264    * Sets the help texts.
265    *
266    * @return array An array of help texts
267    */
268   public function getHelps()
269   {
270     return $this->helps;
271   }
272
273   /**
274    * Sets a help text.
275    *
276    * @param string $name  The field name
277    * @param string $help  The help text
278    */
279   public function setHelp($name, $help)
280   {
281     $this->helps[$name] = $help;
282   }
283
284   /**
285    * Gets a text help by field name.
286    *
287    * @param  string $name  The field name
288    *
289    * @return string The help text or an empty string if it is not defined
290    */
291   public function getHelp($name)
292   {
293     return array_key_exists($name, $this->helps) ? $this->helps[$name] : '';
294   }
295
296   /**
297    * Returns true if the widget schema needs a multipart form.
298    *
299    * @return bool true if the widget schema needs a multipart form, false otherwise
300    */
301   public function needsMultipartForm()
302   {
303     foreach ($this->fields as $field)
304     {
305       if ($field->needsMultipartForm())
306       {
307         return true;
308       }
309     }
310
311     return false;
312   }
313
314   /**
315    * Renders a field by name.
316    *
317    * @param  string  $name        The field name
318    * @param  string  $value       The field value
319    * @param  array   $attributes  An array of HTML attributes to be merged with the current HTML attributes
320    * @param  array   $attributes  An array of errors for the field
321    *
322    * @return string  An HTML string representing the rendered widget
323    */
324   public function renderField($name, $value = null, $attributes = array(), $errors = array())
325   {
326     if (is_null($widget = $this[$name]))
327     {
328       throw new InvalidArgumentException(sprintf('The field named "%s" does not exist.', $name));
329     }
330
331     if ($widget instanceof sfWidgetFormSchema && $errors && !$errors instanceof sfValidatorErrorSchema)
332     {
333       $errors = new sfValidatorErrorSchema($errors->getValidator(), array($errors));
334     }
335
336     // we clone the widget because we want to change the id format temporarily
337     $clone = clone $widget;
338     $clone->setIdFormat($this->options['id_format']);
339
340     return $clone->render($this->generateName($name), $value, array_merge($clone->getAttributes(), $attributes), $errors);
341   }
342
343   /**
344    * Renders the widget.
345    *
346    * @param  string $name        The name of the HTML widget
347    * @param  mixed  $values      The values of the widget
348    * @param  array  $attributes  An array of HTML attributes
349    * @param  array  $errors      An array of errors
350    *
351    * @return string An HTML representation of the widget
352    */
353   public function render($name, $values = array(), $attributes = array(), $errors = array())
354   {
355     if (is_null($values))
356     {
357       $values = array();
358     }
359
360     if (!is_array($values) && !$values instanceof ArrayAccess)
361     {
362       throw new InvalidArgumentException('You must pass an array of values to render a widget schema');
363     }
364
365     $formFormat = $this->getFormFormatter();
366
367     $rows = array();
368     $hiddenRows = array();
369     $errorRows = array();
370
371     // render each field
372     foreach ($this->positions as $name)
373     {
374       $widget = $this[$name];
375       $value = isset($values[$name]) ? $values[$name] : null;
376       $error = isset($errors[$name]) ? $errors[$name] : array();
377       $widgetAttributes = isset($attributes[$name]) ? $attributes[$name] : array();
378
379       if ($widget instanceof sfWidgetForm && $widget->isHidden())
380       {
381         $hiddenRows[] = $this->renderField($name, $value, $widgetAttributes);
382       }
383       else
384       {
385         $field = $this->renderField($name, $value, $widgetAttributes, $error);
386
387         // don't add a label tag and errors if we embed a form schema
388         $label = $widget instanceof sfWidgetFormSchema ? $this->getFormFormatter()->generateLabelName($name) : $this->getFormFormatter()->generateLabel($name);
389         $error = $widget instanceof sfWidgetFormSchema ? array() : $error;
390
391         $rows[] = $formFormat->formatRow($label, $field, $error, $this->getHelp($name));
392       }
393     }
394
395     if ($rows)
396     {
397       // insert hidden fields in the last row
398       for ($i = 0, $max = count($rows); $i < $max; $i++)
399       {
400         $rows[$i] = strtr($rows[$i], array('%hidden_fields%' => $i == $max - 1 ? implode("\n", $hiddenRows) : ''));
401       }
402     }
403     else
404     {
405       // only hidden fields
406       $rows[0] = implode("\n", $hiddenRows);
407     }
408
409     return $this->getFormFormatter()->formatErrorRow($this->getGlobalErrors($errors)).implode('', $rows);
410   }
411
412   /**
413    * Gets errors that need to be included in global errors.
414    *
415    * @param  array  $errors  An array of errors
416    *
417    * @return string An HTML representation of global errors for the widget
418    */
419   public function getGlobalErrors($errors)
420   {
421     $globalErrors = array();
422
423     // global errors and errors for non existent fields
424     if (!is_null($errors))
425     {
426       foreach ($errors as $name => $error)
427       {
428         if (!isset($this->fields[$name]))
429         {
430           $globalErrors[] = $error;
431         }
432       }
433     }
434
435     // errors for hidden fields
436     foreach ($this->positions as $name)
437     {
438       if ($this[$name] instanceof sfWidgetForm && $this[$name]->isHidden())
439       {
440         if (isset($errors[$name]))
441         {
442           $globalErrors[$this->getFormFormatter()->generateLabelName($name)] = $errors[$name];
443         }
444       }
445     }
446
447     return $globalErrors;
448   }
449
450   /**
451    * Generates a name.
452    *
453    * @param string $name  The name
454    *
455    * @param string The generated name
456    */
457   public function generateName($name)
458   {
459     $format = $this->getNameFormat();
460
461     if ('[%s]' == substr($format, -4) && preg_match('/^(.+?)\[(.+)\]$/', $name, $match))
462     {
463       $name = sprintf('%s[%s][%s]', substr($format, 0, -4), $match[1], $match[2]);
464     }
465     else if (false !== $format)
466     {
467       $name = sprintf($format, $name);
468     }
469
470     if ($parent = $this->getParent())
471     {
472       $name = $parent->generateName($name);
473     }
474
475     return $name;
476   }
477
478   /**
479    * Gets the parent widget schema.
480    *
481    * @return sfWidgetFormSchema The parent widget schema
482    */
483   public function getParent()
484   {
485     return $this->parent;
486   }
487
488   /**
489    * Sets the parent widget schema.
490    *
491    * @parent sfWidgetFormSchema $parent  The parent widget schema
492    */
493   public function setParent(sfWidgetFormSchema $parent = null)
494   {
495     $this->parent = $parent;
496   }
497
498   /**
499    * Returns true if the schema has a field with the given name (implements the ArrayAccess interface).
500    *
501    * @param  string $name  The field name
502    *
503    * @return bool true if the schema has a field with the given name, false otherwise
504    */
505   public function offsetExists($name)
506   {
507     return isset($this->fields[$name]);
508   }
509
510   /**
511    * Gets the field associated with the given name (implements the ArrayAccess interface).
512    *
513    * @param  string  $name  The field name
514    *
515    * @return sfWidget The sfWidget instance associated with the given name, null if it does not exist
516    */
517   public function offsetGet($name)
518   {
519     return isset($this->fields[$name]) ? $this->fields[$name] : null;
520   }
521
522   /**
523    * Sets a field (implements the ArrayAccess interface).
524    *
525    * @param string   $name    The field name
526    * @param sfWidget $widget  An sfWidget instance
527    */
528   public function offsetSet($name, $widget)
529   {
530     if (!$widget instanceof sfWidget)
531     {
532       throw new InvalidArgumentException('A field must be an instance of sfWidget.');
533     }
534
535     if (!isset($this->fields[$name]))
536     {
537       $this->positions[] = $name;
538     }
539
540     $this->fields[$name] = clone $widget;
541
542     if ($widget instanceof sfWidgetFormSchema)
543     {
544       $this->fields[$name]->setParent($this);
545       $this->fields[$name]->setNameFormat($name.'[%s]');
546     }
547   }
548
549   /**
550    * Removes a field by name (implements the ArrayAccess interface).
551    *
552    * @param string
553    */
554   public function offsetUnset($name)
555   {
556     unset($this->fields[$name]);
557     if (false !== $position = array_search($name, $this->positions))
558     {
559       unset($this->positions[$position]);
560
561       $this->positions = array_values($this->positions);
562     }
563   }
564
565   /**
566    * Returns an array of fields.
567    *
568    * @return sfWidget An array of sfWidget instance
569    */
570   public function getFields()
571   {
572     return $this->fields;
573   }
574
575   /**
576    * Gets the positions of the fields.
577    *
578    * The field positions are only used when rendering the schema with ->render().
579    *
580    * @return array An ordered array of field names
581    */
582   public function getPositions()
583   {
584     return $this->positions;
585   }
586
587   /**
588    * Sets the positions of the fields.
589    *
590    * @param array An ordered array of field names
591    *
592    * @see getPositions()
593    */
594   public function setPositions($positions)
595   {
596     $positions = array_values($positions);
597     if (array_diff($positions, array_keys($this->fields)) || array_diff(array_keys($this->fields), $positions))
598     {
599       throw new InvalidArgumentException('Positions must contains all field names.');
600     }
601
602     $this->positions = $positions;
603   }
604
605   /**
606    * Moves a field in a given position
607    *
608    * Available actions are:
609    *
610    *  * sfWidgetFormSchema::BEFORE
611    *  * sfWidgetFormSchema::AFTER
612    *  * sfWidgetFormSchema::LAST
613    *  * sfWidgetFormSchema::FIRST
614    *
615    * @param string   The field name to move
616    * @param constant The action (see above for all possible actions)
617    * @param string   The field name used for AFTER and BEFORE actions
618    */
619   public function moveField($field, $action, $pivot = null)
620   {
621     if (false === $fieldPosition = array_search($field, $this->positions))
622     {
623       throw new InvalidArgumentException(sprintf('Field "%s" does not exist.', $field));
624     }
625     unset($this->positions[$fieldPosition]);
626     $this->positions = array_values($this->positions);
627
628     if (!is_null($pivot))
629     {
630       if (false === $pivotPosition = array_search($pivot, $this->positions))
631       {
632         throw new InvalidArgumentException(sprintf('Field "%s" does not exist.', $pivot));
633       }
634     }
635
636     switch ($action)
637     {
638       case sfWidgetFormSchema::FIRST:
639         array_unshift($this->positions, $field);
640         break;
641       case sfWidgetFormSchema::LAST:
642         array_push($this->positions, $field);
643         break;
644       case sfWidgetFormSchema::BEFORE:
645         if (is_null($pivot))
646         {
647           throw new LogicException(sprintf('Unable to move field "%s" without a relative field.', $field));
648         }
649         $this->positions = array_merge(
650           array_slice($this->positions, 0, $pivotPosition),
651           array($field),
652           array_slice($this->positions, $pivotPosition)
653         );
654         break;
655       case sfWidgetFormSchema::AFTER:
656         if (is_null($pivot))
657         {
658           throw new LogicException(sprintf('Unable to move field "%s" without a relative field.', $field));
659         }
660         $this->positions = array_merge(
661           array_slice($this->positions, 0, $pivotPosition + 1),
662           array($field),
663           array_slice($this->positions, $pivotPosition + 1)
664         );
665         break;
666       default:
667         throw new LogicException(sprintf('Unknown move operation for field "%s".', $field));
668     }
669   }
670
671   public function __clone()
672   {
673     foreach ($this->fields as $name => $field)
674     {
675       // offsetSet will clone the field and change the parent
676       $this[$name] = $field;
677     }
678
679     foreach ($this->formFormatters as &$formFormatter)
680     {
681       $formFormatter = clone $formFormatter;
682       $formFormatter->setWidgetSchema($this);
683     }
684   }
685 }
686
Note: See TracBrowser for help on using the browser.