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

NAME

RPC::ExtDirect - Expose Perl code to Ext JS RIA applications through Ext.Direct remoting

SYNOPSIS

 package Foo::Bar;
 
 use RPC::ExtDirect Action => 'Fubar',
                    before => \&package_before_hook,
                    after  => \&package_after_hook,
                    ;
  
 sub foo_custom_hook {
    # Check something, return true
    return 1;
 }
 
 sub foo : ExtDirect(2, before => \&foo_custom_hook) {
    my ($class, $arg1, $arg2) = @_;
  
    # do something, store results in scalar
    my $result = ...;
  
    return $result;
 }
  
 # This method doesn't need hooks for some reason
 sub bar
    : ExtDirect(
        params => ['foo', 'bar'], before => 'NONE', after => 'NONE',
      )
 {
    my ($class, %arg) = @_;
  
    my $foo = $arg{foo};
    my $bar = $arg{bar};
  
    # do something, returning scalar
    my $result = eval { ... };
  
    # or throw an exception if something's wrong
    die "Houston, we've got a problem: $@\n" if $@;
  
    return $result;
 }
  
 sub baz : ExtDirect(formHandler) {
    my ($class, %arg) = @_;
  
    my @form_fields    = grep { !/^file_uploads$/  } keys %arg;
    my @uploaded_files = @{ $arg{file_uploads}     };
  
    # do something with form fields and files
    my $result = { ... };
  
    return $result;
 }
  
 sub package_before_hook {
    my ($class, %params) = @_;
  
    # Unpack parameters
    my ($method, $env) = @params{ qw/method _env/ };
  
    # Decide if user is authorized to call this method
    my $authorized = check_authorization($method, $env);
  
    # Positive
    return 1 if $authorized;
  
    # Negative, return error string
    return 'Not authorized';
 }
  
 sub package_after_hook {
    my ($class, %params) = @_;
  
    # Unpack parameters
    my ($method, $result, $ex) = @params{ qw/method result exception/ };
    
    # Log the action
    security_audit_log($method, $result, $ex);
 }

DESCRIPTION

Abstract

This module provides an easy way to map Perl code to Ext.Direct RPC interface used with Ext JS JavaScript framework.

What Ext.Direct is for?

Ext.Direct is a high level RPC protocol that allows easy and fast integration of server components with JavaScript interface. Client side stack is built in Ext JS core and is used by many components like data Stores, Forms, Grids, Charts, etc. Ext.Direct supports request batching, file uploads, event polling and many other features.

Besides simplicity and ease of use, Ext.Direct allows to achieve very clean code and issue separation both on server and client sides, which in turn results in simplified code, greater overall software quality and shorter development times.

From Perl module developer perspective, Ext.Direct is just a method attribute; it doesn't matter if it's called from Perl code or through Ext.Direct. This approach, in particular, allows for multi-tiered testing:

  • Server side methods can be tested without setting up HTTP environment with the usual tools like Test::More

  • Server side classes can be tested as a whole via Ext.Direct calls using Perl client

  • Major application components are tested with browser automation tools like Selenium.

For more information on Ext.Direct, see http://www.sencha.com/products/extjs/extdirect/.

Terminology

Ext.Direct uses the following terms, followed by their descriptions:

Configuration

Description of server side calls exposed to client side. Includes information on Action and Method names, as well as argument number and/or names

API

JavaScript chunk that encodes Configuration. Usually generated by application server and retrieved by client once upon startup. Another option is to embed API declaration in client side application code.

Router

Server side component that receives remoting calls, dispatches requests, collects and returns call Results or Exceptions.

Action

Namespace unit; collection of Methods. The nearest Perl analog is package, other languages may call it a Class. Since the actual calling code is JavaScript, Action names should conform to JavaScript naming rules (i.e. no '::', use dots instead).

Method

Subroutine exposed through Ext.Direct API to be called by client side. Method is fully qualified by Action and Method names using dot as delimiter: Action.Method.

Result

Any data returned by Method upon successful or unsuccessful call completion. This includes application logic errors. 'Not authenticated' and alike events should be returned as Results, not Exceptions.

Exception

Fatal error, or any other unrecoverable event in application code. Calls that produce Exception instead of Result are considered unsuccessful; Ext.Direct provides built in mechanism for managing Exceptions.

Exceptions are not used to indicate errors in application logic flow, only for catastrophic conditions. Nearest analog is status code 500 for HTTP responses.

Examples of Exceptions are: request JSON is broken and can't be decoded; called Method dies because of internall error; Result cannot be encoded in JSON, etc.

Event

An asynchronous notification that can be generated by server side and passed to client side, resulting in some reaction. Events are useful for status updates, progress indicators and other predictably occuring conditions and events.

Event Provider

Server side script that gets polled by client side every N seconds; default N is 3 but it can be changed in client side configuration.

USING RPC::EXTDIRECT

In order to export subroutine to ExtDirect interface, use ExtDirect(n, ...) attribute in sub declaration. Note that there can be no space between attribute name and opening parentheses. In Perls older than 5.12, attribute declaration can't span multiple lines, i.e. the whole ExtDirect(n, ...) should fit in one line.

