Model-View-Controller

Ezku 15.06.05 00:24

"Hello, world!" in Model-View-Controller. Yksinkertainen MVC-toteutus esimerkin kera. Toivon tästä olevan hyötyä. PHP 5.0. Saatavina myös helpommin ymmärrettävänä zippinä, ks. kommentit.

 Tekstiversio  Arvo: 22 (34 ääntä)  Äänestä: +  -
<?php
/**
 *      Name:  framework/libs.php
 *      Purpose:      Include core libraries, define default page and action
 */

error_reporting(E_STRICT | E_ALL);

define('PATH_FRAMEWORK',        realpath(dirname(__FILE__)));
define('PATH_CONTROLLER',       PATH_FRAMEWORK.'/Controller');
define('PATH_FILTER',      PATH_FRAMEWORK.'/Filter');
define('PATH_MODEL',        PATH_FRAMEWORK.'/Model');
define('PATH_VIEW',          PATH_FRAMEWORK.'/View');


function __autoload($class)
{
        include_once(PATH_FRAMEWORK.'/'.$class.'.php');
}

/**
 *      Name:  framework/FilterManager.php
 *      Purpose:      Manage pre and post-execution Filters for a Controller
 */

class FilterManager
{
        public $controller = NULL;
        public $prefilters = array();
        public $postfilters = array();
       
        /**
         * @param object Controller
         */

        public function __construct(Controller $c)
        {
                $this->controller = $c;
        }
       
        /**
         * Add a filter to be executed before Controller execution
         */

        public function addPreFilter($f)
        {
                $this->prefilters[] = Filter::create($f);
        }
        /**
         * Add a filter to be executed after Controller execution
         */

        public function addPostFilter($f)
        {
                $this->postfilters[] = Filter::create($f);
        }
       
        /**
         * Execute 1) prefilters 2) Controller 3) postfilters
         * @param mixed passed to Controller, optional
         */

        public function execute($param = NULL)
        {
                foreach($this->prefilters as $f)
                        $f->execute($this);
               
                $this->controller->execute($param);
               
                foreach($this->postfilters as $f)
                        $f->execute($this);
        }
}


/**
 *      Name:  framework/Filter.php
 *      Purpose:      Provide site-wide auxiliary function plugins
 */

abstract class Filter
{
        abstract function execute(FilterManager $fm);
       
        public function create($type)
        {
                $path = PATH_FILTER.'/'.$type.'.php';
                $class = $type.'Filter';
                return Factory::Generic($path, $class);
        }
}

/**
 *      Name:  framework/Controller.php
 *      Purpose:      Provide basic Controller implementation
 */

abstract class Controller
{
        abstract public function execute($parameter);
       
        /**
         * Create a Controller of certain type
         * @param string Controller type
         * @param object parent Controller, optional
         */

        static function create($type, $parent = NULL)
        {
                $type = ucfirst($type);
                $path = PATH_CONTROLLER.'/'.$type.'.php';
                $class = $type.'Controller';
                $args = array();
                if($parent instanceof Controller)
                        $args[] = $parent;
               
                return Factory::Generic($path, $class, $args);
        }
}

/**
 *      Name:  framework/FrontController.php
 *      Purpose:      Dispatch page request to a PageController
 */

class FrontController extends Controller
{
        protected $default_page = NULL;
        protected $default_action = NULL;

        /**
         * FrontController constructor
         * @param string default page
         * @param string default action
         */

        public function __construct($default_page, $default_action)
        {
                $this->default_page = $default_page;
                $this->default_action = $default_action;
        }
       
        /**
         * Create Controller based on $request, execute it, render the created view
         * @param array request resource (typically $_GET)
         */

        public function execute($request)
        {
                $page = (isset($request['page']) && is_string($request['page'])) ? $request['page']$this->default_page;
                $action = (isset($request['action']) && is_string($request['action'])) ? $request['action']$this->default_action;
               
                $p = Controller::create($page);
                $p->execute($action);
               
                $view = $p->getView();
                $view->render();
        }
}

/**
 *      Name:  framework/PageController.php
 *      Purpose:      Govern application logic.
 *      Allow extending Controllers to host page actions, interact with the View and execute sub-actions
 */

abstract class PageController extends Controller
{
        protected $parent = NULL;
        protected $children = array();
        protected $view = NULL;
        protected $model = NULL;
        protected $name = NULL;
       
