Pull to refresh

Каркас для web-приложений, построенный на CodeIgniter

Reading time 9 min
Views 36K
image
Наверняка, многие веб-программисты изучали и, может быть, даже использовали такой замечательный фреймворк как CodeIgniter. Мой выбор пал на него ввиду того, что у него самый низкий порог вхождения, он наиболее прост в изучении, хорошая документация, быстрый и т.д. и т.п. Для простых проектов самое «оно», чтоб попробовать свои силы именно как разработчик. Само собой, для более серьезных проектов лучше использовать более функциональные и навороченные фреймворки.

Далее буду описывать, как я «апгрейдил» CodeIgniter, чтобы использовать этот каркас для разных проектов, т.к. базовый его функционал и примеры из документации, мягко говоря, очень простые, а в жизни всё гораздо сложнее. Итак, начнем-с.

Перед прочтением очень рекомендуется ознакомиться с официальной документацией по CodeIgniter, т.к. в статье предполагается, что вы хотя бы прочитали основные темы и тему «Класс Template Parser» и выполнили эти примеры.
Первое, что очень неудобно в базовой конфигурации – это разделение контроллеров, моделей и отображений по разным папкам без возможности объединения какого-то модуля в одну папку. Т.е. если вы хотите написать, к примеру, модуль “News”, выводящий новости, ваш модуль расползется по трем разным папкам controllers, models, views. И вскоре будет непонятно, какой контроллер относится к какой модели и к каким «вьюшкам». Это хорошо, если у вас один такой модуль. А если их больше 10, то контролировать это становится очень тяжело.

Да, можно внутри папок (controllers, models, views) создавать подпапки (например, news, menu, comments) и кидать туда наши контроллеры, модели и вьюшки, как сказано в документации, но, мне кажется, это всё равно неудобно.
Гораздо удобнее было бы, если бы у нас была папка modules, а в ней мы создавали наш модуль News, т.е. папку, а внутри нее уже 3 папки (контр-ы, модели, вьюхи). Данный функционал нам предоставляет расширение HMVC для CodeIgniter. Скачать и прочитать инструкцию по установке можно по этой ссылке.

Это расширение позволит нам внутри каждого модуля располагать свои модели, контроллеры, вьюшки, относящиеся только к этому модулю. Также можно внутри этих модулей создавать подпапки внутри папок моделей, контр-ов или вьюшек, что очень удобно для больших модулей, имеющих много файлов.

Более того, это позволяет нам загружать несколько контроллеров или моделей с разных модулей и строить удобную нам структуру (или запускать из одной модели контроллер другого модуля и т.п.).
В этом я, конечно, ничего нового не открыл. Поэтому идем дальше.
В базовом CI-е URL разбирается следующим образом:
example.com/class/function/ID

Т.е. первый сегмент – это вызываемый контроллер (класс), второй – функция в нем, третий – параметр, передаваемый в функцию (может быть и четвертый и пятый). Честно говоря, даже для средненького проекта это очень неудобно, поэтому я решил выстроить свою логику обработки URL, что дает мне полную свободу действий. Для этого редактируем файл routes.php в папке application/config и прописываем в него:
$route['default_controller'] = "main";
$route[':any'] = "main";

Из этого видно, что в любом случае будет загружаться контроллер main и запускаться функция index(). Далее создаем файл в папке application/controllers и называем его «main.php». Не забываем, что мы установили расширение HMVC, поэтому наши контроллеры теперь будут наследоваться не от CI_Controller, а от MX_Controller. Этот контроллер будет главным, и через него будет «проходить» всё. В то же время он будет очень простым и будет просто передавать управление в другие модули. Так выглядит функция index() у меня:
function index()
{
   session_start();  // сессии я использую, хотя базовый CI нет
   $this->check_lang();  // проверяет язык из урла
   $this->check_module();  // проверяет модуль из урла
   $this->tp->load_tpl($this->tp->tpl); // загружает шаблон и проверяет на модули
   $this->tp->print_page(); // выводит шаблон с проработанными модулями на экран
}

