Pomoc - Szukaj - Użytkownicy - Kalendarz
Pełna wersja: Pierwsze kroki z MVC
Forum PHP.pl > Forum > PHP > Object-oriented programming
Vielta
Hej!
W celu lepszego zrozumienia idei MVC postanowiłem troszkę się pobawić i korzystając z wielu dostępnych mi źródeł napisać coś na wzór pseudo-MVC-frameworka. Mam jednak parę pytań, z czego najważniejsze to czy to się w ogóle trzyma kupy?

Autoloader wygląda następująco:
  1. <?php
  2. function __autoload($className) {
  3. if(file_exists(ROOT.DIRECTORY_SEPARATOR.'library'.DIRECTORY_SEPARATOR.strtolower($className).'.class.php')) {
  4. require_once ROOT.DIRECTORY_SEPARATOR.'library'.DIRECTORY_SEPARATOR.strtolower($className).'.class.php';
  5. } elseif(file_exists(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.strtolower($className).'.php')) {
  6. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR.strtolower($className).'.php';
  7. } elseif(file_exists(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'models'.DIRECTORY_SEPARATOR.strtolower($className).'.php')) {
  8. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'models'.DIRECTORY_SEPARATOR.strtolower($className).'.php';
  9. } elseif(file_exists(ROOT.DIRECTORY_SEPARATOR.'library'.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.strtolower($className).'.class.php')) {
  10. require_once ROOT.DIRECTORY_SEPARATOR.'library'.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR.strtolower($className).'.class.php';
  11. } else {
  12. // nie ma takiego pliku, jakis error handler?
  13. }
  14. }
Mam taki pseudo "front Controller", który ma za zadanie odpalenie odpowiedniego kontrolera na podstawie $_SERVER['request'] i wygląda on tak:
  1. <?php
  2. class FrontController {
  3. private $_queryString;
  4. private $_controller;
  5. private $_action;
  6. private $_dispatch;
  7. public function callHook($url) {
  8. $urlArray = explode('/', $url);
  9.  
  10. $controller = $urlArray[0]?$urlArray[0]:'main';
  11. array_shift($urlArray);
  12. $action = $urlArray[0]?$urlArray[0]:'index';
  13. array_shift($urlArray);
  14. $queryString = $urlArray;
  15.  
  16. $controllerName = $controller;
  17. $controller = ucfirst($controller);
  18. $model = $controller;
  19. $controller .= 'Controller';
  20.  
  21. $this->_queryString = $queryString;
  22. $this->_controller = $controller;
  23. $this->_action = $action;
  24. if(class_exists($controller)) {
  25. $this->_dispatch = new $controller($model, $controllerName, $action);
  26. } else {
  27. $this->_action = 'error404';
  28. $this->_controller = 'error';
  29. $this->_queryString = $_SERVER;
  30. $this->_dispatch = new ErrorController('error', 'error', 'error404');
  31. }
  32. }
  33.  
  34. public function addHelper($class) {
  35. if($class instanceof TemplateHelper) {
  36. $this->_dispatch->addTemplateHelper($class);
  37. } else {
  38. $this->_dispatch->addModule($class);
  39. }
  40. }
  41.  
  42. public function run() {
  43. if((int)method_exists($this->_controller, 'init')) {
  44. $this->_dispatch->init();
  45. }
  46. $this->_dispatch->{$this->_action}($this->_queryString);
  47. $this->_dispatch->render();
  48. }
  49. }
Klasa Controller, po której dziedziczy każdy kontroler wygląda natomiast tak
  1. class Controller {
  2. protected $_model;
  3. protected $_controller;
  4. protected $_action;
  5. protected $_template;
  6. protected $_modules;
  7.  
  8. public function __construct($model, $controller, $action) {
  9. $this->_controller = $controller;
  10. $this->_model = $model;
  11. $this->_action = $action;
  12. $this->_template = new Template($this->_controller, $this->_action);
  13. }
  14.  
  15. public function addModule($module) {
  16. $this->_modules[get_class($module)] = $module;
  17. }
  18.  
  19. public function addTemplateHelper($helper) {
  20. $this->_template->addTemplateHelper($helper);
  21. }
  22.  
  23. public function set($name, $value) {
  24. $this->_template->set($name, $value);
  25. }
  26.  
  27. public function render() {
  28. $this->_template->render();
  29. }
  30. }
