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

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
Поделиться публикацией
Комментарии 27
    +2
    Скачать… ZIP… Сделайте аккаунт на github, сделайте форк CI и периодически сливайте свои изменения с изменениями в основной ветке.
      +3
      будет сделано
        0
        Спасибо. Опубликуйте ссылку как сделайте, зафолловлю :)
        Вопрос, пока в сорцы не глядел, вы используете встроенный шаблонизатор CI или какой-то сторонний?
          0
          Расширил встроенный. Честно говоря, в CI много чего надо расширять, если делаешь реальный проект. К сожалению, чтоб рассказать обо всем, получится слишком большая статья. Поэтому ограничился пока что этим, чтоб тем, кто скачает, было понятней.

          На github скоро залью, только чуть позже… а то на работе я :)
      +2
      По-моему, это лишнее. Один модуль — один файл контроллера и один файл модели, а вьюхи можно в папочку с именем модуля положить.
        0
        Не соглашусь с вами про «один файл». Когда смотрел что такое CI и пробовал работать с ним, напрягло писать много много много функций в одном файле, хотелось много мелких, но не хватило желания разбираться. Тут уж всё индивидуально и автор показал очень хороший пример… Даже разбудил желание опять побаловаться с CI
        0
        В идеале оно, может быть, и лучше. Но когда модуль сложный, то модель получается слишком толстой, поэтому решил разбивать на «подмодели». Т.е. есть главная модель и дополнительные, которые пишутся при необходимости. Но если вы всё же решили делать 1 модуль, 1 контроллер, 1 модель, то данная (моя) реализация это не запрещает.

        P.S. У меня тоже есть модули, в которых всё по одному. Простенькие.
          0
          Это ответ Sanchous-у
            +2
            Вам никто в CI не мешает писать модели без модуля и загружать в контроллере какие угодно модели.
              0
              Да, но нет возможности собирать вместе контроллер, модели и вьюшки в одном месте, относящиеся к одному модулю. Всё разбросано. Я же написал про это в статье, зачем я использую HMVC.
                0
                Собственно, это совсем не главная проблема игнайтера. Самый весомый его минус – неумение запускать контроллер из контроллера. Потому я лениво переползаю на Кохана. В остальном игнайтер прекрасен.
                  –1
                  с HMVC эта проблема исчезает, поэтому я и использую это расширение для CI
                    0
                    Я использовал ModularCI от Wanwizarda, но такая вещь, все же, должна быть стандартной.
                    –1
                    Мне тоже кажется его неумение вызывать методы других контроллеров жирнющим таким минусом.
                    Разработчики говорят что в 2.0.4 (т.е. ближайшем) релизе они выкатят эту функцию. Пока есть решения вроде таких. А в PyroCMS, к примеру, в ожидании этой функции в качестве заглушки 404-ю страницу вообще подтягивают через curl обращаясь к сайту (!) Хотя сама по себе ЦМС-ка одна из немногих внятных на CI.
              0
              Как не раз отмечали коллеги, на Хабре все всегда появляется вовремя. Вот и данная статья появилась именно в тот момент, когда я вплотную занялся разработкой на CI. Огромное спасибо!
                0
                Всегда пожалуйста. Буду только рад, если мои наработки кому-то принесут пользу.
                0
                Если вы не против, я хотел бы добавить вашу доработку к бандлам.
                  0
                  Раньше работал с CI, сейчас новый проект решил делать на Kohana именно из-за наличия HMVC. Пока готовлюсь, читаю доки Kohana. Но вот из вашей статьи узнал, что в CI есть HMVC.

                  Теперь я думаю — а может нафиг Kohana, продолжить использовать CI? Документация у Kohana все-таки безобразная даже на английском, на русском сведения обрывочные, обычно устаревшие на пару подверсий.

                  Вот не идет у меня Kohana и всё тут. Меня даже на официальный форум не пускают: http://habrahabr.ru/qa/12370/.

                  Как вы оцениваете потенциал CI с HMVC? Не буду ли я слабаком, если вернусь на CI с Kohana? Не будет ли это регрессом?
                    +1
                    Вообще-то HMVC в CI был еще года два назад, когда мы писали новую версию cyberfight.ru (она на другом домене). Конечно, HMVC не нативно, но на родном форуме по CI всё очень грамотно и просто расписано.

                    У нас (в веб-студии) вообще все сайты на CI с HMVC :)
                      +2
                      Передо мной стоял тот же выбор что и у вас. Начинал изучать Kohana зная очень хорошо CI.
                      Для себя вопрос решил просто. «Нужно ли ближайший проект сделать быстро или у меня будет время повозиться с изучением Kohana и делать проект на нем?» Ответ был прост. «Времени нет».
                      Писал на CI так же быстро как и раньше:)
                      HMVC в CI ставится быстро и удобно, в вики всё хорошо расписано.
                        0
                        У меня была аналогичная ситуация. Поэтому решил переделать немного CI под свои нужды и начать работать. Возможно, как закончу свой проект, начну изучать какой-нибудь другой фреймворк.
                        +1
                        У codeigniter'a дока исключительная. Лучшая. Позавидует любой фреймворк. И переводы оперативно подтягиваются на русский. Сейчас доступны даже для версии codeigniter 2.0.0
                        0
                        Хотите быстро — пользуйтесь фремворком с генерацией кода, у нас на работе все сайты и серверная часть для всех приложений на Symfony. А за статью спасибо, интересно покопаться в исходниках и посмотреть на структуру проекта.
                          0
                          Полгода назад пошел по такому же пути — как по мне, так это оптимальный путь, если двигаться к своей CMSке. Отличие лишь в наличии у меня smarty и папки themes — не храню макеты в application/view, так как их тоже полезно хранить в одной папке, вместе с css, js и img, если это одна тема оформления.
                            +1
                            Тоже на основе CodeIgniter собрал определённый каркас. Вместо класса View использовал Smarty, также прикрутил модульность — HMVC.
                              +1
                              В свое время нашел cibonfire.com/ — та же модульность и HMVC, готовая админка и еще несколько вкусностей.
                                0
                                Зарегистрировался на github.com и добавил в репозиторий. Надеюсь, все сделал правильно. Вот ссылка https://github.com/IbrahimKZ/codeigniter

                                Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

                                Самое читаемое