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...

Pokazywanie postów oznaczonych etykietą dobre praktyki programowania w CakePHP. Pokaż wszystkie posty
Pokazywanie postów oznaczonych etykietą dobre praktyki programowania w CakePHP. Pokaż wszystkie posty

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

środa, 10 grudnia 2008

Dobre praktyki programowania w CakePHP #1

Postanowiłem zbierać gdzieś drobne usprawnienia, przy programowaniu w cake'u, żeby móc później zebrać to w jakąś sensowną listę do zapoznania się "cake'owcom" w naszej firmie.
Padło na blog - zawsze to coś czym można się podzielić z całym światem.

Problem #1:
Mamy taką strukturę (przedstawię za pomocą modeli):
model Photo (id, name) i Tag (id, name),
Photo hasAndBelongsToMany Tag.

W jednym miejscu potrzebne było znalezienie Photos takich, które są powiązane z Tagiem, którego name = 'abc'.

Zostało to wykonane w sposób następujący:
$photos = $this->Photo->Tag->find(" Tag.name ='abc' " );
W widoku z kolei ktoś zrobił
foreach($photos['Photo'] as $photo); gdyż z relacji założył, że wyszukując tag, dostanie powiązane z nim Photos.

Jak widać, dobrał się do tych danych "od tyłu"...
Na pierwszy rzut oka wygląda na to, że jest ok. Ale po jakimś czasie okazało się, że zdjęcia powinny być w kategorii, czyli doszedł nam
Category
i Photo belongsTo Category (photo wzbogacił się o pole category_id).

I poprzednie zadanie musiało zostać zmodyfikowane. Otóż zamiast 'abc' ktoś mógł podać id dla Category w którym jest Photo. Nie można zmienić kodu po prostu dodając warunek, gdyż wyszukujemy tagi, a one nie mają żadnego cateogry_id dla Photos . 

Nie można zrobić drugiego szukania
$photos2 = $this->Photo->findAll("Photo.category_id = 50") i zrobić zwykłego array_merge, gdyż strulktura zwracanych tablic jest inna
1: Tag.Photo.{n}.id
vs
2: {n}.Photo.id

Więc trzeba robić pętle, która drugą tablicę zmieli i przerobi tak, żeby wyglądała jak pierwsza (i z automatu działała w widoku).
Nawet w tym momencie wydaje nam się, że wszystko gra (jeśli przymknęliśmy oko na nieeleganckie rozwiązanie ze zmianą struktury drugiej tablicy) tak naprawdę brniemy coraz głębiej.

Wyobraźmy sobie, że za dwa miesiące klient ma tak dużo Photos pasujących do kryterium, ze potrzebuje pagination + sortowanie (po nazwie zdjęcia). Jesteśmy w kropce, bo nie załatwimy pagination w zapytaniu sql za pomocą limit - gdyż mamy dwa zapytania. Z sortowaniem to samo. Jak dalej się uprzemy, że brniemy coraz dalej w grząskie tereny - skończymy implementując sortowanie na zcalonych dwóch tablicach - koszmar. Nie od teog są kontrolery!

Odpowiedź (nie idealna, ale też całkiem sensowna) :
Długo się głowiłem jak ten problem ugryźć.

Po pierwsze trzeba przyjąć, że nie próbuję exploitować cake'owych funkcjonalności i dobierać się do dany "wstecz" relacji. W naszym przykładzie - potrzebuję Photos, więc wywołuję metodę z modelu Photo. Takim exploitem jest wykorzystanie modelu (Tag), który jest w relacji z Photo (relacja w przód) i założenie, że Tag też będzie w symetrycznej relacji z Photo i wykorzystam ją do moich celów (tu właśnie jest to działanie wstecz).

Po drugie trzeba wbić sobie do głowy zasadę "Skinny Controller, Fat Model". Jeśli z zasady grzebię tylko w kontrolerach, a boję się ruszyć model - polegnę. Skończę tak, że modele potrafią tylko to co zaimplementowane zostało przez team Cake'a. A cała logika będzie się odbywać w kontrolerze, mimo, że nie cała do niego należy (choćby sortowanie, kryteria wyszukiwania).
Dlatego od samego początku, gdy poczujesz leniwca na ramieniu, który sugeruje, żeby dobrać się do danych od tyłu - przypomnij sobie, że to nie ładne rozwiązanie.

Należałoby raczej na samym początku zdefiniować metodę modelu
Photo::customFind($conditions = null)
W niej wysmarzyć prostego sql'a (napiszę go "na sztywno", w w następnych wskazówkach podam jak poprawnie budować query w modelach)
"Select * from photos as Photo
   Inner Join  photos_tags as pt on (pt.photo_id = Photo.id)
   Inner Join tags as Tag on (pt.tag_id = Tag.id)
WHERE $conditions";
Wtedy na samym początku w conditions bez problemu możemy podać Tag.name = 'abc'.

Po przyjściu nowego problemu wystarczy, ze conditions będzie:
Tag.name = 'abc' or Photo.category_id = 'abc'

Jak potrzebuję sortowanie - dodaję sobie parametr $orderBy i doklejam do mojego query.
Tak samo z pagination etc.

Zauważ, że w pierwszym wypadku Controler puchł, a Model był pusty (oprócz relacji). W przypadku drugim Controler powiększa się co najwyżej o dodatkowe parametry w wywołaniu metody $this->Photo->cunstomFind
Struktura zwracanych wyników jest pod kontrolą i nie trzeba zmieniać widoku.

Zamiast siedzieć 5 godzin nad nowymi problemami, wstukujemy kilka sprytnych linijek i idziemy na piwko.

Podsumowując, jedna z dobrych praktych programowaniu w cakePHP brzmi:
Nigdy nie dobieraj się do swoich danych z d*** strony

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.