I'm considering writing an updated/enhanced version of the very excellent Catalyst::Model::Adaptor. Proposed documentation follows. I'm interested in a) Good Idea or Bad Idea? b) How does the proposal look?
Thanks! Feel free to comment below or hit me up on IRC (I'm typically floating around these days under 'jnap' handle).
PROPOSED SPEC/POD BELOW
<pre>
package CatalystX::Model::Adaptor;
use Moose;
=head1 NAME
CatalystX::Model::Adaptor - Adapt a class for use as a Catalyst Model
=head1 SYNOPSIS
package MyApp:Photos;
use Moose;
use MooseX::Types::Path::Class qw(dir);
has directory => (
is => 'ro',
isa => Dir,
coerce => 1,
required => 1,
handles => sub {
find => 'file',
all => 'children',
}
);
package MyApp::Web::Model::Photos;
use Moose;
use Moose::Util::TypeConstraints 'duck_type';
use File::Spec;
extends 'CatalystX::Model::Adaptor';
__PACKAGE_->config({
class => 'MyApp::Photos',
isa => duck_type(qw/find all/);
args => [{
directory => File::Spec->catdir('home','johnap','photos_dir'),
}],
});
package MyApp::Web::Controller::Photos;
use Moose;
extends 'Catalyst::Controller';
sub photos :Chained(/) PathPart('photos') {
my ($self, $c) = @_;
$c->stash(photos => $c->model('Photos'));
}
sub all :Chained(photos) PathPart('') Args(0) {
my ($self, $c) = @_;
$c->stash(all_photos => $c->stash->{photos}->all);
}
sub photo :Chained(photos) PathPart('') Args(1) {
my ($self, $c, $id) = @_;
if(my $photo = $c->stash->{photos}->find($id)) {
$c->stash(photo => $photo);
} else {
$c->go('/not_found_error');
}
}
=head1 DESCRIPTION
This is a Moosified and extended version of L<Catalyst::Model::Adaptor>.
Although we have broad goals to be reasonably compatible with
L<Catalyst::Model::Adaptor> it is not our goal to replace it, since there is
essentially nothing wrong with it; it solves its target problem domain. Rather
it is our goal to offer an enhanced version of the Adaptor pattern that takes
advantages of what L<Moose> offers as well as incorporates evolving programming
practice.
=head1 EXAMPLE
The following is example usage. Assuming you have a class such as the following
for all examples.
package MyApp::Person;
use Moose;
use Moose::Util::TypeConstraints 'enum';
has 'name' => (is=>'ro', isa=>'Str', required=>1);
has 'age' => (is=>'ro', isa=>'Str', required=>1);
has 'gender' => (is=>'ro', isa=>enum(qw/female male other/), required=>1);
sub is_older_than {
my ($self, $age) = @_;
return $self->age > $age ? 1:0;
}
1;
=head2 Singleton Adaptor
You wish a create one instance of this class shared across all Controllers and
requests.
package MyApp::Web::Model::SiteOwner;
use Moose;
extends 'CatalystX::Model::Adaptor';
__PACKAGE__->config({
class => 'MyApp::Person',
args => [{name=>'John Napiorkowski', age=>40, gender=>'male'}],
});
You may also wish to enforce a type constraint test on the adapted class.
This would allow you to swap in/out difference underlying models while enforcing
a particular type or interface:
__PACKAGE__->config({
class => 'MyApp::Person',
isa => [ANY MOOSE TYPE CONSTRAINT]
does => [ANY MOOSE ROLE]
args => [{name=>'John Napiorkowski', age=>40, gender=>'male'}],
});
## Examples
__PACKAGE__->config({
class => 'MyApp::Person',
isa => 'Person'
does => 'hasName'
args => [{name=>'John Napiorkowski', age=>40, gender=>'male'}],
});
use Moose::Util::TypeConstraints qw(duck_type);
__PACKAGE__->config({
class => 'MyApp::Person',
isa => duck_type(qw/age name/),
args => [{name=>'John Napiorkowski', age=>40, gender=>'male'}],
});
The 'isa' and 'does' options function identically to same named options in
L<Moose> attributes. The above examples show how to define constraints and
interface via configuration, however, since L<CatalystX::Moose::Adaptor> is
just a L<Moose> class, you can also extend the 'class' attribute:
package MyApp::Web::Model::SiteOwner;
use Moose;
extends 'CatalystX::Model::Adaptor';
has '+class' => (isa=>'Person', does=>'hasName');
__PACKAGE__->config({
class => 'MyApp::Person',
args => [{name=>'John Napiorkowski', age=>40, gender=>'male'}],
});
=head2 Factory Adaptors
In some cases you may wish to construct instances of your adapted class more
than once per running application. You may wish to do this when the current
request or session is in some way informing construction of your class. Or
you may be adapting a class that is not completely safe to run persistently (
such as when you need to adapt a class that has a memory leak you have not yet
fixed). Although it is suspect for a Model in MVC to be aware of the current
context (this issue has been widely covered and I don't intend to discuss this
in detail now) there are use cases for models that are informed by user agent
input collected by the Controller. Additionally, you may wish to allow your
Model to depend on other Models. Although there are methods to avoid this type
of need, rather than dictate academic purity we present several canonical
methods for making your Adaptor into a Factory. Caution is encouraged.
We define two types of factories. Both are modeled after L<Catalyst::Model::Factory>
and L<Catalyst::Model::Factory::PerRequest>. For detailed examples of these I
refer you to the specific documentation:
L<CatalystX::Model::Factory>, L<CatalystX::Model::Factory::PerRequest>
Simple example follows. In this example a L<Catalyst> application has an action
which takes one argument and is used to dynamically create a model of a file on
the filesystem reflecting that argument.
package MyApp::File;
use Moose;
use MooseX::Types::Path::Class qw(File Dir);
has 'directory' => (is=>'ro', isa=>Dir, required=>1, coerce=>1);
has 'file' => (is=>'ro', isa=>File, required=>1, coerce=>1);
## Some additional logic/methods, etc.
package MyApp::Web::Model::File;
use File::Spec;
use Moose;
extends 'CatalystX::Model::Factory';
__PACKAGE__->config({
class => 'MyApp::File',
args => [{directory => File::Spec->catdir('home','johnnap','public')}],
});
package MyApp::Web::Controller::File;
use Moose;
extends 'Catalyst::Controller';
sub root :Path('') Args(1) {
my ($self, $c, $filename) = @_;
if(my $file_obj = $c->model('File', file=>$filename)) {
$c->stash(file => $file_obj);
} else {
$c->go('/errors_not_found');
}
}
=head1 COMPATIBILITY ISSUES FROM Catalyst::Model::Adaptor
Functionality is intended to be 100% backwardly compatibly with the test suite
of L<Catalyst::Model::Adaptor>, although we don't promise to cover all your
use cases that significantly deviate from that module's intended problem domain.
In other words if you are using L<Catalyst::Model::Adaptor> in ways similar to
the test suite and documented examples you should be fine and we'd consider
your problems as bugs in our code. However I'd like to point out that there is
nothing really wrong with L<Catalyst::Model::Adaptor> and you shouldn't
consider this a replacement. Our goal is to bring functionality enhancements
and additional constraints that L<Moose> offers us. The goal of backward
compatibility is to allow people the possibility of simplifying their
dependency list and to take advantage of existing documentation, community
knowledge and tests. Nevertheless this is a major difference and source of
possible issues.
Non Moose based adapted classes are 'wrapped' via L<MooseX::NonMoose>. This is
one of the more major differences from L<Catalyst::Model::Adaptor> and a
likely source of trouble for you. You can disable this behavior in the following
way:
__PACKAGE__->config({
use_nonmoose => 0,
});
L<CatalystX::Model::Adaptor> consumes a larger symbol namespace which may conflict
with any custom code you have added to your Models (although the author of
L<Catalyst::Model::Adaptor> warns that this is probably not a good idea anyway.)
=head1 ATTRIBUTES
This class defines the following attributes.
=head2 class (ClassName)
The class name we are adapting. Will be automatically loaded if not already
used via L<Class::MOP/load>
=head2 isa
Any L<Moose> style type constraint that you want the constructed class to
conform with.
=head2 does
A L<Moose::Role> that the constructed class conforms with.
=head2 traits (ArrayRef[ClassName])
Runtime traits that you want applied to the constructed class (not to the
adaptor, for that just use the standard L<Moose> practice for adding roles).
You may wish this so that you can alter the incoming constructed class function
for operating correctly under L<Catalyst>.
=head2 args (ArrayRef)
A list of arguments which will be passed to the adapted class at construction
time. Canonically args are wrapped in an ArrayRef, which is dereferenced
before being sent to the class constructor. As a shortcut for common usage, if
the args are a HashRef, the HashRef will be passed to the class constructor AS
IS.
=head1 COOKBOOK
L<CatalystX::Model::Adaptor> is written in L<Moose>, which allows you to more
easily take advantage of all Moose features. For example, you can declare
attributes and consume roles without additional code, as well as take advantage
of L<Moose>'s meta introspectability; this opens the possibility of writing
code that can interpret your models.
For example you can decorate your class with L<MooseX::MetaDescription::Description>
as in the following example:
package MyApp::Web::Model::SiteOwner;
use Moose;
extends 'CatalystX::Model::Adaptor';
use metaclass 'MooseX::MetaDescription::Meta::Class' => (
description => {
'title' => 'The Website Administrator',
}
);
__PACKAGE__->config({
class => 'MyApp::Person',
args => [{name=>'John Napiorkowski', age=>40, gender=>'male'}],
});
Then in you Controller:
my $site_owner = $c->model('SiteOwner');
$c->log->info($site_owner->meta->{title}.': '.$site_owner->name );
In your log output you'd get something like:
[info] The Website Administrator: John Napiorkowski
=head1 SEE ALSO
The following modules or resources may be of interest.
L<Catalyst::Model::Adaptor>, L<Moose>, L<MooseX::NonMoose>
=head1 AUTHOR
John Napiorkowski C<< <[email protected]> >>
=head1 COPYRIGHT & LICENSE
Copyright 2010, John Napiorkowski C<< <[email protected]> >>
This program is free software; you can redistribute it and/or modify
it under the same terms as Perl itself.
=cut
1;
</pre>
Comments
You can follow this conversation by subscribing to the comment feed for this post.