Jednoduchý MVC framework napsaný v PHP
Vysvětlovat, co je podstatou návrhového vzoru MVC, by asi pro zkušené tvůrce webových aplikací bylo nošením dříví do lesa. Stručně řečeno jde o způsob, jak oddělit logiku práce s daty od jejich reprezentace. Takové rozhraní poskytuje snad každá platforma pro vývoj webových aplikací: Java jako JSP a knihovny tagů, .NET balíček Web.Forms, díky kterému lze ve vývojovém prostředí „naklikat“ ve Visual Studiu stejně jako by šlo o desktopovou aplikaci. V PHP je situace jiná. Žádné oficiální MVC řešení neexistuje. Proprietální si však může každý napsat sám. Já zde popíšu to, které jsem využil například na aplikaci raillog.
Jak by se Vám líbilo, kdyby soubor index.php Vaší aplikace vypadal takto?
<?php
// načtení knihoven aplikace
require('_application.php');
// spuštění
SampleApplication::run();
?>
Vím, je to trochu strohé a může to znamenat problémy v dalším vývoji, ale mně to vyhovuje ... a nevypadá to úžasně?
Podstatou frameworku je, že všechny důležité součásti jsou definovány pomocí rozhraní a od nich odvozených tříd. Globálních proměnných obsahujících odkazy na důležité součásti aplikace jsem se zbavil použitím návrhového vzoru Singleton. Možnosti objektově–orientovaného návrhu se taky hodí. Aplikační data jsou reprezentována jako hierarchie objektů sdružující datové atributy a základní operace s nimi. Konfigurační data lze přijímat z INI souborů (parse_ini_file), XML (simplexml_load_file), od uživatele (pole $_GET a $_POST), serveru ($_SERVER) nebo lze napsat databázový backend. Pomocí prefixů názvů objektů supluji úlohu balíčků. V následujících odstavcích na komentovaných zdrojových kódech ukážu řešení standardního problému „Hello, World!“ tak, jak ho řeší můj framework.
<?php
// načtení knihoven
require('_api.php');
require('_view.php');
interface Application {
public static function run();
}
class SampleApplication implements Application {
// sdílená instance aplikace
private static $instance;
private $dataParser;
private $caughtException;
// konstruktor aplikace, vytváří instanci parseru dat
// skryt kvůli zajištění jedinečnosti instance aplikace
private function __construct() {
$this->dataParser = new SampleDataParser();
}
// metoda pro přístup ke sdílené instanci aplikace
public static function getInstance() {
if (is_null(self::$instance))
self::$instance = new SampleApplication();
return self::$instance;
}
// metoda načítající data do modelu aplikace
private function load() { $this->dataParser->parse(); }
// metoda zobrazující data pomocí určené šablony
private function view($pathToTemplate) {
SampleViewer::vizualize($pathToTemplate);
}
// poskytuje instanci výjimky nastalé při běhu aplikace
public function getCaughtException() {
return $this->caughtException;
}
// statická metoda spouštějící aplikaci
public static function run() {
$app = SampleApplication::getInstance();
try {
$app->load();
$app->view(
SampleRequest::getInstance()->getParam('template')
);
} catch (Exception $e) {
$app->caughtException = $e;
$app->view('views/__error.php');
}
}
}
?>
Soubor _application.php obsahuje kostru aplikace. Rozhraní Application definuje základní metodu pro spuštění aplikace. Jak vidíte, hlavní běhová logika je obsažena v třídě SampleApplication právě v metodě run(). Napřed se vytvoří instance aplikace, načtou se data a pak se spustí jejich vizualizace. Co a jak se zobrazí, to záleží právě na šabloně. To však není vše. V API aplikace mají své místo i třída pro model dat, parser dat, objekt požadavku sdružující všechna data potřebná pro úspěšné provedení požadavku uživatele a definice výjimek vznikajících během chybových stavů. Tyto objekty najdete v souboru _api.php.
<?php
interface DataParser {
public function parse();
}
class SampleDataParser implements DataParser {
// metoda, která načte data a uloží je do modelu
public function parse() {
$model =& SampleModel::getInstance();
$model->setContent('Hello, World!');
}
}
class SampleModel {
private static $instance;
private function __construct() {}
// vrací obsah modelu
public function getContent() { return $this->content; }
// nastaví obsah modelu
public function setContent($content) {
$this->content = $content;
}
public static function getInstance() {
if (is_null(self::$instance)) self::$instance = new SampleModel();
return self::$instance;
}
}
class SampleRequest {
private static $instance;
private $properties;
private function __construct() {
// zde se načtou všechny data o požadavku
// nastavení šablony podle jména souboru aplikace
$this->properties['template'] =
'views/_' . substr($_SERVER['SCRIPT_NAME'], strrpos($_SERVER['SCRIPT_NAME'], '/') + 1);
}
// metoda pro získání konfiguračního parametru podle ID
public function getParam($key) {
if (!array_key_exists($key, $this->properties)) {
throw new SampleConfigKeyNotDefinedException($key);
} else {
return $this->properties[$key];
}
}
public static function getInstance() {
if (is_null(self::$instance)) self::$instance = new SampleRequest();
return self::$instance;
}
}
class SampleConfigKeyNotDefinedException extends Exception {
private $badConfigKey;
public function __construct($badConfigKey) {
parent::__construct('', 0);
$this->badConfigKey = $badConfigKey;
}
public function __toString() {
return 'Konfigurační parametr s ID ' . $this->getBadConfigKey() . ' není definován!';
}
}
?>
Pomocí těch objetků je definována datová vrstva a logika aplikace. Rozhraní DataParser definuje (a třída SampleDataParser implementuje) metodu společnou všem parserům dat. Ta načte potřebná data, získá sdílenou instanci modelu a data do něj uloží. SampleModel je kontejner sdružující všechny data aplikace. V tomto příkladě jde jen o jediný řetězec, ale může jít i mnohem složitější strukturu obsahující vzájemně provázané objekty, pole a data získaná výpočtem z těchto dat. Vše přístupné přes „gettery“. SampleRequest je objekt reprezentující data o relaci přijatá od uživatele (z formuláře či jako parametry adresy) ze serveru, z konfigurace aplikace a z nich vypočtená data. Při požadavku na neexistující konfigurační klíč požadavku je vyvolána výjimka SampleConfigKeyNotDefinedException. Poznamenejme, že aplikace zjišťuje adresu šablony tak, že vezme adresu skriptu aplikace (např. index.php) a předpokládá, že šablona je umístěna v soubor shodného jména s doplněným znakem „_“ na začátku a umístěného v adresáři views/ — takže v tomto případě views/_index.php.
<?php
interface Viewer {
public static function vizualize($pathToTemplate);
}
class SampleViewer implements Viewer {
private function __construct() {}
// zobrazení dat pomocí načtení šablony
public static function vizualize($pathToTemplate) {
// pokud není nalezena, je vyvolána výjimka
if (file_exists($pathToTemplate)) {
require($pathToTemplate);
} else {
throw new SampleTemplatedNotFoundException($pathToTemplate);
}
}
}
class SampleConfigKeyNotDefinedException extends Exception {
private $badPath;
public function __construct($badPath) {
parent::__construct('', 0);
$this->badPath = $badPath;
}
public function __toString() {
return 'Chyba! Nenalezena šablona pro zobrazení požadavku. Umístění požadované šablony: ' . $this->getBadConfigKey();
}
}
?>
Jak vypadají jednotlivé šablony?
_index.php:
<?php
header('Content-Type: text/plain; charset=UTF-8');
echo(SampleModel::getInstance()->getContent());
?>
__error.php:
<?php
header('Content-Type: text/plain; charset=UTF-8');
echo(SampleApplication::getInstance()->getCaughtException());
?>
Nyní před sebou máte kompletní aplikaci. Jak vidíte, spousta věcí je udělaná napůl a nebo se spoléhá na ruční zásah uživatele. Jde o to, že jednotlivé komponenty lze napsat podle potřeby (ale ta ještě nenastala). Možná by celé řešení chtělo otestovat na složitější aplikaci, která data nejen zobrazuje, ale také načítá od uživatele a umožňuje jejich správu. Rovněž by nebylo špatné implementovat něco jako knihovny tagů, aby byly šablony psány v abstraktním jazyce a ne přímo pomocí konstrukcí jazyka PHP. Trochu jiný přístup nabízí PRADO, ale s tímto tento framework srovnávat nemůžu. Berte to jako inspiraci nebo návod, jak rychle oddělit vzhled a logiku ve vaší aplikaci ...
Komentáře:
Nový komentář:
Komentáře musí být před publikováním schváleny. Děkuji!
proc ne pouzit nazev Controller (podle MVC) ale Application?
nechat abstraktni tridy
Controller
Model
View
byl by mensi zmatek pro zacatecniky, jinak pekny vzorek
odpověz na tento komentář 09.09. 2008, 11:17Zdravím,
nevím, co dělám špatně, ale když jsem použil tyto skripty, hlásí mi to chybu:
Fatal error: Cannot redeclare class SampleConfigKeyNotDefinedException in /var/www/mvc/_view.php on line 32
odpověz na tento komentář- tento komentář inspiroval Milan — #3
10.09. 2008, 21:47inspirováno Jan-Sebastian Fabík — #2 Místo řádku:
odpověz na tento komentář 17.02. 2009, 12:13class SampleConfigKeyNotDefinedException extends Exception
má zřejmě být řádek
class SampleTemplatedNotFoundException extends Exception