2012 Feed

Roles and Inheritance

OX - 2012-12-13

Roles

OX applications are just a special type of Moose class, and this includes supporting role application. You can define roles for various parts of your application, and then compose them into the main application class. The only difference is that in order to support composition of things like routes and services, you will need to use the OX::Role module instead of Moose::Role. This will provide the appropriate support for defining router components and services.


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 
27: 
28: 
29: 
30: 
31: 
32: 
33: 

 

package MyApp::Auth;
use OX::Role;

has auth => (
    is => 'ro',
    isa => 'MyApp::Controller::Auth',
    dependencies => ['model'],
);

router as {
    route '/login' => 'auth.login';
    route '/logout' => 'auth.logout';
};

package MyApp;
use OX;

with 'MyApp::Auth';

has model => (
    is => 'ro',
    isa => 'MyApp::Model',
);

has root => (
    is => 'ro',
    isa => 'MyApp::Controller::Root',
    dependencies => ['model'],
);

router as {
    route '/' => 'root.index';
};

 

Here, /, /login, and /logout will all be valid routes for the application - the role composition process will merge the routes defined in each role into the main application's router. This follows the normal role composition algorithm, so you can do things like override a route from a role by defining a new route for the same path in the class:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 

 

package MyApp;
use OX;

has model => (
    is => 'ro',
    isa => 'MyApp::Model',
);

has root => (
    is => 'ro',
    isa => 'MyApp::Controller::Root',
    dependencies => ['model'],
);

router as {
    route '/' => 'root.index';
    route '/login' => 'root.login';
};

with 'MyApp::Auth';

 

Here, /login will call the root.login action rather than the auth.login action. Note how the role application needs to happen at the end in this case, since otherwise OX will think you are trying to define a route for a path that already has a route defined for it and throw an error (this is effectively the same kind of ordering issue that sometimes comes up in Moose roles with method modifiers).

One other thing to note is that roles don't support the wrap or wrap_if keywords. This is because middleware application is really an application-global effect, and so middleware defined in a role would affect not only routes defined in that role, but all routes defined in the class (and other roles that the class consumes). This could potentially be quite confusing, so we currently don't allow it. This may change in the future if we can come up with a way to make this make more sense.

Inheritance

OX applications also support inheritance. Not only can attributes and methods be overridden as expected, but routes, mounts, and middleware also participate in inheritance. This provides a level of reuse by allowing you to create another application with the same basic structure, but a few extra features.

For instance, if you have an application that looks like this:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 
20: 
21: 
22: 
23: 
24: 
25: 
26: 

 

package MyApp;
use OX;

has dsn => (
    is => 'ro',
    isa => 'Str',
    value => 'dbi:mysql:myapp',
);

has model => (
    is => 'ro',
    isa => 'MyApp::Model',
    dependencies => ['dsn'],
);

has root => (
    is => 'ro',
    isa => 'MyApp::Controller::Root',
    dependencies => ['model'],
);

router as {
    route '/' => 'root.index';
    route '/register' => 'root.register';
    route '/login' => 'root.login';
};

 

you can create a specialization of it for testing via something like this:


1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
10: 
11: 
12: 
13: 
14: 
15: 
16: 
17: 
18: 
19: 

 

package MyApp::Test;
use OX;

extends 'MyApp';

has '+dsn' => (
    value => 'dbi:SQLite::memory:',
);

has debug => (
    is => 'ro',
    isa => 'MyApp::Controller::Debug',
    dependencies => ['model'],
);

router as {
    route '/dump_users' => 'debug.dump_users';
    route '/dump_accesses' => 'debug.dump_accesses';
};

 

This way, the application will always access an in-memory database (rather than your actual production database), and it provides a couple extra endpoints to test that the underlying code is working properly. You can then use MyApp::Test in your test suite instead of MyApp, and have an application that just works properly in a test environment, rather than having to specially configure your real application every time.

Gravatar Image This article contributed by: Jesse Luehrs <jesse.luehrs@iinteractive.com>