CodeIgniter3のForm_validationクラスが配列入力値のバリデーションには対応しているが、エラーメッセージは対応していない。

表形式の入力フォームで、1列目がテキスト1、2列がテキスト2と2行以上あるフォームでは、2行目でバリデーションエラーとなろうが1行目でバリデーションエラーとなろうがエラーメッセージは、同一のメッセージが1つ表示されるのみである。何行目というのはエラーメッセージには出力されない。

今回CSVでデータ登録する機能が必要で、エラーメッセージに何行目であるかを表示する必要があったため、Form_validationクラスの_executeメソッドを改良した。

MY_Form_validationクラスを作成

system/libraries/Form_validation.phpから_executeメソッドをコピー

_executeメソッドの第4引数の$cyclesが入力項目が配列時にインデックス番号が入るものなのでこれを利用してエラーメッセージに行番号を付加することにする。

2016/09/15 追記 3.0.6 の「_execute」メソッドを変更しています。3.1.0 でも動作することは確認しました。
3.1.0 の「_execute」メソッドについては下に載せました。

// --------------------------------------------------------------------

/**
 * Executes the Validation routines
 *
 * @param    array
 * @param    array
 * @param    mixed
 * @param    int
 * @return    mixed
 */
protected function _execute($row, $rules, $postdata = NULL, $cycles = 0)
{
    // If the $_POST data is an array we will run a recursive call
    //
    // Note: We MUST check if the array is empty or not!
    //       Otherwise empty arrays will always pass validation.
    if (is_array($postdata) && ! empty($postdata))
    {
        foreach ($postdata as $key => $val)
        {
            $this->_execute($row, $rules, $val, $key);
        }

        return;
    }

    // 入力項目配列時のメッセージ対応 下記の”行目”は言語ファイルから取得するのが望ましい
    $row_msg = '';
    if ($row['is_array'] === TRUE) {
        $row_msg = ($cycles+1).'行目';
    }
    
    // If the field is blank, but NOT required, no further tests are necessary
    $callback = FALSE;
    if ( ! in_array('required', $rules) && ($postdata === NULL OR $postdata === ''))
    {
        // Before we bail out, does the rule contain a callback?
        foreach ($rules as &$rule)
        {
            if (is_string($rule))
            {
                if (strncmp($rule, 'callback_', 9) === 0)
                {
                    $callback = TRUE;
                    $rules = array(1 => $rule);
                    break;
                }
            }
            elseif (is_callable($rule))
            {
                $callback = TRUE;
                $rules = array(1 => $rule);
                break;
            }
            elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
            {
                $callback = TRUE;
                $rules = array(array($rule[0], $rule[1]));
                break;
            }
        }

        if ( ! $callback)
        {
            return;
        }
    }

    // Isset Test. Typically this rule will only apply to checkboxes.
    if (($postdata === NULL OR $postdata === '') && ! $callback)
    {
        if (in_array('isset', $rules, TRUE) OR in_array('required', $rules))
        {
            // Set the message type
            $type = in_array('required', $rules) ? 'required' : 'isset';

            $line = $this->_get_error_message($type, $row['field']);

            // Build the error message
            $message = $this->_build_error_msg($line, $this->_translate_fieldname($row['label']));

            // Save the error message
            $this->_field_data[$row['field']]['error'] = $message.$row_msg;

            if ( ! isset($this->_error_array[$row['field']]))
            {
                $this->_error_array[$row['field']] = $message.$row_msg;
            } else {
                // 入力項目配列時のメッセージ対応
                // 既に同一のエラーがある場合のみ、行番号を付加する
                if (strpos($this->_error_array[$row['field']],$message) !== FALSE) {
                    $this->_error_array[$row['field']] .= ',' .$row_msg;
                }
            }
        }

        return;
    }

    // --------------------------------------------------------------------

    // Cycle through each rule and run it
    foreach ($rules as $rule)
    {
        $_in_array = FALSE;

        // We set the $postdata variable  the current data in our master array so that
        // each cycle of the loop is dealing with the processed data from the last cycle
        if ($row['is_array'] === TRUE && is_array($this->_field_data[$row['field']]['postdata']))
        {
            // We shouldn't need this safety, but just in case there isn't an array index
            // associated with this cycle we'll bail out
            if ( ! isset($this->_field_data[$row['field']]['postdata'][$cycles]))
            {
                continue;
            }

            $postdata = $this->_field_data[$row['field']]['postdata'][$cycles];
            $_in_array = TRUE;
        }
        else
        {
            // If we get an array field, but it's not expected - then it is most likely
            // somebody messing with the form on the client side, so we'll just consider
            // it an empty field
            $postdata = is_array($this->_field_data[$row['field']]['postdata'])
                ? NULL
                : $this->_field_data[$row['field']]['postdata'];
        }

        // Is the rule a callback?
        $callback = $callable = FALSE;
        if (is_string($rule))
        {
            if (strpos($rule, 'callback_') === 0)
            {
                $rule = substr($rule, 9);
                $callback = TRUE;
            }
        }
        elseif (is_callable($rule))
        {
            $callable = TRUE;
        }
        elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
        {
            // We have a "named" callable, so save the name
            $callable = $rule[0];
            $rule = $rule[1];
        }

        // Strip the parameter (if exists) from the rule
        // Rules can contain a parameter: max_length[5]
        $param = FALSE;
        if ( ! $callable && preg_match('/(.*?)\[(.*)\]/', $rule, $match))
        {
            $rule = $match[1];
            $param = $match[2];
        }

        // Call the function that corresponds to the rule
        if ($callback OR $callable !== FALSE)
        {
            if ($callback)
            {
                if ( ! method_exists($this->CI, $rule))
                {
                    log_message('debug', 'Unable to find callback validation rule: '.$rule);
                    $result = FALSE;
                }
                else
                {
                    // 入力項目配列時においてコールバック関数の第3引数をindexとする
                    if ($row['is_array'] === TRUE) {
                        $result = $this->CI->$rule($postdata,$param,$cycles);
                    } else {
                        // Run the function and grab the result
                        $result = $this->CI->$rule($postdata, $param);
                    }
                }
            }
            else
            {
                $result = is_array($rule)
                    ? $rule[0]->{$rule[1]}($postdata)
                    : $rule($postdata);

                // Is $callable set to a rule name?
                if ($callable !== FALSE)
                {
                    $rule = $callable;
                }
            }

            // Re-assign the result to the master data array
            if ($_in_array === TRUE)
            {
                $this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
            }
            else
            {
                $this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
            }

            // If the field isn't required and we just processed a callback we'll move on...
            if ( ! in_array('required', $rules, TRUE) && $result !== FALSE)
            {
                continue;
            }
        }
        elseif ( ! method_exists($this, $rule))
        {
            // If our own wrapper function doesn't exist we see if a native  function does.
            // Users can use any native PHP function call that has one param.
            if (function_exists($rule))
            {
                // Native PHP functions issue warnings if you pass them more parameters than they use
                $result = ($param !== FALSE) ? $rule($postdata, $param) : $rule($postdata);

                if ($_in_array === TRUE)
                {
                    $this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
                }
                else
                {
                    $this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
                }
            }
            else
            {
                log_message('debug', 'Unable to find validation rule: '.$rule);
                $result = FALSE;
            }
        }
        else
        {
            $result = $this->$rule($postdata, $param);

            if ($_in_array === TRUE)
            {
                $this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
            }
            else
            {
                $this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
            }
        }

        // Did the rule test negatively? If so, grab the error.
        if ($result === FALSE)
        {
            // Callable rules might not have named error messages
            if ( ! is_string($rule))
            {
                $line = $this->CI->lang->line('form_validation_error_message_not_set').'(Anonymous function)';
            }
            else
            {
                $line = $this->_get_error_message($rule, $row['field']);
            }

            // Is the parameter we are inserting into the error message the name
            // of another field? If so we need to grab its "field label"
            if (isset($this->_field_data[$param], $this->_field_data[$param]['label']))
            {
                $param = $this->_translate_fieldname($this->_field_data[$param]['label']);
            }

            // Build the error message
            $message = $this->_build_error_msg($line, $this->_translate_fieldname($row['label']), $param);

            // Save the error message
            $this->_field_data[$row['field']]['error'] = $message.$row_msg;

            if ( ! isset($this->_error_array[$row['field']]))
            {
                $this->_error_array[$row['field']] = $message.$row_msg;

            } else {
                // 入力項目配列時のメッセージ対応
                // 既に同一のエラーがある場合のみ、行番号を付加する
                if (strpos($this->_error_array[$row['field']],$message) !== FALSE) {
                    $this->_error_array[$row['field']] .= ',' .$row_msg;
                }
            }

            return;
        }
    }
}