        /**
         * PageController constructor
         * @param object parent-PageController
         */

        public function __construct($parent = NULL)
        {
                $this->name = get_class($this);
                if($parent instanceof Controller)
                {
                        $this->parent = $parent;
                        $this->view = $parent->view;
                }
                else
                {
                        $this->view = new View();
                }
                $this->initialize();
        }
       
        /**
         * Initialization: associate PageController with appropriate Model
         */

        abstract protected function initialize();
       
        /**
         * Execute PageController action
         * @param string action name
         */

        public function execute($action)
        {
                        $this->checkAction($action);
                       
                        $this->onExecute();
                        $this->$action();
                        $this->afterExecute();
        }
       
        /**
         * Return View-object associated with PageController
         */

        public function getView()
        {
                return $this->view;
        }
       
        /**
         * Check that an action is allowed, throw Exception on error
         * @param string action name
         */

        protected function checkAction($action)
        {
                if(method_exists($this, $action))
                {
                        $method = new ReflectionMethod(get_class($this), $action);
                        $reserved = array('__construct', 'execute', 'getView');
                        if(in_array($action, $reserved) || $method->isProtected() || $method->isPrivate() || $method->isStatic())
                                throw new Exception('Tried to execute reserved action '.$action.' in '.$this->name);
                }
                else
                        throw new Exception($this->name.' does not support action '.$action);
        }
       
        /**
         * Execute action in a child-controller
         * @param string Controller type
         * @param string method name
         */

        protected function subAction($page, $action)
        {
                if(!isset($this->children[$page]))
                        $this->children[$page] = Controller::create($page, $this);
                $this->children[$page]->execute($action);
        }
       
        /**
         * Event handler: ran before executing an action
         */

        protected function onExecute() {}
        /**
         * Event handler: ran after executing an action
         */

        protected function afterExecute() {}
}


/**
 *      Name:  framework/View.php
 *      Purpose:      Handle templates
 */

class View
{
        public $templates = array();
        public $vars = array();
       
        /**
         * Assign template variable
         * @param string variable name
         * @param mixed variable value
         */

        public function assign($name, $value)
        {
                $this->vars[$name] = $value;
        }
       
        /**
         * Add template to stack
         * @param string filename
         */

        public function add($filename)
        {
                $path = PATH_VIEW.'/'.$filename;
                if(file_exists($path))
                        $this->templates[] = $path;
                else
                        throw new Exception('Template file '.$filename.' could not be found.');
        }
       
        /**
         * Clear template stack
         */

        public function clear()
        {
                $this->templates[] = array();
        }
       
        /**
         * Render template stack
         */

        public function render()
        {
                extract($this->vars);
                foreach($this->templates as $filename)
                        include $filename;
                $this->clear();
        }
}


/**
 *      Name:  framework/Model.php
 *      Purpose:      Provide basic Model implementation.
 *     
 *      In reality, this could be eg. database abstraction. Now it's just a shell.
 */

abstract class Model
{
        /**
         * Create a Model of certain type
         * @param string Model type
         */

        public function create($type)
        {
                $path = PATH_MODEL.'/'.$type.'.php';
                $class = $type.'Model';
                return Factory::Generic($path, $class);
        }
}


/**
 *      Name:  framework/Factory.php
 *      Purpose:      Host generic object factoring methods
 */

class Factory
{
        /**
         * Generic object factory method
         * @param string class definition path
         * @param string class name
         * @param array class constructor arguments
         */

        static public function Generic($path, $class, $args = array())
        {
                if(file_exists($path))
                {
                        include_once($path);
                        if(class_exists($class))
                        {
                                $arglist = array();
                                foreach($args as $index => $value)
                                        $arglist[] = '$args['.$index.']';
                                $arglist = implode(',',$arglist);
                               
                                $new_class = create_function('$class, $args', 'return new $class('.$arglist.');');
                                return $new_class($class, $args);
                        }
                        else
                                throw new Exception($path.' did not contain definition for '.$class);
                }
                else
                {
                        throw new Exception('Class definition file '.$path.' could not be found.');
                }
        }
}





/******************************************************************************
 *      Example with a pre-filter, a single page and action
 *****************************************************************************/






/**
 *      Name:  index.php
 *      Purpose:      Function as a single access point to the site
 */

include('framework/libs.php');

$default_page = 'Message';
$default_action = 'index';

