Re: Poeksperymentujmy z MVC

Do napisania tego wpisu zainspirował mnie post u Zyx-a pod tym samym tytułem. Zyx przedstawił w nim świeżą koncepcję php-owego frameworka opartego o paradygmat MVC wraz z przykładami kodu. Uzasadnieniem dlaczego “wynajdywał koło na nowo” było to, że uważał, że istniejące frameworki tak na prawdę nie realizują paradygmatu MVC a tylko jego mutację zwaną też MVP.

Ten artykuł nie jest bezpośrednio polemiką z Zyx-em a raczej bardziej próbą zademonstrowania podejścia alternatywnego zarówno do tego jakie prezentuj on jak i popularne frameworki.

Chciałbym więc przedstawić pewien zarys frameworka opartego o wzorzec MVP (a raczej MTV ;>) i zasady programowania funk(cyjnego/funkcjonalnego). Napisałem już jeden podobny framework do opisywanego na którym postawiona jest jedna z stron w moim portfolio i dobrze się sprawuje. Natomiast to co opisuje nie posiada jeszcze implementacji i jest w zasadzie kolejną iteracją mojej poprzedniej próby.

Dla jasności przypomnę jeszcze jakie są różnice między MVC i MVP. Oba wzorce służą do separacji warstwy prezentacji aplikacji (View) od warstwy logiki biznesowej (Model).

W MVC kontroler na podstawie requestu łączy odpowiednie modele z odpowiednimi widokami. Widok pobiera odpowiednie dane z modelu wg swojego uznania i tworzy odpowiedź.

W MVP kontroler wyciąga dane potrzebne do renderowania strony z modeli i przekazuje je do widoku. Jak widać w MVP widok nie jest zależny od modelu, po prostu renderuje dane, natomiast w MVC ma świadomość czym jest model (jaką klasą) i potrzebuje konkretnego interfejsu aby móc z nim się porozumiewać.

Wzorzec MVP powstał w celu zapewnienia lepszej testowalności i większej niezależności widoku od modelu.

Sposób działania mojego frameworka.Plik index:

$oContainerConfig = new CContainerConfigXml('config/object-config.xml');
$oFactory = new CContainerManager($oContainerConfig);

$oFlowConfig = new CFlowConfigXml('config/flow-config.xml');
$oExecutor = new CExecutor();
$oPageResolver = new CPageResolver($oFlowConfig);

$oFrontController = new CFrontController($oPageResolver, $oExecutor, $oFactory, $oFlowConfig);

echo $oFrontController->execute($_REQUEST, $_ENV, $_SERVER);

W pliku index widzimy kilka obiektów:

  • $oFactory jest kontenerem IoC, który konfiguruje wszystkie obiekty jakie istnieją w aplikacji.
  • $oFlowConfig jest obiektem który zwraca nam flow całej aplikacji.
  • $oExecutor jest obiektem wykonującym flow
  • $oPageResolver jest obiektem który na podstawie adresu zwraca informację jaka strona jest żądana oraz parsuje parametry

Przepływ sterowania wygląda tak:

  • PageResolver na podstawie $_SERVER i danych z FlowConfig wybiera stronę do pokazania
  • FrontController wyciąga z $oFlowConfig informacje o czynnościach, które należy wykonać dla danej strony oraz przekazuje je do $oExecutor aby je zrealizował
  • $oExecutor wykonuję odpowiednie Command-y i zbiera dane do widoku oraz zwraca je do FrontControllera
  • FrontController wyciąga z $oFlowConfig informacje o rodzaju widoku który wyrenderuje odpowiedź i przekazuje mu zmienne, zajmuje się również wszelkimi redirectami, które miałby wcześniej miejsce.
  • FrontController dostaje od widoku wyrenderowaną stronę i zwraca do index.php gdzie jest echowana
  • FrontController łapie również wszystkie wyjątki które mogą wystąpić i przeprowadza cały proces renderowania strony z błędem zgodnie z ww. flow

Teraz pewnie zastanawiacie się czym są wymienione między wierszami Command-y? Są to jakby pojedyncze akcje kontrolera, ale silnie wyabstrachowane. Zwracają array() z zmiennymi do widoku albo informacji o redyrekcji na inny adres. Reagują na konkretne eventy takie jak pojawienie się jakiejś zmiennej z get-a lub posta.

Przykład:


