C::H::M::DBIC::Schema - convert to Moose
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Helper / Model / DBIC / Schema.pm
1 package Catalyst::Helper::Model::DBIC::Schema;
2
3 use Moose;
4 no warnings 'uninitialized';
5
6 our $VERSION = '0.24';
7
8 use Carp;
9 use Tie::IxHash ();
10 use Data::Dumper ();
11 use List::Util ();
12
13 use namespace::clean -except => 'meta';
14
15 =head1 NAME
16
17 Catalyst::Helper::Model::DBIC::Schema - Helper for DBIC Schema Models
18
19 =head1 SYNOPSIS
20
21   script/create.pl model CatalystModelName DBIC::Schema MyApp::SchemaClass \
22     [ create=dynamic | create=static ] [ roles=role1,role2... ] \
23     [ Schema::Loader opts ] [ dsn user pass ] \
24     [ other connect_info args ]
25
26 =head1 DESCRIPTION
27
28 Helper for the DBIC Schema Models.
29
30 =head2 Arguments:
31
32 C<CatalystModelName> is the short name for the Catalyst Model class
33 being generated (i.e. callable with C<$c-E<gt>model('CatalystModelName')>).
34
35 C<MyApp::SchemaClass> is the fully qualified classname of your Schema,
36 which might or might not yet exist.  Note that you should have a good
37 reason to create this under a new global namespace, otherwise use an
38 existing top level namespace for your schema class.
39
40 C<create=dynamic> instructs this Helper to generate the named Schema
41 class for you, basing it on L<DBIx::Class::Schema::Loader> (which
42 means the table information will always be dynamically loaded at
43 runtime from the database).
44
45 C<create=static> instructs this Helper to generate the named Schema
46 class for you, using L<DBIx::Class::Schema::Loader> in "one shot"
47 mode to create a standard, manually-defined L<DBIx::Class::Schema>
48 setup, based on what the Loader sees in your database at this moment.
49 A Schema/Model pair generated this way will not require
50 L<DBIx::Class::Schema::Loader> at runtime, and will not automatically
51 adapt itself to changes in your database structure.  You can edit
52 the generated classes by hand to refine them.
53
54 C<roles> is the list of roles to apply to the model, see
55 L<Catalyst::Model::DBIC::Schema> for details.
56
57 C<Schema::Loader opts> are described in L</TYPICAL EXAMPLES> below.
58
59 C<connect_info> arguments are the same as what
60 DBIx::Class::Schema::connect expects, and are storage_type-specific.
61 For DBI-based storage, these arguments are the dsn, username,
62 password, and connect options, respectively.  These are optional for
63 existing Schemas, but required if you use either of the C<create=>
64 options.
65
66 Use of either of the C<create=> options requires L<DBIx::Class::Schema::Loader>.
67
68 =head1 TYPICAL EXAMPLES
69
70   # Use DBIx::Class::Schema::Loader to create a static DBIx::Class::Schema,
71   #  and a Model which references it:
72   script/myapp_create.pl model CatalystModelName DBIC::Schema \
73     MyApp::SchemaClass create=static dbi:mysql:foodb myuname mypass
74
75   # Same, with extra connect_info args
76   script/myapp_create.pl model CatalystModelName DBIC::Schema \
77     MyApp::SchemaClass create=static dbi:SQLite:foo.db '' '' \
78     AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
79     on_connect_do='["select 1", "select 2"]'
80
81   # Same, but with extra Schema::Loader args (separate multiple values by commas):
82   script/myapp_create.pl model CatalystModelName DBIC::Schema \
83     MyApp::SchemaClass create=static db_schema=foodb components=Foo,Bar \
84     exclude='^wibble|wobble$' moniker_map='{ foo => "FFFFUUUU" }' \
85     dbi:Pg:dbname=foodb myuname mypass
86
87   # See DBIx::Class::Schema::Loader::Base for list of options
88
89   # Create a dynamic DBIx::Class::Schema::Loader-based Schema,
90   #  and a Model which references it:
91   script/myapp_create.pl model CatalystModelName DBIC::Schema \
92     MyApp::SchemaClass create=dynamic dbi:mysql:foodb myuname mypass
93
94   # Reference an existing Schema of any kind, and provide some connection information for ->config:
95   script/myapp_create.pl model CatalystModelName DBIC::Schema \
96     MyApp::SchemaClass dbi:mysql:foodb myuname mypass
97
98   # Same, but don't supply connect information yet (you'll need to do this
99   #  in your app config, or [not recommended] in the schema itself).
100   script/myapp_create.pl model ModelName DBIC::Schema My::SchemaClass
101
102 =cut
103
104 has helper => (is => 'ro', isa => 'Catalyst::Helper', required => 1);
105
106 has schema_class => (is => 'ro', isa => 'Str', required => 1);
107
108 has loader_args => (is => 'rw', isa => 'HashRef');
109 has connect_info => (is => 'rw', isa => 'HashRef');
110
111 has old_schema => (is => 'rw', isa => 'Bool', lazy => 1, default => sub {
112     my $self = shift;
113
114     my @schema_pm   = split '::', $self->schema_class;
115     $schema_pm[-1] .= '.pm';
116     my $schema_file =
117     File::Spec->catfile($self->helper->{base}, 'lib', @schema_pm);
118
119     if (-f $schema_file) {
120         my $schema_code = do { local (@ARGV, $/) = $schema_file; <> };
121         return 1 if $schema_code =~ /->load_classes/;
122     }
123
124     0;
125 });
126
127 =head1 METHODS
128
129 =head2 mk_compclass
130
131 This is called by L<Catalyst::Helper> with the commandline args to generate the
132 files.
133
134 =cut
135
136 sub mk_compclass {
137     my ($package, $helper, $schema_class, @args) = @_;
138
139     my $self = $package->new(helper => $helper, schema_class => $schema_class);
140
141     $helper->{schema_class} = $schema_class
142         or croak "Must supply schema class name";
143
144     my $create = '';
145     if ($args[0] && $args[0] =~ /^create=(dynamic|static)\z/) {
146         $create = $1;
147         shift @args;
148
149         if ($args[0] && $args[0] =~ /^roles=(.*)\z/) {
150             $helper->{roles} = '['
151                 .(join ',' => map { qq{'$_'} } (split /,/ => $1))
152                 .']';
153             shift @args;
154         }
155
156         if (@args) {
157             $self->_parse_loader_args(\@args);
158
159             if (List::Util::first { /dbi:/ } @args) {
160                 $helper->{setup_connect_info} = 1;
161
162                 $helper->{connect_info} =
163                     $self->_build_helper_connect_info(\@args);
164
165                 $self->_parse_connect_info(\@args) if $create eq 'static';
166             }
167         }
168     }
169
170     $helper->{generator} = ref $self;
171     $helper->{generator_version} = $VERSION;
172
173     if ($create eq 'dynamic') {
174         $self->helper->{loader_args} = $self->_build_helper_loader_args;
175         $self->_gen_dynamic_schema;
176     } elsif ($create eq 'static') {
177         $self->_gen_static_schema;
178     }
179
180     $self->_gen_model;
181 }
182
183 sub _parse_loader_args {
184     my ($self, $args) = @_;
185
186     my %loader_args = $self->_read_loader_args($args);
187
188     while (my ($key, $val) = each %loader_args) {
189         next if $key =~ /^(?:components|constraint|exclude)\z/;
190
191         $loader_args{$key} = eval $val;
192         croak "syntax error for loader args key '$key' with value '$val': $@"
193             if $@;
194     }
195
196     my @components =
197     $self->_build_loader_components(delete $loader_args{components});
198
199     for my $re_opt (qw/constraint exclude/) {
200         $loader_args{$re_opt} = qr/$loader_args{$re_opt}/
201         if exists $loader_args{$re_opt};
202     }
203
204     tie my %result, 'Tie::IxHash';
205
206     %result = (
207         relationships => 1,
208         (%loader_args ? %loader_args : ()),
209         (!$self->old_schema ? (
210                 use_namespaces => 1
211             ) : ()),
212         (@components ? (
213                 components => \@components
214             ) : ())
215     );
216
217     $self->loader_args(\%result);
218
219     wantarray ? %result : \%result;
220 }
221
222 sub _read_loader_args {
223     my ($self, $args) = @_;
224
225     my %loader_args;
226
227     while (@$args && $args->[0] !~ /^dbi:/) {
228         my ($key, $val) = split /=/, shift(@$args), 2;
229
230         if ((my @vals = split /,/ => $val) > 1) {
231             $loader_args{$key} = \@vals;
232         } else {
233             $loader_args{$key} = $val;
234         }
235     }
236
237     wantarray ? %loader_args : \%loader_args;
238 }
239
240 sub _build_helper_loader_args {
241     my $self = shift;
242
243     my $args = $self->loader_args;
244
245     tie my %loader_args, 'Tie::IxHash';
246
247     while (my ($arg, $val) = each %$args) {
248         if (ref $val) {
249             $loader_args{$arg} = $self->_data_struct_to_string($val);
250         } else {
251             $loader_args{$arg} = qq{'$val'};
252         }
253     }
254
255     \%loader_args
256 }
257
258 sub _build_loader_components {
259     my ($self, $components) = @_;
260
261     my @components = $self->old_schema ? () : ('InflateColumn::DateTime');
262
263     if ($components) {
264         $components = [ $components ] if !ref $components;
265         push @components, @$components;
266     }
267
268     wantarray ? @components : \@components;
269 }
270
271 sub _build_helper_connect_info {
272     my ($self, $connect_info) = @_;
273
274     my @connect_info = @$connect_info;
275
276     my ($dsn, $user, $password) = splice @connect_info, 0, 3;
277
278     tie my %helper_connect_info, 'Tie::IxHash';
279
280     %helper_connect_info = (
281         dsn => qq{'$dsn'},
282         user => qq{'$user'},
283         password => qq{'$password'}
284     );
285
286     for (@connect_info) {
287         if (/^\s*{.*}\s*\z/) {
288             my $hash = eval $_;
289             croak "Syntax errorr in connect_info hash: $_: $@" if $@;
290             my %hash = %$hash;
291
292             for my $key (keys %hash) {
293                 my $val = $hash{$key};
294
295                 if (ref $val) {
296                     $val = $self->_data_struct_to_string($val);
297                 } else {
298                     $val = qq{'$val'};
299                 }
300
301                 $helper_connect_info{$key} = $val;
302             }
303
304             next;
305         }
306
307         my ($key, $val) = split /=/, $_, 2;
308
309         $helper_connect_info{$key} = $self->_quote_unless_struct($val);
310     }
311
312     \%helper_connect_info
313 }
314
315 sub _data_struct_to_string {
316     my ($self, $data) = @_;
317
318     local $Data::Dumper::Terse = 1;
319     local $Data::Dumper::Quotekeys = 0;
320     local $Data::Dumper::Indent = 0;
321     local $Data::Dumper::Useqq = 1;
322
323     return Data::Dumper->Dump([$data]);
324 }
325
326 sub _parse_connect_info {
327     my ($self, $connect_info) = @_;
328
329     my @connect_info = @$connect_info;
330
331     my ($dsn, $user, $password) = splice @connect_info, 0, 3;
332
333     tie my %connect_info, 'Tie::IxHash';
334     @connect_info{qw/dsn user password/} = ($dsn, $user, $password);
335
336     for (@connect_info) {
337         if (/^\s*{.*}\s*\z/) {
338             my $hash = eval $_;
339             croak "Syntax errorr in connect_info hash: $_: $@" if $@;
340
341             %connect_info = (%connect_info, %$hash);
342
343             next;
344         }
345
346         my ($key, $val) = split /=/, $_, 2;
347
348         $connect_info{$key} = eval $val;
349         croak "syntax error for connect_info key '$key' with value '$val': $@"
350             if $@;
351     }
352
353     $self->connect_info(\%connect_info);
354
355     \%connect_info
356 }
357
358 sub _quote_unless_struct {
359     my ($self, $val) = @_;
360
361     $val = qq{'$val'} if $val !~ /^\s*[[{]/;
362
363     $val;
364 }
365
366 sub _gen_dynamic_schema {
367     my $self = shift;
368
369     my $helper = $self->helper;
370
371     my @schema_parts = split(/\:\:/, $self->schema_class);
372     my $schema_file_part = pop @schema_parts;
373
374     my $schema_dir  = File::Spec->catfile(
375         $helper->{base}, 'lib', @schema_parts
376     );
377     my $schema_file = File::Spec->catfile(
378         $schema_dir, $schema_file_part . '.pm'
379     );
380
381     $helper->mk_dir($schema_dir);
382     $helper->render_file('schemaclass', $schema_file);
383 }
384
385 sub _gen_static_schema {
386     my $self = shift;
387
388     croak "cannot load schema without connect info" unless $self->connect_info;
389
390     my $helper = $self->helper;
391
392     my $schema_dir = File::Spec->catfile($helper->{base}, 'lib');
393
394     eval { Class::MOP::load_class('DBIx::Class::Schema::Loader') };
395     croak "Cannot load DBIx::Class::Schema::Loader: $@" if $@;
396
397     DBIx::Class::Schema::Loader->import(
398         "dump_to_dir:$schema_dir", 'make_schema_at'
399     );
400
401     make_schema_at(
402         $self->schema_class,
403         $self->loader_args,
404         [$self->connect_info]
405     );
406 }
407
408 sub _gen_model {
409     my $self = shift;
410     my $helper = $self->helper;
411
412     $helper->render_file('compclass', $helper->{file} );
413 }
414
415 =head1 SEE ALSO
416
417 General Catalyst Stuff:
418
419 L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
420 L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
421
422 Stuff related to DBIC and this Model style:
423
424 L<DBIx::Class>, L<DBIx::Class::Schema>,
425 L<DBIx::Class::Schema::Loader>, L<Catalyst::Model::DBIC::Schema>
426
427 =head1 AUTHOR
428
429 Brandon L Black, C<blblack@gmail.com>
430
431 Contributors:
432
433 Rafael Kitover, C<<rkitover at cpan.org>>
434
435 =head1 LICENSE
436
437 This library is free software, you can redistribute it and/or modify
438 it under the same terms as Perl itself.
439
440 =cut
441
442 1;
443
444 __DATA__
445
446 =begin pod_to_ignore
447
448 __schemaclass__
449 package [% schema_class %];
450
451 use strict;
452 use base qw/DBIx::Class::Schema::Loader/;
453
454 __PACKAGE__->loader_options(
455     [%- FOREACH key = loader_args.keys %]
456     [% key %] => [% loader_args.${key} %],
457     [%- END -%]
458
459 );
460
461 =head1 NAME
462
463 [% schema_class %] - L<DBIx::Class::Schema::Loader> class
464
465 =head1 SYNOPSIS
466
467 See L<[% app %]>
468
469 =head1 DESCRIPTION
470
471 Dynamic L<DBIx::Class::Schema::Loader> schema for use in L<[% class %]>
472
473 =head1 GENERATED BY
474
475 [% generator %] - [% generator_version %]
476
477 =head1 AUTHOR
478
479 [% author.replace(',+$', '') %]
480
481 =head1 LICENSE
482
483 This library is free software, you can redistribute it and/or modify
484 it under the same terms as Perl itself.
485
486 =cut
487
488 1;
489
490 __compclass__
491 package [% class %];
492
493 use strict;
494 use base 'Catalyst::Model::DBIC::Schema';
495
496 __PACKAGE__->config(
497     schema_class => '[% schema_class %]',
498     [% IF roles %]roles => [% roles %],[% END %]
499     [% IF setup_connect_info %]connect_info => {
500         [%- FOREACH key = connect_info.keys %]
501         [% key %] => [% connect_info.${key} %],
502         [%- END -%]
503
504     }[% END %]
505 );
506
507 =head1 NAME
508
509 [% class %] - Catalyst DBIC Schema Model
510
511 =head1 SYNOPSIS
512
513 See L<[% app %]>
514
515 =head1 DESCRIPTION
516
517 L<Catalyst::Model::DBIC::Schema> Model using schema L<[% schema_class %]>
518
519 =head1 GENERATED BY
520
521 [% generator %] - [% generator_version %]
522
523 =head1 AUTHOR
524
525 [% author.replace(',+$', '') %]
526
527 =head1 LICENSE
528
529 This library is free software, you can redistribute it and/or modify
530 it under the same terms as Perl itself.
531
532 =cut
533
534 1;