try
{
        $fc = new FrontController($default_page, $default_action);
        $fm = new FilterManager($fc);
        $fm->addPreFilter('CleanRequest');
        $fm->execute($_GET);
}
catch(Exception $e)
{
        echo 'Exception: '.$e->getMessage();
}

/**
 *      Name:  framework/Filter/CleanRequest.php
 *      Purpose:      Clean request data ($_GET, $_POST, $_COOKIE and $_REQUEST) from magic quotes.
 */

class CleanRequestFilter
{
        public $isMagic = NULL;
        public function execute(FilterManager $fm)
        {
                if($this->isMagic = get_magic_quotes_gpc()) {
                        $_POST    = $this->fixMagicQuotes($_POST);
                        $_GET     = $this->fixMagicQuotes($_GET);
                        $_COOKIE  = $this->fixMagicQuotes($_COOKIE);
                        $_REQUEST = $this->fixMagicQuotes($_REQUEST);
                }
        }

  /**
   * Cleans arrays ruined by MagicQuotes=On.
   */

        protected function fixMagicQuotes($aList, $aIsTopLevel = true)
        {
                $gpcList = array();
                $isMagic = $this->isMagic;
                foreach ($aList as $key => $value)
                {
                        if(is_array($value))
                        {
                                $decodedKey = ($isMagic && !$aIsTopLevel)?stripslashes($key):$key;
                                $decodedValue = self::fixMagicQuotes($value, false);
                        }
                        else
                        {
                                $decodedKey = stripslashes($key);
                                $decodedValue = ($isMagic)?stripslashes($value):$value;
                        }
                        $gpcList[$decodedKey] = $decodedValue;
                }
                return $gpcList;
        }
}

/**
 *      Name:   framework/Model/Message.php
 */

class MessageModel extends Model
{
        protected $message = 'Hello, world!';
        public function getMessage()
        {
                return $this->message;
        }
}

/**
 *      Name:   framework/Controller/Message.php
 *      Purpose:      PageController for Message
 */

class MessageController extends PageController
{
        protected function initialize()
        {
                $this->model = Model::create('Message');
        }
       
        public function index()
        {
                $message = $this->model->getMessage();
                $this->view->assign('message', $message);
                $this->view->add('message_index.htm');
        }
}

/**
 *      Name:  framework/View/message_index.htm
 */

?>

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
        <title> Model-View-Controller example </title>
  <meta http-equiv="content-type" content="text/html; charset=iso-8859-1" />
</head>
<body>
        <h1><?php echo $message ?></h1>
</body>
</html>

editoitu: 00:56 15.6.05
Ezku 00:54 15.6.05 
"Mikä se MVC sitten on?" kysyttiin. No sehän on "an architecture for building applications that separate the data (model) from the user interface (view) and the processing (controller)" (Answers.com).

MVC:n laatimiseen on jotakuinkin googolplex eri tapaa, ja tässä on niistä vain yksi. Mikäli suunnittelette yhtään laajempia sivustoja, on MVC ehdottomasti tutustumisen arvoinen - vaikka ette ehkä sitten juuri minun versiostani pitäneetkään. Kyseessä olevan yleisesti hyväksi havaitun peruskaavan soveltaminen saattaa säästää huomattavasti aikaa kehityksessä ja tuottaa tuloksena selkeämpiä kokonaisuuksia. Esimerkkinä Ruby on Rails ja sen pohjalta PHP:lle tehty Cake.

Lisähuomautuksena: MVC on alunperin työpöytäohjelmistoja, ei PHP:n kaltaisia stateless-arkkitehtuureja varten suunniteltu. Tämän vuoksi suuri osa netissä pyörivästä materiaalista sisältää ehkä hieman PHP:lle luonteettomia rakenteita - usein jopa PHP-versiot itse. Oma versioni näyttää siis hieman erilaiselta.
Ezku 01:00 15.6.05 
Zipattu versio on saatavissa täältä. Kysykää, kommentoikaa.
T.M. 01:58 15.6.05 
No multa ei paljoo kommenttia heru, kun en tiedä mihin tälläsiä edes tarvitaan, ja mitä tästä hyödytään :)
editoitu: 03:29 15.6.05
Ezku 02:26 15.6.05 
Hieman selvennystä sitten, sekä käyttöön että peruskonseptiin:

- Filttereitä voidaan asettaa ajettavaksi ennen tai jälkeen sivun suorituksen (ks. index.php). Näihin voidaan sijoittaa esimerkiksi autentikointia tai tietokantayhteyksien muodostamista. [Tämä on pientä lisää, eikä varsinaisesti ole osa MVC:tä, mutta sopii hyvin tähän yhteyteen. Esimerkin MagicQuotes-fiksi on oikein käyttökelpoinen, suosittelen ehdottomasti!]

- FrontControllerille - tässä FilterManagerin kautta - syötetty parametritaulukko kertoo sille, mitä sivua ja sen osa-aluetta (tässä $_GET['page'] ja $_GET['action']) ollaan hakemassa. Mikäli niitä ei ole tarjolla, turvaudutaan oletusarvoihin (index.php). FrontController osaa luoda asianmukaisen PageController-objektin ja suorittaa sillä actionin, joka siis on luokan metodi - esimerkiksi tässä tapauksessa HelloController->World(). [Tajusinkin tässä että esimerkin luokat olisi ehkä voinut nimetä havainnollisemminkin. Noh.]

- PageControllerin metodissa suoritetaan varsinainen sivun logiikka, eli haetaan dataa Modelista tai syötetään sille käyttäjän antamaa dataa, syötetään sivun näyttämiseen tarvittava data Viewille ja kerrotaan sille mitä templateja käytetään.

- Modelissa voi sijaita esimerkiksi tietokanta-abstraktio tai muu vastaava järjestelmä, eikä PageControllerin tarvitse tietää siitä mitään. PageController voi antaa yksinkertaisen komennon, kerätä tulokset talteen ja antaa Viewille, joka käsittelee sen pelkällä foreachilla - huolimatta tuloksen mahdollisesta monimutkaisuudesta. Se voi olla esimerkiksi ResultSet-objekti, joka iteroitaessa hakee MySQL-kyselyn tuloksesta aina seuraavan rivin. Mahdollisimman tehokasta ja täysin näkymätöntä templaten tekijälle.

- Sama pätee Viewiin - siihen voi itse asiassa pultata esimerkiksi Smartyn tai muuhun kehittyneempään template-enginen ilman suurempia muutoksia.

Kun ohjelman kolme eri osa-aluetta, Model (datan hallinta), View (datan esittäminen) ja Controller (hallinnan ja esityksen yhteennaittaminen), erotetaan toisistaan, saadaan helpommin ymmärrettäviä, laajennettavia ja käytettäviä kokonaisuuksia. Sitä hyötyä on MVC:stä.
editoitu: 10:03 15.6.05
harja 09:52 15.6.05 
Jopas oli kiva esimerkki. On kuitenkin hyvä sanoa, että tässä on sovellettu myös muita suunnittelumalleja, kuin pelkästään MVC:tä, esimerkiksi Chain of Responsibilityä, mutta se on sivuseikka. Tämä tavara on ehkä vasta-alkajalle turhan monimutkaista, eikä näitä kannata turhaan miettiä vasta, kun oikeasti alkaa olemaan olio-ohjelmoinnin perusteet kunnolla hallussa ja tuntuu siltä, etteivät omat suunnittelumallit enää tunnu toimivilta ja tarpeeksi joustavilta. Usein, varsinkin kun pääsee sisään tähän ihmeelliseen suunnittelumallien (design patterns) maailmaan, niin tuntuu, että alkaa ajatella ihan liian monimutkaisesti yksinkertaisia asioita. Siksi lienee paikallaan antaa neuvo, että pysähtyy ja miettii aina vielä kerran mitä on tekemässä ja kun on saanut päätöksen aikaiseksi, miettii vielä kerran!

MVC-paradigmaa ollaan tuotu jo aikaisemmin myös web-ohjelmointipuolelle, varsinkin J2EE-maailmassa, joten sinänsä tällainen esimerkki on oiva myös PHP-kielelle.

EDIT:
Ihan pieni juttu tuli tuossa mieleen. Olisi hyvä, jos FrontController ottaisi konstruktorissaan (eksplisiittinen) nuo DEFAULT_PAGE ja DEFAULT_ACTION-toiminteet, jolloin olion tila voidaan tarkasti määrittää ilman ulkoisia riippuvuuksia globaaleista muttujista (hyi, hyi :))