A klasa Template, która odpowiada za wczytanie i wyrenderowanie odpowiedniej templatki (jest dość primitywna)
  1. <?php
  2. class Template {
  3. protected $_variables = array();
  4. protected $_controller;
  5. protected $_action;
  6. protected $_helpers;
  7.  
  8. public function __construct($controller, $action) {
  9. $this->_controller = $controller;
  10. $this->_action = $action;
  11. }
  12.  
  13. public function __set($name, $value) {
  14. $this->set($name, $value);
  15. }
  16.  
  17. public function set($name, $value) {
  18. $this->_variables[$name] = $value;
  19. }
  20.  
  21. public function addTemplateHelper($helper) {
  22. $this->_helpers[get_class($helper)] = $helper;
  23. }
  24.  
  25. public function getHelper($helper) {
  26. return $this->_helpers[$helper];
  27. }
  28.  
  29. public function load($helper) {
  30. $template = new Template('helpers', $helper);
  31. $template->render();
  32. }
  33.  
  34. public function render($doNotRenderHeader = false) {
  35. extract($this->_variables);
  36.  
  37. if($doNotRenderHeader == false) {
  38. if(file_exists(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.$this->_controller.DIRECTORY_SEPARATOR.'header.phtml')) {
  39. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.$this->_controller.DIRECTORY_SEPARATOR.'header.phtml';
  40. } elseif(file_exists(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'header.phtml')) {
  41. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'header.phtml';
  42. }
  43. }
  44. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.$this->_controller.DIRECTORY_SEPARATOR.$this->_action.'.phtml';
  45.  
  46. if($doNotRenderHeader) {
  47. if(file_exists(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.$this->_controller.DIRECTORY_SEPARATOR.'footer.phtml')) {
  48. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.$this->_controller.DIRECTORY_SEPARATOR.'footer.phtml';
  49. } elseif(file_exists(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'footer.phtml')) {
  50. require_once ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'views'.DIRECTORY_SEPARATOR.'footer.phtml';
  51. }
  52. }
  53. }
  54. }


Jeśli komukolwiek udało się przebrnąć przez te linijki kodu mam 3 pytania:
a) Czy udało mi się uchwycić ideę OOP
cool.gif Czy udało mi się uchwycić ideę MVC
c) Co mogę poprawić, żeby kod był bardziej OOP/MVC.

Przepraszam za chaotyczność (?) tego postu ale mam grypę i logiczne myślenie jest ciężkawe :<.

Pozdrawiam i z góry dzięki,
Vielta
wookieb
O kur... co to za autoloader.
Poczytaj o include_path

Dlaczego pytacie się "czy robię zgodnie z OOP, co mogę zrobić by było bardziej OOP"? W pani domu wyczytaliście, że jest moda na skrót OOP?
Główną zasadą projektowania klas w OOP jest odpowiedzenie sobie na pytanie
"Co mi da wprowadzenie obiektu? Czy łatwo mogę rozszerzyć możliwości klasy? Jakie ograniczenia sobie stwarzam?" i w myśl tego projektować.
Mniej więcej załapałeś o co chodzi, brakuje mi silnego typu argumentów http://pl2.php.net/manual/pl/language.oop5.typehinting.php
Brakuje również phpdoca o którym mam nadzieję będziesz pamiętać.
Vielta
Nie wiem jak ty ale ja pani domu nie czytam ;P (wolę sekrety serca!)

O Phpdoc z reguły pamiętam, teraz nie używałem bo pisałem to tylko i wyłącznie w celach edukacyjnych. W sumie fakt, powinienem dodać jak wrzucałem na forum, mea culpa.

Cytat
Co mi da wprowadzenie obiektu? Czy łatwo mogę rozszerzyć możliwości klasy? Jakie ograniczenia sobie stwarzam?
Odpowiedziałem sobie na te pytania, co mi to daje? Brak konieczności powtarzania niektórych bloków kodu, oddzielone od siebie wszystko co związane z bazą/widok/logika. W sumie mogę łatwo rozszerzyć ale nie jestem pewien do jednego, [u]nie przemawia do mnie motyw tego, że żeby odwołać się do Template muszę to robić przez Controller.

Średnio widzę miejsce gdzie Type Hinting mógł bym użyć
Crozin
Piszesz o MVC, a tymczasem w kodzie jest jedynie podstawa kontrolera (który nie wiadomo czemu ma mieć jeden model, referencję do innego kontrolera (wtf?) i narzucony jakiś szablon). Gdzie jest reszta (tj. Widok/Model), gdzie jakiś przykład finalnego zastosowania?
Vielta
To z modelem zmieniłem chwilę po wrzuceniu posta na forum, faktycznie było to dość głupie.

To frontcontroller to po prostu taka nazwa, w sumie dispatcher pasowało by lepiej, z założenia była to po prostu klasa, która zajmuje się requestem i uruchamia odpowiedni controller. A jak wg. Ciebie powinno to wyglądać? Szablon powinienem sam ustalać osobno dla każdego kontrolera? Nie lepiej dać tak jak tutaj szablon zależny od kontrolera z nazwą z góry ustaloną?

Klasę widoku podałem przecież w 1. poście a klasa model jest póki co na tyle mało rozwinięta, że pewnie wrócę z nią tutaj za parę dni gdy skończę pisać.


