Полезное и приятное для разработчика в Mojolicious

в 8:57, , рубрики: mojolicious, perl, Веб-разработка

Про 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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js