Introducere in Design Patterns – MVC (partea I)
13Introducere in Design Patterns
Problema?
Una dintre cele mai mari probleme ce sunt intalnite in proiectele de anvergura este organizarea eficienta pentru a putea lucra cu o echipa de programatori. De asemenea, cu totii realizam la un moment dat ca scriem cod prost. Eu definesc codul prost ca fiind nepregatit pentru reutilizare in alte aplicatii. Nu de multe ori suntem nevoiti sa facem sisteme de inregistrare, de autentificare, avem nevoie de numeroase modalitati de a accesa bazele de date etc. Dar, toate aceste probleme care pot fi scrise o data si folosite ulterior au ceva in comun : o data implimentate, modificarile ce trebuie aduse pentru a putea fi refolosite sunt minime. Insa ce ne facem daca incercam sa editam acel cod dupa un an?
Solutia?
Evident, solutia sta in mainile noastre. Trebuie sa gandim o modalitate de lucru ce se potriveste perfect pentru orice tip de proiect, spunem noi, trebuie sa stabilim arhitectura ce o au in comun toate proiectele. Practic, un punct de reper pentru a descoperi un programator bun, indiferent de limbajele in care lucreaza, este abilitatea acestuia de a aplica tehnici cunoscute in industrie pentru a arhitecturiza proiectele intr-un mod eficient. Design Patterns (sau proiectarea modulelor) sunt un set de solutii ce pot fi aplicate la diverse probleme ce se intalnesc frecvent in cadrul programarii orientate pe obiecte (POO). Cateva din cele mai populare design patternuri folosite la scara larga sunt Singleton, Factory, Registry si MVC. Acestea sunt prezentate foarte frumos in articolul Design Patterns de pe Tutorial Web sau pe WorldIT.
Primele trei modalitati de lucru sunt simple, in vreme ce MVC reprezinta o structura complexa si foarte eficienta, ce se potriveste cu aproape orice tip de proiect, fiind recomandat si folosit in cele de anvergura. In urmatoarele zile ma voi concentra pe explicarea intr-un mod cat mai clar pe utilitatea si modul de lucru a acestei arhitecturi – MVC.
Introducere in MVC
Ce este MVC?
MVC, sau Model-View-Controller este un sablon arhitectural folosit in industria de software development(inclusiv web development). Aceasta modalitate de lucru reuseste cu succes izolarea partii logice de interfata proiectului, rezultand in aplicatii extrem de usor de modificat. In organizarea MVC, modelul reprezinta informatia (datele) de care are nevoie aplicatia, viewerul corespunde cu elementele de interfata iar controller-ul reprezinta sistemul comunicativ si decizional ce proceseaza datele informationale, facand legatura intre model si view.
Despre Model, View si Controller
Modelul reprezinta partea de hard-programming, partea logica a aplicatiei. El are in responsabilitate actiunile si operatiile asupra datelor, autentificarea utilizatorilor, integrarea diverselor clase ce permit procesarea informatiilor din diverse baze de date.
View-ul se ocupa de afisarea datelor, practic aceasta parte a programului va avea grija de cum vede end-userul informatia procesata de controller. O data ce functiile sunt executate de model, viewului ii sunt oferite rezultatele, iar acesta le va trimite catre browser. In general viewul este o mini-aplicatie ce ajuta la randarea unor informatii, avand la baza diverse template-uri.
Controller-ul reprezinta creierul aplicatiei. Aceasta face legatura intre model si view, intre actiunile userului si partea decizionala a aplicatiei. In functie de nevoile utilizatorului, controllerul apeleaza diverse functii definite special pentru sectiunea de site in care se afla userul. Functia se va folosi de model pentru a prelucra (extrage, actualiza) datele, dupa care informatiile noi vor fi trimise catre view, ce le va afisa apoi prin template-uri.
Structura unei aplicatii folosind arhitectura MVC
Acest tutorial va prezenta structura ce o folosesc personal pentru a organiza in format MVC un nou framework. Aceasta serie de articole se va invarti in jurul acestei structuri.
• application
aplicatie1
controller
model
view
aplicatie2
aplicatie3
• config
• db
• library
cache
controller
model
dbs
view
templates
• public
aplicatie1
css
img
js
swf
aplicatie2
aplicatie3
• tmp
cache
logs
sessions
Tutorial MVC – crearea structurii de baza a framework-ului
Definirea unui singur punct accesibil din partea clientului
Prima problema ce o intalnim este stabilirea unui singur punct de intrare, un index.php care va avea grija de tot si toate. Pentru a usura acest proces ne vom folosi de URL redirecting si permalinks, care sunt evident configurate din .htaccess.
Mai intai adaugam un fisier .htaccess in root-ul arhitecturii cu urmatorul cod. Acesta va redirecta totul catre folderul /public al aceleiasi arhitecturi.
<IfModule mod_rewrite.c> RewriteEngine on RewriteRule ^$ public/ [L] RewriteRule (.*) public/$1 [L] </IfModule>
;
Urmeaza sa cream un nou fisier .htaccess in directorul /public.
<IfModule mod_rewrite.c> RewriteEngine On RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteRule ^(.*)$ index.php?url=$1 [PT,L] </IfModule>
Fisierul public/index.php
Aceasta pagina va avea declarate cateva constante si va incarca bootstrap-ul.
<?php /** * Use the DS to separate the directories in other defines */ if(!defined('DS')) define('DS',DIRECTORY_SEPARATOR); /** * The full path to the directory which holds "app", WITHOUT a trailing DS. * */ if(!defined('ROOT')) define('ROOT',dirname(dirname(__FILE__))); /** * The actual directory name for the "app". * */ if (!defined('APP_DIR')) define('APP_DIR', ROOT . DS . 'application'); /** * The actual WWW_ROOT Directory. It has a trailing DS. * */ if (!defined('WWW_ROOT')) define('WWW_ROOT', dirname(__FILE__) . DS); if(!defined('CORE_PATH')) define('CORE_PATH',ROOT . DS . 'library' . DS ); if(!defined('CONFIG_DIR')) define('CONFIG_DIR',ROOT . DS . 'config' ); if(file_exists(CORE_PATH . 'bootstrap.php')) require_once(CORE_PATH . 'bootstrap.php'); else trigger_error('Framework core could not be found. Check the value of CORE_PATH. IT should point to '.DS.' library.'.DS.'bootstrap.php .'); ?>
Fisierul library/basics.php
Acest fisier contine, dupa cum spune si numele, cateva functii de baza, de care ne vom izbi pe tot parcursul procesului de dezvoltare al arhitecturii. Functia setReporting() stabileste daca suntem in timpul dezvoltarii si trateaza variabile pe ecran, in vreme ce in afara starii de developer, erorile vor fi stocate pe disc. Functia killMagicQuotes() va cauta magic quotes si le va elimina, unregisterGlobals() va elimina variabilele globale, __autoload() este una din functiile magice ce se bazeaza pe overloading ce va face putina magie : va incarca toate fisierele necesare pentru clase. Functia clear este functia care se va apela recursiv curatand un vector de orice dimensiune/adancime prin htmlentities.
<?php /** Check if environment is development and display errors **/ function setReporting() { if(Configure::read('Dev.Enviroment') === TRUE) { error_reporting(E_ALL); ini_set('display_errors','On'); } else { error_reporting(E_ALL); ini_set('display_errors','Off'); ini_set('log_errors','On'); ini_set('error_log',ROOT.DS.'tmp'.DS.'logs'.DS.'error.log'); } } /** Check for magic quotes and remove it **/ function stripSlashDeep($value) { return (is_array($value) ? array_map('stripSlashDeep',$value) : stripslashes($value)); } function killMagicQuotes() { if(get_magic_quotes_gpc()) { $_GET = stripSlashesDeep($_GET); $_POST = stripSlashesDeep($_POST); $_COOKIE = stripSlashesDeep($_COOKIE); //$_SERVER = stripSlashesDeep($_SERVER); //$_SESSION = stripSlashesDeep($_SESSION); } } /** Check register globals and remove them **/ function unregisterGlobals() { if (ini_get('register_globals')) { $array = array('_SESSION', '_POST', '_GET', '_COOKIE', '_REQUEST', '_SERVER', '_ENV', '_FILES'); foreach ($array as $value) foreach ($GLOBALS[$value] as $key => $var) if ($var === $GLOBALS[$key]) unset($GLOBALS[$key]); } } /** Autoload any classes that are required **/ function __autoload($className) { if (file_exists(LIB_CONTROLLER . DS . strtolower($className) . '.php')) require_once(LIB_CONTROLLER . DS . strtolower($className) . '.php'); else if (file_exists(LIB_MODEL . DS . strtolower($className) . '.php')) require_once(LIB_MODEL . DS . strtolower($className) . '.php'); else if (file_exists(LIB_VIEW . DS . strtolower($className) . '.php')) require_once(LIB_VIEW . DS . strtolower($className) . '.php'); else if (file_exists(APP_CONTROLLER . DS . strtolower($className) . '.php')) require_once(APP_CONTROLLER . DS . strtolower($className) . '.php'); else if (file_exists(APP_MODEL . DS . strtolower($className) . '.php')) require_once(APP_MODEL . DS . strtolower($className) . '.php'); else if(file_exists(LIB_MODEL . DS . 'dbs' . DS . strtolower($className) . '.php')) require_once(LIB_MODEL . DS . 'dbs' . DS . strtolower($className) . '.php'); else { /* Error Generation Code Here **/ // trigger_error('We couldn`t find '.$className.' .'); } } ?>
Fisierul library/bootstrap.php
In acest fisier vom incarca cateva din cele mai importante fisiere. Vom apela functiile din basics.php prezentate anterior pentru a curata variabilele si a filtra putin informatiile. Acest fisier va incarca una din clasele cele mai importante : dispatcherul, sau cel care spune cine sa faca ce. Dispatcherul functioneaza in urmatorul mod : www.worldit.info/controller/actiune/query. Pentru a intelege mai exact cum functioneaza dispatcherul ne vom imagina ca avem urmatorul link :
• www.worldit.info/news/show/id/1
Controllerul e news
Modelul este new
Viewul este este show
Actiunea este show
Query-ul este un array (id,1)
<?php if (!defined('PHP5')) { define('PHP5', (PHP_VERSION >= 5)); } if(!defined('BOOTSTRAP')) { require_once(ROOT . DS . 'library' . DS . 'paths.php'); require_once(LIBRARY_DIR . DS . 'basics.php'); require_once(LIBRARY_DIR . DS . 'dispatcher.php'); require_once(LIBRARY_DIR . DS . 'cache.php'); require_once(LIBRARY_DIR . DS . 'configure.php'); require_once(CONFIG_DIR . DS . 'core.php'); require_once(CONFIG_DIR . DS . 'config.php'); } setReporting(); killMagicQuotes(); unregisterGlobals(); $Dispatcher = new Dispatcher($_GET['url'],array('render' => TRUE, 'layout' => TRUE)); ?>
Fisierul library/paths.php
Acest fisier contine cateva constante ce ne vor ajuta pe parcurs la utilizarea eficienta a cailor relative si absolute ale fisierelor framework-ului.
<?php if(!defined('APP')) define('APP', 'apptest'); if (!defined('APP_CONTROLLER')) define('APP_CONTROLLER', APP_DIR . DS . APP . DS . 'controller'); if (!defined('APP_MODEL')) define('APP_MODEL', APP_DIR . DS . APP . DS . 'model'); if (!defined('APP_VIEW')) define('APP_VIEW', APP_DIR . DS . APP . DS . 'view'); if (!defined('LIBRARY_DIR')) define('LIBRARY_DIR', ROOT . DS . 'library'); if (!defined('LIB_CONTROLLER')) define('LIB_CONTROLLER', LIBRARY_DIR . DS . 'controller'); if (!defined('LIB_MODEL')) define('LIB_MODEL', LIBRARY_DIR . DS . 'model'); if (!defined('LIB_VIEW')) define('LIB_VIEW', LIBRARY_DIR . DS . 'view'); if (!defined('LIB_TEMPLATES')) define('LIB_TEMPLATES', LIB_VIEW . DS . 'templates'); if (!defined('TMP')) define('TMP', ROOT . DS . 'tmp'); if (!defined('CACHE')) define('CACHE', TMP . DS . 'cache'); ?>
Clasa Object – baza controalelor (library/controller/object.php)
Aceasta clasa este putin mai dezvoltata decat o clasa abstracta, prezentand cateva din functiile de baza necesare in toate clasele noastre ulterioare. Prezinta functiile __construct(), __destruct(), __toString() si log(), create special doar pentru overloading ulterior in clasele copii. Functia _set() este utila pentru a seta variabile dinamic iar dispatchMethod() apeleaza o functie din interiorul obiectului, fiind o optimizare smechera pentru imbunatatirea performantelor.
<?php class Object { /** * Log object * * @var FrameworkLog * @access protected **/ var $_log; /** * Returns a singleton instance * * @return object * @access public * @static **/ public static function &getInstance() { static $instance = array(); $name = get_class($this); if (!$instance) $instance[0] =& new $name(); return $instance[0]; } /** * Class constructor, overridden in descendant classes. **/ public function __construct() { //this will be overridden by other classes } /** * Class destructor, overridden in descendant classes. **/ public function __destruct() { //this will be overridden by other classes } /** * Object-to-string conversion. * Each class can override this method as necessary. * * @return string The name of this class * @access public **/ public function __toString() { $class = get_class($this); return $class; } /** * Stop execution of the current script, with an error code * * @param $status - exit with an error code * @return void * @access public **/ public function _stop($status = 0) { exit($status); } /** * API for logging events. * * @param string $msg Log message * @param integer $type Error type constant. * @return boolean Success of log write * @access public **/ public function log($msg = '', $error = 0) { //this will be overriden soon return FALSE; } /** * You can add multiple proprieties of the object in a single line of code. * * @param array $properties An associative array containing properties and corresponding values. * @return void * @access protected **/ function _set($prop = array()) { if (is_array($prop) && !empty($prop)) { $vars = get_object_vars($this); foreach ($prop as $key => $val) if (array_key_exists($key, $vars)) $this->{$key} = $val; } } /** * Calls a method on this object with the given parameters. Provides an OO wrapper * for call_user_func_array, and improves performance by using straight method calls * in most cases. * * @param string $method Name of the method to call * @param array $params Parameter list to use when calling $method * @return mixed Returns the result of the method call * @access public **/ public function dispatchMethod($method, $params = array()) { switch (sizeof($params)) { case 0:return $this->{$method}(); case 1:return $this->{$method}($params[0]); case 2:return $this->{$method}($params[0], $params[1]); case 3:return $this->{$method}($params[0], $params[1], $params[2]); case 4:return $this->{$method}($params[0], $params[1], $params[2], $params[3]); default:return call_user_func_array(array(&$this, $method), $params); break; } } } ?>
Clasa Dispatcher – managerul MVC (library/dispatcher.php)
Aceasta clasa, dupa cum spuneam si mai sus, reprezinta unul din creierii principali, o parte foarte importanta ce dirijeaza toate creierasele mai mici (controllerele). Functia dispatch() proceseaza prin intermediul unor functii auxiliare (_extractParams(),_parseUrl(),_restructureParams(),_getController()) parametri oferiti de utilizator prin intermediul URI si incarca controllerul de care avem nevoie, la actiunea ceruta cu parametrii (query) oferiti. De aceasta parte a procesului se ocupa functia _invoke().
<?php class Dispatcher extends Object { /** * the params for this request * * @var array * @access public **/ var $params = NULL; /** * Constructor. **/ function __construct($url = NULL, $additionalParams = array()) { return $this->dispatch($url, $additionalParams); } /** * Dispatches and invokes given URL, handing over control to the involved controllers, and then renders the results. (if autorender) * @param string $url URL information to work on * @param array $additionalParams Settings array which is melded with the GET params * @return boolean Success * @access public **/ function dispatch($url = NULL, $additionalParams = array()) { if(is_array($url)) { $url = $this->_extractParams($url); } $url = $this->_parseUrl($url); $this->_restructureParams($url,$additionalParams); $controller =& $this->_getController(); if(is_object($controller)) { if(isset($this->params['render'])) $controller->autoRender = $this->params['render']; else $controller->autoRender = FALSE; if(isset($this->params['layout'])) $controller->autoLayout = $this->params['layout']; else $controller->autoLayout = FALSE; $controller->here = APP_CONTROLLER; //TODO : Sure? $controller->_set($this->params); return $this->_invoke($controller); } else { $this->params = NULL; trigger_error('Invalid controller',E_USER_NOTICE); } return FALSE; } public function _invoke(&$controller) { $controller->startProc(); $output = $controller->dispatchMethod($this->params['action'], $this->params['params']); if ($controller->autoRender) $controller->output = $controller->render(APP_VIEW . DS . $controller->action . DS . $controller->action .$controller->ext); elseif (empty($controller->output)) $controller->output = $output; if(isset($this->params['return']) && $this->params['return']) return $controller->output; echo $controller->output; } /** * Sets the params when $url is passed as an array to string Action * @param array $url information array to work on * @access public **/ function _extractParams($url) { return implode('/', $url); } /** * Sets the params when $url is passed as an array to string Action * @param string $url information array to work on * @access public **/ function _parseUrl($url) { return explode('/',$url); } /** * Restructure Params if we deal with a plugin * @param string $url information array to work on * @param array $additionalParams Settings array which is melded with the GET params * @access protected **/ private function _restructureParams($url, $additionalParams) { $this->params = array(); $this->params['name'] = ($ctrl = ucwords(trim(array_shift($url)))) != '' ? $ctrl : 'Index'; $this->params['controller'] = $this->params['name'].'Controller'; $this->params['model'] = rtrim($this->params['name'],'s'); $this->params['action'] = ($act = array_shift($url)) != '' ? $act : 'index'; $this->params['return'] = isset($additionalParams['return'])?$additionalParams['return']:TRUE; $this->params['layout'] = isset($additionalParams['layout'])?$additionalParams['layout']:TRUE; $this->params['render'] = isset($additionalParams['render'])?$additionalParams['render']:TRUE; unset($additionalParams['return'],$additionalParams['layout'],$additionalParams['render']); $this->params['params'] = array_merge($url, $additionalParams); } /** * Get controller to use, either plugin controller or application controller * @return mixed name of controller if not loaded, or object if loaded * @access private **/ function _getController() { if(class_exists($this->params['controller'])) { $controller =& new $this->params['controller'](); return $controller; } else { return $this->params['controller']; } } } ?>
Cam atat pentru prima parte care deja s-a intins foarte mult. Pana in acest moment am reusit sa introducem o parte din partile componente principale ale unui framework ce foloseste o structura de lucru MVC. In cea de-a doua parte a articolului voi introduce partile componente principale : Model View Controller. Daca ati digerat acest articol si nu aveti rabdare pana voi termina urmatoarea parte, puteti arunca o privire pe intregul framework, care poate fi inteles daca urmariti documentatia scrisa de mine pentru aproape toate functiile. Framework-ul din aceasta arhiva prezinta toate caracteristicile de baza de care are nevoie unul cu exceptia debuggerului. 🙂
Imi place implementarea clasei Object dar de ce static $instance este de tip array 😕
E o poveste mai lunga. La inceput aveam o intentie de a face un array mai „special” de obiecte statice, dar mi-am dat seama ca pana la urma va fi inutil. 🙂
E cumva derivat de-aici? http://anantgarg.com/2009/03/13/write-your-own-php-mvc-framework-part-1/ Cam asa arata, doar ca minified.
Hmm, imi amintesc acum de tutorialul asta. E unul cu care am inceput studiul MVC in urma cu ceva vreme, din el am pastrat doar ce m-a interesat. Prima parte(adica aceasta) a articolului include ceva asemanator cu respectivul articol, restul partilor vei vedea si tu ca nu mai au nici o legatura cu el. 🙂
Mea culpa, si eu am primele 3 pagini de „PHP MVC Tutorials” de pe Google accesate, si mi-a sarit in ochi stripSlashesDeep-ul, de-aia am vrut sa stiu 😀
Salut, imi place articolul tau dar am incercat sa il execut pe calculatorul meu si am intilnit urmatoarele probleme:
Notice: Undefined offset: 0 in C:\xampp\htdocs\MVC\library\shared.php on line 57
Deprecated: Assigning the return value of new by reference is deprecated in C:\xampp\htdocs\MVC\library\controller.class.php on line 15
Deprecated: Assigning the return value of new by reference is deprecated in C:\xampp\htdocs\MVC\library\controller.class.php on line 16
Fatal error: Class ” not found in C:\xampp\htdocs\MVC\library\controller.class.php on line 15
poti sa ma ajuti?
Asta e prima parte a tutorialului. Incearca sa descarci partea a treia, ea contine toate dependetele frameworkului. 🙂
Eu nu gasesc linku de unde pot sa descarc partea a 3, poate ma ajuti tu?
A doua parte si ultima. Enjoy.
Poti explica putin fisierele .htaccess, te rog 😀 In rest foarte tare articolul
Vezi aici 🙂
foarte util, pacat ca de abia acum am aflat de MVC
Foarte fain . Super articolul doar am citit pana acum . Looks nice