@wookieb
Ten autoloader powinien tak wyglądać mniej więcej?
  1. add_include_path(ROOT.DIRECTORY_SEPARATOR.'library'.DIRECTORY_SEPARATOR);
  2. add_include_path(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR);
  3. add_include_path(ROOT.DIRECTORY_SEPARATOR.'application'.DIRECTORY_SEPARATOR.'models'.DIRECTORY_SEPARATOR);
  4. add_include_path(ROOT.DIRECTORY_SEPARATOR.'library'.DIRECTORY_SEPARATOR.'helpers'.DIRECTORY_SEPARATOR);
  5.  
  6. function __autoload($className) {
  7. require_once $className.'.php';
  8. }
wookieb
Nie wiem po co Ci funkcja add_include_path. Ogólnie dobrze ale wystarczy, że użyjesz http://pl2.php.net/manual/pl/function.set-include-path.php
TYPE HINT zdecydowanie możesz użyć w metodach gdzie wymogiem jest przekazanie obiektu. Np w twoim addTemplateHelper.
Vielta
add_include_path to funkcja z tego linku, który podałeś co bym się nie pogubił dodając te ~5 ścieżek tongue.gif

Co jeszcze powinienem poprawić/nad czym pomyśleć?
Crozin
Cytat
A jak wg. Ciebie powinno to wyglądać?
Wybacz, nie chce mi się n-ty raz pisać o MVC.
Cytat
Klasę widoku podałem przecież w 1. poście
W takim razie wróć do teorii tego wzorca - widok nie ma nic wspólnego z szablonem - może go co najwyżej na samym końcu odpalić, jeżeli zajdzie taka potrzeba.

Cytat
TYPE HINT zdecydowanie możesz użyć w metodach gdzie wymogiem jest przekazanie obiektu. Np w twoim addTemplateHelper.
Przecież w PHP nie da się go nigdzie indziej użyć (no... tam jeszcze tablice, ale to śmiechu warte jest :])
Quantum
Trochę dziwnie na moje oko rozwiązałeś sprawę front controllera, ja u siebie mam zadania routingu/walidacji rozbite na dwie klasy - router oraz dispatcher. Dla tej pierwszej przekazuje w argumencie query string i po wykonaniu dostaje od niej ostateczne dane. Tworzenie obiektu i wywoływanie akcji zrzucam właśnie na klasę front controller. W nim tworzę obiekt dispatcher-a i on sprawdza czy żądany kontroler/akcja istnieje (oraz czy jest poprawny - przydaje się tutaj ReflectionClass) jeżeli tak załącza odpowiedni plik i po utworzeniu obiektu kontrolera odpalamy akcję. Co do autoloader-a to ja to rozwiązałem w ten sposób, że nazwy klas zapisywane są w stylu "underscore", bardziej po zendowsku biggrin.gif Przykładowo z ciągu af_Database_Interface_QueryBuilder usuwam prefix "af" i podkreślniki zamieniam na ukośniki, konwertuje na małe znaki i dostaję ścieżkę "/database/interface/querybuilder". W taki sposób autoloader elegancko odnajduje sobie poprawną ścieżkę. Oczywiście nie musisz robić tego w sposób jaki podałem. Luźne sugestie winksmiley.jpg

pozdrawiam

edit:

Cytat
Cytat
TYPE HINT zdecydowanie możesz użyć w metodach gdzie wymogiem jest przekazanie obiektu. Np w twoim addTemplateHelper.

Przecież w PHP nie da się go nigdzie indziej użyć (no... tam jeszcze tablice, ale to śmiechu warte jest :])

urok tego języka biggrin.gif nie ma to jak dodać obsługę dla obiektów/tablic i zapomnieć o reszcie
Vielta
@Crozin
Nie no spoko, wiem, że ten temat jest poruszany codziennie około 10x na forum tongue.gif W takim razie średnio zrozumiałem co to jest widom, sądziłem, że widok = szablon.
Crozin
Nie sądzisz, że jeżeli by tak było to by się to nazywało... szablonem nie widokiem? Widok to przede wszystkim logika aplikacji - a szablon - to już ostatni etap tej warstwy (nie zawsze konieczny).
Vielta
Zapewne tak by się nazywało. Odpowiedz mi tylko na 1 pytanie jeśli możesz. Do modelu odwołuje się kontroler czy widok?
Crozin
I jedno i drugie - jednak z dużym naciskiem na to ostatnie.
Vielta
A już miałem nadzieję, że zrozumiałem o co chodzi z widokiem.

user wchodzi na stronę -> dispatcher dostaje URL i ładuje odpowiedni controller i jego odpowiednią akcję/metodę -> w metodzie uruchamiamy odpowiedni widok (i/lub model) -> widok wykorzystuje model tylko i wyłącznie w celu pobierania danych a controller dodawania/edytowania.