変更点

29~32行
行番号を付加するメッセージ変数を設定するのを追加

88行
エラーメッセージに行番号変数を付加

92~93行
エラーメッセージ配列変数に同一のエラーメッセージ文字列があれば、行番号変数を付加
これは、複数行にわたってエラーがある場合に「1行目,2行目」とエラー箇所を表すためです。

175~180行
独自バリデーションのコールバック関数呼び出し時におけるエラーメッセージ対応
コールバック関数を作成する場合は、第3引数を配列のindex値としている。

278行
エラーメッセージに行番号変数を付加

283~285行
エラーメッセージ配列変数に同一のエラーメッセージ文字列があれば、行番号変数を付加
これは、複数行にわたってエラーがある場合に「1行目,2行目」とエラー箇所を表すためです。

これで、エラーメッセージに行番号が付加されることとなります。
入力要素が配列では無い場合は、通常通りのエラーメッセージが表示されます。

こんな方法で対応はしたけど、他にあるのだろうか。
ただ、入力要素が配列のバリデーションに対応しているんだから、エラーメッセージも標準対応できないものだろうかと思う。

3.1.0の_excuteメソッド

// --------------------------------------------------------------------

/**
 * Executes the Validation routines
 *
 * @param    array
 * @param    array
 * @param    mixed
 * @param    int
 * @return    mixed
 */