Последние две строчки из класса «tp» пока что трогать не будем. Базовый CI не использует сессии PHP, а вместо них сохраняет данные в Cookies (ввергая новичков в заблуждение, называя свою библиотеку Session, хотя она работает с Cookies). Я всё же решил использовать родные сессии PHP, хотя и не отказался полностью от функционала, предлагаемого CI для работы с Cookies (использую и то, и то).

Итак, первое, что я делаю, проверяю на язык (проекты у меня чаще мультиязычные).
    function check_lang()
    {
        if ($s=$this->uri->segment(1))
        {
            switch ($s)
            {
                case 'ru': define('LANG','ru'); break;
                case 'en': define('LANG','en'); $this->config->set_item('language', 'english');  break;  
                default: show_404('page');
            }    
        }
        else
        {
            define('LANG','ru');
        }
    }

Видно, что первый сегмент в URL будет отвечать за язык. В файле application/config/config.php укажите:
$config['language'] = 'russian'; 

Чтоб изменить язык конфигурации из контроллера, используйте вот это
$this->config->set_item('language', 'english');

Если вы не используете мультиязычность, просто пропустите это.

Функция check_module() должна проверить второй сегмент УРЛа (или первый, если вы не используете мультиязычность) на то, является ли он допустимым модулем, т.е. я заранее в конструкторе прописываю разрешенные модули, например, так:
    function __construct()
    {
        $this->modules=array('auth','cabinet','ads','root'); // разрешенные модули
    }

Затем в функции проверяю:
    function check_module()
    {
        if ($m=$this->uri->segment(2))
        {
            if (in_array($m,$this->modules))
            {
                $this->common->load_module($m);
                $this->tp->tpl=$this->$m->tpl;
            }
            else
            {
                show_404('page');   
            }
        }   
        else
        {
            $this->load_main_page(); // Если нет второго сегмента, то загружает главную страницу
        }
    }

Таким образом, если второй сегмент пустой, то грузится главная страница. Если в URL допустимый модуль, то грузится он, если нет, то выкидывает на 404. Далее я создаю 2 модели tp.php и common.php в папке application/models, которые будут доступны у меня везде (прописываю их в автозагрузчике application/config/autoload.php):
$autoload['model'] = array('tp','common');  

В «tp» будут описаны функции для работы моего простенького шаблонизатора, расширяющего возможности очень слабенького родного. В «common» пишу все остальные функции, которые будут часто использоваться.
Таким образом, $this->common->load_module($m) будет загружать модуль $m (второй сегмент из URL) и функцию index() в нем. Здесь всё просто:
    function load_module($module)
    {
        if (is_dir('application/modules/'.$module))  // проверяет, существует ли модуль
        {
            $this->load->module($module);
            $this->$module->index();
        }        
    }

Каждый модуль, загружаемый из URL должен использовать какой-нибудь шаблон всей страницы, например, такой
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/html4/strict.dtd">
<html> 
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>{page_title}</title>
<link rel="stylesheet" type="text/css" href="{SITEURL}/css/main.css">
</head>
<body>
<div class="page">
    {HEADER} 
    <div class="content">
        <div class="page_title">
            {title_of_page}
        </div>
        <div class="needwidth">
            <div class="rightside">
                {LOOKED_ADS}
            </div>
            <div class="sam_kontent">
                {MSG}  
                {CONTENT}
            </div>
            <div class="clear"></div>
        </div>
    </div>  
    {FOOTER}  
</div>
</body>
</html>

Основные шаблоны всей страницы создаются в папке application/views/templates (папка templates создается вручную). Разные модули могут использовать одни и те же шаблоны.
Сам каркас контроллера модуля выглядит так:
class News extends MX_Controller {
    private $mname;
    public $tpl; 
    