n is mandatory calling convention declaration; it may be one of the following options:

  • Number of arguments to be passed as ordered list

  • Names of arguments to be passed as hash

  • formHandler: method will receive hash of fields and file uploads

  • pollHandler: method that provides Events when polled by client

Optional method attributes can be specified after calling convention declaration, in hash-like key => value form. Optional attributes are:

  • before: code reference to use as "before" hook. See "HOOKS"

  • instead: code reference to "instead" hook

  • after: code reference to "after" hook.

METHODS

Unlike Ext.Direct specification (and reference PHP implementation, too) RPC::ExtDirect does not impose strict architectural notation on server side code. There is no mandatory object instantiation and no assumption about the code called. That said, an RPC::ExtDirect Method should conform to the following conventions:

  • Be a class method, i.e. be aware that its first argument will be package name. Just ignore it if you don't want it.

  • Ordered (numbered) arguments are passed as list in @_, so $_[1] is the first argument. No more than number of arguments declared in ExtDirect attribute will be passed to Method; any extra will be dropped silently. Less actual arguments than declared will result in Exception returned to client side, and Method never gets called.

    The last argument is an environment object (see "ENVIRONMENT OBJECTS"). For methods that take 0 arguments, it will be the first argument after class name.

  • Named arguments are passed as hash in @_. No arguments other than declared will be passed to Method; extra arguments will be dropped silently. If not all arguments are present in actual call, an Exception will be returned and Method never gets called.

    Environment object will be passed in '_env' key.

  • Form handlers are passed their arguments as hash in @_. Standard Ext.Direct form fields are removed from argument hash; uploaded file(s) will be passed in file_uploads hash element. It will only be present when there are uploaded files. For more info, see "UPLOADS".

    Environment object will be passed in '_env' key.

  • All remoting Methods are called in scalar context. Returning one scalar value is OK; returning array- or hashref is OK too.

    Do not return blessed objects; it is almost always not obvious how to serialize them into JSON that is expected by client side; JSON encoder will choke and an Exception will be returned to the client.

  • If an error is encountered while processing request, throw an exception: die "My error string\n". Note that "\n" at the end of error string; if you don't add it, die() will append file name and line number to the error message; which is probably not the best idea for errors that are not shown in console but rather passed on to JavaScript client.

    RPC::ExtDirect will trim that last "\n" for you before sending Exception back to client side.

  • Poll handler methods are called in list context and do not receive any arguments except environment object. Return values must be instantiated Event object(s), see RPC::ExtDirect::Event for more detail.

HOOKS

Hooks provide an option to intercept method calls and modify arguments passed to the methods, or cancel their execution. Hooks are intended to be used as a shim between task-oriented Methods and Web specifics.

Methods should not, to the reasonable extent, be aware of their environment or care about it; Hooks are expected to know how to deal with Web intricacies but not be task oriented.

The best uses for Hooks are: application or package-wide pre-call setup, user authorization, logging, cleanup, testing, etc.

A hook is a Perl subroutine (can be anonymous, too). Hooks can be of three types:

  • "Before" hook is called before the Method, and can be used to change Method arguments or cancel Method execution. This hook must return numeric value 1 to allow Method call. Any other value will be interpreted as Ext.Direct Result; it will be returned to client side and Method never gets called.

    Note that RPC::ExtDirect will not make any assumptions about this hook's return value; returning a false value like '' or 0 will probably look not too helpful from client side code.

    If this hook throws an exception, it is returned as Ext.Direct Exception to the client side, and the Method does not execute.

  • "Instead" hook replaces the Method it is assigned to. It is the hook sub's responsibility to call (or not call) the Method and return appropriate Result.

    If this hook throws an exception, it is interpreted as if the Method trew it.

  • "After" hook is called after the Method or "instead" hook. This hook cannot affect Method execution, it is intended mostly for logging and testing purposes; its input include Method's Result or Exception.

    This hook's return value and thrown exceptions are ignored.

Hooks can be defined on three levels, in order of precedence: method, package and global. For each Method, only one hook of each type can be applied. Hooks specified in Method definition take precedence over all other; if no method hook is found then package hook applies; and if there is no package hook then global hook gets called, if any. To avoid using hooks for a particular method, use 'NONE' instead of coderef; this way you can specify global and/or package hooks and exclude some specific Methods piecemeal.

