flickr galerie | Twitter @Padáček | Facebook profil | Foursquare profil | LinkedIn profil

Zápisky.info


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 ...

Vloženo 07.02. 2006 v 09:12 | PHP | Zobrazeno 19938x

Komentáře:

  1. [1] od: Max

    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:17
  2. [2] od: Jan-Sebastian Fabík

    Zdraví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ář
    1. tento komentář inspiroval Milan — #3
    10.09. 2008, 21:47
  3. [3] od: Milan

    inspirováno Jan-Sebastian Fabík — #2 Místo řádku:
    class SampleConfigKeyNotDefinedException extends Exception
    má zřejmě být řádek
    class SampleTemplatedNotFoundException extends Exception

    odpověz na tento komentář 17.02. 2009, 12:13

Nový komentář:

V komentáře lze použít následující „tagy“:

  • [a href=http://url.com]titulek odkazu[/a] – bude převeden na odkaz
  • [abbr title=text]abbr[/abbr] – bude převeden na <abbr>.
  • [cite], [code], [em], [strong], [q], [li] — obdobně.

Komentáře musí být před publikováním schváleny. Děkuji!

Kontrola proti SPAMu

Vlož znaky zobrazené na obrázku:


Navigace

Vyhledávání

 Vyhledávání 

Povolené operátory: AND, OR a NOT

Kategorie

Nejčtenější

Poslední komentáře

  • Jirka Hradil: Ten překlad je dobrá práce, jednoduché, srozumitelné :)....
  • honza: Diky autore, ani nevis jak moc jsi mi timhle clankem pomohl. Porad mi kod nefungoval, vymyslel jsem kdeco, ale reseni...
  • Milan: [2] Místo řádku: class SampleConfigKeyNotDefinedException extends Exception má zřejmě být řádek class SampleTemplatedNotFoundException extends Exception...
  • Josef Petrák: [1] Máš data v databázi, třeba Oracle, a chceš z nich udělat nějaký report a ten nabídnout v různých formátech...
  • havlikp: proč nepoužít excel? ;-)...

Odkazy

Statistiky návštěvnosti [TOPlist]
PageRank Checking Icon
Valid XHTML 1.0 Strict

Obsah © 2004 – 2006 Josef Petrák

Grafika © 2004 Dlouhý Webdesign

Redakční systém BLOG:CMS