The Perl Toolchain Summit needs more sponsors. If your company depends on Perl, please support this very important event.

NAME

MVC::Neaf - Not Even A (Web Application) Framework

OVERVIEW

Neaf [ni:f] stands for Not Even A Framework.

The Model is assumed to be just a regular Perl module, no restrictions are imposed on it.

The View is an object with one method, render, receiving a hashref and returning rendered content as string plus optional content-type header.

The Controller is broken down into handlers associated with URI paths. Each such handler receives a MVC::Neaf::Request object containing all it needs to know about the outside world, and returns a simple \%hashref which is forwarded to View.

Please see the example directory in this distribution that demonstrates the features of Neaf.

SYNOPSIS

The following application, outputting a greeting, is ready to run as a CGI script, PSGI application, or Apache handler.

    use MVC::Neaf;

    MVC::Neaf->route( "/app" => sub {
        my $req = shift;

        my $name = $req->param( name => qr/[\w\s]+/, "Yet another perl hacker" );

        return {
            -template => \"Hello, [% name %]",
            -type     => "text/plain",
            name      => $name,
        };
    });
    MVC::Neaf->run;

CREATING AN APPLICATION

THE CONTROLLER

The handler sub receives an MVC::Neaf::Request object and outputs a \%hashref.

It may also die, which will be interpreted as an error 500, UNLESS error message starts with 3 digits and a whitespace, in which case this is considered the return status. E.g. die 404; is a valid method to return "Not Found" right away.

Handlers are set using the route( path => CODEREF ); method discussed below.

THE REQUEST

The Request object is similar to the OO interface of CGI or Plack::Request with some minor differences:

    # What was requested:
    http(s)://server.name:1337/mathing/route/some/more/slashes?foo=1&bar=2

    # What is being returned:
    $req->http_version; # = HTTP/1.0 or HTTP/1.1
    $req->scheme      ; # = http or https
    $req->method      ; # = GET
    $req->hostname    ; # = server.name
    $req->port        ; # = 1337
    $req->path        ; # = /mathing/route/some/more/slashes
    $req->script_name ; # = /mathing/route
    $req->path_info   ; # = /some/more/slashes

    $req->param( foo => '\d+' ); # = 1
    $req->get_cookie( session => '.+' ); # = whatever it was set to before

One major difference is that there's no (easy) way to fetch query parameters or cookies without validation. Just use qr/.*/ if you know better.

Also there are some methods that affect the reply, mainly the headers, like set_cookie or redirect. This is a step towards a know-it-all God object, however, mapping those properties into a hashref turned out to be too cumbersome.

THE RESPONSE

The response may contain regular keys, typically alphanumeric, as well as a predefined set of dash-prefixed keys to control Neaf itself.

-Note -that -dash-prefixed -options -look -antique even to the author of this writing. However, it is a concise and visible way to separate auxiliary parameters from users's data, without requiring a more complex return structure (two hashes, array of arrays etc).

The small but growing list of these -options is as follows:

  • -content - Return raw data and skip view processing. E.g. display generated image.

  • -continue - A callback that receives the Request object. It will be executed AFTER the headers and pre-generated content are served to the client, and may use $req->write( $data ); and $req->close; to output more data.

  • -headers - Pass a hash or array of values for header generation. This is an alternative to MVC::Neaf::Request's push_header method.

  • -jsonp - Used by JS view module as a callback name to produce a jsonp response. Callback MUST be a set of identifiers separated by dots. Otherwise it's ignored for security reasons.

  • -location - HTTP Location: header.

  • -status - HTTP status (200, 404, 500 etc). Default is 200 if the app managed to live through, and 500 if it died.

  • -template - Set template name for TT (Template-based view).

  • -type - Content-type HTTP header. View module may set this parameter if unset. Default: "text/html".

  • -view - select View module. Views are initialized lazily and cached by the framework. TT, JS, Full::Module::Name, and $view_predefined_object are currently supported. New short aliases may be created by MVC::Neaf->load_view( "name" => $your_view ); (see below).

Though more dash-prefixed parameters may be returned and will be passed to the View module as of current, they are not guaranteed to work in the future. Please either avoid them, or send patches.

APPLICATION API

These methods are generally called during the setup phase of the application. They have nothing to do with serving the request.

route( path => CODEREF, %options )

