Выпуск 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.

Ну вот и всё. Как видите, ничего особо сложного нет. Я надеюсь, что через некторое время у меня дойдут руки оформить это всё в виде плагина для моджо, так как с ним в последнее время я работаю довольно активно, и писать один и тот же код постоянно утомляет. По крайней мере, проект на гитхабе под это дело уже создал :)

У кого есть вопросы, задавайте их в комментариях.

Дополнительные материалы:

Александр Ружников


От редактора. Два года журналу | Содержание | Мутационное тестирование
Нас уже 1393. Больше подписчиков — лучше выпуски!

Комментарии к статье