:)Mikko
editoitu: 10:03 15.6.05
egaga 10:02 15.6.05 
Juu kannattaa ennen design patternseja tutustua periaatteisiin, joiden varaan suunnittelumallit sekä muukin laadukas olio-ohjelmointi perustuu:

SRP - The Single Responsibility Principle: A class should have one, and only one, reason to change. http://www.objectmentor.com/resources/articles/srp
OCP - The Open Closed Principle: You should be able to extend a classes behavior, without modifying it. http://www.objectmentor.com/resources/articles/ocp.pdf
LSP - The Liskov Substitution Principle: Derived classes must be substitutable for their base classes. http://www.objectmentor.com/resources/articles/lsp.pdf
DIP - The Dependency Inversion Principle: Depend on abstractions, not on concretions. http://www.objectmentor.com/resources/articles/dip.pdf
ISP - The Interface Segregation Principle: Make fine grained interfaces that are client specific. http://www.objectmentor.com/resources/articles/isp.pdf
akx 13:26 15.6.05 
mikefast kirjoitti:
eksplisiittinen

Herranjumala.
explicit: selvä, varma, tarkka, täsmällinen, yksikäsitteinen, nimenomainen, eksplisiittinen
Tuolle sanalle *ei* siis ole suomenkielistä vastinetta. :O

Esimerkki aika kiva. :)
editoitu: 16:03 15.6.05
Ezku 15:47 15.6.05 
mikefast kirjoitti:
Ihan pieni juttu tuli tuossa mieleen. Olisi hyvä, jos FrontController ottaisi konstruktorissaan (eksplisiittinen) nuo DEFAULT_PAGE ja DEFAULT_ACTION-toiminteet, jolloin olion tila voidaan tarkasti määrittää ilman ulkoisia riippuvuuksia globaaleista muttujista (hyi, hyi :))
Pitää paikkansa. Se oli viime hetken muutos, kun äkkiä tuumasin että nämä defaultithan on hyvä saada ulos FC:stä käyttäjän määriteltäväksi. Korjasin tuon ja muokkasin PageControlleria hieman; yhdellä PC:llähän saa olla vain yksi Model. Lisäsin abstraktin initialize-metodin Modelin asettamista varten.

Kiitoksia kommenteista! Lisää parannusehdotuksia?
editoitu: 16:23 15.6.05
Ezku 16:11 15.6.05 
Nyt muokkasin noita esimerkin nimityksiäkin hieman selkeämpään suuntaan.

Tuosta sitten tekemään vaikka ohjelma joka antaa käyttäjän asettaa uuden viestin - muokkaa MessageModelin getMessage-metodi hakemaan viesti jostakin ja luo vastaava setMessage-metodi, MessageControllerille sitä käyttävä update-metodi ja asianmukaiset Viewit molemmille.

MVC 1.1 zippinä.
editoitu: 22:21 15.6.05
Ezku 22:15 15.6.05 
Arvostaisin, jos miinuksen antanut kommentoisi hieman päätöstään. Liian iso "koodinpätkäksi"? :<
editoitu: 09:30 16.6.05
harja 09:22 16.6.05 
akx kirjoitti:
Herranjumala.
explicit: selvä, varma, tarkka, täsmällinen, yksikäsitteinen, nimenomainen, eksplisiittinen
Tuolle sanalle *ei* siis ole suomenkielistä vastinetta. :O


Eksplisiittinen siinä mielessä, että halutaan sanoa "tätä sinun tulee käyttää jos et tiedä tarkalleen mitä sinun pitäisi käyttää".

Normaalisti kyllä tätä käytetään erottamaan esimerkiksi konversiotapauksissa kaksi erilaista konstruktoria toisistaan, eli kumpaa käytetään.

:)Mikko
schedler 20:01 17.6.05 
Tämähän on jo hyvä esimerkki ohjelmistosuunnittelusta puhtaan koodaamisen sijaan. Pointsit Ezkulle.

Ensimmäinen kerta kun PHP5:ttä näen, ja tuli aika vahvasti Java mieleen. Koskakohan PHP:ssä pudotetaan '$' muuttujien edestä pois?
editoitu: 13:44 18.6.05
Ezku 13:40 18.6.05 
schedler kirjoitti:
Koskakohan PHP:ssä pudotetaan '$' muuttujien edestä pois?
Toivon mukaan ei ollenkaan. Se helpottaa stringien käsittelyä, mahdollistaa kaikenlaista kikkailua ja mielestäni myös selkeyttää syntaksia. Kutsukaa hulluksi jos tahdotte. :) Sen sijaan luokkien käsittely voisi olla muotoa $instance.method(), sillä $instance->method() on yksinkertaisesti tarpeettoman monimutkainen ilmaisu.

