package DBIx::Class::Wrapper; $DBIx::Class::Wrapper::VERSION = '0.008'; use Moose::Role; use Moose::Meta::Class; use Module::Pluggable::Object; use Class::Load; =head1 NAME DBIx::Class::Wrapper - A Moose role to allow your business model to wrap business code around a dbic model. =head1 BUILD STATUS =begin html <a href="https://travis-ci.org/jeteve/DBIx-Class-Wrapper"><img src="https://travis-ci.org/jeteve/DBIx-Class-Wrapper.svg?branch=master"></a> =end html =head1 SYNOPSIS This package allows you to easily extend your DBIC Schema by Optionally wrapping its resultsets and result objects in your own business classes. =head2 Basic usage with no specific wrapping at all package My::Model; use Moose; with qw/DBIx::Class::Wrapper/; 1 Later my $schema = instance of DBIx schema my $app = My::Model->new( { dbic_schema => $schema } ); ## And use the dbic resultsets-ish methods. my $products = $app->dbic_factory('Product'); ## Get a new instance of the Product resultset. ## Use classic DBIC methods as usual. my $p = $products->find(2); my $blue_ps = $products->search({ colour => blue }); =head2 Implement your own product class with business methods. First you need a DBIC factory that will wrap the raw dbic object into your own class of product package My::Model::Wrapper::Factory::Product; use Moose; extends qw/DBIx::Class::Wrapper::Factory/ ; sub wrap{ my ($self , $o) = @_; return My::Model::O::Product->new({o => $o , factory => $self }); } 1; Then your Product business object class package My::Model::O::Product; use Moose; has 'o' => ( isa => 'My::Schema::Product', ## The raw DBIC object class. is => 'ro' , required => 1, handles => [ 'id' , 'name', 'active' ] ## handles standard properties ); ## A business method sub activate{ my ($self) = @_; $self->o->update({ active => 1 }); } Then from your main code, continue using the Product resultset as normal. my $product = $app->dbic_factory('Product')->find(1); ## But you can do $product->activate(); ## so now $product->active() == 1; =head2 Your own specialised resultset Let's say you decide that from now, the bulk of your application should access only active products, leaving unlimited access to all product to a limited set of places. package My::Model::Wrapper::Factory::Product; use Moose; extends qw/DBIx::Class::Wrapper::Factory/; sub build_dbic_rs{ my ($self) = @_; ## Note that you can always access your original business model ## from a factory (method bm). return $self->bm->dbic_schema->resultset('Product')->search_rs({ active => 1}); ## This is a simple example. You can restrict your products set ## according to any current property of your business model for instance. } sub wrap{ .. same .. } 1; Everywhere your application uses $app->dbic_factory('Product') is now restricted to active products only. Surely you want admin parts of your application to access all products. So here's a very basic AllProducts: package My::Model::Wrapper::Factory::AllProduct; use Moose; extends qw/My::Model::Wrapper::Factory::Product/; sub build_dbic_rs{ my ($self) = @_; ## Some extra security. unless( $self->bm->current_user()->is_admin() ){ confess "Sorry you cant access that"; } return $self->bm()->dbic_schema->resultset('Product')->search_rs(); } =head2 Changing the factory base class. Until now, all your custom factories were named My::Model::Wrapper::Factory::<something>. If you want to customise the base class of those custom factories, you can do so by overriding the method _build_dbic_factory_baseclass in your model: package My::Model; use Moose; with qw/DBIx::Class::Wrapper/; sub _build_dbic_factory_baseclass{ return 'My::Model::DBICFactory'; # for instance. } Then implement your factories as subpackages of My::Model::DBICFactory =cut has 'dbic_schema' => ( is => 'rw' , isa => 'DBIx::Class::Schema' , required => 1 ); has 'dbic_factory_baseclass' => ( is => 'ro' , isa => 'Str' , lazy_build => 1); has '_dbic_dbic_fact_classes' => ( is => 'ro' , isa => 'HashRef[Bool]' , lazy_build => 1); sub _build_dbic_factory_baseclass{ my ($self) = @_; return ref ($self).'::Wrapper::Factory'; } sub _build__dbic_dbic_fact_classes{ my ($self) = @_; my $baseclass = $self->dbic_factory_baseclass(); my $res = {}; my $mp = Module::Pluggable::Object->new( search_path => [ $baseclass ]); foreach my $candidate_class ( $mp->plugins() ){ Class::Load::load_class( $candidate_class ); # Code is loaded unless( $candidate_class->isa('DBIx::Class::Wrapper::Factory') ){ warn "Class $candidate_class does not extend DBIx::Class::Wrapper::Factory."; next; } # And inherit from the right class. $res->{$candidate_class} = 1; } return $res; } =head1 METHODS =head2 dbic_factory Returns a new instance of L<DBIx::Class::Wrapper::Factory> that wraps around the given DBIC ResultSet name if such a resultset exists. Dies otherwise. Additionaly, you can set a ad-hoc resulset if you want to locally restrict your original resultset. usage: my $f = $this->dbic_factory('Article'); my $f = $this->dbic_factory('Article' , { dbic_rs => $schema->resultset('Article')->search_rs({ is_active => 1 }) }); =cut sub dbic_factory{ my ($self , $name , $init_args ) = @_; unless( defined $init_args ){ $init_args = {}; } unless( $name ){ confess("Missing name in call to dbic_factory"); } my $class_name = $self->dbic_factory_baseclass().'::'.$name; ## Build a class dynamically if necessary unless( $self->_dbic_dbic_fact_classes->{$class_name} ){ ## We need to build such a class. Moose::Meta::Class->create($class_name => ( superclasses => [ 'DBIx::Class::Wrapper::Factory' ] )); $self->_dbic_dbic_fact_classes->{$class_name} = 1; } ## Ok, $class_name is now there ## Note that the factory will built its own resultset from this model and the name my $instance = $class_name->new({ bm => $self , name => $name , %$init_args }); ## This will die instantly if cannot find a dbic_rs my $dbic_rs = $instance->dbic_rs(); return $instance; } 1;