Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Moduł do tworzenia elementów widoku
Forum PHP.pl > Inne > Oceny
slawooo
Chciałbym zaprezentować i poddać Waszej ocenie moduł ułatwiający oprogramowanie warstwy prezentacji (widoku) w modelu MVC.

W odróżnieniu od tego jak to standardowo bywa w różnych frameworkach PHP, tutaj widok nie jest "gołym" szablonem, nakładki typu smarty czy twig też nie mają z moim pomysłem nic wspólnego. Główna idea opiera się na potraktowaniu warstwy widoku jako kodu w pełni obiektowego, a więc korzystającego ze wszystkich korzyści z niego płynących, takich jak dziedziczenie, enkapsulacja itp.

Cała koncepcja opiera się na obiektach, które nazwałem komponentami. Chodzi tu po prostu o pewne odrębne elementy interfejsu - "komponenty" wizualne, rozumiane zupełnie dowolnie, jak np. menu, okno logowania, komentarze, galeria zdjęć czy cokolwiek innego widocznego na ekranie. Każdy z takich komponentów może składać się z zagnieżdżonych innych komponentów, a na samym "dole" znajdują się gotowe komponenty będące zwykłymi tagami HTML (lub w wyjątkowych przypadkach mogą to być też klasyczne szablony). Dzięki takiemu podejściu, budowanie elementów interfejsu jest prostsze, nie prowadzi do duplikowania kodu, no i kod nie wygląda jak oparty na include z PHP 4.x ;]

Moduł powstał przy tworzeniu aplikacji zbudowanej na frameworku kohana, ale bez większych akrobacji można go dopasować do własnych zastosowań.

Link do źródeł: https://github.com/SlawomirOlchawa/components

Ciekawy jestem Waszych opinii odnośnie takiego rozwiązania, odnośnie samego kodu również.
Przy okazji, jeśli ktoś spotkał się z podobnym podejściem w jakimś z frameworków to chętnie dowiem się więcej na ten temat.

Dla lepszego zrozumienia, przykładowe fragmenty kodu:

Widok:
  1. class Component_Category_Info extends Tag_Block
  2. {
  3. /**
  4.   * @var Model_Category
  5.   */
  6. protected $_category;
  7.  
  8. /**
  9.   * @param Model_Category $category
  10.   */
  11. public function __construct(Model_Category $category)
  12. {
  13. parent::__construct();
  14.  
  15. $this->_category = $category;
  16. Helper_Includer::addCSS('media/app/css/category.css');
  17. }
  18.  
  19. /**
  20.   * @return string
  21.   */
  22. protected function _render()
  23. {
  24. $this->addCSSClass('category_info');
  25.  
  26. $descriptionBlock = new Tag_Block($this->_category->description);
  27. $descriptionBlock->addCSSClass('lightBg');
  28. $descriptionBlock->addCSSClass('description');
  29.  
  30. $this->add($descriptionBlock);
  31.  
  32. return parent::_render();
  33. }
  34. }


Kontroler:
  1. class Controller_Category extends Controller_Entity
  2. {
  3. public function action_index()
  4. {
  5. $info = new Component_Category_Info($this->_entity);
  6. $info->cache($this->_entity->getURL().'/info');
  7. $this->layout->add($info);
  8. }
  9. }
