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):

 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'
);

Czujny Grzegorz Wójcik znalazł błąd w moim przykładzie. Relacja hasOne była błędna. Prawidłowa w omawianym przeze mnie przypadku jest relacja hasMany (więcej szczegółów w komentarzach). Dzięki Kminek!
$hasMany = array( 'RepresentativePhoto' =>
array('className' => 'Photo',
'order' => 'Photo.id ASC'
);

(2)

I wyświetlenie go w widoku:

 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:

$hasMany = 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 jeśli w pięciu różnych widokach musisz wyświetlić tak wybrane zdjęcie?

Prawidłowa odpowiedź:

$hasMany = 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

Share Button

6 thoughts on “Dobre praktyki programowania w CakePHP #2

  1. Hej,

    piszesz fajne posty – jeden z lepszych blogow o Cake po polsku. Tak trzymac!

    Swietna idea, aby dwie tabele polaczyc wiecej niz jedna relacja. Przy czym najnowsza stabilna wersja Cake wywal mi sie na tej linii:

    ‘conditions’ => ‘Photo.representative = 1′

    Jesli pomine nazwe modelu:

    ‘conditions’ => ‘representative = 1′

    jest okej. Nie wiem, pewnie jakis bug.

  2. Masz rację, pominąłem pewną istotną rzecz. Prawdopodobnie powinno być
    $hasOne = array( ‘RepresentativePhoto’ =>
    array(‘className’ => ‘Photo’,
    ‘conditions’ => ‘RepresentativePhoto.representative = 1’,

    ‘order’ => ‘RAND’
    );
    Chodzi o linię ‘conditions’=> …
    zamiast Photo powinno być ReprezentativePhoto, lub inaczej, jeśli inaczej nazwałeś tą relację.

    Dzięki za słowa uznania, na pewno zmotywują mnie do częstszego publikowania :)

  3. hmmm mam maly problem. jesli obok standardowej relacji User hasMany Photo dodam ta dodatkowa relacje tak jak mowisz (czyli User hasOne RepresentativePhoto), to przy listingu userow, jesli dany user ma dwie fotki, jego krotka pojawi sie 2 razy.
    wydaje mi sie, ze w tabeli User powinien byc osobny klucz obcy identyfikujacy ta relacje 1-1? mozesz to potwierdzic?

  4. kminek :
    hmmm mam maly problem. jesli obok standardowej relacji User hasMany Photo dodam ta dodatkowa relacje tak jak mowisz (czyli User hasOne RepresentativePhoto), to przy listingu userow, jesli dany user ma dwie fotki, jego krotka pojawi sie 2 razy.
    wydaje mi sie, ze w tabeli User powinien byc osobny klucz obcy identyfikujacy ta relacje 1-1? mozesz to potwierdzic?

    A próbowałeś? Na której wersji cake? Sprawdziłem na 1.3.2 i mam dwóch userów, pierwszy ma dwie fotki (jedna representative) drugi ma jedną (i jest representatice). Robiąc find(“all”) dostaję taki wynik:

    Array
    (
        [0] => Array
            (
                [User] => Array
                    (
                        [id] => 1
                        [name] => User 1
                    )
    
                [RepresentativePhoto] => Array
                    (
                        [id] => 2
                        [name] => photo 2
                        [user_id] => 1
                        [representative] => 1
                    )
    
                [Photo] => Array
                    (
                        [0] => Array
                            (
                                [id] => 1
                                [name] => photo 1
                                [user_id] => 1
                                [representative] => 0
                            )
    
                        [1] => Array
                            (
                                [id] => 2
                                [name] => photo 2
                                [user_id] => 1
                                [representative] => 1
                            )
    
                    )
    
            )
    
        [1] => Array
            (
                [User] => Array
                    (
                        [id] => 2
                        [name] => User 2
                    )
    
                [RepresentativePhoto] => Array
                    (
                        [id] => 3
                        [name] => photo 3
                        [user_id] => 2
                        [representative] => 1
                    )
    
                [Photo] => Array
                    (
                        [0] => Array
                            (
                                [id] => 3
                                [name] => photo 3
                                [user_id] => 2
                                [representative] => 1
                            )
    
                    )
    
            )
    
    )
    

    Czyli wygląda ok.

    Co do pola w tabeli User – tak, pod warunkiem, że dany user ma dokładnie 1 zdjęcie reprezentacyjne. Jeśli może mieć wiele – zostaje pole binarne representative.

    Ale w tym kontekście to jest kwestia drugorzędna – chciałem pokazać pewien mechanizm.

  5. wlasnie kluczowe w tych rozwazaniach jest pole ‘representative’ :) trzymajac sie przykladu z Twojego komentarza – jesli pierwszy user oznaczy sobie te dwie fotki jako representative (co rozwazasz na koncu artykulu), jego krotka w find(‘all’) pojawi sie dwa razy :)

  6. Faktycznie. Dzięki za wyłuskanie tego. Tak naprawdę jest to relacja (to o czym pisałem na blogu) hasMany a nie hasOne.

    Rozwiązanie tego problemu zależy od tego co tak naprawdę potrzebujemy. Jeśli chcemy, żeby user miał co najwyżej 1 representative photo, to w tabeli users dodajemy representative_photo_id i tworzymy odpowiednią relację hasOne.

    Jeśli user może wybrać wiele takich zdjęć (i na przykład będziemy wyświetlać losowe) to struktura zostaje taka jak podałem, ale definiujemy relację hasMany (nie hasOne).

    Dzięki wielkie za czujność!

Leave a Reply

Your email address will not be published. Required fields are marked *