Set up an URI handler in the application. Any incoming request to uri matching /path (/path/something/else too, but NOT /pathology) will now be directed to CODEREF.

Longer paths are GUARANTEED to be checked first.

Dies if the same method and path combo is given again. Multiple methods may be given for the same path, e.g. when handling REST.

Exactly one leading slash will be prepended no matter what you do. (path, /path and /////path are all the same).

%options may include:

  • method - list of allowed HTTP methods. Default is [GET, POST]. Multiple handles can be defined for the same path, provided that methods do not intersect. HEAD method is automatically handled if GET is present, however, one MAY define a separate HEAD handler explicitly.

  • path_info_regex => qr/.../ - allow URI subpaths to be handled by this handler.

    A 404 error will be generated unless path_info_regex is present and PATH_INFO matches the regex (without the leading slash).

    EXPERIMENTAL. Name and semantics MAY change in the future.

  • view - default View object for this Controller. Must be an object with a render method, or a CODEREF receiving hashref and returning a list of two scalars (content and content-type).

  • cache_ttl - if set, set Expires: HTTP header accordingly.

    EXPERIMENTAL. Name and semantics MAY change in the future.

  • default - a \%hash with default values for handler's return value.

    EXPERIMENTAL. Name and semantics MAY change in the future.

  • description - just for information, has no action on execution. This will be displayed if application called with --list (see MVC::Neaf::CLI).

Also, any number of dash-prefixed keys MAY be present. This is totally the same as putting them into default hash.

alias( $newpath => $oldpath )

Create a new name for already registered route. The handler will be executed as is, but new name will be reflected in Request->path.

Returns self.

static( $req_path => $file_path, %options )

Serve static content located under $file_path.

%options may include:

  • buffer => nnn - buffer size for reading/writing files. Default is 4096. Smaller values may be set, but are NOT recommended.

  • cache_ttl => nnn - if given, files below the buffer size will be stored in memory for cache_ttl seconds.

    EXPERIMENTAL. Cache API is not yet established.

  • allow_dots => 1|0 - if true, serve files/directories starting with a dot (.git etc), otherwise give a 404.

    EXPERIMENTAL

  • dir_index => 1|0 - if true, generate index for a directory; otherwise a 404 is returned, and deliberately so, for security reasons.

    EXPERIMENTAL

  • dir_template - specify template for directory listing (with images etc). A sane default is provided.

    EXPERIMENTAL

  • view - specify view object for rendering dir template. By default a localized TT instance is used.

    EXPERIMENTAL Name MAY be changed (dir_view etc).

  • description - comment. The default is "Static content at $dir"

The content is really handled by MVC::Neaf::X::Files.

File type detection is based on extention. This MAY change in the future. Known file types are listed in %MVC::Neaf::X::Files::ExtType hash. Patches welcome.

Generally it is probably a bad idea to serve files in production using a web application framework. Use a real web server instead.

However, this method may come in handy when testing the application in standalone mode, e.g. under plack web server. This is the intended usage.

pre_route( sub { ... } )

Mangle request before serving it. E.g. canonize uri or read session cookie.

Return value from callback is ignored.

Dying in callback is treated the same way as in normal controller sub.

DEPRECATED. Use Neaf->add_hook( pre_route => ... ) instead.

load_view( $name, $object || coderef || ($module_name, %options) )

Load a view object and cache it under name $name, if $name is true. The loaded view is returned. All subsequent calls to get_view( $name ) would return that object, too.

  • if object is given, just save it.

  • if module name + parameters is given, try to load module and create new() instance.

  • as a last resort, load stock view: TT, JS, or Dumper. Those are prefixed with MVC::Neaf::View::.

If set_forced_view was called, return its argument instead.

set_default ( key => value, ... )

Set some default values that would be appended to data hash returned from any controller on successful operation. Controller return always overrides these values.

Returns self.

DEPRECATED. Use MVC::Neaf->set_path_defaults( '/', { ... } ); instead.

set_path_defaults ( '/path' => \%values )

Use given values as defaults for ANY handler below given path. A value of '/' means global.

Longer paths override shorter ones; route-specific defaults override these; and anything defined inside handler takes over once again.

EXPERIMENTAL Name and meaning MAY change in the future.

set_session_handler( %options )

Set a handler for managing sessions.

If such handler is set, the request object will provide session(), save_session(), and delete_session() methods to manage cross-request user data.

% options may include:

  • engine (required) - an object providing the storage primitives;

  • ttl - time to live for session (default is 0, which means until browser is closed);

  • cookie - name of cookie storing session id. The default is "session".

  • view_as - if set, add the whole session into data hash under this name before view processing.

The engine MUST provide the following methods (see MVC::Neaf::X::Session for details):

  • session_ttl (implemented in MVC::Neaf::X::Session);

  • session_id_regex (implemented in MVC::Neaf::X::Session);

  • get_session_id (implemented in MVC::Neaf::X::Session);

  • create_session (implemented in MVC::Neaf::X::Session);

  • save_session (required);

  • load_session (required);

  • delete_session (implemented in MVC::Neaf::X::Session);

set_error_handler ( status => CODEREF( $req, %options ) )

Set custom error handler.

Status must be either a 3-digit number (as in HTTP), or "view". Other allowed keys MAY appear in the future.

The following options will be passed to coderef:

  • status - status being returned (500 in case of 'view');

  • caller - array with the point where MVC::Neaf->route was set up;

  • error - exception, if there was one.

The coderef MUST return an unblessed hash just like controller does.

In case of exception or unexpected return format text message "Error NNN" will be returned instead.

set_error_handler ( status => \%hash )

Return a static template as { %options, %hash }.

error_template( ... )

DEPRECATED. Same as above, but issues a warning.

on_error( sub { my ($req, $err) = @_ } )

Install custom error handler for dying controller. Neaf's own exceptions and die \d\d\d status returns will NOT trigger it.

E.g. write to log, or something.

Return value from this callback is ignored. If it dies, only a warning is emitted.

run()

Run the applicaton. This should be the last statement in your appication main file.

If called in void context, assumes CGI is being used and instantiates MVC::Neaf::Request::CGI. If command line options are present at the time, enters debug mode via MVC::Neaf::CLI.

Otherwise returns a PSGI-compliant coderef. This will also happen if you application is require'd, meaning that it returns a true value and actually serves nothing until run() is called again.

Running under mod_perl requires setting a handler with MVC::Neaf::Request::Apache2.

EXPORTED FUNCTIONS

Currently only one function is exportable:

neaf_err $error

Rethrow Neaf's internal exceptions immediately, do nothing otherwise.

If no argument if given, acts on current $@ value.

Currently Neaf uses exception mechanism for internal signalling, so this function may be of use if there's a lot of eval blocks in the controller. E.g.

    use MVC::Neaf qw(neaf_err);

    # somewhere in controller
    eval {
        check_permissions()
            or $req->error(403);
        do_something()
            and $req->redirect("/success");
    };

    if (my $err = $@) {
        neaf_err;
        # do the rest of error handling
    };

EXPERIMENTAL FUNCTIONAL SUGAR

In order to minimize typing, a less cumbersome prototyped interface is provided:

    use MVC::Neaf qw(:sugar);

    get '/foo/bar' => sub { ... }, view => 'TT';
    neaf error => 404 => \&my_error_template;

    neaf->run;

It is not stable yet, so be careful when upgrading Neaf.

get '/path' => CODE, %options;

Create a route with GET/HEAD methods enabled. The %options are the same as those of route() method.

head '/path' => CODE, %options;

Create a route with HEAD method enabled. The %options are the same as those of route() method.

post '/path' => CODE, %options;

Create a route with POST method enabled. The %options are the same as those of route() method.

put '/path' => CODE, %options;

Create a route with PUT method enabled. The %options are the same as those of route() method.

get + post '/path' => CODE, %options;

EXPERIMENTAL. Set multiple methods in one go.

neaf->...

Returns default Neaf instance ($MVC::Neaf::Inst), so that neaf->method_name is the equivalent of MVC::Neaf->method_name.

neaf shortcut => @options;

Shorter alias to methods described above. Currently supported:

  • route - route

  • error - set_error_handler

  • view - load_view

  • hook - add_hook

  • session - set_session_handler

  • default - set_path_defaults

  • alias - alias

  • static - static

Also, passing a 3-digit number will trigger set_error_handler, and passing a hook phase (see below) will result in setting a hook.

HOOKS

Hooks are subroutines executed during various phases of request processing. Each hook is characterized by phase, code to be executed, path, and method. Multiple hooks MAY be added for the same phase/path/method combination. ALL hooks matching a given route will be executed, either short to long or long to short (aka "event bubbling"), depending on the phase.

CAUTION Don't overuse hooks. This may lead to a convoluted, hard to follow application. Use hooks for repeated auxiliary tasks such as checking permissions or writing down statistics, NOT for primary application logic.

add_hook ( phase => CODEREF, %options )

Set execution hook for given phase. See list of phases below.

The CODEREF receives one and only argument - the $request object. Return value is ignored.

Use the following primitives to maintain state accross hooks and the main controller:

  • Use session if you intend to share data between requests.

  • Use reply if you intend to render the data for the user.

  • Use stash as a last resort for temporary, private data.

%options may include:

  • path => '/path' - where the hook applies. Default is '/'. Multiple locations may be supplied via [ /foo, /bar ...]

  • exclude => '/path/dont' - don't apply to these locations, even if under '/path'. Multiple locations may be supplied via [ /foo, /bar ...]

  • method => 'METHOD' || [ list ] List of request HTTP methods to which given hook applies.

  • prepend => 0|1 - all other parameters being equal, hooks will be executed in order of adding. This option allows to override this and run given hook first. Note that this does NOT override path bubbling order.

HOOK PHASES

This list of phases MAY change in the future. Current request processing diagram looks as follows:

   [*] request created
    . <- pre_route [no path] [can die]
    |
    * route - select handler
    |
    . <- pre_logic [can die]
   [*] execute main handler
    * apply path-based defaults - reply() is populated now
    |
    . <- pre_content
    ? checking whether content already generated
    |\
    | . <- pre_render [can die - template error produced]
    | [*] render - -content is present now
    |/
    * generate default headers (content type & length, cookies, etc)
    . <- pre_reply [path traversal long to short]
    |
   [*] headers sent out, no way back!
    * output the rest of reply (if -continue specified)
    * execute postponed actions (if any)
    |
    . <- pre_cleanup [path traversal long to short] [no effect on headers]
   [*] request destroyed

pre_route

Executed AFTER the event has been received, but BEFORE the path has been resolved and handler found.

Dying in this phase stops both further hook processing and controller execution. Instead, the corresponding error handler is executed right away.

Options path and exclude are not available on this stage.

May be useful for mangling path. Use $request->set_full_path($new_path) if you need to.

pre_logic

Executed AFTER finding the correct route, but BEFORE processing the main handler code (one that returns \%hash, see route above).

Hooks are executed in order, shorted paths to longer. reply is not available at this stage, as the controller has not been executed yet.

Dying in this phase stops both further hook processing and controller execution. Instead, the corresponding error handler is executed right away.

EXAMPLE: use this hook to produce a 403 error if the user is not logged in and looking for a restricted area of the site:

    MVC::Neaf->set_hook( pre_logic => sub {
        my $request = shift;
        $request->session->{user_id} or die 403;
    }, path => '/admin', exclude => '/admin/static' );

pre_content

This hook is run AFTER the main handler has returned or died, but BEFORE content rendering/serialization is performed.

reply() hash is available at this stage.

Dying is ignored, only producing a warning.

pre_render

This hook is run BEFORE content rendering is performed, and ONLY IF the content is going to be rendered, i.e. no -content key set in response hash on previous stages.

Dying will stop rendering, resulting in a template error instead.

pre_reply

This hook is run AFTER the headers have been generated, but BEFORE the reply is actually sent to client. This is the last chance to amend something.

Hooks are executed in REVERSE order, from longer to shorter paths.

reply() hash is available at this stage.

Dying is ignored, only producing a warning.

pre_cleanup

This hook is run AFTER all postponed actions set up in controller (via -continue etc), but BEFORE the request object is actually destroyed. This can be useful to deinitialize something or write statistics.

The client conection MAY be closed at this point and SHOULD NOT be relied upon.

Hooks are executed in REVERSE order, from longer to shorter paths.

Dying is ignored, only producing a warning.

DEVELOPMENT AND DEBUGGING METHODS

get_routes

Returns a hash with ALL routes for inspection. This should NOT be used by application itself.

set_forced_view( $view )

If set, this view object will be user instead of ANY other view.

See load_view.

Returns self.

server_stat ( MVC::Neaf::X::ServerStat->new( ... ) )

Record server performance statistics during run.

The interface of ServerStat is as follows:

    my $stat = MVC::Neaf::X::ServerStat->new (
        write_threshold_count => 100,
        write_threshold_time  => 1,
        on_write => sub {
            my $array_of_arrays = shift;

            foreach (@$array_of_arrays) {
                # @$_ = (script_name, http_status,
                #       controller_duration, total_duration, start_time)
                # do something with this data
                warn "$_->[0] returned $_->[1] in $_->[3] sec\n";
            };
        },
    );

on_write will be executed as soon as either count data points are accumulated, or time is exceeded by difference between first and last request in batch.

Returns self.

INTERNAL API

CAVEAT EMPTOR.

The following methods are generally not to be used, unless you want something very strange.

new(%options)

Constructor. Usually, instantiating Neaf is not required. But it's possible.

Options are not checked whatsoever.

Just in case you're curious, $MVC::Neaf::Inst is the default instance that handles MVC::Neaf->... requests.

handle_request( MVC::Neaf::Request->new )

This is the CORE of this module. Should not be called directly - use run() instead.

get_view( "name" )

Fetch view object by name. Uses load_view w/o additional params if needed. This is for internal usage.

run_test( \%PSGI_ENV, %options )

run_test( "/path?param=value", %options )

Run a PSGI request and return a list of ($status, HTTP::Headers, $whole_content ).

Returns just the content in scalar context.

Just as the name suggests, useful for testing only (it reduces boilerplate).

Continuation responses are supported.

%options may include:

  • method - set method (default is GET)

  • override = \%hash - force certain data in ENV

  • cookie = \%hash - force HTTP_COOKIE header

MORE EXAMPLES

See the examples directory in this distro or at https://github.com/dallaylaen/perl-mvc-neaf/tree/master/example for complete working examples. These below are just code snippets.

All of them are supposed to start and end with:

    use strict;
    use warnings;
    use MVC::Neaf;

    # ... snippet here

    MVC::Neaf->run;

Static content

    MVC::Neaf->static( '/images' => "/local/images" );
    MVC::Neaf->static( '/favicon.ico' => "/local/images/icon_32x32.png" );

RESTful web-service returning JSON

    MVC::Neaf->route( '/restful' => sub {
        # ...
    }, method => 'GET', view => 'JS' );

    MVC::Neaf->route( '/restful' => sub {
        # ...
    }, method => 'POST', view => 'JS' );

    MVC::Neaf->route( '/restful' => sub {
        # ...
    }, method => 'PUT', view => 'JS' );

Form submission

    use MVC::Neaf::X::Form;

    my %profile = (
        name => [ required => '\w+' ],
        age  => '\d+',
    );
    my $validator = MVC::Neaf::X::Form->new( \%profile );

    MVC::Neaf->route( '/submit' => sub {
        my $req = shift;

        my $form = $req->form( $validator );
        if ($req->is_post and $form->is_valid) {
            do_somethong( $form->data );
            $req->redirect( "/result" );
        };

        return {
            -template   => 'form.tt',
            errors      => $form->error,
            fill_values => $form->raw,
        };
    } );

More examples to follow as usage (hopefuly) accumulates.

BUGS

Lots of them, this software is still under heavy development.

* Apache2 handler is a joke and requires work. It can still serve requests though.

Please report any bugs or feature requests to https://github.com/dallaylaen/perl-mvc-neaf/issues.

Alternatively, email them to bug-mvc-neaf at rt.cpan.org, or report through the web interface at http://rt.cpan.org/NoAuth/ReportBug.html?Queue=MVC-Neaf. I will be notified, and then you'll automatically be notified of progress on your bug as I make changes.

SUPPORT

This is BETA software. Feel free to email the author to get instant help! Or you can comment the announce at the Perlmonks forum.

You can find documentation for this module with the perldoc command.

    perldoc MVC::Neaf
    perldoc MVC::Neaf::Request

You can also look for information at:

SEE ALSO

The Kelp framework has very similar concept.

ACKNOWLEDGEMENTS

Ideas were shamelessly stolen from Catalyst, Dancer, and PSGI.

Thanks to Eugene Ponizovsky aka IPH for introducing me to the MVC concept.

LICENSE AND COPYRIGHT

Copyright 2016 Konstantin S. Uvarin.

This program is free software; you can redistribute it and/or modify it under the terms of either: the GNU General Public License as published by the Free Software Foundation; or the Artistic License.

See http://dev.perl.org/licenses/ for more information.