    function __construct()
    {
        $this->tpl='p_default.tpl'; // шаблон страницы
        $this->mname=strtolower(get_class());// имя модуля
    } 
    
    public function index()
    {                
        // здесь выполняем всякие действия, грузим модели и т.д.
        $this->tp->parse('CONTENT', $this->mname.'/'.$this->mname.'.tpl');
    }
}

В нашем модуле News создаем 3 папки – controllers, models, views. В папке controllers создаем файл news.php и записываем в него код, описанный выше. В функции index() выстраиваем свою логику. Я, к примеру, гружу там модель news_model.php, находящуюся в папке models модуля news. Уже в модели описываю функции для работы с базой или другие сложные функции, связанные с этим модулем.

В конце концов, весь результат, полученный из модуля news, записывается в метку CONTENT, которая заменяется в шаблоне на этот результат. Чтоб понять, каким образом это происходит, необходимо рассказать, как я построил логику моего «шаблонизатора».
Я лишь расширил возможности базового «Парсера» и привел в «человеческий вид». Если вы не знаете, как работает базовый, вначале лучше разберитесь с этим.

Итак, рассмотрим модель «tp».

Здесь есть 2 public переменные — $D, $tpl. $D – это наш глобальный массив, который в конце концов заменит в шаблоне все метки вида {LABEL} на содержимое $this->D[‘LABEL’], проработанное в модулях. $tpl – это основной главный шаблон всей страницы, который прописывается в каждом модуле из УРЛа и передается затем в главный контроллер main, где вызывается $this->tp->load_tpl($this->tp->tpl).

Выше мы уже видели функцию parse(). В эту функцию обязательно должны передаваться 2 параметра, первый – это метка, в которую будет сохранен результат (кусок html), второй – этот самый кусок html, находящийся в папке views. Но parse() проверит этот html на наличие в нем меток и проработает их, в случае необходимости:
    function parse($label, $tpl)
    {
        $TPL=$this->load->view($tpl, FALSE, TRUE);
        $pattern = '/{[A-Za-z0-9_]+}/'; // метки могут быть лишь такими
        preg_match_all($pattern, $TPL, $MODULES); // находит метки в шаблоне
        foreach ($MODULES[0] as $MODULE)
        {
            $module=substr($MODULE,1,-1);
            if (!isset($this->D[$module])) $this->D[$module]=$this->lang->line($module); // если они не определены, то смотрит в langs
        } 
        if (isset($this->D[$label]))
        {
            $this->D[$label].=$this->parser->parse($tpl, $this->D, TRUE);   
        }
        else
        {
            $this->D[$label]=$this->parser->parse($tpl, $this->D, TRUE);    
        } 
    }

Из этого всего должно быть понятно, что если создать внутри модуля news в папке views файл news.tpl и написать туда простейший html, например
<h1>Мой первый модуль!</h1>

Затем запустить example.com/ru/news, то запуститься главный контроллер main.php, который передаст управление в модуль news, там загрузится шаблон p_default.tpl (из кода выше).

Затем контроллер модуля news заменит {CONTENT} на содержания файла application/modules/news/views/news.tpl и выведет содержимое шаблона p_default.tpl на экран.
Но… это пока что теоретически, ведь мы не описали функции $this->tp->load_tpl($this->tp->tpl) и $this->tp->print_page().

Функция load_tpl() принимает в качестве параметра шаблон, который является главным, т.е. шаблоном всей страницы (в папке views/templates). Затем этот шаблон проверяется на другие метки, которые могут быть либо модулями, либо просто переменными (как например, заголовок или копирайт).
Метки в верхнем регистре и с числами – это модули, в нижнем – простые переменные. Если замена меткам не найдена, то они просто затираются (удаляются). Вот сам код:
    function load_tpl($tpl_name)
    {
        $TPL=$this->load->view('templates/'.$tpl_name, FALSE, TRUE);
        $pattern = '/{[A-Z0-9_]+}/';
        $pattern2 = '/{[a-z_]+}/';
        preg_match_all($pattern, $TPL, $MODULES); // находит модули
        preg_match_all($pattern2, $TPL, $VALUES); // находит переменные
        
        foreach ($MODULES[0] as $MODULE)
        {
            $module=substr($MODULE,1,-1);
            if (!isset($this->D[$module]))
            {
                $this->D[$module]='';
                $this->common->load_module(strtolower($module));     
            }
        }
        foreach ($VALUES[0] as $VALUE)
        {
            $value=substr($VALUE,1,-1);
            if (!isset($this->D[$value]))
            {
                $this->D[$value]='';
            }
        }
        $this->D['TPL']=$tpl_name;
    }

