Niedawno przy projekcie trafiliśmy na ciekawy problem. Chciałbym się podzielić z Wami tym czego się nauczyliśmy.
Problem przedstawię w bardzo uproszczonej formie, bo trudno bez machania rękoma przy zabazgranej tablicy wyjaśnić go w całości.
Problem
Mamy formularz dodawania kosztów, który jest dość specyficzny. Można dodać “łysy koszt” i wtedy pojawia się formularz z wyborem firmy i innymi atrybutami. Można jednak z widoku konkretnej firmy wybrać opcję “dodaj koszt” – w takim wypadku lista wyboru firm się nie pojawia.
Dość naturalne jest załatwić to kontrolerem. Są różne możliwości, jedną z nich jest takie zdefiniowanie akcji:
function add($entryId=null) {
if(!empty($this->data)){
$this->Cost->save($this->data);
// redirect w jakieś przyjemne okolice
}
if(is_null($entryId)){
$this->set("entry", $this->Cost->Entry->read(null, $entryId);
}else{
$this->set("entries", $this->Cost->Entry->find("list");
}
}
W widoku wiadomo – jeśli jest $entry, wyświetlimy $entry["Entry"]["name"] i w jakimś ukrytym inpucie wrzucimy id, jeśli istnieje $entries – wyświetlimy selecta.
Na razie w kontrolerze wygląda to wystarczająco zgrabnie, żeby tego nie ruszać. Ale jeśli znajdziesz się w sytuacji, gdy ilość przypadków jest coraz większa – możesz się zorientować, że dodajesz kolejne argumenty do metody add i rozbudowujesz tą metodę. W takiej sytuacji może Ci pomóc takie podejście.
Propozycja rozwiązania
Po pierwsze olej zwykłe parametry funkcji, a zacznij używać parametrów “named”. Czyli zamiast:
costs/add/1/3/45/23
costs/add/2///22
będziesz miał
costs/add/cost_id:1/entry_id:3/param3:45/param4:23
costs/add/cost_id:2/param4:22
Po drugie – przenieś logikę działania do modelu. np:
//model Cost
function giveMeProperData($params){
if(isset($params["cost_id")){
// do sth
return $some_array;
}
if(isset($params["entry_id")){
// do sth
return $other_array;
}
return array();
}
Kontroler uprość do granic możliwości:
function add($entryId=null) {
if(!empty($this->data)){
$this->Cost->save($this->data);
// redirect w jakieś przyjemne okolice
}
$this->data = $this->Cost->giveMeProperData($this->params["named"];
}
A w widoku w zależności od tego w jaki sposób wygląda $this->data renderuj widok.
Jeśli w tablicy jest “entry_id” to wyświetl zawartość $this->data["Entry"]["name"], a wartość $this->data["Entry"]["id"] przypisz do jakiegoś ukrytego pola. Itd.
Co zyskujesz?
- Kontroler robi to co do niego należy. Przestaje go interesować przypadek właśnie rozpatrywany. Jego interesuje tylko czy został przesłany formularz. Jeśli tak – spróbuj zapisać i przekierować, jeśli nie – pobierz dla niego dane i wyświetl (przekaż do widoku)
- Testowanie modeli, choć nie tak proste, jest o niebo łatwiejsze od testowania kontrolerów i widoków – możesz pokryć ważną część aplikacji testami. Możesz testować, czy metoda giveMeProperData() zwraca to, czego się spodziewasz dla konkretnych argumentów.
- Jeśli pokryjesz tą metodę testami będziesz mógł w komfortowych warunkach refaktoryzować ten kod – jak się pojawią powtórzenia wyodrębnisz wspólne części itd. Gdyby to siedziała w kontrolerze – nikt nie miałby ochoty tam zajrzeć, a testowanie polegało by głównie na odświeżaniu widoków w przeglądarce dla każdego z przypadków.
- Teraz widok jest klasą, która “ma wiedzę” na temat tego jak się zachować w zależności od otrzymanych danych (w prostych przypadkach to już się dzieje – chociażby w akcjach “edit”). Wcześniej część tej wiedzy znajdowała się w kontrolerze.
- Przyjemny side-effect wynikający z używania parametrów “named”: Teraz komponując link w widoku zamiast zastanawiać się czy formularz dodawania kosztów dla danej firmy jest pod /costs/add//[id_firmy]/ czy pod /costs/add///[id_firmy] – wiem, że jest pod /costs/add/entry_id:[id_firmy]
Jest oczywiście brak w tym rozwiązaniu – dość sporo kodu wędruje do widoku (który, jak wspomniałem, trudno testować). Jednak pozostawienie architektury w pierwotnym stanie tej kwestii nie rozwiązuje. Na początku masz problem z
- dużą ilością kodu w kontrolerze
- kodem kontrolera, który nie jest pokryty testami więc nie będzie refaktoryzowany
- kodem w widoku, który nie jest pokryty testami
Po zastosowanej zmianie zostanie Ci tylko ostatni punkt. Samodzielnie możesz zdecydować, czy to się opłaca.
