Pull to refresh

Пример доски объявлений на Kohana

Reading time 22 min
Views 16K

Kohana — довольно молодой PHP фреймворк, форк CI, всецело завязанный на ООП. К достоинствам Kohana можно отнести использование всех возможностей PHP5 на 100%, высокую скорость работы, «легковесность» и простоту как использования, так и изучения. Из минусов отчетливо выделяется небольшое комьюнити, как следствие, не шибко качественная документация и небольшое количество модулей и библиотек.

Не так давно своё знакомство с фреймворками я начинал именно с Kohana и, надо сказать, я был удивлен, насколько легко он мне поддался. Думал будет намного сложнее. Но очень сказывалась нехватка документации и примеров кода. Зарывшись в маны и разбор исходников, через некоторое время, я восполнил интересующие меня пробелы относительно Kohana, и поэтому решил написать эту статью, дабы другим хабралюдям, оказавшимся в схожей ситуации не пришлось по ночам не досыпать и лечить головные боли.

Под катом пример доски объявлений, написанный с помощью Kohana, возможно местами он не претендует на рациональность и здравый смысл, но всё-же я надеюсь услышать конструктивную критику.

Статья рассчитана на людей, имеющих понятие об MVC и ООП, но не имевших, либо мало имевших, дело с фреймворками.


Начнем


Недавно передо мной встала задача написать небольшую доску объявлений на базе новостного сайта, где пользователи могли бы оставлять свои объявления о купле, продаже и прочем. Написать очень быстро. Честно говоря, до этого случая весь мой опыт с фреймворками заключался в установке сэндбокса Symfony и последующем его удалением. А всё, что я знал о фреймворках, так это то, что большинство из них «используют MVC» и то, что они очень облегчают жизнь. Т.к. в то время я уже почитывал Хабр, мне почему-то запала в голову одна из публикаций, которая утверждала, что фреймворк Kohana «cоздан быть легким, быстрым и простым в использовании». Думаю именно поэтому я выбрал его. Итак,

Что мы хотим получить в результате?


Мы хотим, чтобы у нас были такие возможности в отношении пользователей:
  • авторизация/регистрация пользователей
  • просмотр объявлений всеми пользователями;
  • добавлять объявления могут только зарегистрированные;
  • править/удалять могут только авторы и админы;

Условия для категорий:
  • существуют главные категории;
  • существуют под-категории;
  • под-категории содержат объявления;


С тем, что от нас нужно, мы определились, теперь придумаем, как мы это будем реализовывать.