in5ane
Ani Twój przykład z postu, ani nic na githubi'e nie obrazuje mi dokładnie działania tego. Pokaż jakiś trochę większy szablon. Nie widzę potrzeby tworzyć html'a z poziomu PHP i zapamiętywać składnię Twojego modułu.
slawooo
  1. class Component_List extends Tag_Block
  2. {
  3. /**
  4.   * @var Model_Abstract_Entity
  5.   */
  6. protected $_entities;
  7.  
  8. /**
  9.   * @var string
  10.   */
  11. protected $_noResultsInfo = 'Brak wyników do wyświetlenia.';
  12.  
  13. /**
  14.   * @param Model_Abstract_Entity $entities
  15.   */
  16. public function __construct(Model_Abstract_Entity $entities)
  17. {
  18. parent::__construct();
  19.  
  20. $this->_entities = $entities;
  21. }
  22.  
  23. /**
  24.   * @param Model_Abstract_Entity $entity
  25.   * @return Tag_HyperLink
  26.   */
  27. protected function _getListItem(Model_Abstract_Entity $entity)
  28. {
  29. return new Tag_HyperLink($entity->name, $entity->getURL());
  30. }
  31.  
  32. /**
  33.   * @return string
  34.   */
  35. protected function _render()
  36. {
  37. $entityList = $this->_entities->findAll();
  38.  
  39. if ($entityList->count() === 0)
  40. {
  41. $info = new Tag_Paragraph($this->_noResultsInfo);
  42. $info->addCSSClass('light');
  43. $this->add($info);
  44. }
  45. else
  46. {
  47. $list = new Tag_List();
  48. $list->addCSSClass('light');
  49. $this->add($list);
  50.  
  51. foreach ($entityList as $entity)
  52. {
  53. $link = $this->_getListItem($entity);
  54. $list->add($link);
  55. }
  56. }
  57.  
  58. return parent::_render();
  59. }
  60. }


  1. class Component_ListWithPhotosSmall extends Component_List
  2. {
  3. /**
  4.   * @param Model_Abstract_Entity $entities
  5.   */
  6. public function __construct(Model_Abstract_Entity $entities)
  7. {
  8. parent::__construct($entities);
  9.  
  10. $this->addCSSClass('smallphoto_list');
  11.  
  12. Helper_Includer::addCSS('media/mod/photos/css/main.css');
  13. }
  14.  
  15. /**
  16.   * @param Model_Abstract_Entity $entity
  17.   * @return Tag_HyperLink
  18.   */
  19. protected function _getListItem(Model_Abstract_Entity $entity)
  20. {
  21. $tile = new Component_Photo_TileSmall($entity->main_photo);
  22. $name = new Tag_HyperLink(Text::limit_chars($entity->name,45), $entity->getURL());
  23. $name->addCSSClass('name');
  24.  
  25. $result = new Tag_Block();
  26. $result->add($tile);
  27. $result->add($name);
  28.  
  29. return $result;
  30. }
  31. }


Przykład komponentu zawierającego listę elementów. Komponent występuje w dwóch wersjach:
1) lista zawiera odnośniki tekstowe (klasa Component_List)
2) lista zawiera odnośniki graficzne (klasa Component_ListWithPhotosSmall)

