Multi-Row Validation in Laravel 4

Das Problem

Immer wieder mal benötige ich ein Formular welches mehrere Datensätze gleichzeitig validieren soll. Das ist z.B. praktisch, wenn man eine Rechnung mit mehreren Posten hat und man alle Posten gleichzeitig bearbeiten möchte.

Als Beispiel:

{{ Form::open() }}

@foreach($items AS $item)
  {{ Form::text('reference', $item['reference']) }}
  {{ Form::text('amount', $item['amount']) }}
  <br>
@endforeach

{{ Form::submit('Absenden') }}
{{ Form::close() }}

Da wir ja mehrere Datensätze gleichzeitig bearbeiten wollen, können wir die Form nicht mit {{ Form::model() }} öffnen, sondern müssen die Werte für jede Reihe selbst befüllen. Das ganze hat so aber dennoch keinen Sinn, denn wir haben jetzt mehrere reference und amount Felder.


Die Überlegung

Jede Reihe muss also irgendeinen Identifier haben, damit wir die einzelnen Werte bekommen und später auch die Fehlermeldungen den passenden Feldern zuordnen können. Meistens haben wir sowieso schon ein id Feld, daher benutzern wir das.

Das Formular sieht dann so aus:

// edit_multiple_items.blade.php

{{ Form::open() }}

@foreach($items AS $item)
  {{ Form::text('reference-'.$item['id'], $item['reference']) }}
  {{ Form::text('amount-'.$item['id'], $item['amount']) }}<br>
  {{ $errors->first('reference-'.$item['id']) }}
  {{ $errors->first('amount-'.$item['id']) }}
  <hr />
@endforeach

{{ Form::submit('Absenden') }}
{{ Form::close() }}

Von der Optik mal abgesehen (es ist ja nur ein Beispiel), lassen wir die Fehlermeldungen für die einzelnen Felder auch darstellen.

Wenn wir das Formular nun absenden erhalten wir folgendes array:

// Input::all()
array (size=7)
  '_token' => string 'X21ypXxtW7jB8BNEj7lGfJUqLGEmpk3XrqYoCYUw' (length=40)
  'reference-1' => string 'foo' (length=3)
  'amount-1' => string '10' (length=2)
  'reference-2' => string 'bar' (length=3)
  'amount-2' => string '15' (length=2)
  'reference-3' => string 'baz' (length=3)
  'amount-3' => string '20' (length=2)

Alle Felder haben nun eine eindeutige ID.

Doch wie sieht nun unsere Validierungs-Logik aus?

Der Einfachkeit halber stecken wir alles in den Controller, was bei größeren Projekten keine gute Idee ist. Aber hier ist es uns erstmal egal:

private $rulesTemplate = [
    'reference' => 'required',
    'amount' => 'required',
];

 public function updateItems() {
    $input = Input::all();
    $rules = $this->composeRules($input);
    $validator = Validator::make($input, $rules);

    if($validator->fails()) {
        return Redirect::back()->withInput()->withErrors($validator->errors());
    }
}

private function composeRules($input) {
    $rules = [];
    $keys = array_keys($input);

    foreach ($keys AS $key) {
        $parts = explode('-', $key);
        if ($rule = $this->getValidationRuleFromTemplate($parts[0])) {
            $rules[$key] = $rule;
        }
    }

    return $rules;
}

private function getValidationRuleFromTemplate($field) {
   return array_get($this->rulesTemplate, $field);
}

So - aber nun eins nach dem anderen... Das array $rulesTemplate enthält nun unsere Validations-Regeln, die wir für die einzelnen Felder anwenden möchten.

Die Funktion composeRules nimmt die Keys unserer $input Werte und sucht nach dem Trennzeichen -. Alles was vor dem Trenner steht, ist unser Feldname, alles danach gehört zur ID.

Die Funktion getValidationRuleFromTemplate ist nur eine kleine Hilfsfunktion die uns die Validationsregel aus dem $rulesTemplate zurückgibt.

Aus dem Regeltemplate werden unsere Regeln dynamisch zusammengesetzt und es entsteht folgendes Regelset:

// $rules
array (size=6)
  'reference-1' => string 'required' (length=8)
  'amount-1' => string 'requried' (length=8)
  'reference-2' => string 'required' (length=8)
  'amount-2' => string 'requried' (length=8)
  'reference-3' => string 'required' (length=8)
  'amount-3' => string 'requried' (length=8)

Dieses Ruleset kann dem Validator:: übergeben werden der dann den Rest der arbeit erledigt.


Hier nochmal der ganze Controller:

Controller

// ItemsController.php

class ItemsController {

  private $rulesTemplate = [
      'reference' => 'required',
      'amount' => 'required',
  ];

  public function editItems() {
    // Würde man normalerweise aus einer Datenbank auslesen
    $items = [
        ['id'=>1, 'reference'=>'foo', 'amount' => 10],
        ['id'=>2, 'reference'=>'bar', 'amount' => 15],
        ['id'=>3, 'reference'=>'baz', 'amount' => 20],
    ];
    return View::make('edit_items', compact('items'));
  }

  public function updateItems() {

      $input = Input::all();
      $rules = $this->composeRules($input);
      $validator = Validator::make($input, $rules);

      if($validator->fails()) {
          return Redirect::back()->withInput()->withErrors($validator->errors());
      }

      // TODO: Speichern der Datensätze und redirect
  }

  private function composeRules($input) {
      $rules = [];
      $keys = array_keys($input);

      foreach ($keys AS $key) {
          $parts = explode('-', $key);
          if ($rule = $this->getValidationRuleFromTemplate($parts)) {
              $rules[$key] = $rule;
          }
      }

      return $rules;
  }

  private function getValidationRuleFromTemplate($parts) {
     return array_get($this->rulesTemplate, $parts[0]);
  }

}

Fazit

Nicht die eleganteste Lösung - aber sie funktioniert. Wir haben nun die Möglichkeit mehrere Datensätze in einer View zu ändern und zu validieren. Wer eine schönere Lösung hat kann Sie gerne in den Kommentaren posten.

Wie schon erwähnt würde ich in umfangreicheren Projekten davon absehen im Controller zu validieren. Da würde ich die Validierung in eine eigene Service-Klasse verfrachten.