Сейчас сложилась такая ситуация, что язык Perl незаслуженно забыт. Хочу немного поднять авторитет этого чудесного языка своими заметками.
Эта макро-заметка ориентирована на изучающих Perl, знатоков этого языка, а так же на тех, которые только хотят побольше узнать о Perl. В заметке хочу поделиться просто своим опытом.
Хочется рассмотреть простую ситуацию, которая по моему мнению, часто имеет место быть при разработке малых и средних проектов. А ситуация такая: необходимо создать небольшой (средний) сайт, причем принимается решение отказаться от CMS, так как движок нужен небольшой, наворотов в админке не нужно, сложность примерно 16-24 человеко/часов. Для примера требуется небольшой сайт, который будет содержать статьи определенного типа (обычные текстовые статьи) и новости. Плюс небольшая админка для добавления статей и новостей. Условимся, что у нас есть «большая» разница между этими двумя типами контента в рамках этой статьи.
В таких ситуациях довольно часто принимается решение написать свой велосипед, то есть движок. Рассмотрим именно такую ситуацию, на примере которой рассмотрим так же прелести Perl и CPAN.
Полноценную реализацию MVC не предлагаю, это слишком много для нашего маленького проекта. Для Perl написан вагон и маленькая тележка фреймворков (как MVC, так и не очень), например отличный Catalyst, который очень и очень похож на RubyOnRails (или наоборот, я не в курсе хронологии). Так же есть множество поменьше, для любопытствующих стоит взглянуть сюда.
Мы же для простоты реализуем похожий механизм, но попроще. Итак, посмотрим на составляющие нашего движка (LAMP — это as default) в виде модулей:
1. Данные — DBIx::Class
2. Отображение — Template Toolkit
3. Управление — своими руками
Небольшое отступление. Я давно не люблю папку cgi-bin и всячески стараюсь ее избегать, почти на всех хостингах (а тем более дома) разрешены файлы .htaccess. Создаем такой файл в корневой папке проекта и записываем туда:
Теперь можно исполнять скрипты с расширением .pl прям в текущей директории. Кроме того страницей по умолчанию будет наш скрипт index.pl.
Далее советую всегда создавать конфиг. Вариаций множество, каждому нравится по разному, у меня минимально это выглядит так:
Ничего военного, сюда вносятся глобальные параметры проекта и экспортируются.
Вернемся к движку. Пункт первый — это работа с данными, за нее у нас будет отвечать пакет DBIx::Class, который включает несколько модулей. Для начала создадим простую БД, с которой мы будем работать. Не стоит критично относится к структуре базы, она максимально простая, так же синтаксис без всего лишнего, минимизируем затраты.
Таблица пользователей содержит самые основные данные (учет посещений и прочих нас пока не волнует), таблица categories содержит разделы статей, например «автомото», «спорт», «кулинария» и т.п., таблица articles содержит собственно статьи, а таблица news — новости. В последней есть поле is_put_on_main, которое отвечает за показ новости на главной. Так же почти в каждой таблице я задаю кодировку — это привычка, кто уверен — не делайте.
Хорошо, таблицы у нас есть, теперь необходимо отобразить их в коде. Модуль DBIx::Class позволяет полностью отойти от написания SQL-кода и общаться с таблицами, как с объектами. Работать с этим модулем можно двумя способами: либо вручную описывать структуры каждой таблицы, либо воспользоваться автоматикой. Рассмотрим оба способа по порядку.
Смотрим в код, далее будут пояснения. Создадим в корне нашего проекта папку с именем DB и в ней создадим четыре файла: User.pm, Category.pm, Article.pm, News.pm, вот содержимое этих файлов.
Итак, небольшие пояснения. Имеем четыре очень похожих файла, сначала объявляем базовым модуль DBIx::Class, далее используя механизм __PACKAGE__ вызываем его методы, а именно: load_components — загружаем компоненты для нашего модуля (PK::Auto для работы с автоинкрементированными primary_key, Core — основной набор для работы со связями, строками и столбцами). Далее указываем таблицу, после чего добавляем названия столбцов. Для работы со столбцами таких типов, как datetime, date и timestamp используется небольшой модуль InflateColumn::DateTime. С помощью него поля указанных типов можно использовать в программе, как объекты типа DateTime, со всеми вытекающими удобствами. После чего указываем primary key (если он составной, то указываем несколько полей set_primary_key(qw/name1 name2/);.
Далее находятся знакомые для знающих RubyOnRails методы has_many(), belongs_to() и другие. Эти методы предназначены для создания связей между таблицами.
Документация по чудному модулю DBIx::Class, где все подробно описано, включая туториал и cookbook.
Теперь нам нужно использовать сие чудо, для этого нам нужен модуль DBIx::Class::Shema, который является абстракцией схемы данных. В корневой папке проекта создаем файл с именем, идентичным имени папки с классами, описывающими таблицы, в нашем случае это будет DB.pm Вот как он выглядит у меня.
В целом, использовать DBIx::Class::Schema можно и без функции GetShema(), метод load_classes() автоматически загружает все файлы, найденные в одноименной папке. Я дописал небольшую функцию, что бы удобнее было получать схему. Без этой функции соединение в коде выглядело бы так:
В случае с функией можно написать их несколько либо по другому конфигурировать соединение с разными типа баз.
В «ручном» примере мы вручную задавали все связи между таблицами. Существует модуль DBIx::Class::Shema::Loader, который выполняет загрузку и создание классов автоматически. Для этого необходимо добавить в структуру БД описание внешних ключей (foreign keys). Используя их загрузчик автоматически создаст необходимые связи. Вот как это выглядит:
Так же можно добавить выше функцию GetShema() (см. выше) и использовать ее. В данном случае папка DB и четыре файла в ней становятся не нужными и у нас остается один файл-описание схемы. Загрузчик поддерживает множество опций, которые задают пространство имен для создаваемых классов, параметры генерации имен классов, и другие.
Теперь посмотрим, как это все вместе используется непосредственно в коде.
Далее, покажем наши данные.
Я использую систему шаблонов Template Toolkit. Есть еще несколько систем, например Mason, но так исторически сложилось, что мой выбор пал на Template Toolkit.
Template Toolkit — это система обработки шаблонов. Посмотрим ее использование сразу на примере. Для начала создадим в корне проекта папку tmpl и в ней создадим папку site. В папке tmpl/site создадим файл site следующего содержания:
Далее, сделаем там же файл start_page:
Вот такой простой файл с одной строчкой. Это будет заготовка нашей стартовой страницы. Свяжем все вместе и получим примерно такой код нашего скрипта index.pl:
Две строчки про CGI думаю всем понятны, далее идет создание объекта Template, главным параметром которого является INCLUDE_PATH — пусть к шаблонам. Чуть ниже мы создаем схему данных и соединяемся с базой. Далее мы создаем хэш, в который будем складывать все переменные, которые необходимо передать в шаблон. В нашем случае мы передаем только одну переменную content, эта переменная используется в директиве PROCESS в шаблоне site. Еще ниже мы запускаем обработку шаблона и указываем стартовый шаблон — site, а так же передаем хэш переменных.
В шаблоне site используется директива PROCESS, она запускает вложенную обработку другого шаблона, имя которого передано параметром, но так, как у нас имя хранится в переменной, то мы указываем это непосредственно — [% PROCESS $content %]. Таким образом в тело шаблона site вставится содержимое шаблона start_page. Добавим немного разнообразия. На главной странице мы должны отображать статьи и новости, но не все, а, скажем, последние десять. К тому же новости только те, которые помечены соответствующим флагом в таблице. Перед обработкой шаблона добавим в наш скрипт несколько строк:
Следует заметить, что мы использовали [] для создания спискового контекста, иначе в скалярном контексте функция search() возвращает объект типа ResultSet, а нам нужен именно массив данных.
Итак, подробно описывать не имеет смысла, так как все довольно явственно. Единственное, это использование параметров rows/page. Они необходимы для создания так называемых pager-ов, с помощью которых удобно организовывать постраничный вывод, а так же применяются для простого отбора записей, что является частным случаем. Так же кол-во статей и новостей можно вынести в конфиг.
Далее, изменим шаблон start_page:
Отмечу использование поля added_at, как объекта. Для него вызывается метод dmy(), который форматирует дату в формат ДД-ММ-ГГГГ с переданным разделителем, в нашем случае точка. Объект DateTime поддерживает локали и корректно отображает дату в зависимости от текущей (или выбранной) локали. Так же он содержит множество методов для форматирования и работы с датами.
Я пока намеренно не добавлял валидные ссылки, сделаю это позже.
В целом мы видим два похожих блока, которые стоит вынести в отельный файл. Создадим файл short_note в папке tmpl/site:
Теперь наш шаблон start_page примет такой вид:
Теперь мы вызываем обработку шаблона short_note и передаем ему в качестве параметра note текущую новость или статью.
В шаблоне выполняется проверка на наличие поля category, что у нас будет признаком статьи, в этом случае мы выводим название раздела.
На нашем портальчике так же необходимы шаблоны для отображения полной статьи или новости, отображение списка категорий статей, поисковая форма и результаты поиска. В целом, добавится еще несколько шаблонов, которые мало чем будут отличаться от вышеприведенных в плане сложности.
Выше мы условились не применять всяческих фреймворков, попробуем сделать минимум своими руками. Для этого сделаем следующую простую структуру (ламерскую, да-да):
Итак, каждую ссылку в скрипте будет сопровождать параметр a — action. Он будет задавать текущий контекст. Таким образом, ссылки выше в шаблонах можно сменить на такие:
Эта макро-заметка ориентирована на изучающих Perl, знатоков этого языка, а так же на тех, которые только хотят побольше узнать о Perl. В заметке хочу поделиться просто своим опытом.
Хочется рассмотреть простую ситуацию, которая по моему мнению, часто имеет место быть при разработке малых и средних проектов. А ситуация такая: необходимо создать небольшой (средний) сайт, причем принимается решение отказаться от CMS, так как движок нужен небольшой, наворотов в админке не нужно, сложность примерно 16-24 человеко/часов. Для примера требуется небольшой сайт, который будет содержать статьи определенного типа (обычные текстовые статьи) и новости. Плюс небольшая админка для добавления статей и новостей. Условимся, что у нас есть «большая» разница между этими двумя типами контента в рамках этой статьи.
Проблема
В таких ситуациях довольно часто принимается решение написать свой велосипед, то есть движок. Рассмотрим именно такую ситуацию, на примере которой рассмотрим так же прелести Perl и CPAN.
Полноценную реализацию MVC не предлагаю, это слишком много для нашего маленького проекта. Для Perl написан вагон и маленькая тележка фреймворков (как MVC, так и не очень), например отличный Catalyst, который очень и очень похож на RubyOnRails (или наоборот, я не в курсе хронологии). Так же есть множество поменьше, для любопытствующих стоит взглянуть сюда.
Мы же для простоты реализуем похожий механизм, но попроще. Итак, посмотрим на составляющие нашего движка (LAMP — это as default) в виде модулей:
1. Данные — DBIx::Class
2. Отображение — Template Toolkit
3. Управление — своими руками
Небольшое отступление. Я давно не люблю папку cgi-bin и всячески стараюсь ее избегать, почти на всех хостингах (а тем более дома) разрешены файлы .htaccess. Создаем такой файл в корневой папке проекта и записываем туда:
Options +ExecCGI
AddHandler cgi-script pl
DirectoryIndex index.pl
Теперь можно исполнять скрипты с расширением .pl прям в текущей директории. Кроме того страницей по умолчанию будет наш скрипт index.pl.
Далее советую всегда создавать конфиг. Вариаций множество, каждому нравится по разному, у меня минимально это выглядит так:
package Conf;
use warnings;
use strict;
BEGIN
{
use Exporter;
our (@ISA, @EXPORT);
@ISA = qw(Exporter);
@EXPORT = qw(
$DB_Host $DB_Port $DB_Name $DB_User $DB_Pass
);
}
our $DB_Host = "host";
our $DB_Port = 3306;
our $DB_Name = "our_db";
our $DB_User = "our_table";
our $DB_Pass = "our_password";
1;
Ничего военного, сюда вносятся глобальные параметры проекта и экспортируются.
Данные
Структура БД
Вернемся к движку. Пункт первый — это работа с данными, за нее у нас будет отвечать пакет DBIx::Class, который включает несколько модулей. Для начала создадим простую БД, с которой мы будем работать. Не стоит критично относится к структуре базы, она максимально простая, так же синтаксис без всего лишнего, минимизируем затраты.
create table users (
id smallint not null primary key auto_increment,
name varchar(32) not null,
pass varchar(32) not null);
create table categories (
id int not null primary key auto_increment,
name varchar(128) not null) charset cp1251;
create table articles (
id int not null primary key auto_increment,
category_id int not null,
title varchar(255) not null,
content text not null,
author varchar(128) not null comment 'Author of article',
added_at timestamp not null,
added_by smallint not null comment 'Admin user ID') charset cp1251;
create table news (
id int not null primary key auto_increment,
added_at timestamp not null,
title varchar(255) not null,
content text not null,
is_put_on_main bool not null default 0 comment 'Show on main page?',
added_by smallint not null) charset cp1251;
Таблица пользователей содержит самые основные данные (учет посещений и прочих нас пока не волнует), таблица categories содержит разделы статей, например «автомото», «спорт», «кулинария» и т.п., таблица articles содержит собственно статьи, а таблица news — новости. В последней есть поле is_put_on_main, которое отвечает за показ новости на главной. Так же почти в каждой таблице я задаю кодировку — это привычка, кто уверен — не делайте.
Отображение в код
Хорошо, таблицы у нас есть, теперь необходимо отобразить их в коде. Модуль DBIx::Class позволяет полностью отойти от написания SQL-кода и общаться с таблицами, как с объектами. Работать с этим модулем можно двумя способами: либо вручную описывать структуры каждой таблицы, либо воспользоваться автоматикой. Рассмотрим оба способа по порядку.
Ручной метод
Смотрим в код, далее будут пояснения. Создадим в корне нашего проекта папку с именем DB и в ней создадим четыре файла: User.pm, Category.pm, Article.pm, News.pm, вот содержимое этих файлов.
# file User.pm
package DB::User;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('users');
__PACKAGE__->add_columns(qw/id name pass/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many('articles' => 'DB::Article',
{ 'foreign.added_by' => 'self.id' });
__PACKAGE__->has_many('news' => 'DB::News',
{ 'foreign.added_by' => 'self.id' });
1;
# file Category.pm
package DB::Category;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/PK::Auto Core/);
__PACKAGE__->table('categories');
__PACKAGE__->add_columns(qw/id name/);
__PACKAGE__->set_primary_key('id');
__PACKAGE__->has_many('articles' => 'DB::Article',
{ 'foreign.category_id' => 'self.id' });
1;
# file Article.pm
package DB::Article;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/InflateColumn::DateTime PK::Auto Core/);
__PACKAGE__->table('articles');
__PACKAGE__->add_columns(qw/id category_id title content added_by author/);
__PACKAGE__->add_columns('added_at' => { data_type => 'timestamp' });
__PACKAGE__->set_primary_key('id');
__PACKAGE__->belongs_to('category' => 'DB::Category',
{ 'foreign.id' => 'self.category_id' });
__PACKAGE__->belongs_to('user' => 'DB::User',
{ 'foreign.id' => 'self.added_by' });
1;
# file News.pm
package DB::News;
use base qw/DBIx::Class/;
__PACKAGE__->load_components(qw/InflateColumn::DateTime PK::Auto Core/);
__PACKAGE__->table('news');
__PACKAGE__->add_columns(qw/id title content is_put_on_main added_by/);
__PACKAGE__->add_columns('added_at' => { data_type => 'timestamp' });
__PACKAGE__->set_primary_key('id');
__PACKAGE__->belongs_to('user' => 'DB::User',
{ 'foreign.id' => 'self.added_by' });
1;
Итак, небольшие пояснения. Имеем четыре очень похожих файла, сначала объявляем базовым модуль DBIx::Class, далее используя механизм __PACKAGE__ вызываем его методы, а именно: load_components — загружаем компоненты для нашего модуля (PK::Auto для работы с автоинкрементированными primary_key, Core — основной набор для работы со связями, строками и столбцами). Далее указываем таблицу, после чего добавляем названия столбцов. Для работы со столбцами таких типов, как datetime, date и timestamp используется небольшой модуль InflateColumn::DateTime. С помощью него поля указанных типов можно использовать в программе, как объекты типа DateTime, со всеми вытекающими удобствами. После чего указываем primary key (если он составной, то указываем несколько полей set_primary_key(qw/name1 name2/);.
Далее находятся знакомые для знающих RubyOnRails методы has_many(), belongs_to() и другие. Эти методы предназначены для создания связей между таблицами.
Документация по чудному модулю DBIx::Class, где все подробно описано, включая туториал и cookbook.
Теперь нам нужно использовать сие чудо, для этого нам нужен модуль DBIx::Class::Shema, который является абстракцией схемы данных. В корневой папке проекта создаем файл с именем, идентичным имени папки с классами, описывающими таблицы, в нашем случае это будет DB.pm Вот как он выглядит у меня.
package SDB;
use base qw/DBIx::Class::Schema/;
use Conf;
__PACKAGE__->load_classes();
sub GetSchema()
{
my $dsn = "dbi:mysql:$DB_Name:$DB_Host";
my $sch = __PACKAGE__->connect($dsn, $DB_User, $DB_Pass);
return $sch;
}
1;
В целом, использовать DBIx::Class::Schema можно и без функции GetShema(), метод load_classes() автоматически загружает все файлы, найденные в одноименной папке. Я дописал небольшую функцию, что бы удобнее было получать схему. Без этой функции соединение в коде выглядело бы так:
my $dsn = "dbi:mysql:$DB_Name:$DB_Host";
my $sch = DB->connect($dsn, $DB_User, $DB_Pass);
В случае с функией можно написать их несколько либо по другому конфигурировать соединение с разными типа баз.
Автоматический метод
В «ручном» примере мы вручную задавали все связи между таблицами. Существует модуль DBIx::Class::Shema::Loader, который выполняет загрузку и создание классов автоматически. Для этого необходимо добавить в структуру БД описание внешних ключей (foreign keys). Используя их загрузчик автоматически создаст необходимые связи. Вот как это выглядит:
package DB;
use base qw/DBIx::Class::Schema::Loader/;
__PACKAGE__->loader_options(
inflect_singular => 1,
components => qw/InflateColumn::DateTime/
);
1;
# Использование
use DB;
my $sch = DB->connect( $dsn, $user, $password, $attrs);
Так же можно добавить выше функцию GetShema() (см. выше) и использовать ее. В данном случае папка DB и четыре файла в ней становятся не нужными и у нас остается один файл-описание схемы. Загрузчик поддерживает множество опций, которые задают пространство имен для создаваемых классов, параметры генерации имен классов, и другие.
Использование схемы
Теперь посмотрим, как это все вместе используется непосредственно в коде.
use DB;
my $sch = DB->GetShema();
# Поиск пользователя по id
my $user = $sch->resultset('User')->find({ id => $id });
# Добавление новости
my $new_id = $sch->resultset('Category')->populate(
[
[qw/title content is_put_on_main added_by/],
[$ntitle, $ncontent, 0, $user_id]
]);
# Удаление статьи
$sch->resultset('Article')->find({ id => $aid })->delete;
Далее, покажем наши данные.
Отображение
Я использую систему шаблонов Template Toolkit. Есть еще несколько систем, например Mason, но так исторически сложилось, что мой выбор пал на Template Toolkit.
Template Toolkit — это система обработки шаблонов. Посмотрим ее использование сразу на примере. Для начала создадим в корне проекта папку tmpl и в ней создадим папку site. В папке tmpl/site создадим файл site следующего содержания:
Portal
[% PROCESS $content %]
Далее, сделаем там же файл start_page:
News and articles
Вот такой простой файл с одной строчкой. Это будет заготовка нашей стартовой страницы. Свяжем все вместе и получим примерно такой код нашего скрипта index.pl:
#!/usr/bin/perl -w
use strict;
use CGI;
use Template;
use Conf;
use DB;
# инициализируем CGI
my $q = CGI->new;
my %p = $q->Vars;
# ...шаблоны
my $tmpl = Template->new(
{
INCLUDE_PATH => 'tmpl/site',
INTERPOLATE => 1,
EVAL_PERL => 1
}) || die "$Template::ERROR\n";
# ...данные
my $sch = DB->GetShema();
# теперь мы готовы к работе
my $tmpl_vars = {};
$tmpl_vars->{content} = 'start_page';
print $q->header(-type => 'text/html', -charset => 'windows-1251');
$tmpl->process('site', $tmpl_vars) || die $tmpl->error(), "\n";
Две строчки про CGI думаю всем понятны, далее идет создание объекта Template, главным параметром которого является INCLUDE_PATH — пусть к шаблонам. Чуть ниже мы создаем схему данных и соединяемся с базой. Далее мы создаем хэш, в который будем складывать все переменные, которые необходимо передать в шаблон. В нашем случае мы передаем только одну переменную content, эта переменная используется в директиве PROCESS в шаблоне site. Еще ниже мы запускаем обработку шаблона и указываем стартовый шаблон — site, а так же передаем хэш переменных.
В шаблоне site используется директива PROCESS, она запускает вложенную обработку другого шаблона, имя которого передано параметром, но так, как у нас имя хранится в переменной, то мы указываем это непосредственно — [% PROCESS $content %]. Таким образом в тело шаблона site вставится содержимое шаблона start_page. Добавим немного разнообразия. На главной странице мы должны отображать статьи и новости, но не все, а, скажем, последние десять. К тому же новости только те, которые помечены соответствующим флагом в таблице. Перед обработкой шаблона добавим в наш скрипт несколько строк:
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
Следует заметить, что мы использовали [] для создания спискового контекста, иначе в скалярном контексте функция search() возвращает объект типа ResultSet, а нам нужен именно массив данных.
Итак, подробно описывать не имеет смысла, так как все довольно явственно. Единственное, это использование параметров rows/page. Они необходимы для создания так называемых pager-ов, с помощью которых удобно организовывать постраничный вывод, а так же применяются для простого отбора записей, что является частным случаем. Так же кол-во статей и новостей можно вынести в конфиг.
Далее, изменим шаблон start_page:
Новости
[% FOREACH n = news %]
[% n.added_at.dmy('.') %] [% n.title %]
[% n.content FILTER html %]
[% END %]
Статьи
[% FOREACH a = articles %]
[% a.added_at.dmy('.') %] [% a.title %]
Раздел: [% a.category.name %]
[% a.content FILTER html %]
[% END %]
Отмечу использование поля added_at, как объекта. Для него вызывается метод dmy(), который форматирует дату в формат ДД-ММ-ГГГГ с переданным разделителем, в нашем случае точка. Объект DateTime поддерживает локали и корректно отображает дату в зависимости от текущей (или выбранной) локали. Так же он содержит множество методов для форматирования и работы с датами.
Я пока намеренно не добавлял валидные ссылки, сделаю это позже.
В целом мы видим два похожих блока, которые стоит вынести в отельный файл. Создадим файл short_note в папке tmpl/site:
[% text = node.content;
IF text.length > 512;
text = text.substr(0, 512);
END %]
[% note.added_at.dmy('.') %] [% note.title %]
[% IF note.category %]
Раздел: [% note.category.name %]
[% END %]
[% text FILTER html %]
Теперь наш шаблон start_page примет такой вид:
Новости
[% FOREACH n = news %]
[% PROCESS short_note note = n %]
[% END %]
Статьи
[% FOREACH a = articles %]
[% PROCESS short_note note = a %]
[% END %]
Теперь мы вызываем обработку шаблона short_note и передаем ему в качестве параметра note текущую новость или статью.
В шаблоне выполняется проверка на наличие поля category, что у нас будет признаком статьи, в этом случае мы выводим название раздела.
На нашем портальчике так же необходимы шаблоны для отображения полной статьи или новости, отображение списка категорий статей, поисковая форма и результаты поиска. В целом, добавится еще несколько шаблонов, которые мало чем будут отличаться от вышеприведенных в плане сложности.
Управление
Выше мы условились не применять всяческих фреймворков, попробуем сделать минимум своими руками. Для этого сделаем следующую простую структуру (ламерскую, да-да):
my $act = $p{'a'} || 'start';
if ($act eq 'start')
{
}
elsif ($act eq 'article')
{
}
elsif ($act eq 'news')
{
}
# ....
else
{
}
Итак, каждую ссылку в скрипте будет сопровождать параметр a — action. Он будет задавать текущий контекст. Таким образом, ссылки выше в шаблонах можно сменить на такие:
Далее, нужно подумать о безопасности. Сейчас нас интересует только параметр id, на данный момент других у нас не используется. Сделаем самый простой финт ушами непосредственно перед разбором контекста:
$p{'id'} =~ s/\D//g if ($p{'id'});
То есть, если у нас есть какой-то номер, будь то статьи, новости или каталога, то мы вырежем оттуда все не-цифры. Простой и дубовый метод.
Рассмотрим далее примеры кода для контекстов.
if ($act eq 'start')
{
$tmpl_vars->{content} = 'start_page';
my $articles = [$sch->resultset('Article')->search(undef,
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
my $news = [$sch->resultset('News')->search(
{
is_put_on_main => 1
},
{
order_by => 'added_at desc',
rows => 10,
page => 1
})];
$tmpl_vars->{articles} = $articles;
$tmpl_vars->{news} = $news;
}
elsif ($act eq 'article')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('Article')->find({ id => $p{'id'} });
}
elsif ($act eq 'category')
{
$tmpl_vars->{content} = 'category';
$tmpl_vars->{category} = $sch->resultset('Category')->find({ id => $p{'id'} });
}
elsif ($act eq 'news')
{
$tmpl_vars->{content} = 'full_article';
$tmpl_vars->{article} = $sch->resultset('News')->find({ id => $p{'id'} });
}
else
{
# см. ниже
}
Поиск я не рассматривал, он довольно прост, из формы мы передаем введенные данные для поиска среди статей и новостей и выводим результаты. Так же стоит помнить, что в Perl существуют удобные модули для проверки данных на валидность, переданных из форм, например HTML-CheckArgs или HTML-QuickCheck. Существую более продвинутые инструменты, например HTML-Widget или HTML-Tag. Это полноценные системы для создания виджетов и проверки данных на валидность. Очень удобны в коде, а так же удобны для повторного использования. Единожды созданный виджет можно использовать во множестве приложений.
В последнем случае есть небольшая дилемма: что делать, если задан неверный контекст. Некоторые склоняются к выводу ошибки (для этого нужно создать шаблон, например error_action, и просто указать на него), я же склонен отправлять всех на главную:
print $q->header(-location => '?a=start');
exit;
Это не умно и не круто, зато безболезненно. Для тех, кто беспокоится о лишнем запросе к серверу, можно сделать следующим образом (до обработки контекста):
my %action = (
'start' => 'Main page',
'news' => 'News page',
'article' => 'Full article',
# ....
);
my $act = ( $p{'act'} && defined( $actions{$p{'act'}} )) ? $p{'act'} : 'start';
То есть, если задан контекст и он присутствует в списке — использовать его, иначе установить 'start'. Хэш контекстов используется вместо массива для облегчения проверки - defined(...).
Администрирование
Для администрирования необходимо создать некий инструмент. С точки зрения модели построения админка ничем не отличается от вышеприведенной системы, кроме авторизации и нюансов с текстом. Рекомендутеся создать отдельную папку для шаблонов, например tmpl/admin.
Для авторизации я использую два инструмента: Digest::SHA1 и CGI::Session. Первый обеспечивает шифрование, второй — сессии.
Итак, рассмотрим на простом примере применение этих инструментов. Пример намеренно упрощен до безобразия.
Шаблоны:
[%# Шаблон login %]
[% IF err %]
Wrong login
[% END %]
/>
Login: />
Password: />
/>
В скрипте админки нужно дописать вход и выход из системы, а так же сессии:
use CGI::Session;
use Digest::SHA1 qw(sha1_hex);
# ... после CGI загружаем сессию
my $s = CGI::Session->load(undef, undef, { Directory => 'ssss' } );
# ... после определения контекста
if ($s->empty && $act !~ /login(_form)?|logout/)
{
print $q->header(-location => '?a=login_form');
exit;
}
else
{
my $user = $sch->resultset('User')->find({ id => $s->param('uid') });
$tmpl_vars->{user} = $user;
}
if ($act eq 'login_form')
{
$tmpl_vars->{content} = 'login_form';
}
elsif ($act eq 'login')
{
unless (my $u = &login($p{'login'}, $p{'pass'}))
{
$tmpl_vars->{content} = 'login';
$tmpl_vars->{err} = 1;
}
else
{
$s = $s->new;
$s->param('uid', $u->id);
print $s->header(-location => '?a=start');
exit;
}
}
elsif ($act eq 'logout')
{
$s->delete;
print $q->header(-location => '?a=login');
exit;
}
# и небольшая функция
sub login
{
my ($u, $p) = @_;
my $pp = sha1_hex($p);
my $res = $sch->resultset('User')->search({
name => $u,
pass => $pp
});
my $user = $res->next;
return $user;
}
Пример сильно дубовый, но тем не менее, он показывает суть.
Модуль CGI::Session поддерживает хранение сессий как в файле, так и в БД. Так же необходимо указать срок истечения — expired. В примере использовано хранение в файлах в каталоге ssss.
Модуль Digest::SHA1 - как альтернатива MD5.
Следующий нюанс касается создания форм для ввода данных. Во-первых, необходимо создать так называемые CRUD-методы (CReate, Update, Delete). Для этого, например, существует модуль DBIx::Class::WebForm. Так же по запросу CRUD на CPAN можно найти еще несколько подобных модулей.
Во-вторых, необходимо организовать удобный ввод текста статей и новостей. Лично я использую FCKeditor, хотя есть множество других. Такие редакторы довольно просто интегрируются в страничку и дают пользователям удобство и счастье в жизни.
В-третьих, стоит позаботится о валидации данных из форм. Например, модуль DBIx::Class::Validation проверяет данные перед отсылкой в базу, так же есть всевозможные валидаторы данных из форм, которые работают совместно с виджетами или формами, например CGI::FormBuilder, CGI::QuickForm и т.д. По запросу "Form", "Validate" или "Widget" можно найти множество модулей для этих целей.
Заключение
В этой "небольшой" заметке я хотел поделиться своим небольшим опытом в создании простых приложений. Заметка вышла не маленькая, но, надеюсь, информативная. Приведенный здесь метод создания приложений очень эффективен в плане повторного использования, единожды созданные кусочки сайта можно потом использовать в других местах, почти ничего не меняя. Так же мы достаточно эффективно разделили вид и код, а так же абстрагировались от непосредственного написания SQL-запросов.
Вот архив примера, а здесь - рабочий пример. На создание примера ушло меньше часа.
-NOT_FOR_HOLYWARS-