Druga klasa dziedziczy po pierwszej całą logikę tworzenia i wyświetlania listy, nadpisując jedynie metodę wyświetlającą pojedynczy element listy.
phpion
Jak dla mnie to przerost formy nad treścią. Jeśli jednak w Twoich projektach się sprawdza to ok. Domyślam się, że idea jest analogiczna do komponentów z Symfony 1 - widoki posiadające własną logikę (np. pobieranie danych z bazy). W tym przypadku jednak sam widok to był zwykły szablon, a nie element ze składanego w klasie HTMLa.
lukaskolista
Pozwolę sobie zwrócić uwagę na kilka rzeczy:
1. Przestrzenie nazw - ich brak w moim odczuciu dyskwalifikuje Twój projekt. Wiem, w kohanie ich nie ma, znam ten framework jak własną kieszeń i też dlatego go porzuciłem (na rzecz symfony)
2. Prywatne właściwości i obiekty zaczynające się od _ - kompletnie nie ogarniam tej praktyki (może ktoś mi wyjaśni skąd się to wzięło), jak będziesz chciał zmienić widoczność metody z np. chronionej na publiczną, to w całym projekcie będziesz zmieniał jej nazwę?
3. Brak interface'ów - czemu render jest protected?
  1. abstract class Component
  2. {
  3. protected $_cacheSettings = null;
  4.  
  5. public function render()
  6. {
  7. if (empty($this->_cacheSettings))
  8. {
  9. $content = $this->_render();
  10. }
  11. ....
  12. }
Publiczna metoda render() wywołuje prywatną, kiepski pomysł, już na pierwszy rzut oka źle to wygląda. Na pewno słyszałeś o dziedziczeniu i konstrukcji
  1. parent::method()
Wspominam o tym dlatego, że w publicznym render() korzystasz z cache'u. Co, jeżeli w jednym elemencie chcę używać cache'u, a w innym nie? Nie bardzo wiem, jak miałbym to osiągnąć. Do takich rzeczy, jak cache polecam aspekty (dosyć skomplikowany temat, bo wbrew pozorom stwarza kolejne problemy) lub dekoratory.

  1. class Template extends Component
  2. {
  3. ...
  4. if (defined('Kohana::DEBUG') AND Kohana::$environment == Kohana::DEBUG) {...}
  5. ....
  6. }
Samo wystąpienie nazwy frameworka świadczy o tym, że Twoja biblioteka nie jest przenośna i w moim vendorze się nie znajdzie, bo nie będę specjalnie kohany dociągał.

Na sam koniec najważniejsze:
  1. class Tag extends TagSingle {
  2. public function add(Component $component = null) {}
  3. }

Powyższe to tylko przykład. Uzależniasz działanie praktycznie wszystkiego od konkretnej implementacji, co z zasady jest błędem. Powinieneś zastosować interface'y (wspomniałem o tym wcześniej, ale tutaj kontekst jest inny), np. ComponentInterface plus zostawić to, co jest jako powiedzmy natywna dla Twojej biblioteki, aczkolwiek niekonieczna warstwa abstrakcji.
slawooo
Dzięki za konkretną odpowiedź.

1. Masz oczywiście rację, choć przestrzenie nazw w PHP są nieco niedopracowane... (https://pornel.net/phpns/pl)
Zresztą jakiś czas temu próbowałem przerobić cały większy projekt (składający się również z modułu z tego wątku) na przestrzenie nazw, ale nie wystarczyło mi cierpliwości smile.gif
Dokładnie nie pamiętam, ale utknąłem gdzieś na styku frameworka właśnie. Poza tym kilkanaście linijek "use" na początku każdego pliku zniechęciło mnie dość skutecznie.

2. To taka zaszłość z C++ (gdzie "this" jest opcjonalne), W PHP faktycznie nie ma uzasadnienia.
Jedyny plus, to wg mnie lepsza czytelność no i jak zauważyłeś wymuszenie przemyślenia widoczności metod na początku smile.gif

3. Co do render, to muszę dokładniej wyjaśnić, wg mnie wszystko jest ok. Metoda publiczna - "render" zawiera ogólny mechanizm renderowania, w tym również obsługę cache. Oczywiście można ją nadpisać/rozszerzyć i dodać jakieś nowe działanie, ale generalnie komponenty dziedziczą ją bez żadnych zmian. Jest wywoływana przy tworzeniu głównego szablonu (layoutu) w kontrolerze i dlatego właśnie jest publiczna.

Natomiast metoda chroniona - "_render" nie jest (i nie powinna być) wywoływana nigdzie na zewnątrz komponentu i dlatego właśnie jest chroniona smile.gif Opisuje tworzenie danego komponentu, bez wnikania w ogólne sprawy takie jak cache (aby nie duplikować kodu cache w każdym komponencie) i bardzo często wykorzystuje dziedziczenie i konstrukcję "parent::_render()".

Dzięki temu rozbiciu właśnie użycie cache jest bardzo proste - aby włączyć cache dla danego elementu wystarczy użyć:
  1. $element->cache($name)
(przykład jest w pierwszym poście). Domyślnie cache jest wyłączone i dla każdego elementu który ma być cacheowany trzeba użyć powyższej linijki.

Tak nawiasem mówiąc to konstrukcja z dwiema metodami (jedna publiczna, druga prywatna/chroniona) jest implementacją wzorca "metoda szablonowa" - https://pl.wikipedia.org/wiki/Metoda_szablo...zec_projektowy).

4. Faktycznie biblioteka nie jest przenośna i bez kohany nie ruszy, ale takich miejsc jest dosłownie kilka (i nie są to kluczowe fragmenty kodu) i łatwo można ją przerobić na potrzeby innego frameworka. Choć przyznam, że już mi się nie chciało za to zabierać smile.gif

5. Możesz rozwinąć co masz tutaj na myśli mówiąc o interfejsach? "Component" jest klasą abstrakcyjną, a więc znacznie bliżej jej do interfejsu niż "konkretnej implementacji".
Teoretycznie mógłbym stworzyć interfejs dla komponentu i zawrzeć w nim metodę "render", a klasa "Component" by go implementowała, tylko czy nie byłaby to sztuka dla sztuki?
lukaskolista
Cytat
1. Masz oczywiście rację, choć przestrzenie nazw w PHP są nieco niedopracowane... (https://pornel.net/phpns/pl)
Jak wszystko smile.gif jednak w PHP 7 ten problem został już rozwiązany, miejmy nadzieję, że termin 25 listopada jako termin wydania RTM będzie ostateczny.

Cytat
2. To taka zaszłość z C++ (gdzie "this" jest opcjonalne), W PHP faktycznie nie ma uzasadnienia.
Jedyny plus, to wg mnie lepsza czytelność no i jak zauważyłeś wymuszenie przemyślenia widoczności metod na początku smile.gif
Wszystkie sensowne IDE mają nawigację po strukturze klas, jak ktoś korzysta z notepad++ jako IDE, to sory, ale nie należy się nim przejmować.

Cytat
3. Co do render, to muszę dokładniej wyjaśnić, wg mnie wszystko jest ok. Metoda publiczna - "render" zawiera ogólny mechanizm renderowania, w tym również obsługę cache. Oczywiście można ją nadpisać/rozszerzyć i dodać jakieś nowe działanie, ale generalnie komponenty dziedziczą ją bez żadnych zmian. Jest wywoływana przy tworzeniu głównego szablonu (layoutu) w kontrolerze i dlatego właśnie jest publiczna.
Tutaj mamy inne podejście, ale uargumentowałeś swoje i przyjmuję te argumenty, chociaż dalej mi się to nie podoba (intuicja).

Cytat
4. Faktycznie biblioteka nie jest przenośna i bez kohany nie ruszy, ale takich miejsc jest dosłownie kilka (i nie są to kluczowe fragmenty kodu) i łatwo można ją przerobić na potrzeby innego frameworka. Choć przyznam, że już mi się nie chciało za to zabierać smile.gif
Innym programistom tym bardziej się nie będzie chciało, większość jak na dzień dobry dostanie exception, to się do niej zrazi.

Cytat
5. Możesz rozwinąć co masz tutaj na myśli mówiąc o interfejsach? "Component" jest klasą abstrakcyjną, a więc znacznie bliżej jej do interfejsu niż "konkretnej implementacji".
Teoretycznie mógłbym stworzyć interfejs dla komponentu i zawrzeć w nim metodę "render", a klasa "Component" by go implementowała, tylko czy nie byłaby to sztuka dla sztuki?
Dobrze mnie zrozumiałeś, właśnie o to mi chodzi. Nie, nie będzie to sztuka dla sztuki. Nawet abstrakcyjna klasa jest już jakąś implementacją. Jedynie interface jest bytem pozbawionym jakiejkolwiek cechy danej implementacji. Co w przypadku, gdy mój komponent ma zupełnie inną logikę, niż klasa abstrakcyjna Component? Ok, mógłbym nadpisać metody, ale były by puste, bo przecież moja logika jest zupełnie inna i to jest bez sensu. Cechą elastycznego kodu jest wykorzystanie interface'ów. Weźmy takiego Doctrine'a (mega kombajn, a działa znakomicie). Jak sobie popatrzysz w jego kod, to tam wszędzie są interface'y. Nawet konkretna implementacja, jaką jest Doctrine ORM (implementacja mapowania na obiekty dla relacyjnych baz dancych) nigdy nie wymaga żadnej abstrakcji, a zawsze interface (np. EntityManagerInterface) dzieki czemu mogę napisać sobie swój manager encji bazujący np. na plikach o ile dorobię do tego logikę zapytań SQL.

Nie umiem tego lepiej wytłumaczyć, to trzeba poczuć.
To jest wersja lo-fi głównej zawartości. Aby zobaczyć pełną wersję z większą zawartością, obrazkami i formatowaniem proszę kliknij tutaj.
Invision Power Board © 2001-2018 Invision Power Services, Inc.