class CommandCrud {
    protected $_factory;
    public function __construct($oFactory){
        $this->_factory = $oFactory;
    }
    public function execute($params, $post){
        $sModelName = $params[0];
        $iId = $params[1];
        $oRepository = $this->_factory->factory($ModelName.'Repository');
        $oEntity = $oRepository->find($iId);
        $aVars = array('metadata' => $oEntity->getMetadata());

        if($params['page'] == 'edit'){
              $oEntity->fromArray($post);
              if($oEntity->save()){
                  return array('redirect' => '@referer');
              }
        }
        if($params['page'] =='delete'){
             if($oEntity->delete()){
                  return array('redirect' => 'http://www.example.com');
             }
        }
        return $aVars;
    }
}

Powyższy Command znajduje model po parametrach requestu i wykonuje w zależności od nich odpowiednie operacje Create Read Update Delete pobiera również metadane modelu potrzebne do wyrenderowania formularza.

Przykładowy flow-config w formacie xml:

<flow-config>
    <flow name="frontend" template="front_default">
        <view class="CViewHtml"/>
        <data from="AuthService" method="authenticate" on="post[auth]" bind-to="auth_result">
            <param name="post[auth][username]"/>
            <param name="post[auth][password]" />
        </data>
        <data from="LinksService" method="getLinks" bind-to="links" />
        <page match="index">
            <data from="NewsService" method="getPosts" bind-to="content" />
        </page>
        <page match="article" template="article">
            <data from="NewsService" method="getPost" bind-to="content">
                <param name="@1" />
            </data>
            <data from="NewsService" method="getComments" bind-to="comment_list">
                <param name="@1" />
            </data>
            <command name="addComment" on="post[comment]" bind-to="comment_result" />
        </page>
        <page match="gallery" template="gallery">
            <data from="GalleryService" method="getList" bind-to="content" />
            <data from="GalleryService" method="getLatestPictures" bind-to="latest" />
            <page match="gallery/show">
                <data from="GalleryService" method="getPicture" bind-to="content">
                    <param name="@1" />
                </data>
            </page>
        </page>
        <page match="gallery/download">
            <view class="CViewFile" />
            <data from="MediaService" method="getFile" bind-to="file">
                <param name="@1" />
            </data>
        </page>
    </flow>
    <flow name="backend">
        <page match="/edit/[a-z]+/">
            <command name="CommandCrud" redirect="@result"/>
        </page>
    </flow>
    <error-flow>
       <page match="404">
        <!-- -->
       </page>
       <page match="500">
        <!-- -->
       </page>
    </error-flow>
</flow-config>

Są w nim zdefiniowane dwa flow-y frontend i backend – w zasadzie są to odrębne aplikacje oraz error-flow zawierający flow dla stron błędów.

Tag datawykorzystywany jest do wykonania jakiejś metody na modelu gdy nie jest potrzebna jakaś głębsza logika do której używane są commandy.

Tag data można ustawić tak by reagował na jakiś event np. pierwszy tag data który wykonuje metodę AuthService::authenticate która służy do logowania reaguje na pojawienie się w tablicy $_POST nie pustej zmiennej pod kluczem auth (atrybuton). Tagi param służą do przekazania do ww. metody parametrów. Wynik z wywołania będzie przypisany do widoku w zmiennej auth_result(atrybutbind-to). Można też tworzyć automatyczną redyrekcję atrybutami on-success i on-failure reagują one na zmienne true i false zwracane z metod modelu.

Tag page służy do grupowania wykonania tylko dla jakiejś strony, posiada atrybut match dzięki któremu $oPageResolver może działać.

Tag command deklaruje nam wywołanie Command-a, atrybuty podobe jak w tagu data

Tag view deklaruje nam jaki typ widoku będzie użyty do renderowania.

Najfajniejszą rzeczą w tej xml-owej konfiguracji jest dziedziczenie. Co to oznacza, tzn. że strona zadeklarowana tagiem page zagnieżdżona gdzieś na dole hierarchii dziedziczy wszystko co zostało wykonane wyżej. Umożliwia nam to np. wyświetlanie wspólnych layoutów dla kilku stron w których są tylko inne dane. W przykładzie tego xml-a dzięki takiej jego konstrukcji możliwe jest wykonanie logowania na każdej stronie.

