Выпуск 25. Март 2015
← От редактора. Два года журналу | Содержание | Мутационное тестирование →Подключение в Mojolicious модели для бизнес-логики
Рассмотрен вариант автоматического подключения классов моделей из указанной директории
Mojolicious — фреймворк для написания веб-приложений, имеющий в своём арсенале много полезного функционала как поначалу кажется, на все случаи жизни. Но когда начинаешь с ним плотно работать и пытаться написать большое приложение, можно наткнуться на ситуацию, когда нужные решения отсутствуют в базовом наборе. Лично для меня недостатком моджо явилось отсутствие в нём моделей. Притом, что странно, данный функционал не реализован ни в самом моджо, ни в плагинах к нему, поиск по CPAN и в Гугле не выявил соответствующих плагинов.
За время работы с фреймворком Catalyst я привык к MVC, и не хотелось отказываться от данного принципа разработки в моджо.
Вкратце напомню, в чём суть MVC. Приложение явно разделяется на три части: контроллер, модель и представление данных. В контроллер поступают запросы от пользователя, и в них же ведётся предварительная обработка данных (выведение переданных параметров, их валидация, проверка авторизации и пр.). Бизнес-логика и работа с базой данных вынесена в модель. Например, получение суммы балланса пользователя из базы данных, услуги, подключенные у пользователя, и т.п. Полученные данные возвращаются обратно в контроллер и далее отображаются пользователю через представление (view). Как правило, для представления используются шаблонизаторы наподобие Template::Toolkit. Контроллер в данной схеме выступает тонкой прослойкой между пользовательским запросом, моделью и представлением. В самом контроллере, как правило, бизнес-логику не размещают, иначе получаются «толстые уродливые контроллеры», в которых функционал размазан и плохо поддаётся тестированию.
Итак, чего хочется? Хочется моделей как в Catalyst, чтобы из контроллера можно было делать вызовы вида:
sub my_controller {
my $self = shift;
# ...
$self->model('ModelName')->model_method;
# ...
}
и чтобы фреймворк сам подгружал все модели из соответствующей директории.
В моджо программисту самому предлагается дописывать необходимый функционал (например, вынести в модули отдельной директорией), либо писать обработку данных и бизнес-логику прямо в контроллерах. Подобный подход применяется в веб-фреймворках на других языках программирования, например, laravel на php, flask на python и revel на Go.
Мне же, напомню, хотелось явного определения моделей в коде. После вдумчивого гугления, просмотра кода других разработчиков и длительного курения мануалов я выработал для себя реализацию моделей в моджо, которой хочу поделиться с вами ниже.
Реализация
Итак, для создания модели делаются следующие шаги (покажу на примере нового приложения):
- Генерим mojo-приложение:
mojo generate app MyApp && cd my_app/lib
. - Создаём необходимые файлы и директории:
touch MyApp/Model.pm && mkdir MyApp/Model && touch MyApp/Model/Base.pm
. - Правим
MyApp.pm
:
package MyApp;
use Mojo::Base 'Mojolicious';
use MyApp::Model; # <-- подключаем модуль с моделью
sub startup {
my $self = shift;
#################################################
# подключаем модель
my $model = MyApp::Model->new(app => $self);
# создадим соответствующий хелпер для вызова модели
# из контроллеров
$self->helper(
model => sub {
my ($self, $model_name) = @_;
return $model->get_model($model_name);
}
);
#################################################
my $r = $self->routes;
$r->get('/')->to('root#index');
}
1;
Правим MyApp/Model.pm
:
package MyApp::Model;
use Mojo::Loader;
use Mojo::Base -base;
use Carp qw/ croak /;
has modules => sub { {} };
sub new {
my ($class, %args) = @_;
my $self = $class->SUPER::new(%args);
my $model_packages = Mojo::Loader->search('MyApp::Model');
for my $pm (grep { $_ ne 'MyApp::Model::Base' } @{$model_packages}) {
my $e = Mojo::Loader->load($pm);
croak "Loading '$pm' failed: $e" if ref $e;
my ($basename) = $pm =~ /MyApp::Model::(.*)/;
$self->modules->{$basename} = $pm->new(%args);
}
}
sub get_model {
my ($self, $model) = @_;
return $self->modules->{$model} || croak "Unknown model '$model'";
}
1;
Правим MyApp/Model/Base.pm
:
package MyApp::Model::Base;
use Mojo::Base -base;
has 'app';
1;
Теперь подробнее о том, что было сделано.
В файле MyApp.pm
мы создали объект класса MyApp::Model
, которому в конструктор передали текущий объект. Передача в конструктор текущего объекта необходима для того, чтобы потом из модели можно было обращаться ко всем методам текущего класса.
В MyApp/Model.pm
в конструкторе мы находим все файлы в директории MyApp/Model/
используя класс Mojo::Loader
и метод search
. Тут необходимо уточнить, что Mojo::Loader
не умеет рекурсивно обходить каталог, т.е. если директория MyApp/Model
имеет вид:
MyApp/Model
|_ModelFile.pm
|_ModelFile2.pm
|_MoreModel
|_MoreModel1.pm
|_MoreModel2.pm
то Mojo::Loader->search('MyApp::Model')
проигнорирует директорию MoreModel
и загрузит модули только из корневой директории. Как вариант, можно дополнительно в цикле перебрать все поддиректории в корневой директории и скормить их также Mojo::Loader
для загрузки модулей.
Далее по коду. В конструкторе модели мы подгружаем каждый найденный модуль с моделью (за исключением MyApp::Model::Base
, о нём дальше) и загружаем его в хеш modules
. Метод get_model
возвращает класс, соответствующий запрашиваемому, либо падает с ошибкой.
В файле MyApp/Model/Base.pm
мы указываем все методы, которые будут наследоваться остальными моделями. Исходя из названия становится понятно, что это родительский класс для остальных модулей с моделями. Строка
has 'app';
говорит о том, что необходимо представить элемент объекта
$self->{app}
как метод класса. Т.е. вызов $self->app->config
равносилен $self->{app}->config
. Это синтаксический сахар от создателя моджо.
Что такое app
можно понять, взглянув на код модуля MyApp.pm
. Это объект самого верхнего класса MyApp.pm
, который мы передали в конструктор.
Теперь приведём пример простейшего модуля с моделью, например, MyApp/Model/MyModel.pm
:
package MyApp::Model::MyModel;
use Mojo::Base 'MyApp::Model::Base';
# получить все данные из конфига
sub get_config_data {
my $self = shift;
return $self->app->config;
}
1;
Вызвать данный метод из контроллера легче лёгкого:
my $config = $self->model('MyModel')->get_config_data;
В данном случае будет вызван хелпер model
, определёный в методе MyApp::startup
. Данный хелпер вернёт вызов метода get_model
объекта класса MyApp::Model
.
Ну вот и всё. Как видите, ничего особо сложного нет. Я надеюсь, что через некторое время у меня дойдут руки оформить это всё в виде плагина для моджо, так как с ним в последнее время я работаю довольно активно, и писать один и тот же код постоянно утомляет. По крайней мере, проект на гитхабе под это дело уже создал :)
У кого есть вопросы, задавайте их в комментариях.
Дополнительные материалы:
← От редактора. Два года журналу | Содержание | Мутационное тестирование →