Kiitoksia kommenteista.
harja 13:58 20.6.05 
Sen sijaan luokkien käsittely voisi olla muotoa $instance.method(), sillä $instance->method() on yksinkertaisesti tarpeettoman monimutkainen ilmaisu.


$instance.method menis sekasin "."-katenointioperaattorin kanssa. Paha tehdä parseria :-P Nykyinen operaattori onkin lainatu C-kielestä, koska siihen PHP kuitenkin nojaa aika paljon.

:)Mikko
Ezku 17:21 20.6.05 
mikefast kirjoitti:
$instance.method menis sekasin "."-katenointioperaattorin kanssa. Paha tehdä parseria :-P
No niinpä tietysti. Kappas kun en tullut lainkaan ajatelleeksi.
empty 15:50 21.6.05 
Aikas hyvältä näyttää näin nopeesti vilkaistuna, paljon samaa kuin omassa MVC-frameworkissani. Ihan vaan ehdotuksena, itse ratkaisin tuon action-metodien suorituksen näinkin yksinkertaisella tavalla:

        public function execute($action)
        {
                       $action .= 'action_'.$action; //eli metodin nimeen laitetaan 'action_'- etuliite, jolloin muita metodeita ei pääse suorittamaan, ja koodaajankin on helpompi erottaa action-metodit muista.
                       if (!method_exists($this, $action)) throw new Exception('Eipä löytynyt metodia');
                       $this->onExecute();
                        $this->$action();
                        $this->afterExecute();
        }


Jolloin MessageController näyttäisi tältä:

class MessageController extends PageController
{       

...
        public function action_index()
        {
...
        }
}

sllz 11:49 18.7.05 
Mitä hyötyy täst on
rainmikko 10:07 8.9.05 
Minulla heittää herjan:
Fatal error: Trying to clone an uncloneable object of class ReflectionMethod in ***/mikko/framework/PageController.php on line 69
ja tuo rivi 69 on:
$method = new ReflectionMethod(get_class($this), $action);
Missä mahtaisi olla vika?
PHP:n versio on 5.0.4
rainmikko 22:17 24.9.05 
Ape 15:20 22.12.05 
Ei toimi. Tulee: Parse error: parse error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' in /mbnet/l/levelup/testi.php on line 26
editoitu: 22:06 22.12.05
Ezku 22:02 22.12.05 
Ape kirjoitti:
Ei toimi. Tulee: Parse error: parse error, unexpected T_STRING, expecting T_OLD_FUNCTION or T_FUNCTION or T_VAR or '}' in /mbnet/l/levelup/testi.php on line 26
Kyllä toimii. PHP-versiosi on väärä (MBnetissä on 4.x ja skripti vaatii 5.0:n, kuten kuvauksessa lukee).
editoitu: 21:41 26.2.06
tgs3 00:11 18.2.06 
Ei perkele =D Ei joku vois selittää (vielä kerran) sillein yksinkertaisesti mikä on MVC? Ekan kerran putosin PHP ohjelmoinnissa totaalisesti pois kärryiltä! =P

EDIT: Aa no NYT mä tajusin! =D
Irksome 08:41 1.2.08 
Sitä tikulla silmään joka vanhoja kaivelee, mutta onkos tuolla pieni erhe?


PHP
        /**
         * Clear template stack
         */

        public function clear()
        {
                $this->templates[] = array();
        }


Tuohan ei templates-arrayta tyhjennä, vaan lisää perään yhden tyhjän taulukon.
qin 14:49 1.2.08 
Annoin miinuksen, koska pätkä on liian pitkä eikä muutenkaan vaikuta kiinnostavalta.
editoitu: 18:58 1.2.08
Irksome 18:58 1.2.08 
qin kirjoitti:
Annoin miinuksen, koska pätkä on liian pitkä eikä muutenkaan vaikuta kiinnostavalta.

Ehkä HIEMAN väärät periaatteet jakaa pojoja sulla :) Vinkkihän on mitä erinomaisin ja jos et itse siitä saa mitään irti, se ei välttämättä ole vinkin vika.