3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2013 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
13 use Zend\Form\Element\Collection
;
14 use Zend\Form\Exception
;
15 use Zend\InputFilter\CollectionInputFilter
;
16 use Zend\InputFilter\InputFilter
;
17 use Zend\InputFilter\InputFilterAwareInterface
;
18 use Zend\InputFilter\InputFilterInterface
;
19 use Zend\InputFilter\InputFilterProviderInterface
;
20 use Zend\InputFilter\InputProviderInterface
;
21 use Zend\Stdlib\ArrayUtils
;
22 use Zend\Stdlib\Hydrator\HydratorInterface
;
24 class Form
extends Fieldset
implements FormInterface
31 protected $attributes = array(
36 * How to bind values to the attached object
40 protected $bindAs = FormInterface
::VALUES_NORMALIZED
;
43 * Whether or not to bind values to the bound object on successful validation
47 protected $bindOnValidate = FormInterface
::BIND_ON_VALIDATE
;
50 * Base fieldset to use for hydrating (if none specified, directly hydrate elements)
52 * @var FieldsetInterface
54 protected $baseFieldset;
57 * Data being validated
59 * @var null|array|Traversable
64 * @var null|InputFilterInterface
69 * Whether or not to automatically scan for input filter defaults on
70 * attached fieldsets and elements
74 protected $useInputFilterDefaults = true;
77 * Has the input filter defaults been added already ?
81 protected $hasAddedInputFilterDefaults = false;
84 * Whether or not validation has occurred
88 protected $hasValidated = false;
91 * Result of last validation operation
95 protected $isValid = false;
98 * Is the form prepared ?
102 protected $isPrepared = false;
105 * Prefer form input filter over input filter defaults
109 protected $preferFormInputFilter = true;
112 * Are the form elements/fieldsets wrapped by the form name ?
116 protected $wrapElements = false;
119 * Validation group, if any
123 protected $validationGroup;
127 * Set options for a form. Accepted options are:
128 * - prefer_form_input_filter: is form input filter is preferred?
130 * @param array|Traversable $options
131 * @return Element|ElementInterface
132 * @throws Exception\InvalidArgumentException
134 public function setOptions($options)
136 parent
::setOptions($options);
138 if (isset($options['prefer_form_input_filter'])) {
139 $this->setPreferFormInputFilter($options['prefer_form_input_filter']);
146 * Add an element or fieldset
148 * If $elementOrFieldset is an array or Traversable, passes the argument on
149 * to the composed factory to create the object before attaching it.
151 * $flags could contain metadata such as the alias under which to register
152 * the element or fieldset, order in which to prioritize it, etc.
154 * @param array|Traversable|ElementInterface $elementOrFieldset
155 * @param array $flags
156 * @return \Zend\Form\Fieldset|\Zend\Form\FieldsetInterface|\Zend\Form\FormInterface
158 public function add($elementOrFieldset, array $flags = array())
160 // TODO: find a better solution than duplicating the factory code, the problem being that if $elementOrFieldset is an array,
161 // it is passed by value, and we don't get back the concrete ElementInterface
162 if (is_array($elementOrFieldset)
163 ||
($elementOrFieldset instanceof Traversable
&& !$elementOrFieldset instanceof ElementInterface
)
165 $factory = $this->getFormFactory();
166 $elementOrFieldset = $factory->create($elementOrFieldset);
169 parent
::add($elementOrFieldset, $flags);
171 if ($elementOrFieldset instanceof Fieldset
&& $elementOrFieldset->useAsBaseFieldset()) {
172 $this->baseFieldset
= $elementOrFieldset;
179 * Ensures state is ready for use
181 * Marshalls the input filter, to ensure validation error messages are
182 * available, and prepares any elements and/or fieldsets that require
187 public function prepare()
189 if ($this->isPrepared
) {
193 $this->getInputFilter();
195 // If the user wants to, elements names can be wrapped by the form's name
196 if ($this->wrapElements()) {
197 $this->prepareElement($this);
199 foreach ($this->getIterator() as $elementOrFieldset) {
200 if ($elementOrFieldset instanceof FormInterface
) {
201 $elementOrFieldset->prepare();
202 } elseif ($elementOrFieldset instanceof ElementPrepareAwareInterface
) {
203 $elementOrFieldset->prepareElement($this);
208 $this->isPrepared
= true;
213 * Ensures state is ready for use. Here, we append the name of the fieldsets to every elements in order to avoid
214 * name clashes if the same fieldset is used multiple times
216 * @param FormInterface $form
219 public function prepareElement(FormInterface
$form)
221 $name = $this->getName();
223 foreach ($this->byName
as $elementOrFieldset) {
224 if ($form->wrapElements()) {
225 $elementOrFieldset->setName($name . '[' . $elementOrFieldset->getName() . ']');
228 // Recursively prepare elements
229 if ($elementOrFieldset instanceof ElementPrepareAwareInterface
) {
230 $elementOrFieldset->prepareElement($form);
236 * Set data to validate and/or populate elements
238 * Typically, also passes data on to the composed input filter.
240 * @param array|\ArrayAccess|Traversable $data
241 * @return Form|FormInterface
242 * @throws Exception\InvalidArgumentException
244 public function setData($data)
246 if ($data instanceof Traversable
) {
247 $data = ArrayUtils
::iteratorToArray($data);
249 if (!is_array($data)) {
250 throw new Exception\
InvalidArgumentException(sprintf(
251 '%s expects an array or Traversable argument; received "%s"',
253 (is_object($data) ?
get_class($data) : gettype($data))
257 $this->hasValidated
= false;
259 $this->populateValues($data);
265 * Bind an object to the form
267 * Ensures the object is populated with validated values.
269 * @param object $object
272 * @throws Exception\InvalidArgumentException
274 public function bind($object, $flags = FormInterface
::VALUES_NORMALIZED
)
276 if (!in_array($flags, array(FormInterface
::VALUES_NORMALIZED
, FormInterface
::VALUES_RAW
))) {
277 throw new Exception\
InvalidArgumentException(sprintf(
278 '%s expects the $flags argument to be one of "%s" or "%s"; received "%s"',
280 'Zend\Form\FormInterface::VALUES_NORMALIZED',
281 'Zend\Form\FormInterface::VALUES_RAW',
286 if ($this->baseFieldset
!== null) {
287 $this->baseFieldset
->setObject($object);
290 $this->bindAs
= $flags;
291 $this->setObject($object);
298 * Set the hydrator to use when binding an object to the element
300 * @param HydratorInterface $hydrator
301 * @return FieldsetInterface
303 public function setHydrator(HydratorInterface
$hydrator)
305 if ($this->baseFieldset
!== null) {
306 $this->baseFieldset
->setHydrator($hydrator);
309 return parent
::setHydrator($hydrator);
313 * Bind values to the bound object
315 * @param array $values
318 public function bindValues(array $values = array())
320 if (!is_object($this->object)) {
321 if ($this->baseFieldset
=== null ||
$this->baseFieldset
->allowValueBinding() == false) {
325 if (!$this->hasValidated() && !empty($values)) {
326 $this->setData($values);
327 if (!$this->isValid()) {
330 } elseif (!$this->isValid
) {
334 $filter = $this->getInputFilter();
336 switch ($this->bindAs
) {
337 case FormInterface
::VALUES_RAW
:
338 $data = $filter->getRawValues();
340 case FormInterface
::VALUES_NORMALIZED
:
342 $data = $filter->getValues();
346 $data = $this->prepareBindData($data, $this->data
);
348 // If there is a base fieldset, only hydrate beginning from the base fieldset
349 if ($this->baseFieldset
!== null) {
350 $data = $data[$this->baseFieldset
->getName()];
351 $this->object = $this->baseFieldset
->bindValues($data);
353 $this->object = parent
::bindValues($data);
358 * Parse filtered values and return only posted fields for binding
360 * @param array $values
361 * @param array $match
364 protected function prepareBindData(array $values, array $match)
367 foreach ($values as $name => $value) {
368 if (!array_key_exists($name, $match)) {
372 if (is_array($value) && is_array($match[$name])) {
373 $data[$name] = $this->prepareBindData($value, $match[$name]);
375 $data[$name] = $value;
382 * Set flag indicating whether or not to bind values on successful validation
384 * @param int $bindOnValidateFlag
386 * @throws Exception\InvalidArgumentException
388 public function setBindOnValidate($bindOnValidateFlag)
390 if (!in_array($bindOnValidateFlag, array(self
::BIND_ON_VALIDATE
, self
::BIND_MANUAL
))) {
391 throw new Exception\
InvalidArgumentException(sprintf(
392 '%s expects the flag to be one of %s::%s or %s::%s',
400 $this->bindOnValidate
= $bindOnValidateFlag;
405 * Will we bind values to the bound object on successful validation?
409 public function bindOnValidate()
411 return (static::BIND_ON_VALIDATE
=== $this->bindOnValidate
);
415 * Set the base fieldset to use when hydrating
417 * @param FieldsetInterface $baseFieldset
419 * @throws Exception\InvalidArgumentException
421 public function setBaseFieldset(FieldsetInterface
$baseFieldset)
423 $this->baseFieldset
= $baseFieldset;
428 * Get the base fieldset to use when hydrating
430 * @return FieldsetInterface
432 public function getBaseFieldset()
434 return $this->baseFieldset
;
438 * Check if the form has been validated
442 public function hasValidated()
444 return $this->hasValidated
;
450 * Typically, will proxy to the composed input filter.
453 * @throws Exception\DomainException
455 public function isValid()
457 if ($this->hasValidated
) {
458 return $this->isValid
;
461 $this->isValid
= false;
463 if (!is_array($this->data
) && !is_object($this->object)) {
464 throw new Exception\
DomainException(sprintf(
465 '%s is unable to validate as there is no data currently set',
470 if (!is_array($this->data
)) {
471 $data = $this->extract();
472 if (!is_array($data)) {
473 throw new Exception\
DomainException(sprintf(
474 '%s is unable to validate as there is no data currently set',
481 $filter = $this->getInputFilter();
482 if (!$filter instanceof InputFilterInterface
) {
483 throw new Exception\
DomainException(sprintf(
484 '%s is unable to validate as there is no input filter present',
489 $filter->setData($this->data
);
490 $filter->setValidationGroup(InputFilterInterface
::VALIDATE_ALL
);
492 $validationGroup = $this->getValidationGroup();
493 if ($validationGroup !== null) {
494 $this->prepareValidationGroup($this, $this->data
, $validationGroup);
495 $filter->setValidationGroup($validationGroup);
498 $this->isValid
= $result = $filter->isValid();
499 $this->hasValidated
= true;
501 if ($result && $this->bindOnValidate()) {
506 $this->setMessages($filter->getMessages());
513 * Retrieve the validated data
515 * By default, retrieves normalized values; pass one of the
516 * FormInterface::VALUES_* constants to shape the behavior.
519 * @return array|object
520 * @throws Exception\DomainException
522 public function getData($flag = FormInterface
::VALUES_NORMALIZED
)
524 if (!$this->hasValidated
) {
525 throw new Exception\
DomainException(sprintf(
526 '%s cannot return data as validation has not yet occurred',
531 if (($flag !== FormInterface
::VALUES_AS_ARRAY
) && is_object($this->object)) {
532 return $this->object;
535 $filter = $this->getInputFilter();
537 if ($flag === FormInterface
::VALUES_RAW
) {
538 return $filter->getRawValues();
541 return $filter->getValues();
545 * Set the validation group (set of values to validate)
547 * Typically, proxies to the composed input filter
549 * @throws Exception\InvalidArgumentException
550 * @return Form|FormInterface
552 public function setValidationGroup()
554 $argc = func_num_args();
556 throw new Exception\
InvalidArgumentException(sprintf(
557 '%s expects at least one argument; none provided',
562 $argv = func_get_args();
563 $this->hasValidated
= false;
566 $this->validationGroup
= $argv;
570 $arg = array_shift($argv);
571 if ($arg === FormInterface
::VALIDATE_ALL
) {
572 $this->validationGroup
= null;
576 if (!is_array($arg)) {
580 $this->validationGroup
= $arg;
585 * Retrieve the current validation group, if any
589 public function getValidationGroup()
591 return $this->validationGroup
;
595 * Prepare the validation group in case Collection elements were used (this function also handle the case where elements
596 * could have been dynamically added or removed from a collection using JavaScript)
598 * @param FieldsetInterface $formOrFieldset
600 * @param array $validationGroup
602 protected function prepareValidationGroup(FieldsetInterface
$formOrFieldset, array $data, array &$validationGroup)
604 foreach ($validationGroup as $key => &$value) {
605 if (!$formOrFieldset->has($key)) {
609 $fieldset = $formOrFieldset->byName
[$key];
611 if ($fieldset instanceof Collection
) {
612 if (!isset($data[$key]) && $fieldset->getCount() == 0) {
613 unset ($validationGroup[$key]);
619 if (isset($data[$key])) {
620 foreach (array_keys($data[$key]) as $cKey) {
621 $values[$cKey] = $value;
627 if (!isset($data[$key])) {
628 $data[$key] = array();
630 $this->prepareValidationGroup($fieldset, $data[$key], $validationGroup[$key]);
636 * Set the input filter used by this form
638 * @param InputFilterInterface $inputFilter
639 * @return FormInterface
641 public function setInputFilter(InputFilterInterface
$inputFilter)
643 $this->hasValidated
= false;
644 $this->hasAddedInputFilterDefaults
= false;
645 $this->filter
= $inputFilter;
650 * Retrieve input filter used by this form
652 * @return null|InputFilterInterface
654 public function getInputFilter()
656 if ($this->object instanceof InputFilterAwareInterface
) {
657 if (null == $this->baseFieldset
) {
658 $this->filter
= $this->object->getInputFilter();
660 $name = $this->baseFieldset
->getName();
661 if (!$this->filter
instanceof InputFilterInterface ||
!$this->filter
->has($name)) {
662 $filter = new InputFilter();
663 $filter->add($this->object->getInputFilter(), $name);
664 $this->filter
= $filter;
669 if (!isset($this->filter
)) {
670 $this->filter
= new InputFilter();
673 if (!$this->hasAddedInputFilterDefaults
674 && $this->filter
instanceof InputFilterInterface
675 && $this->useInputFilterDefaults()
677 $this->attachInputFilterDefaults($this->filter
, $this);
678 $this->hasAddedInputFilterDefaults
= true;
681 return $this->filter
;
685 * Set flag indicating whether or not to scan elements and fieldsets for defaults
687 * @param bool $useInputFilterDefaults
690 public function setUseInputFilterDefaults($useInputFilterDefaults)
692 $this->useInputFilterDefaults
= (bool) $useInputFilterDefaults;
697 * Should we use input filter defaults from elements and fieldsets?
701 public function useInputFilterDefaults()
703 return $this->useInputFilterDefaults
;
707 * Set flag indicating whether or not to prefer the form input filter over element and fieldset defaults
709 * @param bool $preferFormInputFilter
712 public function setPreferFormInputFilter($preferFormInputFilter)
714 $this->preferFormInputFilter
= (bool) $preferFormInputFilter;
719 * Should we use form input filter over element input filter defaults from elements and fieldsets?
723 public function getPreferFormInputFilter()
725 return $this->preferFormInputFilter
;
729 * Attach defaults provided by the elements to the input filter
731 * @param InputFilterInterface $inputFilter
732 * @param FieldsetInterface $fieldset Fieldset to traverse when looking for default inputs
735 public function attachInputFilterDefaults(InputFilterInterface
$inputFilter, FieldsetInterface
$fieldset)
737 $formFactory = $this->getFormFactory();
738 $inputFactory = $formFactory->getInputFilterFactory();
740 if ($fieldset instanceof Collection
&& $fieldset->getTargetElement() instanceof FieldsetInterface
) {
741 $elements = $fieldset->getTargetElement()->getElements();
743 $elements = $fieldset->getElements();
746 if (!$fieldset instanceof Collection ||
$inputFilter instanceof CollectionInputFilter
) {
747 foreach ($elements as $element) {
748 $name = $element->getName();
750 if ($this->preferFormInputFilter
&& $inputFilter->has($name)) {
754 if (!$element instanceof InputProviderInterface
) {
755 if ($inputFilter->has($name)) {
758 // Create a new empty default input for this element
759 $spec = array('name' => $name, 'required' => false);
761 // Create an input based on the specification returned from the element
762 $spec = $element->getInputSpecification();
765 $input = $inputFactory->createInput($spec);
766 $inputFilter->add($input, $name);
769 if ($fieldset === $this && $fieldset instanceof InputFilterProviderInterface
) {
770 foreach ($fieldset->getInputFilterSpecification() as $name => $spec) {
771 $input = $inputFactory->createInput($spec);
772 $inputFilter->add($input, $name);
777 foreach ($fieldset->getFieldsets() as $childFieldset) {
778 $name = $childFieldset->getName();
780 if (!$childFieldset instanceof InputFilterProviderInterface
) {
781 if (!$inputFilter->has($name)) {
782 // Add a new empty input filter if it does not exist (or the fieldset's object input filter),
783 // so that elements of nested fieldsets can be recursively added
784 if ($childFieldset->getObject() instanceof InputFilterAwareInterface
) {
785 $inputFilter->add($childFieldset->getObject()->getInputFilter(), $name);
787 if ($fieldset instanceof Collection
&& $inputFilter instanceof CollectionInputFilter
) {
790 $inputFilter->add(new InputFilter(), $name);
795 $fieldsetFilter = $inputFilter->get($name);
797 if (!$fieldsetFilter instanceof InputFilterInterface
) {
798 // Input attached for fieldset, not input filter; nothing more to do.
802 // Traverse the elements of the fieldset, and attach any
803 // defaults to the fieldset's input filter
804 $this->attachInputFilterDefaults($fieldsetFilter, $childFieldset);
808 if ($inputFilter->has($name)) {
809 // if we already have an input/filter by this name, use it
813 // Create an input filter based on the specification returned from the fieldset
814 $spec = $childFieldset->getInputFilterSpecification();
815 $filter = $inputFactory->createInputFilter($spec);
816 $inputFilter->add($filter, $name);
818 // Recursively attach sub filters
819 $this->attachInputFilterDefaults($filter, $childFieldset);
824 * Are the form elements/fieldsets names wrapped by the form name ?
826 * @param bool $wrapElements
829 public function setWrapElements($wrapElements)
831 $this->wrapElements
= (bool) $wrapElements;
836 * If true, form elements/fieldsets name's are wrapped around the form name itself
840 public function wrapElements()
842 return $this->wrapElements
;
846 * Recursively extract values for elements and sub-fieldsets, and populate form values
850 protected function extract()
852 if (null !== $this->baseFieldset
) {
853 $name = $this->baseFieldset
->getName();
854 $values[$name] = $this->baseFieldset
->extract();
855 $this->baseFieldset
->populateValues($values[$name]);
857 $values = parent
::extract();
858 $this->populateValues($values);