1<?php
2/**
3 * Zend Framework (http://framework.zend.com/)
4 *
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
8 */
9
10namespace Zend\Form\View\Helper;
11
12use Zend\Form\ElementInterface;
13use Zend\Form\Element\MultiCheckbox as MultiCheckboxElement;
14use Zend\Form\Exception;
15use Zend\Form\LabelAwareInterface;
16
17class FormMultiCheckbox extends FormInput
18{
19    const LABEL_APPEND  = 'append';
20    const LABEL_PREPEND = 'prepend';
21
22    /**
23     * The attributes applied to option label
24     *
25     * @var array
26     */
27    protected $labelAttributes;
28
29    /**
30     * Where will be label rendered?
31     *
32     * @var string
33     */
34    protected $labelPosition = self::LABEL_APPEND;
35
36    /**
37     * Separator for checkbox elements
38     *
39     * @var string
40     */
41    protected $separator = '';
42
43    /**
44     * Prefixing the element with a hidden element for the unset value?
45     *
46     * @var bool
47     */
48    protected $useHiddenElement = false;
49
50    /**
51     * The unchecked value used when "UseHiddenElement" is turned on
52     *
53     * @var string
54     */
55    protected $uncheckedValue = '';
56
57    /**
58     * Form input helper instance
59     *
60     * @var FormInput
61     */
62    protected $inputHelper;
63
64    /**
65     * Form label helper instance
66     *
67     * @var FormLabel
68     */
69    protected $labelHelper;
70
71    /**
72     * Invoke helper as functor
73     *
74     * Proxies to {@link render()}.
75     *
76     * @param  ElementInterface|null $element
77     * @param  null|string           $labelPosition
78     * @return string|FormMultiCheckbox
79     */
80    public function __invoke(ElementInterface $element = null, $labelPosition = null)
81    {
82        if (!$element) {
83            return $this;
84        }
85
86        if ($labelPosition !== null) {
87            $this->setLabelPosition($labelPosition);
88        }
89
90        return $this->render($element);
91    }
92
93    /**
94     * Render a form <input> element from the provided $element
95     *
96     * @param  ElementInterface $element
97     * @throws Exception\InvalidArgumentException
98     * @return string
99     */
100    public function render(ElementInterface $element)
101    {
102        if (!$element instanceof MultiCheckboxElement) {
103            throw new Exception\InvalidArgumentException(sprintf(
104                '%s requires that the element is of type Zend\Form\Element\MultiCheckbox',
105                __METHOD__
106            ));
107        }
108
109        $name = static::getName($element);
110
111        $options = $element->getValueOptions();
112
113        $attributes         = $element->getAttributes();
114        $attributes['name'] = $name;
115        $attributes['type'] = $this->getInputType();
116        $selectedOptions    = (array) $element->getValue();
117
118        $rendered = $this->renderOptions($element, $options, $selectedOptions, $attributes);
119
120        // Render hidden element
121        $useHiddenElement = method_exists($element, 'useHiddenElement') && $element->useHiddenElement()
122            ? $element->useHiddenElement()
123            : $this->useHiddenElement;
124
125        if ($useHiddenElement) {
126            $rendered = $this->renderHiddenElement($element, $attributes) . $rendered;
127        }
128
129        return $rendered;
130    }
131
132    /**
133     * Render options
134     *
135     * @param  MultiCheckboxElement $element
136     * @param  array                $options
137     * @param  array                $selectedOptions
138     * @param  array                $attributes
139     * @return string
140     */
141    protected function renderOptions(MultiCheckboxElement $element, array $options, array $selectedOptions, array $attributes)
142    {
143        $escapeHtmlHelper = $this->getEscapeHtmlHelper();
144        $labelHelper      = $this->getLabelHelper();
145        $labelClose       = $labelHelper->closeTag();
146        $labelPosition    = $this->getLabelPosition();
147        $globalLabelAttributes = array();
148        $closingBracket   = $this->getInlineClosingBracket();
149
150        if ($element instanceof LabelAwareInterface) {
151            $globalLabelAttributes = $element->getLabelAttributes();
152        }
153
154        if (empty($globalLabelAttributes)) {
155            $globalLabelAttributes = $this->labelAttributes;
156        }
157
158        $combinedMarkup = array();
159        $count          = 0;
160
161        foreach ($options as $key => $optionSpec) {
162            $count++;
163            if ($count > 1 && array_key_exists('id', $attributes)) {
164                unset($attributes['id']);
165            }
166
167            $value           = '';
168            $label           = '';
169            $inputAttributes = $attributes;
170            $labelAttributes = $globalLabelAttributes;
171            $selected        = (isset($inputAttributes['selected']) && $inputAttributes['type'] != 'radio' && $inputAttributes['selected']);
172            $disabled        = (isset($inputAttributes['disabled']) && $inputAttributes['disabled']);
173
174            if (is_scalar($optionSpec)) {
175                $optionSpec = array(
176                    'label' => $optionSpec,
177                    'value' => $key
178                );
179            }
180
181            if (isset($optionSpec['value'])) {
182                $value = $optionSpec['value'];
183            }
184            if (isset($optionSpec['label'])) {
185                $label = $optionSpec['label'];
186            }
187            if (isset($optionSpec['selected'])) {
188                $selected = $optionSpec['selected'];
189            }
190            if (isset($optionSpec['disabled'])) {
191                $disabled = $optionSpec['disabled'];
192            }
193            if (isset($optionSpec['label_attributes'])) {
194                $labelAttributes = (isset($labelAttributes))
195                    ? array_merge($labelAttributes, $optionSpec['label_attributes'])
196                    : $optionSpec['label_attributes'];
197            }
198            if (isset($optionSpec['attributes'])) {
199                $inputAttributes = array_merge($inputAttributes, $optionSpec['attributes']);
200            }
201
202            if (in_array($value, $selectedOptions)) {
203                $selected = true;
204            }
205
206            $inputAttributes['value']    = $value;
207            $inputAttributes['checked']  = $selected;
208            $inputAttributes['disabled'] = $disabled;
209
210            $input = sprintf(
211                '<input %s%s',
212                $this->createAttributesString($inputAttributes),
213                $closingBracket
214            );
215
216            if (null !== ($translator = $this->getTranslator())) {
217                $label = $translator->translate(
218                    $label,
219                    $this->getTranslatorTextDomain()
220                );
221            }
222
223            if (! $element instanceof LabelAwareInterface || ! $element->getLabelOption('disable_html_escape')) {
224                $label = $escapeHtmlHelper($label);
225            }
226
227            $labelOpen = $labelHelper->openTag($labelAttributes);
228            $template  = $labelOpen . '%s%s' . $labelClose;
229            switch ($labelPosition) {
230                case self::LABEL_PREPEND:
231                    $markup = sprintf($template, $label, $input);
232                    break;
233                case self::LABEL_APPEND:
234                default:
235                    $markup = sprintf($template, $input, $label);
236                    break;
237            }
238
239            $combinedMarkup[] = $markup;
240        }
241
242        return implode($this->getSeparator(), $combinedMarkup);
243    }
244
245    /**
246     * Render a hidden element for empty/unchecked value
247     *
248     * @param  MultiCheckboxElement $element
249     * @param  array                $attributes
250     * @return string
251     */
252    protected function renderHiddenElement(MultiCheckboxElement $element, array $attributes)
253    {
254        $closingBracket = $this->getInlineClosingBracket();
255
256        $uncheckedValue = $element->getUncheckedValue()
257            ? $element->getUncheckedValue()
258            : $this->uncheckedValue;
259
260        $hiddenAttributes = array(
261            'name'  => $element->getName(),
262            'value' => $uncheckedValue,
263        );
264
265        return sprintf(
266            '<input type="hidden" %s%s',
267            $this->createAttributesString($hiddenAttributes),
268            $closingBracket
269        );
270    }
271
272    /**
273     * Sets the attributes applied to option label.
274     *
275     * @param  array|null $attributes
276     * @return FormMultiCheckbox
277     */
278    public function setLabelAttributes($attributes)
279    {
280        $this->labelAttributes = $attributes;
281        return $this;
282    }
283
284    /**
285     * Returns the attributes applied to each option label.
286     *
287     * @return array|null
288     */
289    public function getLabelAttributes()
290    {
291        return $this->labelAttributes;
292    }
293
294    /**
295     * Set value for labelPosition
296     *
297     * @param  mixed $labelPosition
298     * @throws Exception\InvalidArgumentException
299     * @return FormMultiCheckbox
300     */
301    public function setLabelPosition($labelPosition)
302    {
303        $labelPosition = strtolower($labelPosition);
304        if (!in_array($labelPosition, array(self::LABEL_APPEND, self::LABEL_PREPEND))) {
305            throw new Exception\InvalidArgumentException(sprintf(
306                '%s expects either %s::LABEL_APPEND or %s::LABEL_PREPEND; received "%s"',
307                __METHOD__,
308                __CLASS__,
309                __CLASS__,
310                (string) $labelPosition
311            ));
312        }
313        $this->labelPosition = $labelPosition;
314
315        return $this;
316    }
317
318    /**
319     * Get position of label
320     *
321     * @return string
322     */
323    public function getLabelPosition()
324    {
325        return $this->labelPosition;
326    }
327
328    /**
329     * Set separator string for checkbox elements
330     *
331     * @param  string $separator
332     * @return FormMultiCheckbox
333     */
334    public function setSeparator($separator)
335    {
336        $this->separator = (string) $separator;
337        return $this;
338    }
339
340    /**
341     * Get separator for checkbox elements
342     *
343     * @return string
344     */
345    public function getSeparator()
346    {
347        return $this->separator;
348    }
349
350    /**
351     * Sets the option for prefixing the element with a hidden element
352     * for the unset value.
353     *
354     * @param  bool $useHiddenElement
355     * @return FormMultiCheckbox
356     */
357    public function setUseHiddenElement($useHiddenElement)
358    {
359        $this->useHiddenElement = (bool) $useHiddenElement;
360        return $this;
361    }
362
363    /**
364     * Returns the option for prefixing the element with a hidden element
365     * for the unset value.
366     *
367     * @return bool
368     */
369    public function getUseHiddenElement()
370    {
371        return $this->useHiddenElement;
372    }
373
374    /**
375     * Sets the unchecked value used when "UseHiddenElement" is turned on.
376     *
377     * @param  bool $value
378     * @return FormMultiCheckbox
379     */
380    public function setUncheckedValue($value)
381    {
382        $this->uncheckedValue = $value;
383        return $this;
384    }
385
386    /**
387     * Returns the unchecked value used when "UseHiddenElement" is turned on.
388     *
389     * @return string
390     */
391    public function getUncheckedValue()
392    {
393        return $this->uncheckedValue;
394    }
395
396    /**
397     * Return input type
398     *
399     * @return string
400     */
401    protected function getInputType()
402    {
403        return 'checkbox';
404    }
405
406    /**
407     * Get element name
408     *
409     * @param  ElementInterface $element
410     * @throws Exception\DomainException
411     * @return string
412     */
413    protected static function getName(ElementInterface $element)
414    {
415        $name = $element->getName();
416        if ($name === null || $name === '') {
417            throw new Exception\DomainException(sprintf(
418                '%s requires that the element has an assigned name; none discovered',
419                __METHOD__
420            ));
421        }
422        return $name . '[]';
423    }
424
425    /**
426     * Retrieve the FormInput helper
427     *
428     * @return FormInput
429     */
430    protected function getInputHelper()
431    {
432        if ($this->inputHelper) {
433            return $this->inputHelper;
434        }
435
436        if (method_exists($this->view, 'plugin')) {
437            $this->inputHelper = $this->view->plugin('form_input');
438        }
439
440        if (!$this->inputHelper instanceof FormInput) {
441            $this->inputHelper = new FormInput();
442        }
443
444        return $this->inputHelper;
445    }
446
447    /**
448     * Retrieve the FormLabel helper
449     *
450     * @return FormLabel
451     */
452    protected function getLabelHelper()
453    {
454        if ($this->labelHelper) {
455            return $this->labelHelper;
456        }
457
458        if (method_exists($this->view, 'plugin')) {
459            $this->labelHelper = $this->view->plugin('form_label');
460        }
461
462        if (!$this->labelHelper instanceof FormLabel) {
463            $this->labelHelper = new FormLabel();
464        }
465
466        return $this->labelHelper;
467    }
468}
469