Czasem zdarza się, że musimy wykonać jakiś mało skomplikowany projekt. Zastanawiamy się wtedy, czy nie wystarczyłoby napisać prostego szkieletu strony przy użyciu instrukcji include. Bo czy warto wytaczać na muchy armatę w postaci jakiegoś zaawansowanego frameworka, jak np. Zend Framework?
Problem w tym, że proste projekty w przyszłości lubią się rozrastać. Początkowo ciągniemy wcześniejszy pomysł z użyciem include, ale w końcu dochodzimy do wniosku, że najlepiej to byłoby teraz to wszystko przepisać od nowa. Rozwiązanie, które tutaj zaproponuję, jest niemal tak samo niewielkie, jak proste dołączanie plików za pomocą include. Pozawala jednak stworzyć szkielet aplikacji według najlepszych praktyk programowania zorientowanego obiektowo opartego o wzorzec projektowy MVC (ang. Model-View-Controller). Co więcej, mimo niewielkich rozmiarów, w oparciu o ten framework można stworzyć również naprawdę sporą i złożoną aplikację, co zapewni możliwość rozwoju Twojego projektu w przyszłości.
<?php /** * PHP Light MVC. * Extremely small but powerful MVC framework for PHP. * @package PHP Light MVC * @version 1.4.1 * @author Sławomir Kokłowski {@link http://www.kurshtml.boo.pl} * @copyright Do NOT remove this comment! * @license LGPL */ /** * Auto load function. * To autoload class dir_subdir_ClassName, put it in a file "./dir/subdir/ClassName.class.php" * Base directory is current directory with this __autoload function. * If class was not found "404 Not Found" HTTP header would be sent and script would exit. * @param string $className Name of class to autoload */ function __autoload($className) { if (preg_match('/^[a-z][0-9a-z]*(_[0-9a-z]+)*$/i', $className)) { $path = dirname(__FILE__) . '/' . str_replace('_' , '/', $className) . '.class.php'; if (file_exists($path)) { require_once $path; return; } } _Controller::http404(); } /** * Model. * Requirements: * - PDO library. */ abstract class _Model { /** * Data Source Name. * @static string */ static $dsn; /** * Database username. * @static string */ static $user; /** * Database password. * @static string */ static $password; /** * Database connection. * Singleton. * @static PDO */ protected static $db; /** * DVO class name. * @var string */ protected $className; /** * SQL array. * To implement method i.e. $this->getItems() set "getItems" key and write prepared statement SQL as value. * @var array */ protected $sql = array(); private $sth = array(); /** * Constructor. */ function __construct() { if (empty(self::$db) && !empty(self::$dsn)) { self::$db = new PDO(self::$dsn, self::$user, self::$password); self::$db->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION); } } /** * Executes database prepared statement. * Normally it is not required to call this method, because it is caled internally. * @param string $name Key name from $this->sql array * @param array $arguments Parameters to fill in prepered statement * @return array Result */ protected function execute($name, $arguments=array()) { if (!array_key_exists($name, $this->sql)) throw new Exception('Execute of undefined sql ' . $name); if (!array_key_exists($name, $this->sth)) $this->sth[$name] = self::$db->prepare($this->sql[$name]); foreach ($arguments as $key => $value) { switch (gettype($value)) { case 'boolean': $type = PDO::PARAM_BOOL; break; case 'integer': $type = PDO::PARAM_INT; break; case 'NULL': $type = PDO::PARAM_NULL; break; default: $type = PDO::PARAM_STR; } $this->sth[$name]->bindValue($key, $value, $type); } $this->sth[$name]->execute(); $result = array(); if (preg_match('/^[^A-Z_]*SELECT[^A-Z_]/i', $this->sql[$name])) { while (($object = $this->className ? $this->sth[$name]->fetchObject($this->className) : $this->sth[$name]->fetchObject())) $result[] = $object; } else { $object = (object)array('count' => $this->sth[$name]->rowCount()); if (preg_match('/^[^A-Z_]*(INSERT|REPLACE)[^A-Z_]/i', $this->sql[$name])) $object->id = self::$db->lastInsertId(); $result[] = $object; } return $result; } function __call($name, $arguments) { if (!array_key_exists($name, $this->sql)) throw new Exception('Call to undefined method ' . get_class($this) . '::' . $name . '()'); return $this->execute($name, array_key_exists(0, $arguments) ? $arguments[0] : array()); } } /** * View. * Usually it isn't necessary to extend this class. * To visualize template print object itself. * In template file you have access to special variables: * - array $_config: Configuration reference, * - string $_dir: Base directory with template files. */ class _View { /** * Base directory with template files. * @static string */ static $dir = ''; /** * Global variables passed to every template file. * @static array */ static $var = array(); /** * Template file name. * @var string */ protected $file; /** * Template assigned variables. * @var array */ protected $data = array(); /** * Constructor. * @param string $file Template file name. */ function __construct($file) { $this->file = $file; } function __get($name) { return array_key_exists($name, $this->data) ? $this->data[$name] : null; } function __set($name, $value) { $this->data[$name] = $value; } function __toString() { if (!file_exists(self::$dir . $this->file)) return ''; foreach (array_merge(self::$var, $this->data) as $name => $value) { if ($name != 'this') $$name = $value; } unset($name, $value); $_config = _Controller::$config; $_dir = self::$dir; ob_start(); require $_dir . $this->file; _Controller::$config = $_config; return ob_get_clean(); } } /** * Controller. */ abstract class _Controller { /** * Confiruration data. * @var static */ static $config; /** * 404 Not Found action. * Sends HTTP header and exits. */ static function http404() { header('HTTP/1.1 404 Not Found'); exit; } /** * HTTP redirect action. * Sends HTTP header and exits. * @param string $location Absolute or relative URL * @param int $status HTTP response status code */ static function httpRedirect($location, $status=302) { $location = preg_replace(array('/^([^\r\n]+)/', '/(^|\/)\.(\/|$)/', '/[^\/]*\/\.\.(\/|$)/'), array('$1', '$1', ''), $location); header('Location: ' . (preg_match('/^[0-9a-z.+-]+:/i', $location) ? '' : 'http://' . $_SERVER['SERVER_NAME'] . (preg_match('/^\//', $location) ? '' : rTrim(dirName($_SERVER['SCRIPT_NAME']), '/') . '/')) . $location, true, $status); exit; } } ?>
...i to wszystko! Po usunięciu komentarzy, kod będzie miał ok. 100 linii, a oferuje całkiem sporo:
Automatyczne ładowanie kodu klas - już nie musisz pamiętać o wstawianiu kilkudziesięciu instrukcji require_once, żeby aplikacja w ogóle mogła wystartować; zostanie załadowany tylko ten kod, który rzeczywiście jest potrzebny i dokładnie wtedy, kiedy jest potrzebny
Wsparcie dowolnej bazy danych, do której został zaimplementowany sterownik PDO
Wbudowana najskuteczniejsza ochrona przed atakami SQL injection
Obsługa PHP View - systemu szablonów tak naprawdę dającego największe możliwości (nie musisz się uczyć żadnego kolejnego metajęzyka - jeśli znasz PHP, znasz PHP View)
Wsparcie dla kontrolerów MVC oraz konfiguracji aplikacji
Przykład
Najlepszym dowodem na prostotę, łatwość i szybkość tworzenia aplikacji przy użyciu tego frameworka, będzie chyba przykład. Wspólnie zbudujemy stronę, która będzie wyświetlać listę artykułów oraz pozwoli użytkownikowi przeczytać dowolny z nich.
Baza danych
Przykładowa aplikacja opiera się na bazie danych MySQL.
CREATE TABLE `articles` ( `id` int(10) unsigned NOT NULL AUTO_INCREMENT, `title` varchar(255) NOT NULL, `content` text NOT NULL, PRIMARY KEY (`id`) ); INSERT INTO `articles` (`id`, `title`, `content`) VALUES(1, 'Artykuł 1', 'Lorem ipsum dolor sit amet, consectetur adipiscing elit.'); INSERT INTO `articles` (`id`, `title`, `content`) VALUES(2, 'Artykuł 2', 'Integer vel nisi quis risus imperdiet bibendum.'); INSERT INTO `articles` (`id`, `title`, `content`) VALUES(3, 'Artykuł 3', 'Morbi sem leo, porta vel tristique vitae, bibendum vel felis.');
Pliki
Teraz utworzymy strukturę katalogów. Poniższy opis to tylko przykład. W swojej aplikacji możesz wykorzystać zupełnie inny układ plików - taki, jak będzie Ci najbardziej odpowiadał. Framework nie wymusza tutaj jakiejś specjalnej struktury.
lib
controller
Index.class.php
model
Article.class.php
view
layout
index.phtml
article.phtml
index.phtml
config.php
index.php
index.php
lib/index.php
W tym pliku umieszczamy kod frameworka, przestawiony wcześniej.
lib/config.php
Konfiguracja naszej aplikacji - w tym dane dostępowe do bazy danych.
Warto zauważyć, że choć konfiguracja domyślnie jest zwykłą tablicą asocjacyjną, to poprzez implementację interfejsu ArrayAccess może stać się obiektem. Dzięki temu można zaimplementować dodatkowe metody oraz przeprowadzić walidację wprowadzanych wartości zmiennych konfiguracyjnych.
lib/model/Article.class.php
W tym pliku umieścimy nasz model, oparty o wzorzec projektowy DAO.
<?php class model_Article extends _Model { protected $sql = array( 'getItems' => 'SELECT id, title FROM articles ORDER by title', 'getItem' => 'SELECT * FROM articles WHERE id = :id' ); } ?>
...i to wszystko! Od razu mamy zaimplementowane dwie metody pobierające dane z bazy: getItems i getItem.
Standardowo każda metoda DAO zwraca tablicę obiektów klasy stdClass. Można również zaimplementować własną klasę DVO - tzn. pojedynczy element "tablicy" danych - wystarczy podać jej nazwę w chronionej własności $className - np.:
PHP jako język szablonów? Czemu nie! Nie zapominajmy, że PHP pierwotnie powstał właśnie jako zaawansowany język szablonów. Dzisiaj powoli wraca się do tego. Systemy takie jak np. Smarty mają sporo przeciwników. Po co tworzyć nowy język, mieć problemy z podświetlaniem i podpowiadaniem składni w edytorze, skoro znacznie większe możliwości daje sam PHP? To prawda, że Smarty można skonfigurować w taki sposób, aby niedouczony programista nie wywołał w pliku szablonu np. zapytania SQL. W przypadku PHP View to sami musimy się troszczyć o zachowanie wytycznych jakie powinien spełniać poprawnie zaimplementowany widok MVC, ale czy tylko z tego powodu warto przekreślać możliwości, jakie daje ten rodzaj widoku MVC?
A jeżeli nie podoba nam się rozwlekła składnia <?php echo ...; ?>, to jeśli tylko dyrektywa short_open_tag jest włączona dla naszej instalacji PHP, można skrócić zapis:
W szablonach PHP View korzystniej jest używać alternatywnej składni struktur kontrolnych. Pamiętajmy, że ten plik powinien wyglądać jak szablon wizualizujący HTML, a nie plik PHP z jednym dużym blokiem kodu i wieloma porozrzucanymi w tym kodzie instrukcjami echo lub print.
<?php require_once './lib/index.php'; // biblioteka frameworka _Controller::$config = include './lib/config.php'; // wczytanie konfiguracji aplikacji _View::$dir = './lib/view/'; // ustawienie katalogu bazowego dla plikow z szablonami widoku _Model::$dsn = _Controller::$config['_db']['dsn']; // konfiguracja bazy danych dla modelu _Model::$user = _Controller::$config['_db']['user']; _Model::$password = _Controller::$config['_db']['password']; if (empty($_GET['controller'])) $_GET['controller'] = 'Index'; // przypisanie domyslnego kontrolera if (empty($_GET['action'])) $_GET['action'] = 'index'; // przypisanie domyslnej akcji $controller = 'controller_' . $_GET['controller']; $controller = new $controller; if (!method_exists($controller, $_GET['action'])) _Controller::http404(); // kontroler nie ma takiej akcji echo $controller->$_GET['action'](); // wyswietlenie kodu zrodlowego HTML ?>
Historia zmian
1.0.0 - pierwsza publiczna wersja
1.0.1 - ujednolicone wyrażenie regularne autolodera oraz kod HTTP 404
1.1.1 - klasa rezultatu dziedziczy po modelu, aby nie było konieczność udostępniania publicznie metody execute
1.1.2 - poprawka w implementacji iteratora przez klasę rezultatu w celu wyeliminowania nieprawidłowego dodatkowego pustego, zwracanego elementu na końcu danych
1.2.0 - dane dostępowe bazy danych nie muszą być przechowywane w konfiguracji
1.2.1 - zapytania DML nie są ładowane "leniwie"; prawidłowa obsługa danych zwracanych przez zapytania DML
1.3.0 - zapytania INSERT/REPLACE zwracają identyfikator wstawionego rekordu; globalne zmienne widoku _View::$var, które raz ustawione przekazywane są później do każdego pliku szablonu; akcja przekierowania HTTP _Controller::httpRedirect
1.3.1 - możliwość podania względnego adresu URL przy przekierowaniach _Controller::httpRedirect
1.4.0 - usunięcie klasy _Result (leniwe ładowanie danych - odroczone wykonywanie zapytań DQL) z podstawowej wersji biblioteki
1.4.1 - obsługa klauzuli LIMIT dla MySQL w zapytaniach przygotowanych
_________________
Ostatnio zmieniony przez kurshtml dnia 29.08.2010 14:20, w całości zmieniany 23 razy Użytkownik otrzymał punkt pomocy za ten post od: arve_lek, SkyMan
Niby wszystko ok tylko moim zdaniem lepiej uzywac jakiegos mechanizmu tpl
Widzę, że nie czytałeś uważnie http://nosmarty.net/
Wiesz, że Zend Framework używa czegoś podobnego, tzn. "PHP View" (klasa nazywa się Zend_View)? Świat zaczyna wracać do PHP jako języka szablonów wizualizacji widoku. Poza tym spróbuj napisać inny system szablonów o takich możliwościach w niecałych 40 liniach kodu.
Dostep do rzeczy napisanych w " jest szybszy anizeli tych w '. Roznica niewielka (3-5%) ale przy duzych projektach moze wiele znaczyc. Chce zauwazyc, ze dostep do zmiennych z uzycie ' jest wolniejszy.
A ja nie mowilem o Smarty... Co malo masz mechanizmow tpl lepszych i szybszych od smarty? Wiem, ze takie cos jest w Zend ale nie jestem jego zwolennikiem.
Z ptk tpl bedzie skrocony z ptk kodu wynikowego bedzie taki sam.
Chodzi o latwosc modyfikacji. No juz wystarczy zobaczyc WordPress'a.
Dostep do rzeczy napisanych w " jest szybszy anizeli tych w '
Parsowanie interpolowanych stringów (") jest szybsze w przypadku podstawiania wielu zmiennych. Tak czy inaczej są to na tyle pomijalne różnice, że chyba nie warto o tym dyskutować.
CapaciousCore napisał(a):
Co malo masz mechanizmow tpl lepszych i szybszych od smarty?
I konieczność nauki kolejnego metajęzyka? Problemy z kolorowaniem i podpowiadaniem składni w edytorze. Poza tym zwróć uwagę, że jakikolwiek system szablonów nie weźmiesz, to jego kod będzie wielokrotnie większy niż cała biblioteka, którą zaprezentowałem, a wtedy to nie miałoby żadnego sensu, bo gdybym musiał i tak dołączyć tak obszerny kod, to wolałbym już skorzystać np. z Zenda lub innego znanego frameworka. To nie miał być kolejny pełnowymiarowy framework - konkurencja dla Zenda czy Symfony, tylko rozwiązanie na tyle "lekkie", żeby warto było go zastosować, kiedy wahamy się, czy nie zrobić strony na prostych include'ach. Ważnym argumentem jest również, że nie trzeba się uczyć żadnego kolejnego języka szablonów ani obsługi skomplikowanej biblioteki. Od razu można przystąpić do pracy i szybko zakończyć taki prosty projekt, mając jednocześnie możliwość rozbudowy go w przyszłości.
Łatwość modyfikacji zależy od programisty - czy ma jakiekolwiek pojęcie co powinien, a czego nie powinien robić widok. Jak sobie zrobisz np. mysql_query w szablonie, to sam sobie zaszkodzisz na przyszłość. Nie widzę żadnego powodu, aby dobrze napisany PHP View był trudny do modyfikacji w przyszłości. A WordPress jest właśnie przykładem nieprawidłowo napisanego widoku. Są w nim takie kwiatki, jak np. bezpośrednie odwoływanie się do modelu, wizualizacja danych poza widokiem, a widok zamiast pasywnie wyświetlać dane, samodzielnie aktywnie odwołuje się po dane, tzn. sam nie wiadomo skąd wie, które funkcje powinien wywołać, aby pobrać dane, które musi wyświetlić. Widok nie powinien robić takich rzeczy. Widok nie wie, skąd ma pobrać dane. Wizualizacja nie powinna się odbywać nigdzie poza widokiem. Widok może skorzystać tylko z tych danych, które przekazał mu kontroler.
3-5% robi swoje moim zdaniem :] jak pisac to dobrze. Zreszta w artykule masz na uwadze, ze projekt sie ma rozrosnac :] no wiec sory.
A tam wiekszosc jezykow tpl jest bardzo podobna do siebie. Ma podobne mechanizmy. Jezeli edytor bylby wbudowany w system np. jak WP to przeciez z odpowiednia implementacja mozna dodac kolorowanie jak i podpowiedzi.
Skoro mowisz, ze takie kwiatki to z pewnoscia ktos taki jak Ty zwrocil juz na to uwage i dlaczego to jeszcze nie zostalo zmienione, co ?
kurshtml napisał(a):
które funkcje powinien wywołać, aby pobrać dane, które musi wyświetlić.
Wystarczy pare dni poobcowac i zwykly uzytkownik bedzie mial oglad co i jak. Zreszta bardzo dobra moim zdaniem maja dokumentacje tlumaczaca jak uzywac poszczegolnych funkcji/fragmentow kodu.
kurshtml napisał(a):
Jak sobie zrobisz np. mysql_query w szablonie, to sam sobie zaszkodzisz na przyszłość.
Nie wiem skad trzasnoles taki pomysl ale to jest polityczne samobojstwo.
Napisałem również, że interpolacja jest szybsza tylko w pewnym szczególnym przypadku, a nie zawsze
CapaciousCore napisał(a):
wiekszosc jezykow tpl jest bardzo podobna do siebie
Większość języków programowania jest podobna do siebie. Wystarczy poznać instrukcje sterujące, kilka funkcji i już można zacząć pisać... tylko, że najpierw trzeba poświęcić czas, żeby to poznać, a potem jeszcze zdobyć doświadczenie, żeby pisać kod szybko i w optymalny sposób. Wybacz, ale jeśli szukam prostego rozwiązania na projekt, który skończy się np. za 3 dni, to nie mam ochoty cały dzień grzebać w dokumentacji jakiegoś systemu szablonów.
CapaciousCore napisał(a):
Jezeli edytor bylby wbudowany w system np. jak WP to przeciez z odpowiednia implementacja mozna dodac kolorowanie jak i podpowiedzi.
A ja mam swój ulubiony edytor i nie mam zamiaru używać innego Dlaczego ktoś ma mnie do tego zmuszać?
CapaciousCore napisał(a):
dlaczego to jeszcze nie zostalo zmienione, co ?
Bo programiści WP być może (powtarzam: być może, a nie na pewno) nie do końca czują, jak powinien wyglądać prawidłowy podział warstw w aplikacji Web? Bo żeby to poprawić, trzeba by przepisać wszystko od zera, gdyż tak bardzo jest to fundamentalne założenie obecnego kodu WP?
CapaciousCore napisał(a):
Wystarczy pare dni poobcowac i zwykly uzytkownik bedzie mial oglad co i jak.
Co nie zmienia faktu, że do MVC ta koncepcja pasuje jak pięść do nosa
CapaciousCore napisał(a):
Nie wiem skad trzasnoles taki pomysl ale to jest polityczne samobojstwo.
Daleko trzeba szukać? Widziałem coś takiego w PHP-Nuke lub Post-Nuke. WordPress bezpośrednio nie wykonuje co prawda zapytań SQL w szablonach widoku, ale robi za to coś innego: widok sam wywołuje funkcję, która pobiera dane z bazy. Na dodatek ta funkcja jeszcze ubiera pobrane dane w znaczniki HTML. To gdzie tutaj właściwie jest kontroler, a gdzie widok? Widok przejmuje rolę kontrolera, a przy tym oddaje część swoich zadań w inny obszar aplikacji Spróbuj teraz napisać alternatywny widok, który wyświetla dane np. w formacie PDF, RTF, XML lub innym, bez przepisywania połowy kodu lub wstawiania setek if-ów w wielu miejscach kodu.
W sumie to bardzo dobrze, że wywiązała się ta dyskusja, bo można na konkretnych przykładach poznać, jak nie należy pisać aplikacji.
logeen, a załóżmy teraz, że w szablonie lib/view/layout/index.phtml chcesz wyświetlić nazwę zalogowanego użytkownika, zawsze.
Dochodzą więc dodatkowe założenia:
1. użytkownika trzymamy jako instancję model_User w sesji
2. controller nie musi wiedzieć, czy użytkownik jest zalogowany, ale jak potrzebuje, uzyskuje takie info.
3. jeśli chcemy wygenerować np. RTF czy PDF aplikacja w ogóle nie musi wykorzystywać sesji, bo ani controller ani widok nie potrzebuje informacji o użytkowniku.
czyli w skrócie: fajnie byłoby inicjować dane o użytkowniku leniwie.
Możesz pokazać, jak wg Ciebie "semantycznie" będzie wpleść takie zarządzanie użytkownikiem do obecnego kodu?
Pytam z czystej ciekawości, jak ty byś to zrobił w ogólnym MVC (a ten prosty kod wyżej wydaje się OK do tego celu).
<?php /** * PHP Light MVC data lazy loading extension. * Executes DQL queries exactly when it is needed. * @package PHP Light MVC * @version 1.0.0 * @since 1.4.0 * @author Sławomir Kokłowski {@link http://www.kurshtml.boo.pl} * @copyright Do NOT remove this comment! * @license LGPL */ /** * Model with data lazy loading. */ abstract class Model extends _Model { /** * Lazy executes DQL query. * Normally it is not required to call this method, because it is caled internally. * @param string $name Key name from $this->sql array * @param array $arguments Parameters to fill in prepered statement * @return Result Result object */ protected function execute($name, $arguments=array()) { return preg_match('/^[^A-Z_]*SELECT[^A-Z_]/i', $this->sql[$name]) ? new Result($this, 'executeSql', array($name, $arguments)) : $this->executeSql($name, $arguments); } /** * Executes DQL query immediately. * Normally it is not required to call this method, because it is caled internally. * @param string $name Key name from $this->sql array * @param array $arguments Parameters to fill in prepered statement * @return Result Result object */ protected function executeSql($name, $arguments=array()) { return new Result(parent::execute($name, $arguments)); } } /** * Model result. * Lazy loads data exactly when it is needed. */ class Result extends Model implements ArrayAccess, Iterator, Countable { /** * Result data. * @var array */ protected $data = array(); private $model; private $method; private $arguments = array(); private $executed = false; private $valid; /** * Constructor. * @param mixed $model Model instance or result data if $method parameter is empty * @param string $method Model method name * @param array $arguments Parameters to be passed to the model method, as an indexed array */ function __construct($model, $method=null, $arguments=array()) { if (empty($method)) { $this->data = $model; $this->execute = true; } else { $this->model = $model; $this->method = $method; $this->arguments = $arguments; } } /** * Checks if data is empty. * @return bool */ function isEmpty() { $this->executeData(); return empty($this->data); } function offsetExists($offset) { $this->executeData(); return array_key_exists($offset, $this->data); } function offsetGet($offset) { $this->executeData(); if (isset($this->data[$offset])) return $this->data[$offset]; } function offsetSet($offset, $value) { $this->data[$offset] = $value; } function offsetUnset($offset) { unset($this->data[$offset]); } function current() { $this->executeData(); return current($this->data); } function key() { $this->executeData(); return key($this->data); } function next() { $this->executeData(); $this->valid = next($this->data) !== false; } function rewind() { $this->executeData(); reset($this->data); $this->valid = !$this->isEmpty(); } function valid() { return $this->valid; } function count() { $this->executeData(); return count($this->data); } protected function executeData() { if (!$this->executed && is_object($this->model) && is_a($this->model, '_Model')) { $this->data = call_user_func_array(array($this->model, $this->method), $this->arguments); if (is_object($this->data) && is_a($this->data, 'Result')) { $this->data->executeData(); $this->data = $this->data->data; } $this->executed = true; } } } ?>
<?php class controller_Index extends _Controller { protected $layout; protected $model; protected $modelUser; function __construct() { $this->layout = new _View('layout/index.phtml'); $this->model = new model_Article; $this->modelUser = new model_User; $this->layout->user = $this->modelUser->getCurrentUser(); } function login() { if (!isset($_POST['login']) || !isset($_POST['password']) || !$this->modelUser->login($_POST)) { $this->layout->main = new _View('login.phtml'); return $this->layout; } return $this->index(); } function logout() { $this->modelUser->logout(); return $this->index(); } // ...reszta tak jak wczesniej } ?>
A jeśli chodzi np. o widok PDF, który nie potrzebuje danych o zalogowanym użytkowniku, to są co najmniej dwa sposoby, aby niepotrzebnie nie inicjalizować sesji:
Można utworzyć osobny kontroler, który nie będzie używał metod login, logout ani getCurrentUser klasy model_User
Można wykorzystać ten sam kontroler, ale nie wywoływać w nim metody login ani logout klasy model_User ani nie korzystać ze w szablonie ze zmiennej $user - mimo, iż obiekt CurrentUser zostanie utworzony, w widoku nie wyświetlimy żadnych jego własności, więc sesja nie będzie zainicjalizowana
_________________
Ostatnio zmieniony przez kurshtml dnia 14.03.2010 13:38, w całości zmieniany 3 razy
Tylko dlaczego do sesji nie wrzucić całego użytkownika, tak żeby to sesja martwiła się o jego serializację?
A co myślisz o takim pomyśle, żeby zrobić nową klasę view_Layout, z automatycznie ustawionym szablonem na layout/index.phtml oraz dodanym użytkownikiem:
class view_Layout extends _View { function __construct() { parent::__construct('layout/index.phtml'); $modelUser = new model_User; $this -> user = $modelUser->getCurrentUser(); } }
W takim przypadku, mając kilka controllerów korzystających z głównego szablonu nie trzeba w każdym dodawać użytkownika.
(Bo może oprócz użytkownika w sidebarze strony będziemy chcieli pokazać "popularne artykuły", "ostatnio dodane" itd.)
Czy przeniesienie takiej podstawowej logiki do widoku uważasz za prawidłowe?
Czy przeniesienie takiej podstawowej logiki do widoku uważasz za prawidłowe?
No właśnie nie do końca jestem co do tego przekonany. W takim przypadku chyba lepiej by było utworzyć drugi kontroler, który dziedziczy po pierwszym. W ten sposób nie trzeba by było kolejny raz pobierać danych bieżącego użytkownika i przekazywać je do layoutu, a przy tym nie powstanie "aktywny" widok, który sam wywołuje model, a więc w zasadzie powinien być kontrolerem. Zwróć uwagę, że jeśli w przyszłości szukałbyś miejsca, gdzie są pobierane dane użytkownika, a zapomniałbyś, jak to zostało zaimplementowane, to raczej skierowałbyś się do kontrolera, a nie to widoku. I właśnie o to chodzi - żeby logika biznesowa aplikacji nie była porozrzucana w wielu miejscach, a tylko tam, gdzie jej miejsce - czyli w kontrolerze MVC.
Myślałem jeszcze nad poprzednim problemem z leniwym ładowaniem danych. Rozwiązanie, które podałem wcześniej, rozwiązuje sprawę, ale tylko dla tego konkretnego przypadku. W rzeczywistym projekcie analogiczny problem powstanie np. w przypadku menu - również ma być na każdej stronie w widoku HTML, ale np. w PDF już niekoniecznie. Z tego właśnie powodu, rozbudowałem poprzedni kod frameworka. Teraz do widoku nie jest przekazywana tablica obiektów, tylko obiekt nowej klasy Result. Implementuje on jednak odpowiednie interfejsy, dzięki czemu możemy go w widoku używać prawie tak samo, jak zwykłej tablicy indeksowanej. W przeciwieństwie do tablicy, potrafi on w sposób przezroczysty pobrać dane w momencie, kiedy próbujemy uzyskać do nich dostęp, czyli np. wyświetlić je w widoku. Jeśli tego nie zrobimy, to dane nigdy nie zostaną pobrane, a więc zaoszczędzimy na zapytaniach SQL.
Zastanawiałem się, czy to nie stanowi znowu próby przeniesienia logiki kontrolera do widoku, ale według mnie - nie. Widok sam nie może pobrać tych danych. Nie wie nawet, że są one doładowywane w momencie, kiedy chce je wyświetlić. Widok nie musi wywoływać żadnej specjalnej metody na danych, aby to zrobić. Według mnie nie stanowi to również próby przeniesienia kompetencji kontrolera do modelu ani zadań modelu do jakiejś oddzielnej klasy Result, ponieważ podczas leniwego ładowania danych nie zawiera się żadna ukryta logika biznesowa. Sam obiekt rezultatu co prawda wywołuje metodę modelu, co teoretycznie powinien wykonywać tylko kontroler, ale sam nie decyduje, jaką metodę wykonać. Jest mu to przekazane z zewnątrz w modelu, który został wywołany przez kontroler. Nie stanowi to zatem przeniesienia logiki poza kontroler ani dostępu do danych poza modelem, a jedynie odroczenie na nieco później pobrania danych.
Nowa wersja frameworka już znajduje się w pierwszym poście tego wątku. Udoskonaliłem w niej również jeszcze jedną rzecz. Mianowicie teraz, jeśli widok nie znajdzie pliku szablonu, nie wygeneruje żadnego błędu - po prostu go pominie. Dzięki temu wystarczy w pliku startowym zainicjalizować inny katalog bazowy _View::$dir i mamy gotowy nowy rodzaj widoku, z możliwością wykorzystania z tymi samymi kontrolerami. Jeśli np. w widoku PDF nie znajdzie się szablon wyświetlający użytkownika, to po prostu nie będzie wywołany. Jako zadanie samodzielne można sobie zaimplementować rozszerzony widok - pochodną klasy _View, który w przypadku braku pliku szablonu w aktualnym widoku, pobierze ten plik z jakiejś wersji domyślnej. Możliwości jest sporo i nie trzeba w tym celu modyfikować kodu frameworka.
A oto jak będzie teraz wyglądało rozwiązanie problemu z wyświetlaniem danych użytkownika:
Pisałem o wzorcach projektowych i o MVC właśnie po to, żeby przygotować grunt pod wątek, który właśnie czytasz Zdarzyło mi się po prostu przy okazji takiego niewielkiego projektu napisać prosty framework (to już chyba szósty w mojej historii programisty ) i stwierdziłem, że może się on jeszcze komuś przydać - właśnie dlatego, że jest mały i prosty w obsłudze, a przy tym ma spore możliwości, jak na taką miniaturkę. Wahałem się, czy dokonywać w nim dzisiaj modyfikacji, ale stwierdziłem, że funkcjonalność "leniwego" ładowania danych może się przydać niemal w każdym projekcie, a dzięki temu wzrośnie wydajność. A przy tym kod jest bardziej obiektowy.
Całe wprowadzenie teoretyczne było po to, żeby to co tutaj napisałem było zrozumiałe.... nie, nie planuję tworzyć kursu PHP
Tworzenie kolejnego pełnowymiarowego frameworka uważam za co najmniej nierozsądne, a na pewno mało sensowne. Dzisiaj na rynku mamy tyle gotowych rozwiązań, że raczej nie warto tworzyć nowego. Gotowe rozwiązanie, takie jak np. Zend Framework - oprócz oczywistej zalety, że jest tworzone przez całą grupę programistów - ma jeszcze coś, co często się bagatelizuje, a jest niezmiernie ważne. Znany framework Open Source staje się niepisanym standardem w branży. Przychodząc pracować do jakiejś nowej firmy, albo poprawiając po kimś innym kod, nie musisz poświęcać czasu na studiowanie dokumentacji nowego frameworka - bo już ją znasz. Do tego wszyscy trzymają się jakiegoś ustalonego standardu tworzenia kodu, dzięki czemu łatwiej jest przejąć pracę po kimś.
Framework, który zaproponowałem nie ma na celu rywalizować z "gigantami", a być jedynie alternatywą raczej w małych projektach, kiedy dzięki jego prostocie nie trzeba będzie poświęcać za dużo czasu na jego naukę. Może się również przydać dla osób, które nie znają zbyt dobrze np. Zenda i nie mają czasu lub ochoty w danej chwili się go uczyć. Mój framework pozwoli im względnie szybko stworzyć aplikację, a przy tym mieć możliwość swobodnego jej rozwoju w przyszłości.
Zobacz następny temat Zobacz poprzedni temat Nie możesz pisać nowych tematów Możesz odpowiadać w tematach Nie możesz zmieniać swoich postów Nie możesz usuwać swoich postów Nie możesz głosować w ankietach