Tak to wygląda?:<
Quantum
w kontrolerze tworzysz widok i do nich przypisujesz nieograniczoną ilość modeli, dam Ci przykład z mojego frameworka (przykład pisany na szybko):

  1. // tworzymy model
  2. $users = new af_Users_Model;
  3.  
  4. // tworzymy widok
  5. $view = new af_View('users/cp');
  6. // dodajemy utworzony model do widoku
  7. $view->add_model($users);
  8.  
  9. // jeżeli formularz został wysłany
  10. if ( $this->request->post->is_sent() ) {
  11. // tworzymy obiekt walidatora
  12. $validator = new af_Validator;
  13. // dodajemy regułę
  14. $validator->add_rule(
  15. $this->request->post->password,
  16. $this->translate->password->not_filled_properly,
  17. array('require', 'min_length' => 6, 'max_length' => 20)
  18. );
  19.  
  20. // jeżeli formularz jest poprawny
  21. if ( $validator->is_valid() ) {
  22. // uruchamiamy metodę z modelu do zmiany hasła
  23. $users->change_password($this->session->user->id, $this->request->post->password);
  24. } else {
  25. // pobieramy błędy walidatora i przekazujemy do widoku
  26. $view->assign('errors', $validator->errors);
  27. }
  28. }
  29.  
  30. // ta część odpowiada za wyświetlenie żądanego formatu strony (to już nie jest tutaj istotne, to mój taki mały port z RoR:D)
  31. switch ( $this->response->format ) {
  32. case 'html':
  33. $this->response->text = $view;
  34. break;
  35. }


W tym przypadku można było użyć active record, ale tak było prościej wytłumaczyć jak to powinno wyglądać. W widoku możesz teraz sobie pobierać konkretne dane, które chciałbyś wyświetlić, tutaj raczej nie jest to potrzebne, ale ogólnie to tak to działa smile.gif

pozdrawiam.
Crozin
@Quantum: Mógłbyś pokazać jakąś przykładową implementację widoków/modeli? Bo przy takim interfejsie nie potrafię sobie wyobrazić jak chcesz zrealizować założenia tego wzorca.
Quantum
hmm, a mógłbyś sprecyzować ? co mogłoby przeszkodzić w realizacji tegoż wzorca w takim interfejsie (poza logicznymi ograniczeniami protokołu) ? fw jest dość "świeży" więc możliwe, że czegoś nie przemyślałem, ale ogólnie starałem się przenieść fajne rozwiązania z innych i stworzyć wygodny przynajmniej dla mnie w użyciu smile.gif

  1. class af_Users_Model {
  2. public function __construct() {
  3. $this->database = af_Repository::request('database', 'mysql', 'test_db');
  4. }
  5.  
  6. public function get_with_age($age) {
  7. return $this->database
  8. ->from('users')
  9. ->where('age', $age)
  10. ->select('*');
  11. }
  12. }


model jest w tym momencie bardzo prosty, ale w przyszłości myślałem nad stworzeniem klasy af_Model, po której bym dziedziczył, a jej konstruktor przypisywałby żądane w modelu abstrakcje dostępu do danych, moja koncepcja:
  1. class af_Users_Model extends af_Model {
  2. protected $load = array(
  3. 'database' => 'mysql.test_db'
  4. );
  5.  
  6. public function get_sth() {
  7. return $this->database...
  8. }
  9. }


  1. użytkownicy mający 20 lat:
  2. <br/>
  3. <?php foreach ( $models->users->get_with_age(20) as $user ): ?>
  4. nazwa użytkownika: <?php echo $user->username ?>, e-mail: <?php echo $user->email ?>
  5. <br/>
  6. <?php endforeach; ?>
Vielta
Teraz to mi namieszaliście biggrin.gif

Wg. Quantum'a do modelu odwołuję się w plikach template a wg. Crozin'a odwołuję się w osobnych klasach dla każdego widoku.
Crozin
@Vielta: Quantum potraktował wartstwę widoku jako synonim szablonu, przez co utracił cały sens MVC - czyt.: to co podał nie ma zbyt wiele wspólnego w z MVC.
everth
@Crozin jak to się ma do wytycznych tego postu Cysiaczka (patrz - Uwagi na koniec) winksmiley.jpg
Crozin
Pewnie tak, że powinno tam być: nie pytaj czy rozumiesz MVC - jeżeli wydaje Ci się, że tak jest, to pewnie nie masz racji (podobnie jak twórcy frameworków "mvc" dla PHP (notabede mimo iż z MVC mają tyle wspólnego co bla bla bla bla bla... i tak kilka projektów jest naprawdę dobrych).
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-2025 Invision Power Services, Inc.