В конце нашего главного контроллера main выполняется функция print_page(), которая должна вывести проработанный шаблон на экран:
    function print_page()
    {
        $this->parser->parse('templates/'.$this->D['TPL'], $this->D);    
    }


Все выше описанное я старался как можно больше упростить. На самом деле, у меня все гораздо сложней и расширенней, но это вы сможете сделать сами (т.к. и это, возможно, не все дочитали до конца). В моделе «tp» у меня еще куча всяких функций для шаблонизатора, например:
    function clear($label)
    {
        $this->D[$label]='';        
    }
    
    function kill($label)
    {
        unset($this->D[$label]);        
    }
    
    function assign($label, $value='')
    {
        if (is_array($label))
        {
            foreach ($label as $l=>$v)
            {
                $this->D[$l]=$v;
            }
        }
        else
        $this->D[$label]=$value;
    }

Понять их логику несложно. С помощью $this->tp->assign(‘page_title’, ’Главная страница’), например, можно просто заменить {page_title} на «Главная страница» в шаблоне.

Внутри шаблонов также могут быть модули, которые могут что-то выводить, например, последние новости или меню. В шаблоне они вставляются внутри фигурных скобок, например, {MENU} или {HEADER}. Функция parse(), встретив такую метку, проверит, является ли эта метка модулем, и если да, то заменит ее на то, что выдаст этот модуль. Такие модули также располагаются внутри папки modules и имеют свою M-V-C.
Главный контроллер модуля header, например, выглядит так:
class Header extends MX_Controller {
    public $mname, $tag; 
    function __construct()
    {
        $this->mname=strtolower(get_class());// имя модуля               
        $this->tag=strtoupper($this->mname); // «Тэг» в шаблоне   
    } 

    public function index()
    {     
        $this->load->model($this->mname.'/'.$this->mname.'_model');
        $model=$this->mname.'_model';
        $this->$model->index($this->mname);
        $this->tp->parse($this->tag, $this->mname.'/'.$this->mname.'.tpl');
    }  
}

Последняя функция $this->tp->parse($this->tag, $this->mname.'/'.$this->mname.'.tpl') заменит {HEADER} на содержание файла modules/header/views/header.tpl

Следует отметить, что всегда первым проработает модуль, который загружается из URL и является главным, затем подгружается шаблон и parse() прорабатывает все модули внутри него.
Для наглядности всё, описанное выше, изобразил на картинке



Прошу простить за примитивную графику, я всё же программист, а не дизайнер.

Если лень писать всё это вручную, можете скачать CodeIgniter с моими небольшими доработками и поковыряться там (ссылка внизу). Я специально вырезал оттуда почти все свои функции, модули, модели и всё остальное, ограничившись лишь описанным в статье, дабы вы попробовали расширить функционал сами и не отвлекались на всё остальное.

Конечно, можно было еще много чего дописать, но статья итак получилась длинная.

Буду рад услышать критику профессионалов.

Мой доработанный CodeIgniter можно скачать отсюда.

UPDATE 15.10.2011
Зарегистрировался на github.com и добавил в репозиторий. Надеюсь, все сделал правильно. Вот ссылка https://github.com/IbrahimKZ/codeigniter
Tags:
Hubs:
+37
Comments 27
Comments Comments 27

Articles