protected function _execute($row, $rules, $postdata = NULL, $cycles = 0)
{
    // If the $_POST data is an array we will run a recursive call
    //
    // Note: We MUST check if the array is empty or not!
    //       Otherwise empty arrays will always pass validation.
    if (is_array($postdata) && ! empty($postdata))
    {
        foreach ($postdata as $key => $val)
        {
            $this->_execute($row, $rules, $val, $key);
        }

        return;
    }

    // 入力項目配列時のメッセージ対応
    $row_msg = '';
    if ($row['is_array'] === TRUE) {
        $row_msg = ($cycles+1).'行目';
    }
    
    $rules = $this->_prepare_rules($rules);
    foreach ($rules as $rule)
    {
        $_in_array = FALSE;

        // We set the $postdata variable  the current data in our master array so that
        // each cycle of the loop is dealing with the processed data from the last cycle
        if ($row['is_array'] === TRUE && is_array($this->_field_data[$row['field']]['postdata']))
        {
            // We shouldn't need this safety, but just in case there isn't an array index
            // associated with this cycle we'll bail out
            if ( ! isset($this->_field_data[$row['field']]['postdata'][$cycles]))
            {
                continue;
            }

            $postdata = $this->_field_data[$row['field']]['postdata'][$cycles];
            $_in_array = TRUE;
        }
        else
        {
            // If we get an array field, but it's not expected - then it is most likely
            // somebody messing with the form on the client side, so we'll just consider
            // it an empty field
            $postdata = is_array($this->_field_data[$row['field']]['postdata'])
                ? NULL
                : $this->_field_data[$row['field']]['postdata'];
        }

        // Is the rule a callback?
        $callback = $callable = FALSE;
        if (is_string($rule))
        {
            if (strpos($rule, 'callback_') === 0)
            {
                $rule = substr($rule, 9);
                $callback = TRUE;
            }
        }
        elseif (is_callable($rule))
        {
            $callable = TRUE;
        }
        elseif (is_array($rule) && isset($rule[0], $rule[1]) && is_callable($rule[1]))
        {
            // We have a "named" callable, so save the name
            $callable = $rule[0];
            $rule = $rule[1];
        }

        // Strip the parameter (if exists) from the rule
        // Rules can contain a parameter: max_length[5]
        $param = FALSE;
        if ( ! $callable && preg_match('/(.*?)\[(.*)\]/', $rule, $match))
        {
            $rule = $match[1];
            $param = $match[2];
        }

        // Ignore empty, non-required inputs with a few exceptions ...
        if (
            ($postdata === NULL OR $postdata === '')
            && $callback === FALSE
            && $callable === FALSE
            && ! in_array($rule, array('required', 'isset', 'matches'), TRUE)
        )
        {
            continue;
        }

        // Call the function that corresponds to the rule
        if ($callback OR $callable !== FALSE)
        {
            if ($callback)
            {
                if ( ! method_exists($this->CI, $rule))
                {
                    log_message('debug', 'Unable to find callback validation rule: '.$rule);
                    $result = FALSE;
                }
                else
                {
                    // Run the function and grab the result
//                        $result = $this->CI->$rule($postdata, $param);
                    // 入力項目配列時においてコールバック関数の第3引数をindexとする
                    if ($_in_array === TRUE) {
                        $result = $this->CI->$rule($postdata,$param,$cycles);
                    } else {
                        // Run the function and grab the result
                        $result = $this->CI->$rule($postdata, $param);
                    }
                }
            }
            else
            {
                $result = is_array($rule)
                    ? $rule[0]->{$rule[1]}($postdata)
                    : $rule($postdata);

                // Is $callable set to a rule name?
                if ($callable !== FALSE)
                {
                    $rule = $callable;
                }
            }

            // Re-assign the result to the master data array
            if ($_in_array === TRUE)
            {
                $this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
            }
            else
            {
                $this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
            }
        }
        elseif ( ! method_exists($this, $rule))
        {
            // If our own wrapper function doesn't exist we see if a native  function does.
            // Users can use any native PHP function call that has one param.
            if (function_exists($rule))
            {
                // Native PHP functions issue warnings if you pass them more parameters than they use
                $result = ($param !== FALSE) ? $rule($postdata, $param) : $rule($postdata);

                if ($_in_array === TRUE)
                {
                    $this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
                }
                else
                {
                    $this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
                }
            }
            else
            {
                log_message('debug', 'Unable to find validation rule: '.$rule);
                $result = FALSE;
            }
        }
        else
        {
            $result = $this->$rule($postdata, $param);

            if ($_in_array === TRUE)
            {
                $this->_field_data[$row['field']]['postdata'][$cycles] = is_bool($result) ? $postdata : $result;
            }
            else
            {
                $this->_field_data[$row['field']]['postdata'] = is_bool($result) ? $postdata : $result;
            }
        }

        // Did the rule test negatively? If so, grab the error.
        if ($result === FALSE)
        {
            // Callable rules might not have named error messages
            if ( ! is_string($rule))
            {
                $line = $this->CI->lang->line('form_validation_error_message_not_set').'(Anonymous function)';
            }
            else
            {
                $line = $this->_get_error_message($rule, $row['field']);
            }

            // Is the parameter we are inserting into the error message the name
            // of another field? If so we need to grab its "field label"
            if (isset($this->_field_data[$param], $this->_field_data[$param]['label']))
            {
                $param = $this->_translate_fieldname($this->_field_data[$param]['label']);
            }

            // Build the error message
            $message = $this->_build_error_msg($line, $this->_translate_fieldname($row['label']), $param);

            // Save the error message
            $this->_field_data[$row['field']]['error'] = $message;

            if ( ! isset($this->_error_array[$row['field']]))
            {
                $this->_error_array[$row['field']] = $message.$row_msg;
            } else {
                // 入力項目配列時のメッセージ対応
                // 既に同一のエラーがある場合のみ、行番号を付加する
                if (strpos($this->_error_array[$row['field']],$message) !== FALSE) {
                    $this->_error_array[$row['field']] .= ',' .$row_msg;
                }
            }

            return;
        }
    }
}

29~32行
行番号を付加するメッセージ変数を設定するのを追加

119~124行目
独自バリデーションのコールバック関数呼び出し時におけるエラーメッセージ対応
コールバック関数を作成する場合は、第3引数を配列のindex値としている。

216~223行目
エラーメッセージ配列変数に同一のエラーメッセージ文字列があれば、行番号変数を付加
これは、複数行にわたってエラーがある場合に「1行目,2行目」とエラー箇所を表すためです。

あとForm_validationにほしいのは1つの項目に対する複数のエラーメッセージ表示かぁ
需要ないのかのぉ