Posts Tagged DRY
Zcalenie edit() i add() w kontrolerze
Gdy przyjrzysz się bliżej kontrolerom stworzonym przy pomocy narzędzia ‘bake’ (lub takim, jakie powstają po wykonaniu tutoriala) możesz stwierdzić, że łamią one koncepcję DRY. Jesteś w stanie powiedzieć, które z dwóch metod są niemal identyczne? Jeśli nie – sprawdź, które dwa widoki są niemal identyczne. Ok, może spaliłem swoją zagadkę, bo w tytule tego postu jest wyraźna sugestia o czym będę mówił. Zgadza się.
Najpierw rzućmy okiem na widoki (tu w przykładzie dla Modelu Thing, który ma jedno pole – name):
// app/views/things/add.ctp
<div class="things form">
<?php echo $this->Form->create('Thing');?>
<fieldset>
<legend>
<?php printf(__('Add %s', true), __('Thing', true)); ?>
</legend>
<?php
echo $this->Form->input('name');
?>
</fieldset>
<?php echo $this->Form->end(__('Submit', true));?>
</div>
<div class="actions">
<h3><?php __('Actions'); ?></h3>
<ul>
<li>
<?php echo $this->Html->link(
sprintf(
__('List %s', true),
__('Things', true)
),
array('action' => 'index')
);?>
</li>
</ul>
</div>
// app/views/things/edit.ctp
<div class="things form">
<?php echo $this->Form->create('Thing');?>
<fieldset>
<legend>
<?php printf(__('Edit %s', true), __('Thing', true)); ?>
</legend>
<?php
echo $this->Form->input('id');
echo $this->Form->input('name');
?>
</fieldset>
<?php echo $this->Form->end(__('Submit', true));?>
</div>
<div class="actions">
<h3><?php __('Actions'); ?></h3>
<ul>
<li>
<?php echo $this->Html->link(
__('Delete', true),
array('action' => 'delete', $this->Form->value('Thing.id')),
null,
sprintf(
__('Are you sure you want to delete # %s?', true),
$this->Form->value('Thing.id')
)
); ?>
</li>
<li>
<?php echo $this->Html->link(
sprintf(__('List %s', true), __('Things', true)),
array('action' => 'index')
);?>
</li>
</ul>
</div>
Różnią się dokładnie w trzech linijkach (na 16 łącznie). Jeśli zmienisz coś w jednym (na przykład dodasz pole) to będziesz musiał poprawić formularz w dwóch plikach. Czysta głupota.
Przyjrzyjmy się jeszcze metodom:
// app/controllers/things_controller.ctp
function add() {
if (!empty($this->data)) {
$this->Thing->create();
if ($this->Thing->save($this->data)) {
$this->Session->setFlash(__('The thing has been saved', true));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(
__('The thing could not be saved. Please, try again.', true)
);
}
}
}
function edit($id = null) {
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid thing', true));
$this->redirect(array('action' => 'index'));
}
if (!empty($this->data)) {
if ($this->Thing->save($this->data)) {
$this->Session->setFlash(__('The thing has been saved', true));
$this->redirect(array('action' => 'index'));
} else {
$this->Session->setFlash(
__('The thing could not be saved. Please, try again.', true)
);
}
}
if (empty($this->data)) {
$this->data = $this->Thing->read(null, $id);
}
}
Praktycznie całe ciało metody add() można by wkleić w miejsce drugiego if’a w metodzie edit() i niewiele by się zmieniło, prawda?
Kilka tricków pozwoli nam naprawić to marnotrawstwo. W zasadzie najważniejsza kwestia jest następująca – jak działa metoda $this->Thing->create()? Wymusi dodanie nowego elementu (dlatego, że metoda save() działa jako create lub update w zależności od stanu modelu). Ale nie przekazując żadnych parametrów do tej metody tak naprawdę sprowadzi się do wyczyszczenia $this->data i $this->id (this oznacza w tym przypadku model). W przypadku metody add() nie ma możliwości, żeby jakieś śmieci były w $this->Thing->data, więc możemy spokojnie usunąć tą linijkę – dalej będzie działać tak samo (czy na pewno? – spoiler).
Teraz całkowicie ciało metody add() mogłoby zawrzeć się w metodzie edit(). Dlatego trzeba ją usunąć (razem z jej widokiem). A wszystkie linki, które prowadziły do /things/add niech teraz prowadzą do /things/edit.
Warto sprawdzić, co się teraz dzieje?
W zależności od wersji – 1.2 się wywali, 1.3 pokaże informacje “Invalid thing”. Teraz skupię się na wersji 1.3 – bo na niej sprawdzam na bieżąco efekty pisząc ten wpis. (dla 1.2 jeśli dobrze pamiętam wystarczy wstępnie zmienić sygnaturę metody edit($id) na edit($id=null), dalsze kroki będą analogiczne, choć kod w szczegółach może się różnić)
Od teraz możemy założyć, że jeśli w url’u nie ma podanego id – to znaczy, że chcemy dodać nowy element, zatem tego if’a możemy usunąć:
// app/controllers/things_controller.php
if (!$id && empty($this->data)) {
$this->Session->setFlash(__('Invalid thing', true));
$this->redirect(array('action' => 'index'));
}
Możemy spokojnie teraz dodawać nowe elementy przez formularz- wszystko działa.
Warto jeszcze dopieścić widok – bez sensu jest nagłówek “edit” przy dodawaniu elementu, oraz przycisk “delete”,
fragment z legend poprawiamy na taki (dodałem wcięcia dla przejrzystości):
// app/views/things/edit.ctp
<legend>
<?php printf(
(!is_null($this->data["Thing"]["id"])?__('Edit %s', true): __('Add %s', true)),
__('Thing', true));
?>
</legend>
a pierwszy element li otaczamy if’ami tak:
// app/views/things/edit.ctp
<?php if(!is_null($this->data["Thing"]["id"])): ?>
<li>
<?php echo $this->Html->link(
__('Delete', true),
array('action' => 'delete', $this->Form->value('Thing.id')),
null,
sprintf(
__('Are you sure you want to delete # %s?', true),
$this->Form->value('Thing.id')
)
); ?>
</li>
<?php endif; ?>
I tyle.
Usunięcie $this->Thing->create() nie takie bezpieczne?
Napisałem, że po usunięciu tego elementu nic się złego nie stanie. Oczywiście jest to prawdą pod warunkiem, że wykonasz wszystkie czynności opisane w tym poscie. Jeśli tylko usuniesz $this->Thing->create() narażasz się na niebezpieczeństwo, że ktoś wyśle spreparowanego post’a pod ten adres. Jeśli poda w nim pole o nazwie data[Thing][id] – będzie mógł zmienić dane dla elementu o danym id. Jeśli masz sytuację taką, że jedna grupa może dodawać elementy, a inna je edytować i polegasz tylko na acl’ach (dana grupa ma dostęp do edit(), a kolejna do add()) – to w ten sposób możesz stworzyć lukę. Jednak ta sytuacja jest dość rzadka i to jest raczej temat na inny wpis, więc jeśli interesuje Cię rowiązanie tego problemu – zachęcam do komentowania.
Skróć swoje listingi
Pewnie w niemal każdym swoim widoku index.ctp masz fragment kodu podobny do tego:
<?php
$i = 0;
foreach ($tracks as $track):
$class = null;
if ($i++ % 2 == 0) {
$class = ' class="altrow"';
}
?>
<tr<?php echo $class;?>>
<td>
<?php echo $track['Track']['id']; ?>
</td>
<td>
<?php echo $track['Track']['name']; ?>
</td>
<td>
<?php echo $track['Car']['name']; ?>
</td>
<td>
<?php echo $track['Track']['begin']; ?>
</td>
<td>
<?php echo $track['Track']['end']; ?>
</td>
<td>
<?php echo $track['Track']['length']; ?>
</td>
<td class="actions">
<?php echo $html->link(
__('Edycja', true),
array(
'action' => 'edit',
$track['Track']['id'] ,
$track['Car']['id']
)
);
?>
</td>
</tr>
<?php endforeach; ?>
</table>
Chce Ci się pisać ten kod kilkadziesiąt razy w jednym projekcie? Mnie też nie. Dlatego napisałem prosty element, który to trochę automatyzuje (nazwałem go “list”):
// app/views/elements/list.ctp
<?php
$i = 0;
foreach($elements as $key => $element):
$class = null;
if ($i++ % 2 == 0) {
$class = ' class="altrow"';
}
?>
<tr <?php echo $class?>>
<?php foreach($fields as $path): ?>
<td>
<?php echo Set::classicExtract($element, $path) ?>
</td>
<?php endforeach; ?>
<?php if(isset($links)): ?>
<td>
<?php foreach($links as $name => $url): ?>
<?php
if(!is_array($url["params"])) {
$url["params"] = array($url["params"]);
}
foreach($url["params"] as $key => $path){
$url[$key] = Set::classicExtract(
$element,
$path
);
}
unset($url["params"]);
?>
<?php echo $html->link($name, $url)?>
<?php endforeach; ?>
</td>
<?php endif; ?>
</tr>
<?php endforeach; ?>
Jego użycie jest dość proste. Dla przykładu wygenerujemy ten sam “output” dla pierwszego listingu z tego postu:
echo $this->renderElement(
"list",
array(
"elements" => $tracks,
"fields" => array(
"Track.name", "Car.name", "Track.begin",
"Track.end", "Track.length"
),
"links"=> array(
"edycja" => array(
"controller"=> "tracks", "action"=> "edit",
"params" => array(
"Track.id", "Car.id"
)
)
)
)
)
Opiszę poszczególne parametry przekazywane do tego elementu:
- “elements” – kolekcja (w tym wypadku tablica) elementów zwrócona na przykład przez $this->Track->find(“all”)
- “fields” – pola, które mają się pojawić w komórkach tabeli
- “links” – tu definiujemy jakie linki mają się pojawić w ostatniej kolumnie (zazwyczaj tam znajdują się “opcje”). Indeksy kolejnych elementów to nazwy linków (tutaj “edycja”).
- “controller” i “action” są identycznie używane jak w metodzie HtmlHelper::link()
- “params” jest dość ciekawy. Jeśli chcemy w wygenerowanym linku mieć jeden parametr (najczęściej id), robimy to tak:
"edycja" => array( "controller"=> "cars", "action"=> "edit", "params"=>"Car.id" ),gdy parametrów ma być więcej – ścieżki do nich przekazujemy w tablicy (tak jak w przykładzie):
"edycja" => array( "controller"=> "tracks", "action"=> "edit", "params" => array( "Track.id", "Car.id" ) )Można także używać parametrów typu “named”, wystarczy podać dodatkowo indeksy:
"edycja" => array( "controller"=> "tracks", "action"=> "edit", "params" => array( "parametr_1"=> "Track.id", "parametr_2"=> "Car.id" ) )
Zachęcam do korzystania z tego elementu i zgłaszania wszelkich niedociągnięć
Dobre praktyki programowania w CakePHP #4
O podobnych kwestiach pisałem już przy okazji wpisu #2 z tej serii. Ale warto jeszcze raz przypomnieć o tym, że tak jak zasady projektowania obiektowego tak należy dokładnie zrozumieć co oznacza podział aplikacji na warstwy MVC.
W tym wpisie skupię się na warstwie modelu.
Jest ona czasem nazywana warstwą biznesową aplikacji- i nie jest to przypadek. Dzieje się tak dlatego, że w niej zawarte są (albo powinny być) reguły biznesowe, które dany system realizuje. Przejdę jak najszybciej do przykładu, który to obrazuje.
Wyobraź sobie system zarządzania firmą wypożyczającą samochody. Cake wypiekł Ci CRUD dla modelu Cars i Costs (koszty generowane przez te samochody). Teraz klient informuje Cię o pewnej zasadzie, która brzmi:
“Każdy koszt można powiązać z dowolną ilością samochodów. Koszt może nie być powiązany z żadnym samochodem (koszt ogólny). Dane powiązanie koszt-samochód jest opatrzone konkretną kwotą (kwota powiązania). Jeśli koszt jest powiązany z jednym lub więcej samochodami, to suma kwoty ich powiązań musi się równać wartości kosztu. Czyli koszt musi być całkowicie rozdzielony na samochody, lub wcale.
Na przykładzie: mogę dodać koszt ‘naprawa po wypadku’, który powiążę z jednym samochodem na pełną kwotę. Mogę dodać koszt ‘ubezpieczenie za rok 2008′, które rozdzielę po równo między wszystkimi samochodami i mogę dodać ‘spinacze do biura’, którego nie powiąże z żadnym samochodem”.
To co właśnie usłyszałeś to reguła biznesowa, którą Twoja aplikacja musi obsłużyć.
Od razu oczywiście zabierasz się ochoczo za zdefiniowanie relacji wiele do wielu (HABTM) w modelach Cost i Car. Jeśli uważasz, że to wszystko i modele już będą jak zwykle służyć do wykonywania selectów na bazie- to sygnał, że możesz jeszcze nie rozumieć idei MVC, więc czytaj dalej.
Zabierasz się za implementowanie funkcjonalności. Bez większego zagłębiania się w szczegóły zbudujesz widok, w którym wybierzesz dla danego kosztu ileś samochodów z listy, wypełnisz ich kwoty i po kliknięciu zapisz poleci wszystko do kontrolera, a w nim będziesz miał 4 (o cztery za dużo) foreachów, którymi będziesz sprawdzał, czy suma się zgadza, a jeśli tak – pozwolisz na zapisz, bla, bla, bla. Jesteś zadowolony.
Jednak za miesiąc przychodzi klient i mówi: “fajne jest to przypisywanie kosztów do samochodów, ale ja chciałbym, żeby móc do kosztu przypisać samochody tylko na część kwoty”.
Jako porządny podwykonawca zgadzasz się, dajesz nura w kontroler i rozdmuchujesz go o pięć nowych ifów i trzy foreache, żeby obsłużyć wszystkie niuanse, które właśnie się pojawiły.
Ale licho… ekhm, klient nie śpi: “Skoro można przypisywać samochody do kosztów nie w całości – to chciałbym móc w widoku samochodu możliwość wybrania kosztów i je do niego przypisać”.
I co teraz robisz?
Jeśli zabierasz się za przeniesienie mechanizmów z kontrolera Costs do modelu Cost – może nie pójdziesz jeszcze do piekła. Jeśli zapaliła Ci się czerwona lampka, ale nie wiesz o co chodzi – czytaj dalej. Jeśli zaczynasz kopiować wspomniane mechanizmy do kontrolera Cars – jesteś w ciemnej du… hmm piwnicy. Dlaczego?
To, że łamiesz zasadę MVC to już powinieneś czuć przez skórę. Możesz jeszcze nie wiedzieć czym to grozi. To, że łamiesz zasady projektowania obiektowego – mogłeś nie zauważyć. Ale, że masz w dwóch różnych miejscach (CostsController i CarsController) mechanizm robiący to samo i Ci to nie przeszkadza – za to właśnie trafisz do piekła.
Powtórzenie, które przed chwilą opisałem ma dwie poważne konsekwencje:
1. Mechanizmy dbające o spójność bazy danych masz rozproszone w wielu klasach. Dlatego zmiana reguły biznesowej pociąga za sobą konieczność zmiany wielu dublujących się wierszy kodu. Łatwo wtedy o pomyłkę.
2. Model nie dba samodzielnie o spójność danych. Pisząc w ten sposób aplikację zakładasz, że każdy programista w każdym momencie rozwoju aplikacji (czyli nawet za dwa lata) będzie dokładnie pamiętał tą (i wszystkie inne) reguły biznesowe. Dodając kolejny element, znów będzie musiał samodzielnie zadbać o każdy aspekt spójności danych. Łatwo wtedy o pomyłkę.
Dlatego o spójność danych musi dbać warstwa modelu. Bo ona dba o sama siebie. Jeśli odpowiednio zaprojektujesz model Cost, przy pomocy callback’ów beforeValidate, before i afterSave zawrzesz w modelu zapewnienie spójności, wszystko co pozostanie w przyszłości do zrobienia to przygotowanie odpowiedniej tablicy w kontrolerze i wywołanie $this->Cost->save(…).
Dlatego czwartą zasadę formułuję następująco:
mechanizmy dbające o spójność bazy danych umieszczaj w warstwie modelu
ps. Witam wszystkich po przydługiej przerwie wakacyjno-inietylko