[1.3] Multiple select and validation failure (how to properly validate select with multiple options selected)

Hi,

i want to use a "multiple" select (a select box where you can check several options), so i create a Phalcon\Forms\Select element giving it the "multiple" = true option. The render() method generates the HTML without putting "[]" (opening and closing brackets) after the tag name. This causes PHP to keep only one value in the _POST global variable.

The obvious "solution" (to me) would be to name the select with ending brackets. This way PHP populates _POST with all the selected values but under a key without the brackets (as always) and then the Phalcon validator fails because it looks for a value with brackets (which doesn't exists) !

This issue seems so basic that i feel i must be missing something. As i was not able to find an answer in the documentation or forum i ask it here.

Here is a sample code :

<form method="post">

<?php

$di = new \Phalcon\DI\FactoryDefault(); $form = new Phalcon\Forms\Form();

$selection = new Phalcon\Forms\Element\Select('multsel', array('1'=>'Apple','2'=>'Orange','3'=>'Test String'), array('multiple'=>'yes'));

// $selection = new Phalcon\Forms\Element\Select('multsel[]', array('1'=>'Apple','2'=>'Orange','3'=>'Test String'), array('multiple'=>'yes'));

$selection->addValidators(array(new Phalcon\Validation\Validator\PresenceOf()));

$form->add($selection);

if (count($_POST) > 0) { echo '<pre>'; var_dump($form->isValid($_POST)); var_dump($_POST); echo '</pre>'; }

echo $selection->render('testmultisel'); ?> <input type="submit" name="sbm" value="Submit" /> </form> </pre>


Depending on which line ($selection...) i uncomment i have different behaviour but both are incorrect :

  • with the select named "multsel" i only have 1 value in $_POST
  • with the select named "multsel[]", $form->isValid($_POST) returns false

This issue happens also with checkboxes that are part of the same group : there seem to be no easy way to use them in Phalcon.

Tested using Phalcon 1.3.x



20.4k
  1. Try to change "multiple" => "yes" by "multiple" => "true".
  2. Use $this->request->getPost() instead of $_POST.
  3. Maybe you could add the validator like here http://docs.phalconphp.com/es/latest/reference/validation.html#validation (I think the problem is that your validator takes multsel[], but you get multsel from the POST).

Thanks for answering but i already tried your changes but it doesn't solve the problem. To me it looks like something was forgotten in the Phalcon conception : how PHP handles multiple submitted values.

  1. Try to change "multiple" => "yes" by "multiple" => "true".

This option does not seem to change Phalcon's behaviour regarding the rendering of the name of the select field (it only adds a "multiple" HTML attribute). If i name the field "multsel" (no brackets), PHP will only keep one value in POST. If i name the field "multsel[]" (with brackets), PHP will keep all submitted values in POST but then Phalcon does not find the field.

Either way something is not working properly.

  1. Use $this->request->getPost() instead of $_POST.

Does not change anything as getPost() seems to just return $_POST.

  1. Maybe you could add the validator like here http://docs.phalconphp.com/es/latest/reference/validation.html#validation (I think the problem is that your validator takes multsel[], but you get multsel from the POST).

It is not the way a form object is supposed to work !

This case should work out of the box. In my opinion, Phalcon should automatically add the brackets when mutliple values are expected. It is how PHP works with multiple values !



20.4k

This option does not seem to change Phalcon's behaviour regarding the rendering of the name of the select field (it only adds a "multiple" HTML attribute). If i name the field "multsel" (no brackets), PHP will only keep one value in POST. If i name the field "multsel[]" (with brackets), PHP will keep all submitted values in POST but then Phalcon does not find the field.

This was just for typing it more "atractive" (in my opinion).

Does not change anything as getPost() seems to just return $_POST.

Yeah, most things of Phalcon framework are just like that. But when using a framework, you use all it can offer (you never know when they'll change something with POST, for example).

This case should work out of the box. In my opinion, Phalcon should automatically add the brackets when mutliple values are expected. It is how PHP works with multiple values !

I think the cause of this not being implemented is because of multidimensional PHP values. Because you could also set in your form multsel[][][]. Then you would need to do recursive comprobation to add X brackets to get the correct POST field.

You could do a workaround changing manually the POST key name to delete the brackets.

I think the cause of this not being implemented is because of multidimensional PHP values. Because you could also set in your form multsel[][][]. Then you would need to do recursive comprobation to add X brackets to get the correct POST field.

That is true. But then they could add an option which would indicate the expected name in POST. By default it would be the same name but in some cases we could decide to have two differents.

You could do a workaround changing manually the POST key name to delete the brackets.

I was planning a workaround like that, as i had to create a subclass of Phalcon\Form to add some functionalities it would not be too hard. But i was so surprised that it did not work that i thought i might be missing something. Even if looking at the Phalcon 2 source code (in Zephir) didn't give me much hope.

Thanks for your answers anyway.



1.5k
edited Jan '15

the work around

class Select extends Element
{
    public function render()
    {
        if($this->multiple)
        {
            $copy = $params;
            $copy["name"] = $params[0]."[]"; // or $copy["name"] .= "[]"; or some other forms you have to check.
            Phalcon::Tag::inputField("select", $copy, true);
        }
        else
            Phalcon::Tag::inputField("select", $params, true);
    }
}
class myForm extends Form
{
    public function beforeValidation()
    {
        if($this->get("myMultSelect")->multiple)
        {
            // add your multiple select validator or handle it somehow
        }
        else
        {
            // add your single validator
        }
    }
}

of course if you create a totally different class,

class MultipleSelect extends Element
{
    public function render()
    {
            $copy = $params;
            $copy["name"] = $params[0]."[]"; // or $copy["name"] .= "[]"; or some other forms you have to check.
            Phalcon::Tag::inputField("select", $copy, true);
    }
}
class myForm extends Form
{
    public function initialize()
    {
        $this->add(new MultipleSelect("name"));
    }
}

My workaround in the end was to create a new form element "MultiSelect". I added also a Validator to properly enforce all submitted values to be in the list of options (InclusionIn does not work well with an array as submitted values).

I put the classes here if it can help someone. My base namespace is FWP but that's just me !

<?php
namespace FWP\Forms\Element;

/**
 * Represents a select form element which allows multiple choices.
 *
 * Phalcon handles poorly selects with multiple choices :
 *    - PHP requires that the element name have ending brackets [] otherwise only the last submitted value will be kept in $_POST.
 *    - Phalcon does not put such brackets by default.
 *    - If we add the brackets to the element name then Phalcon will try to access the values in $_POST having the full element name as key (including brackets)
 *      but PHP removes the brackets when storing the values in $_POST !
 *
 * This element overrides the minimum method of the Phalcon Select object to handle the different issues :
 *    - the brackets are added to the name automatically during rendering
 *
 * That way PHP will know to store all the values in $_POST, and Phalcon will find them there under the name without brackets.
 *
 * Also a custom validator will be added to fail if the values submitted are not from the option list.
 * To allow the element to be empty (or not be submitted), the options "allowEmpty" => true must be passed to the constructor.
 *
 * @author kevin
 */
class MultiSelect extends \Phalcon\Forms\Element\Select {

  public function __construct ($name, $options = null, $attributes = null) {
    $attributes['multiple'] = true;
    parent::__construct($name, $options, $attributes);

    if ($this->getAttribute('allowEmpty', false)) {
      // it must be added first because it will stop all other validators if needed
      $this->addValidator(new \FWP\Validation\Validator\AllowEmpty());
    }

    $this->addValidator(new \FWP\Validation\Validator\MultiInclusionIn(array('domain' => array_keys($options))));
  }

  public function render ($attributes = null) {
    // we will swap the name just for rendering time
    $name = $this->getName();
    $this->setName($name.'[]');

    // use the real render() method to avoid duplicating code here
    $render = parent::render($attributes);

    // swap back the name
    $this->setName($name);

    return $render;
  }

}
<?php
namespace FWP\Validation\Validator;

/**
 * Allows to validate an array input against an array of possibilities.
 * Like Phalcon's InclusionIn but working properly for a multiple choices form element.
 *
 * It expects all the options of \Phalcon\Validation\Validator\InclusionIn.
 *
 *
 * @author kevin
 */
class MultiInclusionIn extends \Phalcon\Validation\Validator\InclusionIn {

  public function validate ($validator, $attribute) {
    /* @var $validator \Phalcon\Validation */
    $value = $validator->getValue($attribute);

    $domain = $this->getOption('domain');
    if (!is_array($domain)) {
      throw new \Phalcon\Validation\Exception('Option "domain" must be an array');
    }

    $strict = $this->isSetOption('strict') ? ($this->getOption('strict') ? true : false) : false;

    if (is_array($value)) {
      $ok = true;
      foreach ($value as $val) {
        if (!in_array($val,$domain,$strict)) {
          $ok = false;
          break;
        }
      }
    }
    elseif (!is_null($value)) {
      $ok = in_array($value,$domain,$strict);
    }
    else { $ok = $this->isSetOption('allowEmpty'); }

    if ($ok) { return true; }

    $message = $this->getOption('message');
    if (empty($message)) {
      $message = $validator->getDefaultMessage('InclusionIn');
    }

    $label = $this->getOption('label');
    if (empty($label)) {
      $label = $validator->getLabel($attribute);
    }

    $replace = array(':field' => $label, ':domain' => implode(', ',$domain));

    $validator->appendMessage(new \Phalcon\Validation\Message(strtr($message,$replace), $attribute, 'MultiInclusionIn'));

    return false;
  }

}
<?php
namespace FWP\Validation\Validator;

/**
 * Represent a validator for a form element that allows the element to be absent or empty (= having no value).
 *
 * If an element can be empty or absent this validator must be declared FIRST on the element. It will stop all subsequent validators.
 * This is useful when an element can be absent but, when it is present, must match one or more validators.
 *
 * It works by :
 *    - enforcing "cancelOnFail" to prevent the other validators to run on this element (but validators for other elements will still run)
 *    - failing when absent or empty but without adding an error message (the form will think that it is valid)
 *
 * @author kevin
 */
class AllowEmpty extends \Phalcon\Validation\Validator {

  public function __construct ($options = null) {
    $options['cancelOnFail'] = true;

    parent::__construct($options);
  }

  public function validate ($validator, $attribute) {
    /* @var $validator \Phalcon\Validation */
    $value = $validator->getValue($attribute);

    if ($value == null) {
      return false;
    }

    return true;
  }

}


776
edited Oct '15

I encountered the same problem. Having, used @kevinhatry implementation of the MultiSelect Element it didn't work as expected; selected options where not selected when the form returned. So I decided to user Phalcon Select

$this->add((new Select('field', $options, [
                    'name' => 'field[]',
                    'multiple' => true,
                    'useEmpty' => true,
                    'emptyText' => "Select Option",
                            'emptyValue' => '',
                        ]))->setLabel('My Field')
                        ->addValidators([new MultiInclusionIn(['message' => 'invalid input', 'allowEmpty' => true, 'domain' => array_keys($options)])])
        );

I also modified MultiInclusion validation class to allow empty when the option allowEmpty is true. Below is the modified class;

class MultiInclusionIn extends \Phalcon\Validation\Validator\InclusionIn
{

    public function validate(\Phalcon\Validation $validation, $attribute)
    {
        /* @var $validation \Phalcon\Validation */
        $value = $validation->getValue($attribute);

      $domain = $this->getOption('domain');
        if (!is_array($domain)) {
            throw new \Phalcon\Validation\Exception('Option "domain" must be an array');
        }

        $strict = $this->hasOption('strict') ? ($this->getOption('strict') ? true : false) : false;

        if (is_null($value) && $this->getOption('allowEmpty', false)) {
            return true;
        } elseif (is_array($value)) {
            $ok = true;
            foreach ($value as $val) {
                if (!in_array($val, $domain, $strict)) {
                    $ok = false;
                    break;
                }
            }
        } else {
            $ok = in_array($value, $domain, $strict);
        }

        if ($ok) {
            return true;
        }

        $message = $this->getOption('message');
        if (empty($message)) {
            $message = $validation->getDefaultMessage('InclusionIn');
        }

        $label = $this->getOption('label');
        if (empty($label)) {
            $label = $validation->getLabel($attribute);
        }

        $replace = array(':field' => $label, ':domain' => implode(', ', $domain));

        $validation->appendMessage(new \Phalcon\Validation\Message(strtr($message, $replace), $attribute, 'MultiInclusionIn'));

        return false;
    }

}
edited Nov '15

@magosla, that helped, but I still needed a change as $value was always null. I discovered that $attribut was "field[]" and empty while "field" was filled. I know that is is dirty, but was the only workaround that fixed it for me after hours of desperation:

$value = (substr($attribute, -2) == '[]') ? $validation->getValue(substr($attribute, 0, -2)) : $validation->getValue($attribute);



776

@knallcharge I didn't experinece that in mine. $validation->getValue($attribute) retruned the selected options. I'm using phalcon 2.0.8