switch to MX::Traits, document attributes
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Model / DBIC / Schema.pm
1 package Catalyst::Model::DBIC::Schema;
2
3 use Moose;
4 use mro 'c3';
5 with 'MooseX::Traits';
6 extends 'Catalyst::Model';
7
8 our $VERSION = '0.24';
9
10 use Carp::Clan '^Catalyst::Model::DBIC::Schema';
11 use Data::Dumper;
12 use DBIx::Class ();
13 use Moose::Autobox;
14
15 use Catalyst::Model::DBIC::Schema::Types
16     qw/ConnectInfo SchemaClass CursorClass/;
17
18 use MooseX::Types::Moose qw/ArrayRef Str ClassName Undef/;
19
20 use namespace::clean -except => 'meta';
21
22 =head1 NAME
23
24 Catalyst::Model::DBIC::Schema - DBIx::Class::Schema Model Class
25
26 =head1 SYNOPSIS
27
28 Manual creation of a DBIx::Class::Schema and a Catalyst::Model::DBIC::Schema:
29
30 =over
31
32 =item 1.
33
34 Create the DBIx:Class schema in MyApp/Schema/FilmDB.pm:
35
36   package MyApp::Schema::FilmDB;
37   use base qw/DBIx::Class::Schema/;
38
39   __PACKAGE__->load_classes(qw/Actor Role/);
40
41 =item 2.
42
43 Create some classes for the tables in the database, for example an 
44 Actor in MyApp/Schema/FilmDB/Actor.pm:
45
46   package MyApp::Schema::FilmDB::Actor;
47   use base qw/DBIx::Class/
48
49   __PACKAGE__->load_components(qw/Core/);
50   __PACKAGE__->table('actor');
51
52   ...
53
54 and a Role in MyApp/Schema/FilmDB/Role.pm:
55
56   package MyApp::Schema::FilmDB::Role;
57   use base qw/DBIx::Class/
58
59   __PACKAGE__->load_components(qw/Core/);
60   __PACKAGE__->table('role');
61
62   ...    
63
64 Notice that the schema is in MyApp::Schema, not in MyApp::Model. This way it's 
65 usable as a standalone module and you can test/run it without Catalyst. 
66
67 =item 3.
68
69 To expose it to Catalyst as a model, you should create a DBIC Model in
70 MyApp/Model/FilmDB.pm:
71
72   package MyApp::Model::FilmDB;
73   use base qw/Catalyst::Model::DBIC::Schema/;
74
75   __PACKAGE__->config(
76       schema_class => 'MyApp::Schema::FilmDB',
77       connect_info => {
78                         dsn => "DBI:...",
79                         user => "username",
80                         password => "password",
81                       }
82   );
83
84 See below for a full list of the possible config parameters.
85
86 =back
87
88 Now you have a working Model which accesses your separate DBIC Schema. This can
89 be used/accessed in the normal Catalyst manner, via $c->model():
90
91   my $actor = $c->model('FilmDB::Actor')->find(1);
92
93 You can also use it to set up DBIC authentication with 
94 Authentication::Store::DBIC in MyApp.pm:
95
96   package MyApp;
97
98   use Catalyst qw/... Authentication::Store::DBIC/;
99
100   ...
101
102   __PACKAGE__->config->{authentication}{dbic} = {
103       user_class      => 'FilmDB::Actor',
104       user_field      => 'name',
105       password_field  => 'password'
106   }
107
108 C<< $c->model('Schema::Source') >> returns a L<DBIx::Class::ResultSet> for 
109 the source name parameter passed. To find out more about which methods can 
110 be called on a ResultSet, or how to add your own methods to it, please see 
111 the ResultSet documentation in the L<DBIx::Class> distribution.
112
113 Some examples are given below:
114
115   # to access schema methods directly:
116   $c->model('FilmDB')->schema->source(...);
117
118   # to access the source object, resultset, and class:
119   $c->model('FilmDB')->source(...);
120   $c->model('FilmDB')->resultset(...);
121   $c->model('FilmDB')->class(...);
122
123   # For resultsets, there's an even quicker shortcut:
124   $c->model('FilmDB::Actor')
125   # is the same as $c->model('FilmDB')->resultset('Actor')
126
127   # To get the composed schema for making new connections:
128   my $newconn = $c->model('FilmDB')->composed_schema->connect(...);
129
130   # Or the same thing via a convenience shortcut:
131   my $newconn = $c->model('FilmDB')->connect(...);
132
133   # or, if your schema works on different storage drivers:
134   my $newconn = $c->model('FilmDB')->composed_schema->clone();
135   $newconn->storage_type('::LDAP');
136   $newconn->connection(...);
137
138   # and again, a convenience shortcut
139   my $newconn = $c->model('FilmDB')->clone();
140   $newconn->storage_type('::LDAP');
141   $newconn->connection(...);
142
143 =head1 DESCRIPTION
144
145 This is a Catalyst Model for L<DBIx::Class::Schema>-based Models.  See
146 the documentation for L<Catalyst::Helper::Model::DBIC::Schema> for
147 information on generating these Models via Helper scripts.
148
149 When your Catalyst app starts up, a thin Model layer is created as an 
150 interface to your DBIC Schema. It should be clearly noted that the model 
151 object returned by C<< $c->model('FilmDB') >> is NOT itself a DBIC schema or 
152 resultset object, but merely a wrapper proving L<methods|/METHODS> to access 
153 the underlying schema. 
154
155 In addition to this model class, a shortcut class is generated for each 
156 source in the schema, allowing easy and direct access to a resultset of the 
157 corresponding type. These generated classes are even thinner than the model 
158 class, providing no public methods but simply hooking into Catalyst's 
159 model() accessor via the 
160 L<ACCEPT_CONTEXT|Catalyst::Component/ACCEPT_CONTEXT> mechanism. The complete 
161 contents of each generated class is roughly equivalent to the following:
162
163   package MyApp::Model::FilmDB::Actor
164   sub ACCEPT_CONTEXT {
165       my ($self, $c) = @_;
166       $c->model('FilmDB')->resultset('Actor');
167   }
168
169 In short, there are three techniques available for obtaining a DBIC 
170 resultset object: 
171
172   # the long way
173   my $rs = $c->model('FilmDB')->schema->resultset('Actor');
174
175   # using the shortcut method on the model object
176   my $rs = $c->model('FilmDB')->resultset('Actor');
177
178   # using the generated class directly
179   my $rs = $c->model('FilmDB::Actor');
180
181 In order to add methods to a DBIC resultset, you cannot simply add them to 
182 the source (row, table) definition class; you must define a separate custom 
183 resultset class. See L<DBIx::Class::Manual::Cookbook/"Predefined searches"> 
184 for more info.
185
186 =head1 CONFIG PARAMETERS
187
188 =head2 schema_class
189
190 This is the classname of your L<DBIx::Class::Schema> Schema.  It needs
191 to be findable in C<@INC>, but it does not need to be inside the 
192 C<Catalyst::Model::> namespace.  This parameter is required.
193
194 =head2 connect_info
195
196 This is an arrayref of connection parameters, which are specific to your
197 C<storage_type> (see your storage type documentation for more details). 
198 If you only need one parameter (e.g. the DSN), you can just pass a string 
199 instead of an arrayref.
200
201 This is not required if C<schema_class> already has connection information
202 defined inside itself (which isn't highly recommended, but can be done)
203
204 For L<DBIx::Class::Storage::DBI>, which is the only supported
205 C<storage_type> in L<DBIx::Class> at the time of this writing, the
206 parameters are your dsn, username, password, and connect options hashref.
207
208 See L<DBIx::Class::Storage::DBI/connect_info> for a detailed explanation
209 of the arguments supported.
210
211 Examples:
212
213   connect_info => {
214     dsn => 'dbi:Pg:dbname=mypgdb',
215     user => 'postgres',
216     password => ''
217   }
218
219   connect_info => {
220     dsn => 'dbi:SQLite:dbname=foo.db',
221     on_connect_do => [
222       'PRAGMA synchronous = OFF',
223     ]
224   }
225
226   connect_info => {
227     dsn => 'dbi:Pg:dbname=mypgdb',
228     user => 'postgres',
229     password => '',
230     pg_enable_utf8 => 1,
231     on_connect_do => [
232       'some SQL statement',
233       'another SQL statement',
234     ],
235   }
236
237 Or using L<Config::General>:
238
239     <Model::FilmDB>
240         schema_class   MyApp::Schema::FilmDB
241         traits Caching
242         <connect_info>
243             dsn   dbi:Pg:dbname=mypgdb
244             user   postgres
245             password ""
246             auto_savepoint 1
247             quote_char """
248             on_connect_do   some SQL statement
249             on_connect_do   another SQL statement
250         </connect_info>
251     </Model::FilmDB>
252
253 or
254
255     <Model::FilmDB>
256         schema_class   MyApp::Schema::FilmDB
257         connect_info   dbi:SQLite:dbname=foo.db
258     </Model::FilmDB>
259
260 Or using L<YAML>:
261
262   Model::MyDB:
263       schema_class: MyDB
264       connect_info:
265           dsn: dbi:Oracle:mydb
266           user: mtfnpy
267           password: mypass
268           LongReadLen: 1000000
269           LongTruncOk: 1
270           on_connect_do: [ "alter session set nls_date_format = 'YYYY-MM-DD HH24:MI:SS'" ]
271           cursor_class: 'DBIx::Class::Cursor::Cached'
272           quote_char: '"'
273
274 The old arrayref style with hashrefs for L<DBI> then L<DBIx::Class> options is also
275 supported:
276
277   connect_info => [
278     'dbi:Pg:dbname=mypgdb',
279     'postgres',
280     '',
281     {
282       pg_enable_utf8 => 1,
283     },
284     {
285       auto_savepoint => 1,
286       on_connect_do => [
287         'some SQL statement',
288         'another SQL statement',
289       ],
290     }
291   ]
292
293 =head2 traits
294
295 Array of Traits to apply to the instance. Traits are L<Moose::Role>s.
296
297 They are relative to the C<< MyApp::Model::DB::Trait:: >>, then the C<<
298 Catalyst::Model::DBIC::Schema::Trait:: >> namespaces, unless prefixed with C<+>
299 in which case they are taken to be a fully qualified name. E.g.:
300
301     traits Caching
302     traits +MyApp::DB::Trait::Foo
303
304 A new instance is created at application time, so any consumed required
305 attributes, coercions and modifiers will work.
306
307 Traits are applied at L<Catalyst::Component/COMPONENT> time using L<MooseX::Traits>.
308
309 C<ref $self> will be an anon class if any traits are applied, C<<
310 $self->_original_class_name >> will be the original class.
311
312 When writing a Trait, interesting points to modify are C<BUILD>, L</setup> and
313 L</ACCEPT_CONTEXT>.
314
315 Traits that come with the distribution:
316
317 =over 4
318
319 =item L<Catalyst::Model::DBIC::Schema::Trait::Caching>
320
321 =item L<Catalyst::Model::DBIC::Schema::Trait::Replicated>
322
323 =back
324
325 =head2 storage_type
326
327 Allows the use of a different C<storage_type> than what is set in your
328 C<schema_class> (which in turn defaults to C<::DBI> if not set in current
329 L<DBIx::Class>).  Completely optional, and probably unnecessary for most
330 people until other storage backends become available for L<DBIx::Class>.
331
332 =head1 ATTRIBUTES
333
334 The keys you pass in the model configuration are available as attributes.
335
336 Other attributes available:
337
338 =head2 connect_info
339
340 Your connect_info args normalized to hashref form (with dsn/user/password.) See
341 L<DBIx::Class::Storage::DBI/connect_info> for more info on the hashref form of
342 L</connect_info>.
343
344 =head2 model_name
345
346 The model name L<Catalyst> uses to resolve this model, the part after
347 C<::Model::> or C<::M::> in your class name. E.g. if your class name is
348 C<MyApp::Model::DB> the L</model_name> will be C<DB>.
349
350 =head2 _original_class_name
351
352 The class name of your model before any L</traits> are applied. E.g.
353 C<MyApp::Model::DB>.
354
355 =head2 _default_cursor_class
356
357 What to rest your L<DBIx::Class::Storage::DBI/cursor_class> if a custom one
358 doesn't work out. Defaults to L<DBIx::Class::Storage::DBI::Cursor>.
359
360 =head2 _traits
361
362 Unresolved arrayref of traits passed at C<COMPONENT> time.
363
364 =head2 _resolved_traits
365
366 Traits you used resolved to full class names.
367
368 =head1 METHODS
369
370 =head2 new
371
372 Instantiates the Model based on the above-documented ->config parameters.
373 The only required parameter is C<schema_class>.  C<connect_info> is
374 required in the case that C<schema_class> does not already have connection
375 information defined for it.
376
377 =head2 schema
378
379 Accessor which returns the connected schema being used by the this model.
380 There are direct shortcuts on the model class itself for
381 schema->resultset, schema->source, and schema->class.
382
383 =head2 composed_schema
384
385 Accessor which returns the composed schema, which has no connection info,
386 which was used in constructing the C<schema> above.  Useful for creating
387 new connections based on the same schema/model.  There are direct shortcuts
388 from the model object for composed_schema->clone and composed_schema->connect
389
390 =head2 clone
391
392 Shortcut for ->composed_schema->clone
393
394 =head2 connect
395
396 Shortcut for ->composed_schema->connect
397
398 =head2 source
399
400 Shortcut for ->schema->source
401
402 =head2 class
403
404 Shortcut for ->schema->class
405
406 =head2 resultset
407
408 Shortcut for ->schema->resultset
409
410 =head2 storage
411
412 Provides an accessor for the connected schema's storage object.
413 Used often for debugging and controlling transactions.
414
415 =cut
416
417 has schema => (is => 'rw', isa => 'DBIx::Class::Schema');
418
419 has schema_class => (
420     is => 'ro',
421     isa => SchemaClass,
422     coerce => 1,
423     required => 1
424 );
425
426 has storage_type => (is => 'rw', isa => Str);
427
428 has connect_info => (is => 'ro', isa => ConnectInfo, coerce => 1);
429
430 has model_name => (
431     is => 'ro',
432     isa => Str,
433     required => 1,
434     lazy_build => 1,
435 );
436
437 has _traits => (is => 'ro', isa => ArrayRef);
438 has _resolved_traits => (is => 'ro', isa => ArrayRef);
439
440 has _default_cursor_class => (
441     is => 'ro',
442     isa => CursorClass,
443     default => 'DBIx::Class::Storage::DBI::Cursor',
444     coerce => 1
445 );
446
447 has _original_class_name => (
448     is => 'ro',
449     required => 1,
450     isa => Str,
451     default => sub { blessed $_[0] },
452 );
453
454 sub COMPONENT {
455     my ($class, $app, $args) = @_;
456
457     $args = $class->merge_config_hashes($class->config, $args);
458
459     if (my $traits = delete $args->{traits}) {
460         my @traits = $class->_resolve_traits($traits->flatten);
461         return $class->new_with_traits(
462             traits => \@traits,
463             _original_class_name => $class,
464             _traits => $traits,
465             _resolved_traits => \@traits,
466             %$args
467         );
468     }
469
470     return $class->new(%$args);
471 }
472
473 # we override Catalyst::Component::BUILDARGS, which merges configs, because we
474 # merge configs ourselves in COMPONENT. We also don't pass $app to ->new, so
475 # Moose::Object::BUILDARGS works perfectly.
476 sub BUILDARGS {
477     goto &Moose::Object::BUILDARGS;
478 }
479
480 sub BUILD {
481     my $self = shift;
482     my $class = ref $self;
483     my $schema_class = $self->schema_class;
484
485     if( !$self->connect_info ) {
486         if($schema_class->storage && $schema_class->storage->connect_info) {
487             $self->connect_info($schema_class->storage->connect_info);
488         }
489         else {
490             die "Either ->config->{connect_info} must be defined for $class"
491                   . " or $schema_class must have connect info defined on it."
492                   . " Here's what we got:\n"
493                   . Dumper($self);
494         }
495     }
496
497     if (exists $self->connect_info->{cursor_class}) {
498         eval { Class::MOP::load_class($self->connect_info->{cursor_class}) }
499             or croak "invalid connect_info: Cannot load your cursor_class"
500         . " ".$self->connect_info->{cursor_class}.": $@";
501     }
502
503     $self->setup;
504
505     $self->composed_schema($schema_class->compose_namespace($class));
506
507     $self->schema($self->composed_schema->clone);
508
509     $self->schema->storage_type($self->storage_type)
510         if $self->storage_type;
511
512     $self->schema->connection($self->connect_info);
513
514     $self->_install_rs_models;
515 }
516
517 sub clone { shift->composed_schema->clone(@_); }
518
519 sub connect { shift->composed_schema->connect(@_); }
520
521 sub storage { shift->schema->storage(@_); }
522
523 sub resultset { shift->schema->resultset(@_); }
524
525 =head2 setup
526
527 Called at C<BUILD>> time before configuration, but after L</connect_info> is
528 set. To do something after configuuration use C<< after BUILD => >>.
529
530 =cut
531
532 sub setup { 1 }
533
534 =head2 ACCEPT_CONTEXT
535
536 Point of extension for doing things at C<< $c->model >> time, returns the model
537 instance, see L<Catalyst::Manual::Intro> for more information.
538
539 =cut
540
541 sub ACCEPT_CONTEXT { shift }
542
543 sub _install_rs_models {
544     my $self  = shift;
545     my $class = $self->_original_class_name;
546
547     no strict 'refs';
548
549     my @sources = $self->schema->sources;
550
551     die "No sources found (did you forget to define your tables?)"
552         unless @sources;
553
554     foreach my $moniker (@sources) {
555         my $classname = "${class}::$moniker";
556         *{"${classname}::ACCEPT_CONTEXT"} = sub {
557             shift;
558             shift->model($self->model_name)->resultset($moniker);
559         }
560     }
561 }
562
563 sub _reset_cursor_class {
564     my $self = shift;
565
566     if ($self->storage->can('cursor_class')) {
567         $self->storage->cursor_class($self->_default_cursor_class)
568             if $self->storage->cursor_class ne $self->_default_cursor_class;
569     }
570 }
571
572 {
573     my %COMPOSED_CACHE;
574
575     sub composed_schema {
576         my $self = shift;
577         my $class = $self->_original_class_name;
578         my $store = \$COMPOSED_CACHE{$class}{$self->schema_class};
579
580         $$store = shift if @_;
581
582         return $$store
583     }
584 }
585
586 sub _resolve_traits {
587     my ($class, @names) = @_;
588     my $base  = 'Trait';
589
590     my @search_ns = grep !/^(?:Moose|Class::MOP)::/,
591         $class->meta->class_precedence_list;
592         
593     my @traits;
594
595     OUTER: for my $name (@names) {
596         if ($name =~ /^\+(.*)/) {
597             push @traits, $1;
598             next;
599         }
600         for my $ns (@search_ns) {
601             my $full = "${ns}::${base}::${name}";
602             if (eval { Class::MOP::load_class($full) }) {
603                 push @traits, $full;
604                 next OUTER;
605             }
606         }
607     }
608
609     return @traits;
610 }
611
612 sub _build_model_name {
613     my $self  = shift;
614     my $class = $self->_original_class_name;
615     (my $model_name = $class) =~ s/^[\w:]+::(?:Model|M):://;
616
617     return $model_name;
618 }
619
620 __PACKAGE__->meta->make_immutable;
621
622 =head1 SEE ALSO
623
624 General Catalyst Stuff:
625
626 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
627 L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
628
629 Stuff related to DBIC and this Model style:
630
631 L<DBIx::Class>, L<DBIx::Class::Schema>,
632 L<DBIx::Class::Schema::Loader>, L<Catalyst::Helper::Model::DBIC::Schema>,
633 L<MooseX::Object::Pluggable>
634
635 Traits:
636
637 L<Catalyst::Model::DBIC::Schema::Trait::Caching>,
638 L<Catalyst::Model::DBIC::Schema::Trait::Replicated>
639
640 =head1 AUTHOR
641
642 Brandon L Black, C<blblack at gmail.com>
643
644 Contributors:
645
646 Rafael Kitover, C<rkitover at cpan.org>
647
648 =head1 COPYRIGHT
649
650 This program is free software, you can redistribute it and/or modify it
651 under the same terms as Perl itself.
652
653 =cut
654
655 1;
656 # vim:sts=4 sw=4 et: