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

Debug toolbar