W widokach i akcjach nie wkładamy niczego co dotyczy zapytań ORM-owych.
Ciąg dalszy artykułów z serii: Czy wykorzystujesz dobre praktyki?
W widokach i akcjach nie powinniśmy dodawać żadnych SQL-owych składni (ani Propelowe Criteria, ani też Doctrine Query), jak również innych odwołań ORM-wych.
Odnośnie widoku, chyba nie trzeba się się rozwodzić – pisaliśmy o tym przy jednej z wcześniejszych dobrych praktyk.
Ale dlaczego nie powinno się wkładać SQL-i w akcjach, skoro nawet w dokumentacji Jobeet-a znajdziemy sporo takich zapisów?
Wyobraźmy sobie, że w trakcie realizacji sporej aplikacji, okazała się konieczna zmiana w modelu danych (nie powinno, ale w praktyce niestety się zdarza).
I teraz czeka nas przejrzenie całej nasze aplikacji, modelu (i jeżeli już całkowicie “popłyneliśmy” – i w widoku) w poszukiwaniu odwołań SQL-owych. A wystarczyło zadbać, aby model pozostawić w modelu i od razu mniej niepotrzebnej pracy
Złe rozwiązanie:
class defaultActions extends sfActions
{
public function executeIndex(sfWebRequest $request)
{
$this->products = Doctrine::getTable(‘Product’)
->createQuery(‘p’)
->where(‘p.is_active = ?’, true)
->limit(10)
->execute(); // <- TAK NIE ROBIMY!
}
}
Poprawne rozwiązanie:
// plik: actions.class.php
class defaultActions extends sfActions
{
public function executeIndex(sfWebRequest $request)
{
$this->products = ProductTable::getRecent(10, true);
}
}
i model:
// plik: ProductTable.class.php
class ProductTable extends Doctrine_Table
{
public static function getRecent($max = 10, $published = true)
{
return Doctrine::getTable(‘Product’)
->createQuery(‘p’)
->where(‘p.is_active = ?’, $published)
->limit($max)
->execute();
}
}
Zapraszam do komentowania i dobrej praktyki #5.
Witam,
Po pierwsze gratuluję reaktywacji strony. Mam nadzieję, że polska społeczność symfony się rozrośnie.
A teraz uwagi: 1) Dlaczego metoda statyczna? Dlaczego nie tworzysz instancji klasy tabeli przez Doctrine::getTable(nazwa komponentu) ?
2) Co w przypadku prostych zapytań, które nie powtarzają się? Czy na pewno warto mnożyć metody w klasie Table? Oczywiście w przypadku, jaki opisałeś (zmiana struktury bazy danych) będzie wtedy więcej roboty (przeglądanie modelu + akcji).
ja osobiście przy prostych zapytaniach robię to w akcji wg mnie nie ma sensu tworzyć kolejnych metod żeby np. wyciągnąć jeden rekord z bazy po PK.
Czy ta metoda getRecent() nie powinna przypadkiem czegoś zwrócić? :> Poza tym, static i $this ? Ja rozumiem, że wszyscy się domyślają, o co chodziło autorowi, ale skoro to mają być dobre praktyki, to piszmy bez błędów
dzięki za zwrócenie uwagi – poprawione
.
Metoda statyczna dalej pozostała? Dlaczego?
Nie ma rozroznienia na “proste” i “nie proste” zapytania do bazy. MVC to MVC i nie wolno mieszac modelu z kontrolerem. Bo w przeciwnym wypadku, po co w ogole stosowac ten wzorzec? Przeciez wszystko mozna zrobic w jednym skrypcie (odpowiedniku widoku).
Puchacz:
[code lang=”PHP”]
public function executeAction() { $wynik1 = Doctrine_Query::create("Produkt")->where("kategoria = ?", "samochody")->execute();
$wynik2 = Doctrine::getTable("Produkt")->dajWgKategorii("samochody"); } [/code]
Wg mnie oba te przypadki nie naruszają zasad MVC, bo w kontrolerze mam zapytanie do modelu o dane. Tylko zapisane w inny sposób. Nie widzę tu mieszania modelu z kontrolerem. Możesz wyjaśnić swój punkt widzenia?
Spróbuję jeszcze raz (nie zadziałało formatowanie i wkradł się błąd):
[code lang=”PHP”]
public function executeAction() { $wynik1 = Doctrine_Query::create()->from("Produkt")->where("kategoria = ?", "samochody")->execute();
$wynik2 = Doctrine::getTable("Produkt")->dajWgKategorii("samochody"); } [/code]
@Michał Górny Oczywiście wszystko jest kwestią umowną, również wzorce projektowe. Jednak częściej spotykaną i ogólnie uznaną za dobrą praktykę jest opakowywanie zapytań, nawet jeśli realizuje się je w sposób obiektowy jak w Doctrine czy Hibernate. Tu chodzi wyłącznie o czytelność kodu i łatwość konserwacji. Jeśli lubisz napisać coś raz, a porządnie to warto się stosować do “best practice”.
Puchacz:
Rozumiem, tylko mam obawy, że potem w klasie *Table znajdzie się mnóstwo prostych metod wykorzystanych tylko w pojedynczych miejscach.
Symfony cierpi na brak 4 warstwy – biznesowej (i nie, to nie tylko transakcje w bazie). Propel i Doctrine ją tylko próbuja symulować, ale tak naprawde nie nadają się do tego celu, zwłaszcza, że ucierpiałaby wymienność ORM. Nie mogę jednak przy każdym projekcie tworzyć takiego modelu, bo raz, że terminy gonią, a dwa – małe projekty i tak na tym nic nie zyskają
Sam czasami wrzucam gołą SQLkę do akcji i do tego generuję np. jsona z wyników – tak – zła praktyka, ale jednocześnie skutaczna, ze względu na to, co mówi Michał Górny w ostatnim komentarzu.
Pozdrawiam
@cysiaczek “Symfony cierpi na brak 4 warstwy – biznesowej (i nie, to nie tylko transakcje w bazie).” Czy mógłbyś to rozwinąć ? W jaki sposób framework MVC mógłby wspomóc programistę w tym zadaniu ?
MVC w żaden sposób. Użyłem skrótu myślowego. Chodzi o to, że zdecydowana większość developerów używa modeli generowanych przez Doctrine lub Propel i pakuje do nich wszystko. Przykłady takie jak newsy, arty, czy jakieś proste komentarze nie wymagają wiele, ale weźmy np. taki sklep – Cart, Product… i co? To wystarczy? IMO nie bardzo, bo koszyk zakupowy posiada ciekawe funkcje jak podanie wartości zakupów z uwzględnieniem rabatów, kosztów wysyłki itp. Nie można tego (biorąc projekt serio) wpakować do obiektu odpowiedzialnego za odwzorowanie wiersza tabeli. Potrzebujemy coś wyżej – ShoppingCart, który operuje na obiekcie koszyka, produktach w nim zawartych, obiektach promocji itd. – musi operować na wyższym poziomie abstrakcji. Owo “symfony cierpi” to raczej wniosek z obserwacji developerów, którzy wiążą się z jednym ORMem (sam tak robię
) i potem przy większych projektach płaczą (ja też
), że “coś jest mało elastyczne”. Problemem środowiska SF jest promowanie takich rozwiązań w ramach RAD, którego filozofia nadaje się do małych, kilkudniowych projektów (maksymalnie średnich). Tworzyłem aplikacje, które zaczynały jako małe projekty, a potem rosły, rosły i rosły
Zaryzykuję więc twierdzenie, że do zaleceń developerów SF i ich wizji tworzenia oprogramowania należy podchodzić krytycznie i jednak czasem zaprojektować sobie 4 warstwę, co jednak wymaga kodowania na piechotę.
Pozdrawiam
cysiaczek:
Czyli jeżeli dobrze rozumiem, klasa Cart nie powinna zawierać żadnej logiki biznesowej? Byłaby odpowiedzialna tylko za ORM?
Natomiast logika biznesowa jest zawarta w klasie ShoppingCart która operuje na klasie Cart? Czy dziedziczy z klasy Cart?
Najprostszym przykładem 4 warstwy byłaby klasa obsługująca rejestrację użytkownika. Klasa jest w tym momencie odpowiedzialna za dodanie użytkownika do bazy danych, wygenerowanie kodu aktywacyjnego i wysłania via email. Teoretycznie można to wykonać w kontrolerze, ale jeżeli chcemy mieć ładne obiektowe API to przenosimy do jakiejś oddzielnej klasy. Obiekt takiej klasy karmimy instancją mailera i jakimś DAO do obsłgi użytkowników. W więkoszości wypadków (mniejsze projekty) taka architektura będzie overkillem, ale w większych pozwala rozbijać problemy na małe kawałki, dzięki czemu łatwiej zarządzać takim kodem.
required:
Ja bym w akcji zrobił coś takiego (zakładając użycie doctrina):
[...] $user = $form->save(); new RegisterMail($user)->send(); [...]
Generowanie kodu aktywacyjnego umieściłbym w klasie User (metoda preInsert).
Rozumiem, że Ty byś umieścił generowanie kodu w innej klasie, dlaczego?
Z tego co rozumiem, uważacie że w klasach doctrina nie powinno być logiki biznesowej? Powinny one tylko być tylko odpowiedzialne za dostęp do danych? Dobrze rozumiem? Jeśli tak, to dlaczego?
@Michał Górny Twoje rozwiązanie jest oczywiście prawidłowe, nawet bardziej pożądane w większości wypadków ze względu na prostotę. Z drugiej strony zakładasz, że kod aktywacyjny zależy wyłącznie od obiektu konta użytkownika – a nie od innych parametrów, np jakiś salt ustawiony na poziomie aplikacji. W tym momencie nie jesteś w stanie wygenerować kodu w samym modelu – potrzebujesz jakiegoś kontekstu.