Uwaga, blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com



Subskrybuj ten blog...

wtorek, 17 lutego 2009

Dobre praktyki programowania w CakePHP #2

Przepływ przez architekturę MVC jest i powinien być: Model->Controller->View.

Niestety ze studiów wynieśliśmy to beznadziejne przyzwyczajenie, że funkcja to funkcja i tam się zrobi wszystko co da się zrobić. Dlatego są tacy, którzy nie starają się wpasować w MVC samodzielnie, za to MVC jako tako trzyma ich w koleinach w miarę dobrej architektury. Ci i ich kod jakoś tak naturalnie ciąży w kierunku kontrolerów.
To znaczy: model jest właściwie półprzeźroczystą wartswą dostępu do bazy danych ( findAll() + okazyjnie query() ), w kontrolerze jest wszystko, widoki na razie pominę.

Przykład: Mamy User hasMany Photos. Kluczem obcym jest oczywiście Photo.user_id. Photo.filename to plika w katalogu app/webroot/img. Prosta sprawa, User wrzuca sobie fotki i gdzieś tam one wszystkie są wyświetlane dla danego User(a). Do tej pory załatwił wszystko cake - wygenerował nam definicję tej relacji i nie zaprzątamy sobie tym głowy.
Przychodzi jednak czas, że gdzieś tam na stronie jest lista Userów i przy każdym przydałaby się jedna fotka wśród tych, które dodał. Załóżmy przypadek pierwszy, że zakładamy iż reprezentującą fotką jest pierwsza w kolejności (Photo.id - najmniejsze).

Opisany wyżej programista zrobi coś takiego (w widoku):
<?php image($User["Photo"][0]["filename"]; ?>(1),
jeśli pojawia się sprawdzenia isset($User['Photo'][0] to jest to spory sukces.

Jednak przepływ danych M->C->V sugeruje, że nasze myślenie też powinno płynąć w ten sposób. Czyli powiniśmy stosować zadać sobie trzy pytania:
if(to się nadaje do modelu?) {
wrzućToDoModelu($to);
}elseif(to się nadaje do kontrolera?) {
wrzućtoDoKontrolera($to);
else {
pewnie widok, ale może helper, komponent itd.
}
W (1) poleciało do widoku, a nie wiadomo dlaczego! Przecież to, które ze zdjęć z jakiegoś zestawu jest tym szczególnym zależy od modelu danych. Dlaczego ten wybór dokonywany jest w warstwie reprezentacji?

Bardziej prawidłowym podejściem jest dodanie relacji
$hasOne = array( 'RepresentativePhoto' =>
array('className' => 'Photo',
'order' => 'Photo.id ASC'
);
(2)

I wyświetlenie go w widoku:
<?php echo $html->image($User['RepresentativePhoto']['filename']; ) ?>

Jeśli zadałeś sobie pytanie w stylu: ale po co, przecież efekt jest ten sam? To znaczy, że powoli zaczynasz łapać. Masz rację, że efekt jest teraz taki sam. Jednak, jeśli uświadomisz sobie, że program komputerowy się ciągle zmienia, bo taka jest prawda, to powoli zaczniesz rozumieć po co w ogóle powstało coś co się nazywa MVC.

MVC istnieje dlatego, że każdy użytkownik ma pomysły jak ulepszyć dany program. Na nieszczęście najlepsze pomysły pojawiają się, gdy program już isnieje, a nie wtedy gdy próbujesz wysmarzyć specyfikację. Jednym z tych użytkowników jest Twój klient, a klient to ten który ma kasę, a Ty to ten, który chce ją zdobyć.

Dlatego po pół roku okazuje się, że trzeba by zrobić coś fajniejszego. Załóżmy, że User wśród swoich zdjęć może wybrać to jedno jedyne, które ma być reprezentacyjne. Oczywiście dodajemy pole Photo.representative (bool, 0 - nie, 1 - tak). Co teraz zrobisz?
Jeśli pojawiła się w Tobie ochota na wzięcie (1) i wstawienie tam uroczej pętelki, w środku której będzie słodki if sprawdzający to pole - proszę zacznij czytać od początku.

Cwaniak-leń, który nie jest masochistą i nie lubi się przepracowywać, a jednocześnie lubi zadowolonych klientów - weźmie 2 i ją zmodyfikuje:

$hasOne = array( 'RepresentativePhoto' =>
array('className' => 'Photo',
'conditions' => 'Photo.representative = 1',
'order' => 'Photo.id ASC'
);
Jednak linijka vs. foreach z ifem. Wygrałem. Do tego mam bonus taki, że sprawdzanie warunku odwala za mnie serwer sql (do tego został stworzony) co jest niemal zawsze bardziej wydajne od tych samych operacji w php.

Na zakończenie kolejna zmiana - użytkownik może wybrać dowolną ilość Photo jako reprezentacyjne, a przy jego imieniu ma się wyświetlić losowy. Teraz już naprawdę wyrażnie widać, że gdy zmodyfikujemy widok (1) dodając tam jakiś array_randm czy coś, to nie bardzo będzie to wyglądać na warstwę prezentacji danych (bo daną jest już wylosowane zdjęcie, reszta to zaledwie półprodukty).
Wyobraźmy sobie, że mogłoby to wyglądać jakoś tak:
foreach($user["Photo"] as $key => $val) {
if($val['representative'] != 1) {
unset($user['Photo'][$key];
}
}
$representativePhoto = array_rand($User['Photo'], 1);
echo $html->image($representativePhoto['filename']);
Tragedia! Serio chcesz, żeby Twoje widoki wyglądały w ten sposób? A jak w pięciu różnych widokach musisz wyświetlić tak wybrane zdjęcie?

Prawidłowa odpowiedź:

$hasOne = array( 'RepresentativePhoto' =>
array('className' => 'Photo',
'conditions' => 'Photo.representative = 1',

'order' => 'RAND'
);
Tyle.

Kolejna zasada, którą pozwolę sobie oznajmić światu brzmi:
Poznaj Modele i zacznij ich używać. Po coś w końcu ktoś je wymyślił!
Krócej: skinny controller, fat model

1 komentarz:

Anonimowy pisze...

super pomysl - rzeczywiscie tak to powinno byc poprawnie zrobione, generalnie fajna koncepcja ze z miedzy dwoma tabelami mozna ustawiac wiecej niz jedna relacje (w tym przypadku jeden-do-wielu i jeden-do-jeden).

Uwaga! blog przeniesiony

Posty na tym blogu już nie będą się pojawiać. Zapraszam gorąco pod nowy adres: blog.grzegorzpawlik.com
Komentowanie artykułów możliwe jest pod nowym adresem.