Z innych ciekawych rzeczy, popatrzmy na strone /gallery/download ma ona w sobie zadeklarowany widok CViewFile, widok ten każdą zmienną przekazaną do siebie interpretuje jako ścieżkę do pliku. Jeżeli plik jest jeden rozpoczyna procedurę wysyłania (ustawia nagłówki i robi readfile), natomiast jeżeli jest ich więcej, pakuje je w zip-a i również wysyła razem.

Można pomyśleć o innych automatycznych typach widoków, które np renderują wyjście np. w postaci xml-a lub jsona.

Pewnie wiele osób kręci nosem na xml-a, oczywiście nie jest to sztywna konstrukcja i można napisać własny adapter który będzie korzystał z innego formatu konfiguracji (np. z bazy danych), jest to kwestia implementacji odpowiedniego interfejsu.

Jakie są zalety tej koncepcji frameworka (wg.mnie) ?

  • jasny przepływ sterowania
  • wysoka elastyczność
  • modułowa budowa

Ok już się tutaj naprodukowałem, jeżeli dobrnęliście do tego miejsca to znaczy, że zainteresowała Was ta koncepcja. Jestem otwarty na wszelkie konstruktywne uwagi i krytykę oraz pytania w razie niejasności. Jeżeli znajdzie się odpowiednia ilość przychylnych głosów stworzę prototyp i udostępnię go wszem i wobec.

  1. Zyx says:

    De facto przy minimalnym wysiłku dałoby się to zrealizować na każdym normalnym frameworku, jaki istnieje. Dokładasz czytnik XML, fabrykę modeli i lekko przeprogramowujesz Action Controller, by wczytywał nazwę szablonu z XML-a, zamiast z akcji i gotowe. Ostatecznie wychodzi na to, że frameworki nawet MVP nie do końca poprawnie implementują, gdyż – jak słusznie zauważyłeś, i trochę wbrew temu co ja pisałem o MVP – prezenter przekazuje dane do widoku, ale go nie wybiera. Jeśli chodzi o MVC, widok nie musi znać konkretnej klasy – wystarczy mu znajomość interfejsu niezbędna do dokończenia swojego zadania, zaś sam model może interfejsów implementować kilka.

    PS. Mam też pewną uwagę stricte redakcyjno-techniczną. Piszesz dużo, zatem staraj się przykładać większą wagę do interpunkcji oraz języka, jakim się posługujesz. Lektura wpisów byłaby wtedy dużo przyjemniejsza. U Ciebie nie dość, że dość swobodnie traktujesz pojęcie “akapitu”, to jeszcze robisz okropny miks polsko-angielski (“redyrekcja” – rany, cóż to takiego?!). To taka uwaga na marginesie, możesz ją skasować po przeczytaniu.

    PS2. Pauzy stosuje się przy odmienianiu skrótów. Apostrofy – przy odmienianiu przez przypadki wyrazów z literą niemą. Pozostałe wyrazy odmieniamy normalnie: Zyx, Zyxa, Zyxowi itd.

  2. Wojciech Soczyński says:

    ad ps 1 – Co do uwag na temat języka stosowanego na blogu, wiem, że w warstwie językowej nie jest doskonały i między innymi dlatego dużo piszę aby poprawiać styl. Angielskie naleciałości to niestety efekt częstego czytania różnych publikacji w tym języku – czasami chcąc coś napisać kojarzę termin angielski i go spolszczam. W każdym razie staram się poprawiać.

    ad ps 2 – Nawet nie zdawałem sobie sprawy że takie reguły istnieją, dzięki za sugestie.

    Co do meritum, to jeżeli dało by się to zastosować w każdym normalnym frameworku to czemu się nie stosuje (pytanie to samo do twojego kratownicowego podejścia) ?

    Poza tym to co przedstawiłem to nie jest tylko proste wybieranie template’u na podstawie xml-a. Cała rzecz w tym, że ten sposób konfiguracji uwalnia nas od wszelkich dziwnych “hooków”, post i pre dispatch’y – to raz, dwa umożliwia składanie zawartości stron z różnych akcji kontrolera, które są dzięki temu mocno wyizolowane i nie mapują się 1:1 na stronę – to co w Zendzie np. wymaga stosowania jakiegoś dziwnego helpera ActionStack tutaj jest naturalne, trzy ten sposób budowania aplikacji umożliwia składanie jej z klocków – można sobie wyobrazić, że mamy jakiś zbiór akcji kontrolera/commandów, typów widoków oraz modeli i ustawiamy je tak jak nam odpowiada, żeby uzyskać oczekiwany efekt.

  3. Zyx says:

    Nie stosuje się z tego samego powodu, co w przypadku MVC, czyli ktoś raz tak zrobił, zdobyło to popularność, a cała reszta od niego zerżnęła bez większego zastanowienia. Ewentualnie ludzie mają jakieś inne, tajemne techniki do rozwiązywania tego typu problemów :).

    Fajnie byłoby, gdybyś zaprezentował jeszcze kod widoku, ponieważ ta warstwa w MVP jest dość istotna, a tutaj zbywasz ją milczeniem. Ponadto jest też pytanie czy to składanie to uniwersalna właściwość MVP, czy “tylko” efekt Twojej konkretnej implementacji warstwy widokowo/prezenterowej? Oczywiście nie umniejszam tutaj idei jako takiej, ponieważ zgadzam się, że wynalazki w stylu ActionStack to lekkie nieporozumienie oraz że klocki są lepsze :). Tę samą rzecz można zrealizować na 100 różnych sposobów przy wykorzystaniu 100 różnych wzorców projektowych, lepiej lub gorzej. Wszystko zależy od tego, jakie właściwości chcemy osiągnąć.

  4. Wojciech Soczyński says:

    Czy składanie z klocków to uniwersalna właściwość MVP to dobre pytanie. Patrząc na samą ideę MVP można by powiedzieć że tak, natomiast nie widziałem jeszcze żadnej implementacji która by to realizowała, dlatego też zacząłem myśleć nad swoją.


    Jeżeli chodzi o widok to u mnie widoki dzielą się ze względu na typ wyjścia jaki generują, Widok html-owy dostaje zestaw zmiennych od warstwy kontrolera, ustawia nagłówki, wypełnia odpowiedni template zmiennymi i wyrenderowaną stronę zwraca do kontrolera (kontroler może ją w tym momencie np. wrzucić do cache’a), który ją zwraca na wyjście. Inne typy widoków np widok CViewFileczy CViewXml natomiast nie potrzebują szablonów ponieważ działają automatycznie tj. zamiast nich mają w sobie logikę serializującą przypisane do nich zmienne do odpowiedniego formatu.


    Myślę, że to podejście z widokami wspiera ich separacje od reszty warstw, co więcej umożliwia ich testowanie – można sprawdzić co dany widok zwraca dla jakiegoś zestawu zmiennych.

    Ps. jeżeli chodzi o jakiś konkretny kod, mogę spróbować skrobnąć jakiś szybki prototyp i wrzucić go w okolicach wieczora :)

  5. Zyx says:

    Wiesz, ja bym to jednak oddzielił od problematyki MVP, bo taką samą rzecz da się zrealizować na 100 innych sposobów i nie tylko na tym jednym wzorcu. Tym bardziej gdy sam piszesz, że wiele implementacji z tego nie korzysta.

    Analogiczna uwaga dotyczy widoków -> chodziło mi bardziej o pokazanie, jak wygląda spojenie widoku z prezenterem, ale w gołej postaci, bo zbyt dużo szczegółów w kodzie zaciemnia obraz. Z samymi widokami zrobiłeś w sumie identycznie, jak mam w Trinity i myślę, że jest to najoptymalniejsze rozwiązanie kwestii tego, co właściwie wyświetlamy.

  6. Wojciech Soczyński says:

    No nie jest identycznie, podstawowa różnica jest taka, że u mnie widok nic nie wie o modelu. Dostaje tylko zmienne do wyrenderowania i robi z tego jakieś coś. Natomiast u Ciebie widok jest świadomy modelu i wyciąga z niego rzeczy wg uznania (z tego co zrozumiałem).


    Spojenie widoku z prezenterem wygląda u mnie tak, leci pętla po wszystkich commandach które są wykonywalne przy danym żądaniu i stronie i odbiera z nich zmienne do widoku, agreguje je w jednej dużej tablicy i następnie przekazuje do widoku, który robi już swoje. Zacząłem pisać dzisiaj wstępną implementacje, niedługo coś pokażę więc bedziesz mógł zobaczyć jak to wygląda na żywo.

  1. There are no trackbacks for this post yet.

Leave a Reply

Notify me of followup comments via e-mail. You can also subscribe without commenting.