Про Mojolicious на Хабре уже несколько раз писали. Фреймворк успешно развивается и, на мой взгляд, становится удобнее для быстрой разработки с каждым днем.
Под катом я собрал несколько приемов работы с фреймворком, которые серьезно упрощают жизнь мне и, быть может, будут полезны для кого-то еще.
over
Среди методов маршрута в Mojolicious находится метод over(). Метод позволяет накладывать условия на маршрут так, что клиент сможет попасть в указанный в маршруте контроллер только удовлетворив условиям. Об этом уже писал powerman. Использовать метод можно, например, так:
Файл AppName.pm
package AppName;
use Modern::Perl;
use Mojo::Base 'Mojolicious';
use utf8;
# This method will run once at server start
sub startup {
my $self = shift;
$self->plugin('AppName::Helpers::Core'); # библиотеку хэлперов можно подключить из внешнего файла как плагин
my $r = $self->routes; # объект маршрутизатора
# добавление нового условия в маршрутизатор
# здесь условие передает поле user_id из данных сессии пользователя хелперу isAdmin, который должен что-то вернуть
$r->add_condition(
isAdmin => sub {
my ($route, $c, $captures, $pattern) = @_;
return 1 if $c->isAdmin($c->session->{'user_id'});
return undef;
}
);
# при GET-запросе /users пользователь попадет в метод users контроллера sd только если isAdmin что-то вернет
$r->get('/users')->over(isAdmin => 1)->to('sd#users');
# если хэлпер ничего не вернул - пользователь будет отправлен по этому маршруту, если подобного маршрута не найдется - пользователь получит ошибку 404
$r->any('/(*everything')->to('user#main');
}
1;
Код самого хэлпера у меня выглядит так (пользователей у меня мало, так что я храню привилегированных прямо в конфиге):
package AppName::Helpers::Core;
use base 'Mojolicious::Plugin';
use Modern::Perl;
sub register {
my ($self, $app) = @_;
$app->helper(
isAdmin => sub {
# хэлпер для проверки, входит ли данный юзер в список администраторов
# invocation:
# $whatever->isAdmin($user_login)
# outputs: 1|2, undef
my ($self, $user_login) = @_;
return 1 if $user_login eq $user foreach (@{$self->config->{PrivilegedUsers}->{Administrators}});
return 2 if $user_login eq $manager foreach (@{$self->config->{PrivilegedUsers}->{Managers}});
return undef;
}
);
}
1;
Mojolicious::Plugin::Authentication
Данный плагин показался мне самым удобным способом реализации аутентификации пользователей в силу простоты своего использования. Для использования плагина достаточно в AppName.pm подключить его как обычный perl-модуль и добавить примерно такой код:
$self->plugin('authentication',
autoload_user => 1,
# данный метод отвечает за то, что вернет обращение $c->current_user в контроллере
load_user => sub {
my $self = shift;
my $uid = shift;
return {
'id' => $uid,
'name' => $self->session->{'user_id'},
} if $uid;
return undef;
},
validate_user => sub { # непосредственно аутентифицирует пользователя
my $self = shift;
my $username = shift || '';
my $password = shift || '';
my $extradata = shift || {};
my $user = $self->APIrequest(...);
# в моем случае за хранение пользователей отвечает веб-сервис в интранете, которому приложение передает пару логин-пароль и ожидает получить идентификатор пользователя
if (ref($user) eq 'HASH') {
$self->error("API internal error while logging in user: ".$username);
return undef;
}
if ($user->[0] !~ m/^Error:/) {
$self->session->{'user_id'} = $username;
return $user->[0];
}
$self->info("Login for user '$username' failed: $user->[0]");
return undef;
}
);
Плагин также экспортирует условие для маршрутов и маршрут выше может быть переписан так:
$r->get('/users')->over(authenticated => 1)->over(isAdmin => 1)->to('sd#users');
При этом внутри контроллеров не нужно думать ни о чем: если пользователь не удовлетворил условиям, он просто в эти контроллеры не попадет.
Mojolicious::Commands
Этот механизм позволяет использовать консольные комманды в приложении.
Главная прелесть заключается в том, что можно добавлять свои команды, которые будут выполняться в контексте приложения и иметь доступ к его внутренностям (см. документацию). Таким образом, например, по крону я синхронизирую хранящиеся локально в приложении данные с внутренними веб-сервисами компании:
$ ./AppName GetCMDB -h
Usage: APPLICATION GetCMDB
Хуки
Mojolicious предоставляет множество хуков, которые срабатывают при определенных событиях и через которые можно влиять на работу приложения.
Один из них я использую всегда — before_render. Этот хук срабатывает до того, как контроллер передает данные в шаблон и позволяет решить две основные задачи:
Диагностика ошибок
Из хука доступны аргументы вызова рендерера, среди которых, в случае если контроллер решит «упасть» и показать ошибку 500, будет анонимный хэш {exception}, содержащий информацию о причинах проблем. Я использую его для того, чтобы «отловить» ошибку, сообщить о ней через API в Redmine и заставить приложение среагировать — исправить проблему, либо все же показать сообщение об ошибке, но свое.
Кстати, в процессе сообщения об ошибке приложение ищет у себя на диске файл templates/exception.production.html.ep — HTML-шаблон страницы ошибки. Если шаблона нет — используется коробочный, что может удивить пользователей.
Диагностика проблем фронтэнда
Поскольку хуку доступно все содержимое контроллера, можно прямо из него собрать необходимые данные о работе контроллера, и передать из через stash() клиенту. Удобно, если фронтэнд ведет себя не так как задумано и встает вопрос соответствуют ли данные (например, json), передаваемые клиенту тому, что должно быть передано.
Код хука добавляется в AppName.pm и может выглядеть так:
$self->hook(before_render => sub {
my ($c, $args) = @_;
if ($args->{'exception'}) {
# при проблемах с контроллером, готовим свой %snapshot для передачи на страницу ошибки
my %snapshot = map {$_ => $c->stash->{$_}} grep {!/mojo.active_session|mojo.captures|mojo.routed|mojo.secrets|mojo.started|^config$|^exception$/ and defined $c->stash->{$_}} keys %{$c->stash};
# сообщение в Redmine через специальный хелпер
$c->RedmineReport();
}
# снапшот данных контроллера для дэбага фронтэнда
$c->stash(snapshot => { map {$_ => $c->stash->{$_}} grep {!/mojo.active_session|mojo.captures|mojo.routed|mojo.secrets|mojo.started|^config$|^exception$/ and defined $c->stash->{$_}} keys %{$c->stash} });
return;
});
Много данных
Если приложение получается большим — оно может содержать несколько файлов контроллеров и хелперов (подключенных как плагины). По-умолчанию, все файлы Mojo будет искать в AppName/lib, чтобы не плодить множество визуального мусора в одном каталоге, можно разделить файлы по подкаталогам и подключать в AppName.pm, например, так:
# Подключит хэлперы из файлов Core.pm, Lib.pm, CMDB.pm из каталога AppName/lib/Helpers
$self->plugin('AppName::Helpers::Core');
$self->plugin('AppName::Helpers::Lib');
$self->plugin('AppName::Helpers::CMDB');
# Подключит контроллеры из файлов в каталоге AppName/lib/Controllers
$r = $r->namespaces(['AppName::Controllers']);
Вместо заключения
Как всегда, в Perl и решениях на нем построенных, существует множество способов решать задачи просто, быстро и красиво. Я не претендую на то, что мои способы самые-самые, но они, как минимум, весьма полезны в работе.
Автор: crackpot