Для регистрации пользователей будем использовать модуль Auth, который входит в стандартную поставку версии 2.3. Для его использования создадим в базе несколько таблиц:
# в этой таблице мы будем хранить пользователей<br/>
CREATE TABLE IF NOT EXISTS `users` (
 `id` int(11) unsigned NOT NULL auto_increment,
 `username` varchar(32) NOT NULL default '',
 `password` char(50) NOT NULL default '',
 `email` varchar(127) NOT NULL default '',
 `join` int(10) unsigned NOT NULL default '0',
 `last_login` int(10) unsigned NOT NULL default '0',
 `logins` int(10) unsigned NOT NULL default '0',
 PRIMARY KEY (`id`),
 UNIQUE KEY `uniq_username` (`username`),
 UNIQUE KEY `uniq_email` (`email`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;
INSERT INTO `users` (`id`, `username`, `password`, `email`, `join`, `last_login`, `logins`) VALUES (1, 'admin', '098f6bcd4621d373cade4e832627b4f6', 'example@example.com', 1215075372, 0, 0);

# тут у нас будут описания прав пользователей<br/>
CREATE TABLE IF NOT EXISTS `roles` (
 `id` int(4) unsigned NOT NULL auto_increment,
 `name` varchar(32) NOT NULL,
 `description` varchar(255) NOT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `uniq_name` (`name`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=3;
INSERT INTO `roles` (`id`, `name`, `description`) VALUES (1, 'login', 'Зарегистрированный');
INSERT INTO `roles` (`id`, `name`, `description`) VALUES (2, 'admin', 'Админ');

# эта таблица будет содержать привязку прав к пользователю<br/>
CREATE TABLE IF NOT EXISTS `roles_users` (
 `user_id` int(10) unsigned NOT NULL,
 `role_id` int(10) unsigned NOT NULL,
 PRIMARY KEY (`user_id`,`role_id`),
 KEY `fk_role_id` (`role_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;
INSERT INTO `roles_users` (`user_id`, `role_id`) VALUES (1, 2);

# a эта таблица будет использоваться модулем Auth для хранения залогиненых пользователей<br/>
DROP TABLE IF EXISTS `d_user_tokens`;
CREATE TABLE IF NOT EXISTS `user_tokens` (
 `id` int(11) unsigned NOT NULL auto_increment,
 `user_id` int(11) unsigned NOT NULL,
 `user_agent` varchar(40) NOT NULL,
 `token` varchar(32) NOT NULL,
 `created` int(10) unsigned NOT NULL,
 `expires` int(10) unsigned NOT NULL,
 PRIMARY KEY (`id`),
 UNIQUE KEY `uniq_token` (`token`),
 KEY `user_id` (`user_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8;


Дальше создадим таблицы для категорий и объявлений. При этом все категории будут иметь поле parent_id, которое будет указывать на id родительской категории у которых parent_id будет равен 0.
CREATE TABLE IF NOT EXISTS `categories` (
  `id` int(10) unsigned NOT NULL auto_increment,
  `parent_id` int(10) unsigned NOT NULL default '0',
  `name` varchar(150) character set utf8 NOT NULL,
 PRIMARY KEY (`id`),
 KEY `parent_id` (`parent_id`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=14;
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (1, 0, 'Недвижимость');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (2, 1, 'Квартиры');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (3, 1, 'Комнаты');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (4, 1, 'Дома, дачи');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (5, 0, 'Авто');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (6, 5, 'Легковые автомобили');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (7, 5, 'Запчасти, аксессуары');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (8, 5, 'Мотоциклы, мопеды');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (9, 0, 'Работа');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (10, 9, 'Админ персонал');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (11, 9, 'ИТ, интернет');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (12, 9, 'Работа на дому');
INSERT INTO `categories` (`id`, `parent_id`, `name`) VALUES (13, 9, 'Другие сферы');

CREATE TABLE IF NOT EXISTS `items` (
 `id` int(10) unsigned NOT NULL auto_increment,
 `title` varchar(250) character set utf8 NOT NULL,
 `category_id` int(10) unsigned NOT NULL default '0',
 `content` text NOT NULL,
 `user_id` int(10) unsigned NOT NULL,
 `datepub` int(10) unsigned NOT NULL default '0',
 PRIMARY KEY (`id`),
 KEY `title` (`title`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=2;
INSERT INTO `items` (`id`, `title`, `category_id`, `content`, `user_id`, `datepub`) VALUES (1, 'Тестовое объявление', 2, 'Это просто объявление. Таких тут вскоре будет много', 1, 1229251545);


Модели


Модели — это часть MVC, которая представляет данные и реагирует на запросы, являясь конечной инстанцией между скриптом и БД.

Теперь о моделях, которые мы будем использовать. Для работы с пользователями у нас есть стандартные модели модуля Auth, для обращения к статьям и категориям, создадим свои — создайте файл /application/models/category.php и пропишите в нем следующий класс:
class Category_Model extends ORM_Tree{
  protected $children = 'categories';
  protected $has_many = array('items');
}

Теперь создайте ещё один файл /application/models/item.php с таким классом:
class Item_Model extends ORM{
  protected $has_one = array('user');
  protected $belongs_to = array('category');
}

Теперь на русском — для модели категорий мы создали расширенный ORM (об этом немного ниже) класс таблицы `categories` с древовидной структкрой (ORM_Tree), у записи которой могут быть потомки в виде записей из этой же таблицы (protected $children = `categories`, по умолчанию родитель — это поле `parent_id`) и каждая запись которой может иметь много вложенных записей таблицы `items`. А для модели объявлений у нас получился простой ORM класс таблицы `items`, которая содержит одну запись из таблицы `users` и пренадлежит таблице `categories`

Думаю, стоит прояснить, что названия моделей должны быть в единичном числе, т.е. если используется таблица items, то название модели должно быть Item_Model. Также дела обстоят и с объявлениями переменных модели, если предполагается множество объектов ($children, $has_many), то аргумент должен быть во множественном числе, если же объект один ($has_one, $belong_to), то и число — единичное.


Контроллеры


С моделями вроде разобрались, теперь о контроллерах — они служат для отображения информации, являясь прослойкой между моделями и видами.

Какие контроллеры нам нужны? Думаю такие:
  1. для главной страницы, заодно и категорий
  2. для под-категорий
  3. для просмотра объявлений
  4. для входа пользователей
  5. для их регистрации

Поскольку первый со вторым, также третий с четвертым, являются логически взаимосвязанными, объединим их в один, для каждого типа и в итоге получаем три контроллера — category.php, items.php и user.php.

Дальше много кода, большинство из которого интуитивно понятна, и занакома людям, работавшим с ORM и MVC.

Что такое ORM?
Думаю, что если Вы дочитали до сюда, то вопросов по MVC у Вас возникнуть не должно, а вот в отношении ORM они вполне могут появиться. Википедия нам говорит, что ORM — это технология программирования, которая связывает базы данных с концепциями объектно-ориентированных языков программирования, создавая «виртуальную объектную базу данных». Что это значит? Это значит, что вызвав метод factory класса ORM с параметром, к примеру, 'table', мы получим объект, проекцию таблицы `tables` (о множественных и единичных числах я говорил немного выше), и место того, чтобы делать SQL запросы к базе через расширение, или какой-либо абстрактный класс, мы можем обращаться к методам и свойствам вызваного класса. Т.е. например, выборка записей с id равным 5 из базы произойдет не привычным $db->query('SELECT * FROM `table` WHERE (`id` = 5) '), а обращением к ORM проекции — ORM::factory('table')->where('id', 5)->find_all(). Но в отличии от простого абстрактного доступа, ORM предоставляет массу вкусностей, которые сложно понять, не попробовав. Так, если речь идет именно об id, то запись можно получить ещё проще — ORM::factory('table', 5). На примере наших вышеобозначенных моделей, мы можем получить имя категории в которой лежит объявление с id = 5 вызовом ORM::factory('item', 5)->category->name, а не писать кучу SQL запросов.


category.php:
class Category_Controller extends Template_Controller {<br>  public $template = 'index';<br><br>  public function index() { // главная страница<br>    $_result = '';<br>    $_cats = array();<br>    $categories = ORM::factory('category')->where('parent_id', 0)->find_all();<br>    foreach($categories as $l) {<br>      $_tmp = new View('category');<br>      $_tmp->id = $l->id;<br>      $_tmp->name = $l->name;<br>      $_tmp->children = $l->children;<br>      $_cats[] = $_tmp;<br>    }<br>    $_result = new View('category');<br>    $_result->cats = $_cats;<br>    $this->template->content = $_result;<br>  }<br><br>  public function view($params) { // прсмотр категории<br>    $_result = '';<br>    $categories = ORM::factory('category', $params)->children;<br>    foreach($categories as $l) {<br>      foreach ($l->items as $n) {<br>        $_tmp = new View('item');<br>        $_tmp->content = $n;<br>        $_result.= $_tmp;<br>      }<br>    }<br>    $this->template->content = $_result;<br>  }<br><br>  public function viewsub($params) { // просмотр под-категории<br>    $_result = '';<br>    $categories = ORM::factory('category', $params);<br>    foreach($categories->items as $l) {<br>      $_tmp = new View('item');<br>      $_tmp->content = $l;<br>      $_result.= $_tmp;<br>    }<br>    $this->template->content = $_result;<br>  }<br>}

В методе index мы собираем массив главных категорий с объектами подкатегорий в нем и кол-вом объявлений(count) в подкатегориях и выводим это всё дело через вид category, который будет описан в главе «Виды».

Метод view показывает нам содержимое всех подкатегорий категории с id, переданным в параметре (как он туда попадёт — в главе «Окончательные настройки»). Также работает метод viewsub с разницей в том, что он применим для подкатегорий.

Теперь контроллер объявлений, items.php:
class Items_Controller extends Template_Controller {<br><br>  public $template = 'index';<br><br>  public function index() { // редирект на главную, если просто вызван контроллер<br>    url::redirect('/index');<br>  }<br><br>  public function view($arg) { // отображение статьи с id = $arg<br>    $_item = ORM::factory('item', $arg);<br>    $this->template->content = new View('item');<br>    $this->template->content->content = $_item;<br>  }<br><br>  public function edit($arg) { // правка статьи c id = $arg<br>    $_tmp = ORM::factory('item', $arg);<br><br>    if(Auth::instance()->logged_in('admin') || (Auth::instance()->get_user() &&<br>      (Auth::instance()->get_user()->id == $_tmp->user_id))) { // продолжать только если пользователь автор или админ<br>      $category = array();<br>      // наполняем список категорий<br>      foreach(ORM::factory('category')->where('parent_id', 0)->find_all() as $l) {<br>        foreach($l->children as $n) {<br>          $category[$l->name][$n->id] = $n->name;<br>        }<br>      }<br>      // создадим класс для работы с формами<br>      $form = new Forge(url::current());<br>      $form->set_attr('method', 'post');<br>      $form->input('title') // создаем тег <input id = 'title' /><br>           ->label('Заголовок') // делаем для него <label for='title'>Заголовок</label><br>           ->rules('required|length[3,40]') // правила валидации - обязательное, от 3 до 40 символов<br>           ->value($_tmp->title); // и присваиваем ему значение из $_tmp->title<br>      $form->textarea('addtext')<br>           ->label('Текст')<br>           ->rules('required')<br>           ->value($_tmp->content);<br>      $form->dropdown('category')<br>           ->label('Категория')<br>           ->options($category)<br>           ->selected($_tmp->category_id);<br>      $form->submit('Править');<br><br>      // вот такая вот простая валидация формы<br>      if ($form->validate()) {<br>        // создаем и заполняем объект ORM<br>        $new = ORM::factory('item', $arg);<br>        $new->title = $form->inputs['title']->value;<br>        $new->content = $form->inputs['addtext']->value;<br>        $new->category_id = $form->inputs['category']->value;<br>        // а после - сохраняем<br>        $new->save();<br>        url::redirect('/');<br>      }<br>      // отправка формы виду<br>      $this->template->content = $form->render();<br>    } else {<br>      $this->template->content = "Вы не зарегистрированы";<br>    }<br>  }<br><br>  public function add() { // добавление статьи<br>    if(Auth::instance()->logged_in()) { // только для зарегистрированых пользователей<br>      $category = array();<br>      foreach(ORM::factory('category')->where('parent_id', 0)->find_all() as $l) {<br>        foreach($l->children as $n) {<br>          $category[$l->name][$n->id] = $n->name;<br>        }<br>      }<br>      $form = new Forge(url::current());<br>      $form->set_attr('method', 'post');<br>      $form->input('title')<br>           ->label('Заголовок')<br>           ->rules('required|length[3,40]');<br>      $form->textarea('addtext')<br>           ->label('Текст')<br>           ->rules('required');<br>      $form->dropdown('category')<br>           ->label('Категория')<br>           ->options($category)<br>           ->selected(0);<br>      $form->submit('Добавить');<br>      if ($form->validate()) {<br>        $new = ORM::factory('item');<br>        $new->title = $form->inputs['title']->value;<br>        $new->content = $form->inputs['addtext']->value;<br>        $new->category_id = $form->inputs['category']->value;<br>        // вот так инстанция класса Auth дает доступ к пользовательским данным<br>        $new->user_id = Auth::instance()->get_user()->id;<br>        $new->datepub = time();<br>        $new->save();<br>        url::redirect('/');<br>      }<br>      $this->template->content = $form->render();<br>    } else {<br>      $this->template->content = "Вы не зарегистрированы";<br>    }<br>  }<br><br>  public function delete($arg) { // удаление статьи<br>    $new = ORM::factory('item', $arg);<br>    if(Auth::instance()->logged_in('admin') || (Auth::instance()->get_user() &&<br>      (Auth::instance()->get_user()->id == $new->user_id))) { // только автор или админ<br>      $new->delete();<br>      url::redirect('/');<br>    } else {<br>      $this->template->content = "Вы не зарегистрированы";<br>    }<br>  }<br>}

Думаю, единственный, до этого времени не встречавшийся нам класс — это Forge. Он используется для облегчения работы с формами, но, к сожалению, он исключен из стандартной поставки Kohana. Где его взять, я расскажу ниже.

Ну и контроллер пользователей, user.php:
class User_Controller extends Template_Controller {<br><br>  public $template = 'index';<br><br>  public function login() { // вход<br>    if (Auth::instance()->logged_in()) { // если не зарегистрированы, то на главную<br>      url::redirect('/');<br>    } else {    // создание формы<br>      $form = new Forge;<br>      $form->set_attr('method', 'post');<br>      $form->input('username')<br>           ->label('Логин')<br>           ->rules('required|length[4,32]');<br>      $form->password('password')<br>           ->label('Пароль')<br>           ->rules('required|length[4,40]');<br>      $form->submit('Войти');<br>      if ($form->validate()) {<br>        $user = ORM::factory('user', $form->username->value);<br>        // если вход успешен, то на главную<br>        if (Auth::instance()->login($user, $form->password->value)) {<br>          url::redirect('/');<br>        } else {<br>          // если нет, то вывести ошибку<br>          $form->password->add_error('login_failed', 'Неверное имя пользователя, или пароль.');<br>        }<br>      }<br>    }<br>    $this->template->content = $form->render();<br>  }<br><br>  public function logout() { // выход<br>    if(Auth::instance()->logged_in()) {<br>      Auth::instance()->logout(TRUE);<br>    }<br>    url::redirect('/');<br>  }<br><br>  public function register() { // регистрация<br>    if(Auth::instance()->logged_in()) {<br>      url::redirect('/');<br>    } else {<br>      $form = new Forge(url::current(), 'Регистрация');<br>      $form->set_attr('method', 'post');<br>      $form->input('username')<br>           ->label('Логин')<br>           // допускается только пароль из латиницы, с цифрами знаками '_' и '-' от 4 до 32 символов<br>           ->rules('required|length[4,32]|valid_alpha_dash');<br>      $form->password('password')->label('Пароль');<br>      $form->password('password2')<br>           ->label('Опять пароль')<br>           ->rules('required|length[6,40]|valid_alpha_dash')<br>           // это поле должно совпадать с полем 'password'<br>           ->matches($form->password);<br>      $form->input('email')<br>           ->label('E-Mail')<br>           ->rules('required|valid_email');<br>      $form->submit('Регистрация');<br>      if ($form->validate()) {<br>        $user = ORM::factory('user', $form->username->value);<br>        // проверяем, нет ли уже такого пользователя<br>        if (!$user->username_exists($form->username->value)) {<br>          $user->username = $form->username->value;<br>          $user->password = $form->password->value;<br>          $user->email = $form->email->value;<br>          // добавляем пользователя и ставим для него права<br>          if($user->save() && $user->add(ORM::factory('role', 'login'))) {<br>            Auth::instance()->login($user, $form->password->value);<br>            url::redirect('/');<br>          }<br>        }<br>      }<br>    }<br>    $this->template->content = $form->render();<br>  }<br>}

Интересный момент при создании формы регистрации, а именно метод matches() объекта, который возвращает метод Forge password. При обращении к нему, параметром нужно указать другой объект password, при несоответствии значений которых скрипт выдаст ошибку.

Виды


Виды (представления), отвечают за отображение информации и являются конечной прослойкой между пользователем и приложением.
Теперь немного о видах. Всего в нашем движке мы использовали три вида, поочередно о каждом:

category.php:
<?php foreach($cats as $content): ?><br> <div class="category"><br>  <h3><a href="/category/<?php echo $content->id ?>"><?php echo $content->name ?></a>/h3><br>  <ul><br>  <?php foreach($content->children as $l): ?><br>   <li><a href="/subcategory/<?php echo $l->id ?>"><?php echo $l->name ?></a></li><br>  <?php endforeach; ?><br>  </ul><br> </div><br><?php endforeach; ?>

Как Вы помните, мы ему передавали массив с категориями и подкатегориями, который он и обрабатывает, в цикле выводя их все.

index.php:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"><br><html xmlns="http://www.w3.org/1999/xhtml"><br><head><br> <title>Kohana</title><br></head><br><body><br>  <div id="enter" style="float:right"><br>   <?php if(Auth::instance()->logged_in()) { ?><br>    Привет, <b><?php echo Auth::instance()->get_user()->username ?></b><a href="/logout">Выход</a><br>   <?php } else { ?><br>    <a href="/login">Войти</a><a href="/register">Регистрация</a><br>   <?php } ?><br>  </div><br>  <div id='links'><br>   <a href="/">Главная</a><br>   <a href="/add">Добавить</a><br>  </div><br>  <div id='content'><br>   <?php echo $content ?><br>  </div><br></body><br></html>

Это «обвертка» для всех страниц, которые мы отображаем. В блоке enter у нас стоит такой-себе триггер, который, в зависимости от статуса пользователя отображает или его имя со ссылкой на выход, или ссылки на вход и регистрацию.

item.php:
<div class="item"><br> <?php  if(Auth::instance()->logged_in('admin') || (Auth::instance()->get_user() &&<br>           Auth::instance()->get_user()->id == $content->user_id))  { ?><br>  <span style="float:right"><br>   <a href="/items/edit/<?php echo $content->id ?>">Править</a><br>   <a href="/items/delete/<?php echo $content->id ?>">Удалить</a><br>  </span><br> <?php } ?><br> <h1><?php echo html::anchor('/' . $content->id, html::specialchars($content->title)) ?></h1><br> <div class="other">Опубликовано <?php echo date("j-M-Y ", $content->datepub) ?><br>           пользователем <b><?php echo html::specialchars($content->>user->username) ?></b><br> </div><br> <div class="news"><br> <?php<br>  echo text::auto_p($content->content);<br> ?><br> </div><br></div>

Этот вид показывает нам, само объявление, выводя ссылки на удаление и редактирование в случае, если пользователь админ или автор объявления. Также можно заметить использование двух хелперов — html и text. html::specialchars делает строку «безопасной», html::anchor делает ссылку — первый параметр адрес, второй — текст, ну а text::auto_p автоматически добавляет абзацы к plain text, как сказано в офф. документации «nl2br() on steroids».

Окончательные настройки


Теперь немного по-настраиваем нашу «систему» перед запуском.

Создайте в папке /application/config/ файл database.php и пропишите в нем следуещее:
$config['default']['connection'] = array(
    'type'   => 'mysql',
    'user'   => 'пользователь',
    'pass'   => 'пароль',
    'host'   => 'сервер',
    'database' => 'имя базы',
  );

Это, как Вы поняли, настройки для MySQL.

Далее, откройте файл config.php в той же папке и приведите массив $config['modules'] к виду
$config['modules'] = array
  (
    MODPATH.'auth',
    MODPATH.'forge',
  );

Также следует изменить $config['site_domain'] на '/', если директории Kohana лежат у Вас в корне сайта. И настоятельно рекомендую установить значение $config['index_page'] в пустую строку, в ином случае, Kohana будет генерировать относительные ссылки с вставкой /index.php/ в URL (прим.: site.com/index.php/mail).

А теперь создайте файл routes.php и пропишите в нем
$config['_default'] = 'category';
$config['category/([0-9]+)'] = 'category/view/$1';
$config['subcategory/([0-9]+)'] = 'category/viewsub/$1';
$config['add'] = 'items/add';
$config['items/([0-9]+)'] = 'items/view/$1';
$config['edit/([0-9]+)'] = 'items/edit/$1';
$config['delete/([0-9]+)'] = 'items/delete/$1';
$config['login'] = 'user/login';
$config['logout'] = 'user/logout';
$config['register'] = 'user/register';
$co
Tags:
Hubs:
+51
Comments 82
Comments Comments 82

Articles