Hooks are subject to the following calling conventions:

  • Hook subroutine is called as class method, i.e. first argument is name of the package in which this sub was defined. Ignore it if you don't need it.

  • Hooks receive a hash of the following arguments:

    action

    Ext.Direct Action name for the Method

    method

    Ext.Direct Method name

    package

    Name of the package (not Action) where the Method is declared

    code

    Coderef to the Method subroutine

    param_no

    Number of parameters when Method accepts ordered arguments

    param_names

    Arrayref with names of parameters when Method accepts named arguments

    formHandler

    True if Method handles form submits

    pollHandler

    True if Method handles Event poll requests

    arg

    Arrayref with actual arguments when Method accepts ordered args, single Environment object for poll handlers, hashref otherwise.

    Note that this is a direct link to Method's @_ so it is possible to modify the arguments in "before" hook

    env

    Environment object, see below. Like arg, this is direct reference to the same object that will be passed to Method, so it's possible to modify it in "before" hook

    before

    Coderef to "before" hook for that Method, or undef

    instead

    Coderef to "instead" hook for that Method, or undef

    after

    Coderef to "after" hook for that Method, or undef

    result

    For "after" hooks, the Result returned by Method or "instead" hook, if any. Does not exist for "before" and "instead" hooks

    exception

    For "after" hooks, an exception ($@) thrown by Method or "instead" hook, if any. Does not exist for "before" and "instead" hooks

    method_called

    For "after" hooks, the reference to actual code called as Method, if any. Can be either Method itself, "instead" hook or undef if the call was canceled.

    orig

    A closure that binds Method coderef to its current arguments, allowing to call it as easily as $params{orig}->()

ENVIRONMENT OBJECTS

Since Hooks, and sometimes Methods too, need to be aware of their Web environment, it is necessary to give them access to it in some way without locking on platform specifics. The answer for this problem is environment objects.

An environment object provides platform-agnostic interface for accessing HTTP headers, cookies, form fields, etc, by duck typing. Such object is guaranteed to have the same set of methods that behave the same way across all platforms supported by RPC::ExtDirect, avoiding portability issues.

The interface is modeled after de facto standard CGI.pm:

  • $value = $env->param('name') will retrieve parameter by name

  • @list = $env->param() will get the list of available parameters

  • $cookie = $env->cookie('name') will retrieve a cookie

  • @cookies = $env->cookie() will return the list of cookies

  • $header = $env->http('name') will return HTTP header

  • @headers = $env->http() will return the list of HTTP headers

Of course it is possible to use environment object in a more sophisticated way if you like to, however do not rely on it having a well-known class name as it is not guaranteed.

FILE UPLOADS

Ext.Direct offers native support for file uploading by using temporary forms. RPC::ExtDirect supports this feature; upload requests can be processed in a formHandler Method. The interface aims to be platform agnostic and will try to do its best to provide the same results in all HTTP environments supported by RPC::ExtDirect.

In a formHandler Method, arguments are passed as a hash. If one or more file uploads were associated with request, the argument hash will contain 'file_uploads' key with value set to arrayref of file hashrefs. Each file hashref will have the following keys:

type

MIME type of the file

size

file size, in octets

path

path to temporary file that holds uploaded content

handle

opened IO::Handle for temporary file

basename

name portion of original file name

filename

full original path as sent by client

All files passed to a Method need to be processed in that Method; existence of temporary files is not guaranteed after Method returns.

CAVEATS

In order to keep this module as simple as possible, I had to sacrifice the ability to automatically distinguish inherited class methods. In order to declare inherited class methods as Ext.Direct exportable you have to override them in subclass, like that:

    package foo;
    use RPC::ExtDirect;
    
    sub foo_sub : ExtDirect(1) {
        my ($class, $arg) = @_;
    
        # do something
        ...
    }
    
    package bar;
    use base 'foo';
    
    sub foo_sub : ExtDirect(1) {
        my ($class, $arg) = @_;
    
        # call inherited method
        return __PACKAGE__->SUPER::foo_sub($arg);
    }
    
    sub bar_sub : ExtDirect(2) {
        my ($class, $arg1, $arg2) = @_;
    
        # do something
        ...
    }

On the other hand if you don't like class-based approach, just don't inherit your packages from one another. In any case, declare your Methods explicitly every time and there never will be any doubt about what Method gets called in any given Action.

DEPENDENCIES

RPC::ExtDirect is dependent on the following modules: Attribute::Handlers, "JSON".

BUGS AND LIMITATIONS

In version 2.0, ExtDirect attribute was moved to BEGIN phase instead of default CHECK phase. While this improves compatibility with Apache/mod_perl environments, this also causes backwards compatibility problems with Perl older than 5.12. Please let me know if you need to run RPC::ExtDirect 2.0 with older Perls; meanwhile RPC::ExtDirect 1.x will provide compatibility with Perl 5.6.0 and newer.

There are no known bugs in this module. Please report problems to author, patches are welcome.

SEE ALSO

Alternative Ext.Direct implementations for Perl: CatalystX::ExtJS::Direct by Moritz Onken, http://github.com/scottp/extjs-direct-perl by Scott Penrose, Dancer::Plugin::ExtDirect by Alessandro Ranellucci.

For Web server gateway implementations, see CGI::ExtDirect and Plack::Middleware::ExtDirect modules based on RPC::ExtDirect engine.

For configurable Ext.Direct API options, see RPC::ExtDirect::API module.

AUTHOR

Alexander Tokarev <tokarev@cpan.org>

ACKNOWLEDGEMENTS

I would like to thank IntelliSurvey, Inc for sponsoring my work on version 2.0 of RPC::ExtDirect suite of modules.

LICENSE AND COPYRIGHT

Copyright (c) 2011-2012 by Alexander Tokarev.

This module is free software; you can redistribute it and/or modify it under the same terms as Perl itself. See "perlartistic".