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\Element\Hidden;
13use Zend\Form\ElementInterface;
14use Zend\Form\Element\Select as SelectElement;
15use Zend\Form\Exception;
16use Zend\Stdlib\ArrayUtils;
17
18class FormSelect extends AbstractHelper
19{
20    /**
21     * Attributes valid for the current tag
22     *
23     * Will vary based on whether a select, option, or optgroup is being rendered
24     *
25     * @var array
26     */
27    protected $validTagAttributes;
28
29    /**
30     * Attributes valid for select
31     *
32     * @var array
33     */
34    protected $validSelectAttributes = array(
35        'name'         => true,
36        'autocomplete' => true,
37        'autofocus'    => true,
38        'disabled'     => true,
39        'form'         => true,
40        'multiple'     => true,
41        'required'     => true,
42        'size'         => true
43    );
44
45    /**
46     * Attributes valid for options
47     *
48     * @var array
49     */
50    protected $validOptionAttributes = array(
51        'disabled' => true,
52        'selected' => true,
53        'label'    => true,
54        'value'    => true,
55    );
56
57    /**
58     * Attributes valid for option groups
59     *
60     * @var array
61     */
62    protected $validOptgroupAttributes = array(
63        'disabled' => true,
64        'label'    => true,
65    );
66
67    protected $translatableAttributes = array(
68        'label' => true,
69    );
70
71    /**
72     * @var FormHidden|null
73     */
74    protected $formHiddenHelper;
75
76    /**
77     * Invoke helper as functor
78     *
79     * Proxies to {@link render()}.
80     *
81     * @param  ElementInterface|null $element
82     * @return string|FormSelect
83     */
84    public function __invoke(ElementInterface $element = null)
85    {
86        if (!$element) {
87            return $this;
88        }
89
90        return $this->render($element);
91    }
92
93    /**
94     * Render a form <select> element from the provided $element
95     *
96     * @param  ElementInterface $element
97     * @throws Exception\InvalidArgumentException
98     * @throws Exception\DomainException
99     * @return string
100     */
101    public function render(ElementInterface $element)
102    {
103        if (!$element instanceof SelectElement) {
104            throw new Exception\InvalidArgumentException(sprintf(
105                '%s requires that the element is of type Zend\Form\Element\Select',
106                __METHOD__
107            ));
108        }
109
110        $name   = $element->getName();
111        if (empty($name) && $name !== 0) {
112            throw new Exception\DomainException(sprintf(
113                '%s requires that the element has an assigned name; none discovered',
114                __METHOD__
115            ));
116        }
117
118        $options = $element->getValueOptions();
119
120        if (($emptyOption = $element->getEmptyOption()) !== null) {
121            $options = array('' => $emptyOption) + $options;
122        }
123
124        $attributes = $element->getAttributes();
125        $value      = $this->validateMultiValue($element->getValue(), $attributes);
126
127        $attributes['name'] = $name;
128        if (array_key_exists('multiple', $attributes) && $attributes['multiple']) {
129            $attributes['name'] .= '[]';
130        }
131        $this->validTagAttributes = $this->validSelectAttributes;
132
133        $rendered = sprintf(
134            '<select %s>%s</select>',
135            $this->createAttributesString($attributes),
136            $this->renderOptions($options, $value)
137        );
138
139        // Render hidden element
140        $useHiddenElement = method_exists($element, 'useHiddenElement')
141            && method_exists($element, 'getUnselectedValue')
142            && $element->useHiddenElement();
143
144        if ($useHiddenElement) {
145            $rendered = $this->renderHiddenElement($element) . $rendered;
146        }
147
148        return $rendered;
149    }
150
151    /**
152     * Render an array of options
153     *
154     * Individual options should be of the form:
155     *
156     * <code>
157     * array(
158     *     'value'    => 'value',
159     *     'label'    => 'label',
160     *     'disabled' => $booleanFlag,
161     *     'selected' => $booleanFlag,
162     * )
163     * </code>
164     *
165     * @param  array $options
166     * @param  array $selectedOptions Option values that should be marked as selected
167     * @return string
168     */
169    public function renderOptions(array $options, array $selectedOptions = array())
170    {
171        $template      = '<option %s>%s</option>';
172        $optionStrings = array();
173        $escapeHtml    = $this->getEscapeHtmlHelper();
174
175        foreach ($options as $key => $optionSpec) {
176            $value    = '';
177            $label    = '';
178            $selected = false;
179            $disabled = false;
180
181            if (is_scalar($optionSpec)) {
182                $optionSpec = array(
183                    'label' => $optionSpec,
184                    'value' => $key
185                );
186            }
187
188            if (isset($optionSpec['options']) && is_array($optionSpec['options'])) {
189                $optionStrings[] = $this->renderOptgroup($optionSpec, $selectedOptions);
190                continue;
191            }
192
193            if (isset($optionSpec['value'])) {
194                $value = $optionSpec['value'];
195            }
196            if (isset($optionSpec['label'])) {
197                $label = $optionSpec['label'];
198            }
199            if (isset($optionSpec['selected'])) {
200                $selected = $optionSpec['selected'];
201            }
202            if (isset($optionSpec['disabled'])) {
203                $disabled = $optionSpec['disabled'];
204            }
205
206            if (ArrayUtils::inArray($value, $selectedOptions)) {
207                $selected = true;
208            }
209
210            if (null !== ($translator = $this->getTranslator())) {
211                $label = $translator->translate(
212                    $label,
213                    $this->getTranslatorTextDomain()
214                );
215            }
216
217            $attributes = compact('value', 'selected', 'disabled');
218
219            if (isset($optionSpec['attributes']) && is_array($optionSpec['attributes'])) {
220                $attributes = array_merge($attributes, $optionSpec['attributes']);
221            }
222
223            $this->validTagAttributes = $this->validOptionAttributes;
224            $optionStrings[] = sprintf(
225                $template,
226                $this->createAttributesString($attributes),
227                $escapeHtml($label)
228            );
229        }
230
231        return implode("\n", $optionStrings);
232    }
233
234    /**
235     * Render an optgroup
236     *
237     * See {@link renderOptions()} for the options specification. Basically,
238     * an optgroup is simply an option that has an additional "options" key
239     * with an array following the specification for renderOptions().
240     *
241     * @param  array $optgroup
242     * @param  array $selectedOptions
243     * @return string
244     */
245    public function renderOptgroup(array $optgroup, array $selectedOptions = array())
246    {
247        $template = '<optgroup%s>%s</optgroup>';
248
249        $options = array();
250        if (isset($optgroup['options']) && is_array($optgroup['options'])) {
251            $options = $optgroup['options'];
252            unset($optgroup['options']);
253        }
254
255        $this->validTagAttributes = $this->validOptgroupAttributes;
256        $attributes = $this->createAttributesString($optgroup);
257        if (!empty($attributes)) {
258            $attributes = ' ' . $attributes;
259        }
260
261        return sprintf(
262            $template,
263            $attributes,
264            $this->renderOptions($options, $selectedOptions)
265        );
266    }
267
268    /**
269     * Ensure that the value is set appropriately
270     *
271     * If the element's value attribute is an array, but there is no multiple
272     * attribute, or that attribute does not evaluate to true, then we have
273     * a domain issue -- you cannot have multiple options selected unless the
274     * multiple attribute is present and enabled.
275     *
276     * @param  mixed $value
277     * @param  array $attributes
278     * @return array
279     * @throws Exception\DomainException
280     */
281    protected function validateMultiValue($value, array $attributes)
282    {
283        if (null === $value) {
284            return array();
285        }
286
287        if (!is_array($value)) {
288            return (array) $value;
289        }
290
291        if (!isset($attributes['multiple']) || !$attributes['multiple']) {
292            throw new Exception\DomainException(sprintf(
293                '%s does not allow specifying multiple selected values when the element does not have a multiple attribute set to a boolean true',
294                __CLASS__
295            ));
296        }
297
298        return $value;
299    }
300
301    protected function renderHiddenElement(ElementInterface $element)
302    {
303        $hiddenElement = new Hidden($element->getName());
304        $hiddenElement->setValue($element->getUnselectedValue());
305
306        return $this->getFormHiddenHelper()->__invoke($hiddenElement);
307    }
308
309    /**
310     * @return FormHidden
311     */
312    protected function getFormHiddenHelper()
313    {
314        if (!$this->formHiddenHelper) {
315            if (method_exists($this->view, 'plugin')) {
316                $this->formHiddenHelper = $this->view->plugin('formhidden');
317            }
318
319            if (!$this->formHiddenHelper instanceof FormHidden) {
320                $this->formHiddenHelper = new FormHidden();
321            }
322        }
323
324        return $this->formHiddenHelper;
325    }
326}
327