Привет тебе, коллега-разработчик или просто случайно зашедший на мой блог посетитель. В сегодняшней заметке я хочу рассказать вам о том, как прошел у меня вчерашний вечер (нет, нет, тут не будет ничего личного, аля покатался на роликах, попил пивка в парке). Как и у каждого веб-программиста рано или поздно возникает идея создать свой велосипед, пусть и с квадратными колесами и вместо руля торчащий штырь. Вчерашним велосипедом для меня стал легкий простой фреймворк, хотя с натяжкой его можно так назвать, но есть несколько моментов в нем, которые могут послужить отправной точкой для создание нечто большего. Ну обо всем по порядку.
Структура папок
Так как на меня большое влияние в последнее время оказал Zend Framework, структура папок, а также несколько еще штук будут очень похожи.
В корне мы имеем две папки app и library. Также в корне есть два файла (опять же все как у ZF) .htaccess (будет перенаправлять все запросы в index.php) и сам index.php, который эти самые запросы принимать будет.
Мне этого было более, чем достаточно. По сути этот .htaccess перенаправляет все запросы, если они не ведут на существующую директорию или файл нашему index.php.
index.php
123456789101112131415161718192021
<?php// объявляем нужные константыdefine('APPLICATION_PATH',realpath(dirname(__FILE__).DIRECTORY_SEPARATOR.'app'));define('LIBRARY_PATH',realpath(dirname(__FILE__).DIRECTORY_SEPARATOR.'library'));// добавляем путь к library к indlude pathset_include_path(implode(DIRECTORY_SEPARATOR,array(realpath(dirname(__FILE__).DIRECTORY_SEPARATOR.'library'),get_include_path())));// подключаем Автолоадерrequire_once'MVC/Autoload/Autoloader.php';// регистрируем наш автолоадерspl_autoload_register(array('MVC_Autoload_Autoloader','autoload'));// создаем инстант нашего "Фронт Контроллера"// в кавычках он потому, что это вовсе не паттерн Фронт Контроллер )$frontController=newMVC_FrontController($_SERVER);// запускаем наше приложение$frontController->run();
папка library
В данной папке будет пока две директории MVC и Smarty, первая – это наш фреймворк, вторая как вы уже догадались – шаблонизатор :)
<?phpclassMVC_Autoload_Autoloader{publicstaticfunctionautoload($className){// все как в ZF, чтобы отыскать класс заменяем в нем// '_' на '/'$className=str_replace('_','/',$className);$classPath=LIBRARY_PATH.DIRECTORY_SEPARATOR.$className.'.php';if(file_exists($classPath)&&is_readable($classPath)){// подключаем его, если файл имеется и мы имеем к нему доступrequire_once$classPath;}else{//throw new MVC_Exception("Class name '{$className}' not found");}//echo $classPath;}// тут я планировал указывать какие еще префиксы загружать автоматическиpublicstaticfunctionregisterAutoload(){}}
MVC_Autoload_Resource – будет отвечать за подгрузку контроллеров, правда получился он не особо гибким и пути были жестко прописаны в код (очень не хорошо получилось).
<?phpclassMVC_Autoload_Resource{// думал будет у меня несколько ресурсов// аля контроллеры, модели и прочие штукиprotectedstatic$_resources=array('controller'=>'@App_Controller_(\w+)Controller@',);// метод без проверок, т.к. фреймворк создавался для ознакомительных целейpublicstaticfunctionautoload($className){// пробегаемся по всем ресурсам// и смотрим на что заканчивает классforeach(self::$_resourcesas$resource=>$path){// если на то, что нужно подгружаем егоif(ucfirst($resource)==substr($className,-strlen($resource))){if(preg_match(self::$_resources[$resource],$className,$matches)){self::load($matches[1],$resource);}}}}protectedstaticfunctionload($name,$resourceType){switch($resourceType){case'controller':require_onceAPPLICATION_PATH.DIRECTORY_SEPARATOR.'controllers'.DIRECTORY_SEPARATOR."{$name}Controller.php";break;default:break;}}}
Стукрутра папок в приложении (App)
3 папки: config, controllers, views
config – содержит на данный момент всего один файл с настройками приложения application.php
<?php$settings=array('application'=>array('applicationName'=>'first application'),// самое интересное, наверное, в этом фреймворке'routes'=>array(// все рауты имеют имя, шаблон или регулярное выражение// контроллер, экшн и параметры'static'=>array('template'=>'^(\w+)\.html$','controller'=>'static','action'=>'show','params'=>array('name'=>1)),'main'=>array('template'=>null,'controller'=>'index','action'=>'index'),'dynamic'=>array('template'=>'^(\w+)\/(\w+)$','controller'=>'dynamic','action'=>'show','params'=>array('category'=>1,'article'=>2))),'view'=>array(// где будут лежать наши шаблоны (views)'viewPath'=>APPLICATION_PATH.DIRECTORY_SEPARATOR.'views',// имя нашего шаблона (layout)'template'=>'template.tpl'));
Вскользь хочу упоминуть лишь некоторые классы
MVC_Registry – стандартный паттерн Registry без излишеств, нужен нам для того, чтобы хранить данные и предоставлять доступ к ним из любого места в приложении.
MVC_Request – класс запроса, который мы строим в нашем “Фронт контроллере” и передаем в контроллеры приложения. Содержит параметры в запросе, активный контроллер и экшн.
MVC_View – класс, который просто инициализует Smarty и устанавливает все нужные нам значения путей для шаблонов.
“Front Controller”
Этот класс – точка входа для нашего приложение, хоть и должен был он реализован быть как Одиночка (Singleton)
<?phpclassMVC_FrontController{protected$_routes;protected$_settings;protected$_controller;protected$_action;protected$_params;publicfunction__construct($request){$this->_initConfigs();$this->_initResources();$this->_initRoutes();}protectedfunction_parseRequest(){return$_SERVER['REQUEST_URI'];}publicfunctionrun(){$uri=$this->_parseRequest();$activeRoute=$this->_checkActiveRoute($uri);if(null===$activeRoute)thrownewMVC_Exception('Cannot find active route');$this->_dispatch($activeRoute);$controllerName=sprintf('App_Controller_%sController',ucfirst($this->_controller));$controllerObj=new$controllerName(newMVC_Request(array('params'=>$this->_params,'controller'=>$this->_controller,'action'=>$this->_action)));$methodName=$this->_action.'Action';if(method_exists($controllerObj,$methodName)){$controllerObj->$methodName();$controllerObj->render();}else{thrownewMVC_Exception('action not found');}}protectedfunction_initConfigs(){require_onceAPPLICATION_PATH.DIRECTORY_SEPARATOR.'config'.DIRECTORY_SEPARATOR.'application.php';MVC_Registry::getInstance()->set('settings',$settings);$this->_settings=$settings;}/** * init resource autoloading */protectedfunction_initResources(){spl_autoload_register(array('MVC_Autoload_Resource','autoload'));}/** * init routes */protectedfunction_initRoutes(){if(isset($this->_settings['routes'])){$this->_routes=$this->_settings['routes'];}else{thrownewMVC_Exception('Routes must be specified');}}/** * get active route name * * @param string $uri * @return string active route name */protectedfunction_checkActiveRoute($uri){$uri=substr($uri,1);if(trim($uri)){$activeRoute=null;foreach($this->_routesas$name=>$routeSettings){if(!$routeSettings['template'])continue;if(preg_match('@'.$routeSettings['template'].'@',$uri,$matches)){if(isset($routeSettings['params'])){foreach($routeSettings['params']as$paramName=>$param){$this->_params[$paramName]=$matches[$param];}}$activeRoute=$name;}}}else{$activeRoute='main';}return$activeRoute;}/** * dispatch * * @param string $activeRoute active route name */protectedfunction_dispatch($activeRoute){if(isset($this->_routes[$activeRoute])){$this->_controller=$this->_routes[$activeRoute]['controller'];$this->_action=$this->_routes[$activeRoute]['action'];}}}
Я старался комментировать не особо очевидные места, но если возникнут вопросы, обязательно задавайте их в комментериях внизу заметки. Метод _checkActiveRoute() определяет в зависимости от REQUEST_URI активный раут. И заполняет параметры для контроллера нашего приложения.
Метод _dispatch() по активному рауту определяет нужный в данный момент контроллер и экшн.
Метод run() запускает наш контроллер, передавая ему объект запроса с нужными нам параметрами. После запуска нужного экшна мы запускаем метод контроллера render() который и выводит контент.
MVC_Controller_Abstract
Этот класс раширяют все контроллеры нашего приложения, единственный метод, на котором стоит заостроить внимание это render()
1234567891011121314151617181920
publicfunctionrender(){$templatePath=$this->_view->template_dir.DIRECTORY_SEPARATOR;$templatePath.=strtolower($this->getRequest()->getController().DIRECTORY_SEPARATOR.$this->getRequest()->getAction().'.tpl');if(file_exists($templatePath)&&is_readable($templatePath)){$this->getView()->assign('tplName',$templatePath);if(null===$this->_view->template)$this->getView()->display('template.tpl');else{$this->getView()->display($this->getView()->template);}}else{thrownewMVC_Exception("Template '{$templatePath}' not found");}}
Допустим мы прошли по адресу example.com/ нашего приложения. Наш “Фронт Контроллер” запустит app/controllers/IndexController.php и метод indexAction(). В нем, к примеру, мы присвоим переменной name значение vredniy
Дальше метод MVC_Controller_Abstract::render() отыщет наш шаблон (layout). В данном случает это, находящийся в папке app/views, файл template.tpl. Приведу его содержимое
Это и есть основной шаблон для нашего приложения. Вы спросите, а как же тогда выводится переменная name, значение которой мы присваивали в IndexController::indexAction(). Все просто: встроенная конструкция Smarty {include file=“$tplName”} заменяется содержимым соответствующего экшна видом.
Метода render() в MVC_Controller_Abstract
Вот вроде и все. Данный фреймворк не предназначен для построения сложных сайтов, да и вообще для сайтов. Да и вообще ни для чего он не предназначен, кроме понимания некоторых важных для любого фреймворка вещей. В данном ознакомительном фреймворке куча ошибок и недоработок, реализация или исправления которых могли существенно захломить тестовый фреймворк и намного усложнить его понимание.
Спасибо всем за внимание, надеюсь, хоть часть мною написанного вам пригодится. Ах да, чуть не забыл прилагаю к заметке ссылку на репозиторий, чтобы вы смогли скачать данный фреймворк и запустить у себя.
UPDATE: Сегодня решил, что в связке MVC не может не быть модели, поэтому решил я ее сегодня дописать.
Модель в MVC
Добавил я два класса: первый это MVC_Db_Exception, чтобы выбрасывать эксепшны не просто MVC_Exception. Второй – освновной MVC_Db_Abstract, который мы будем расширять в модели. Приведу его код:
<?phpclassMVC_Db_Abstract{/** * PDO object * * @var Pdo */private$_connection;/** * PDO options * * @var array */protected$_options=array(PDO::MYSQL_ATTR_INIT_COMMAND=>"SET NAMES 'UTF8'");/** * PDO attributes * * @param array|string $attribs */publicfunction__construct(){$this->_initConnection();}// инициализуем соединение, если его еще нетprivatefunction_initConnection(){if(!$this->_connectioninstanceofPDO){$settings=MVC_Registry::get('settings');$dbSettings=$settings['database'];$dsn=sprintf('%s:host=%s;dbname=%s',$dbSettings['adapter'],$dbSettings['params']['host'],$dbSettings['params']['dbname']);try{$this->_connection=newPDO($dsn,$dbSettings['params']['username'],$dbSettings['params']['password'],$this->_options);}catch(PDOException$e){echo'Connection failed: '.$e->getMessage();}}}// возвращаем соединение, которое и будем использовать в моделяхpublicfunctiongetConnection(){return$this->_connection;}}
И внимание, кто пробует это проделать по заметке, будьте внимательны, потому что я чуть дописал MVC_Autoload_Resource, чтобы он помимо контроллеров мог подгружать еще и наши модельки. Самая актуальная версия в репозитории.
Использование модели
Создадим папку app/models, где и будут лежать наши модели. В ней создадим файл PageModel.php с таким содержимым
123456789101112131415161718192021222324
<?phpclassApp_Model_PageModelextendsMVC_Db_Abstract{/** * get page id * * @param int */publicfunctiongetPage($id){$statementString='SELECT id, alias, content FROM pages WHERE %s = ?';if(is_int($id)){$statementString=sprintf($statementString,'id');}else{thrownewMVC_Db_Exception('id must be int');}$statement=$this->getConnection()->prepare($statementString);$statement->execute(array($id));return$statement->fetch();}}
Данная модель умеет извлекать из таблицы pages нужные нам данные с помощью PHP Data Object (PHP) по id. Дамп базы данных я выложил в репозитории в папке data/dumps
Теперь в контроллере попробуем использовать нашу модель. Я взял для примера IndexController, в index экшн которого я дописал следующее.
123456789
publicfunctionindexAction(){// создаем нашу модель$model=newApp_Model_PageModel();// извлекаем из нее нужные нам данные и// отдаем их Smarty, т.е. во View$this->getView()->assign('data',$model->getPage(1));$this->getView()->assign('name','Vredniy');}
В шаблоне views/index/index.tpl я добавил три строчки, которые и будут выводить содержимое: