3 * Zend Framework (http://framework.zend.com/)
5 * @link http://github.com/zendframework/zf2 for the canonical source repository
6 * @copyright Copyright (c) 2005-2015 Zend Technologies USA Inc. (http://www.zend.com)
7 * @license http://framework.zend.com/license/new-bsd New BSD License
10 namespace Zend\Form\Element
;
13 use Zend\Form\Element
;
14 use Zend\Form\ElementInterface
;
15 use Zend\Form\Exception
;
16 use Zend\Form\Fieldset
;
17 use Zend\Form\FieldsetInterface
;
18 use Zend\Form\FormInterface
;
19 use Zend\Stdlib\ArrayUtils
;
21 class Collection
extends Fieldset
24 * Default template placeholder
26 const DEFAULT_TEMPLATE_PLACEHOLDER
= '__index__';
29 * Element used in the collection
31 * @var ElementInterface
33 protected $targetElement;
36 * Initial count of target element
43 * Are new elements allowed to be added dynamically ?
47 protected $allowAdd = true;
50 * Are existing elements allowed to be removed dynamically ?
54 protected $allowRemove = true;
57 * Is the template generated ?
61 protected $shouldCreateTemplate = false;
64 * Placeholder used in template content for making your life easier with JavaScript
68 protected $templatePlaceholder = self
::DEFAULT_TEMPLATE_PLACEHOLDER
;
71 * Whether or not to create new objects during modify
75 protected $createNewObjects = false;
78 * Element used as a template
80 * @var ElementInterface|FieldsetInterface
82 protected $templateElement;
85 * The index of the last child element or fieldset
89 protected $lastChildIndex = -1;
92 * Should child elements must be created on self::prepareElement()?
96 protected $shouldCreateChildrenOnPrepareElement = true;
99 * Accepted options for Collection:
100 * - target_element: an array or element used in the collection
101 * - count: number of times the element is added initially
102 * - allow_add: if set to true, elements can be added to the form dynamically (using JavaScript)
103 * - allow_remove: if set to true, elements can be removed to the form
104 * - should_create_template: if set to true, a template is generated (inside a <span>)
105 * - template_placeholder: placeholder used in the data template
107 * @param array|Traversable $options
110 public function setOptions($options)
112 parent
::setOptions($options);
114 if (isset($options['target_element'])) {
115 $this->setTargetElement($options['target_element']);
118 if (isset($options['count'])) {
119 $this->setCount($options['count']);
122 if (isset($options['allow_add'])) {
123 $this->setAllowAdd($options['allow_add']);
126 if (isset($options['allow_remove'])) {
127 $this->setAllowRemove($options['allow_remove']);
130 if (isset($options['should_create_template'])) {
131 $this->setShouldCreateTemplate($options['should_create_template']);
134 if (isset($options['template_placeholder'])) {
135 $this->setTemplatePlaceholder($options['template_placeholder']);
138 if (isset($options['create_new_objects'])) {
139 $this->setCreateNewObjects($options['create_new_objects']);
146 * Checks if the object can be set in this fieldset
148 * @param object $object
151 public function allowObjectBinding($object)
157 * Set the object used by the hydrator
158 * In this case the "object" is a collection of objects
160 * @param array|Traversable $object
161 * @return Fieldset|FieldsetInterface
162 * @throws Exception\InvalidArgumentException
164 public function setObject($object)
166 if (! is_array($object) && ! $object instanceof Traversable
) {
167 throw new Exception\
InvalidArgumentException(sprintf(
168 '%s expects an array or Traversable object argument; received "%s"',
170 (is_object($object) ?
get_class($object) : gettype($object))
174 $this->object = $object;
175 $this->count
= max(count($object), $this->count
);
183 * @param array|Traversable $data
184 * @throws \Zend\Form\Exception\InvalidArgumentException
185 * @throws \Zend\Form\Exception\DomainException
188 public function populateValues($data)
190 if (! is_array($data) && ! $data instanceof Traversable
) {
191 throw new Exception\
InvalidArgumentException(sprintf(
192 '%s expects an array or Traversable set of data; received "%s"',
194 (is_object($data) ?
get_class($data) : gettype($data))
198 if (! $this->allowRemove
&& count($data) < $this->count
) {
199 throw new Exception\
DomainException(sprintf(
200 'There are fewer elements than specified in the collection (%s). Either set the allow_remove option '
201 . 'to true, or re-submit the form.',
206 // Check to see if elements have been replaced or removed
208 foreach ($this as $name => $elementOrFieldset) {
209 if (isset($data[$name])) {
213 if (! $this->allowRemove
) {
214 throw new Exception\
DomainException(sprintf(
215 'Elements have been removed from the collection (%s) but the allow_remove option is not true.',
223 foreach ($toRemove as $name) {
224 $this->remove($name);
227 foreach ($data as $key => $value) {
228 if ($this->has($key)) {
229 $elementOrFieldset = $this->get($key);
231 $elementOrFieldset = $this->addNewTargetElementInstance($key);
233 if ($key > $this->lastChildIndex
) {
234 $this->lastChildIndex
= $key;
238 if ($elementOrFieldset instanceof FieldsetInterface
) {
239 $elementOrFieldset->populateValues($value);
241 $elementOrFieldset->setAttribute('value', $value);
245 if (! $this->createNewObjects()) {
246 $this->replaceTemplateObjects();
251 * Checks if this fieldset can bind data
255 public function allowValueBinding()
261 * Bind values to the object
263 * @param array $values
264 * @param array $validationGroup
266 * @return array|mixed|void
268 public function bindValues(array $values = [], array $validationGroup = null)
271 foreach ($values as $name => $value) {
272 $element = $this->get($name);
274 if ($element instanceof FieldsetInterface
) {
275 $collection[] = $element->bindValues($value, $validationGroup);
277 $collection[] = $value;
285 * Set the initial count of target element
290 public function setCount($count)
292 $this->count
= $count > 0 ?
$count : 0;
297 * Get the initial count of target element
301 public function getCount()
307 * Set the target element
309 * @param ElementInterface|array|Traversable $elementOrFieldset
311 * @throws \Zend\Form\Exception\InvalidArgumentException
313 public function setTargetElement($elementOrFieldset)
315 if (is_array($elementOrFieldset)
316 ||
($elementOrFieldset instanceof Traversable
&& ! $elementOrFieldset instanceof ElementInterface
)
318 $factory = $this->getFormFactory();
319 $elementOrFieldset = $factory->create($elementOrFieldset);
322 if (! $elementOrFieldset instanceof ElementInterface
) {
323 throw new Exception\
InvalidArgumentException(sprintf(
324 '%s requires that $elementOrFieldset be an object implementing %s; received "%s"',
326 __NAMESPACE__
. '\ElementInterface',
327 (is_object($elementOrFieldset) ?
get_class($elementOrFieldset) : gettype($elementOrFieldset))
331 $this->targetElement
= $elementOrFieldset;
339 * @return ElementInterface|null
341 public function getTargetElement()
343 return $this->targetElement
;
349 * @param bool $allowAdd
352 public function setAllowAdd($allowAdd)
354 $this->allowAdd
= (bool) $allowAdd;
363 public function allowAdd()
365 return $this->allowAdd
;
369 * @param bool $allowRemove
372 public function setAllowRemove($allowRemove)
374 $this->allowRemove
= (bool) $allowRemove;
381 public function allowRemove()
383 return $this->allowRemove
;
387 * If set to true, a template prototype is automatically added to the form
388 * to ease the creation of dynamic elements through JavaScript
390 * @param bool $shouldCreateTemplate
393 public function setShouldCreateTemplate($shouldCreateTemplate)
395 $this->shouldCreateTemplate
= (bool) $shouldCreateTemplate;
401 * Get if the collection should create a template
405 public function shouldCreateTemplate()
407 return $this->shouldCreateTemplate
;
411 * Set the placeholder used in the template generated to help create new elements in JavaScript
413 * @param string $templatePlaceholder
416 public function setTemplatePlaceholder($templatePlaceholder)
418 if (is_string($templatePlaceholder)) {
419 $this->templatePlaceholder
= $templatePlaceholder;
426 * Get the template placeholder
430 public function getTemplatePlaceholder()
432 return $this->templatePlaceholder
;
436 * @param bool $createNewObjects
439 public function setCreateNewObjects($createNewObjects)
441 $this->createNewObjects
= (bool) $createNewObjects;
448 public function createNewObjects()
450 return $this->createNewObjects
;
454 * Get a template element used for rendering purposes only
456 * @return null|ElementInterface|FieldsetInterface
458 public function getTemplateElement()
460 if ($this->templateElement
=== null) {
461 $this->templateElement
= $this->createTemplateElement();
464 return $this->templateElement
;
468 * Prepare the collection by adding a dummy template element if the user want one
470 * @param FormInterface $form
473 public function prepareElement(FormInterface
$form)
475 if (true === $this->shouldCreateChildrenOnPrepareElement
) {
476 if ($this->targetElement
!== null && $this->count
> 0) {
477 while ($this->count
> $this->lastChildIndex +
1) {
478 $this->addNewTargetElementInstance(++
$this->lastChildIndex
);
483 // Create a template that will also be prepared
484 if ($this->shouldCreateTemplate
) {
485 $templateElement = $this->getTemplateElement();
486 $this->add($templateElement);
489 parent
::prepareElement($form);
491 // The template element has been prepared, but we don't want it to be
492 // rendered nor validated, so remove it from the list.
493 if ($this->shouldCreateTemplate
) {
494 $this->remove($this->templatePlaceholder
);
500 * @throws \Zend\Form\Exception\InvalidArgumentException
501 * @throws \Zend\Stdlib\Exception\InvalidArgumentException
502 * @throws \Zend\Form\Exception\DomainException
503 * @throws \Zend\Form\Exception\InvalidElementException
505 public function extract()
507 if ($this->object instanceof Traversable
) {
508 $this->object = ArrayUtils
::iteratorToArray($this->object, false);
511 if (! is_array($this->object)) {
517 foreach ($this->object as $key => $value) {
518 // If a hydrator is provided, our work here is done
519 if ($this->hydrator
) {
520 $values[$key] = $this->hydrator
->extract($value);
524 // If the target element is a fieldset that can accept the provided value
525 // we should clone it, inject the value and extract the data
526 if ($this->targetElement
instanceof FieldsetInterface
) {
527 if (! $this->targetElement
->allowObjectBinding($value)) {
530 $targetElement = clone $this->targetElement
;
531 $targetElement->setObject($value);
532 $values[$key] = $targetElement->extract();
533 if (! $this->createNewObjects() && $this->has($key)) {
534 $this->get($key)->setObject($value);
539 // If the target element is a non-fieldset element, just use the value
540 if ($this->targetElement
instanceof ElementInterface
) {
541 $values[$key] = $value;
542 if (! $this->createNewObjects() && $this->has($key)) {
543 $this->get($key)->setValue($value);
553 * Create a new instance of the target element
555 * @return ElementInterface
557 protected function createNewTargetElementInstance()
559 return clone $this->targetElement
;
563 * Add a new instance of the target element
565 * @param string $name
566 * @return ElementInterface
567 * @throws Exception\DomainException
569 protected function addNewTargetElementInstance($name)
571 $this->shouldCreateChildrenOnPrepareElement
= false;
573 $elementOrFieldset = $this->createNewTargetElementInstance();
574 $elementOrFieldset->setName($name);
576 $this->add($elementOrFieldset);
578 if (! $this->allowAdd
&& $this->count() > $this->count
) {
579 throw new Exception\
DomainException(sprintf(
580 'There are more elements than specified in the collection (%s). Either set the allow_add option ' .
581 'to true, or re-submit the form.',
586 return $elementOrFieldset;
590 * Create a dummy template element
592 * @return null|ElementInterface|FieldsetInterface
594 protected function createTemplateElement()
596 if (! $this->shouldCreateTemplate
) {
600 if ($this->templateElement
) {
601 return $this->templateElement
;
604 $elementOrFieldset = $this->createNewTargetElementInstance();
605 $elementOrFieldset->setName($this->templatePlaceholder
);
607 return $elementOrFieldset;
611 * Replaces the default template object of a sub element with the corresponding
612 * real entity so that all properties are preserved.
616 protected function replaceTemplateObjects()
618 $fieldsets = $this->getFieldsets();
620 if (! count($fieldsets) ||
! $this->object) {
624 foreach ($fieldsets as $fieldset) {
625 $i = $fieldset->getName();
626 if (isset($this->object[$i])) {
627 $fieldset->setObject($this->object[$i]);