release
[catagits/Catalyst-Model-DBIC-Schema.git] / lib / Catalyst / Helper / Model / DBIC / Schema.pm
CommitLineData
ad91060a 1package Catalyst::Helper::Model::DBIC::Schema;
2
73f72d28 3use namespace::autoclean;
f090a149 4use Moose;
ffbf1967 5no warnings 'uninitialized';
018eb0e2 6
ca7cf6f0 7our $VERSION = '0.30';
2201c2e4 8
0f2fd2c0 9use Carp;
2201c2e4 10use Tie::IxHash ();
11use Data::Dumper ();
c4fee9b8 12use List::Util 'first';
4cbe63e7 13use MooseX::Types::Moose qw/Str HashRef Bool ArrayRef/;
14use Catalyst::Model::DBIC::Schema::Types 'CreateOption';
4cbe63e7 15use List::MoreUtils 'firstidx';
5f9a34d7 16use Scalar::Util 'looks_like_number';
2201c2e4 17
ad91060a 18=head1 NAME
19
20Catalyst::Helper::Model::DBIC::Schema - Helper for DBIC Schema Models
21
22=head1 SYNOPSIS
23
2201c2e4 24 script/create.pl model CatalystModelName DBIC::Schema MyApp::SchemaClass \
c34bcab6 25 [ create=dynamic | create=static ] [ traits=trait1,trait2... ] \
2ff00e2b 26 [ Schema::Loader opts ] [ dsn user pass ] \
27 [ other connect_info args ]
0f2fd2c0 28
d89e6c8a 29=head1 DESCRIPTION
30
31Helper for the DBIC Schema Models.
32
33=head2 Arguments:
34
018eb0e2 35C<CatalystModelName> is the short name for the Catalyst Model class
36being generated (i.e. callable with C<$c-E<gt>model('CatalystModelName')>).
6116daed 37
018eb0e2 38C<MyApp::SchemaClass> is the fully qualified classname of your Schema,
6116daed 39which might or might not yet exist. Note that you should have a good
40reason to create this under a new global namespace, otherwise use an
41existing top level namespace for your schema class.
42
018eb0e2 43C<create=dynamic> instructs this Helper to generate the named Schema
6116daed 44class for you, basing it on L<DBIx::Class::Schema::Loader> (which
45means the table information will always be dynamically loaded at
46runtime from the database).
47
018eb0e2 48C<create=static> instructs this Helper to generate the named Schema
6116daed 49class for you, using L<DBIx::Class::Schema::Loader> in "one shot"
50mode to create a standard, manually-defined L<DBIx::Class::Schema>
51setup, based on what the Loader sees in your database at this moment.
52A Schema/Model pair generated this way will not require
53L<DBIx::Class::Schema::Loader> at runtime, and will not automatically
54adapt itself to changes in your database structure. You can edit
55the generated classes by hand to refine them.
56
c34bcab6 57C<traits> is the list of traits to apply to the model, see
2ff00e2b 58L<Catalyst::Model::DBIC::Schema> for details.
59
60C<Schema::Loader opts> are described in L</TYPICAL EXAMPLES> below.
61
018eb0e2 62C<connect_info> arguments are the same as what
6116daed 63DBIx::Class::Schema::connect expects, and are storage_type-specific.
64For DBI-based storage, these arguments are the dsn, username,
65password, and connect options, respectively. These are optional for
66existing Schemas, but required if you use either of the C<create=>
67options.
d89e6c8a 68
a75b6e58 69username and password can be omitted for C<SQLite> dsns.
70
d89e6c8a 71Use of either of the C<create=> options requires L<DBIx::Class::Schema::Loader>.
0b2a7108 72
73=head1 TYPICAL EXAMPLES
74
a75b6e58 75Use DBIx::Class::Schema::Loader to create a static DBIx::Class::Schema,
76and a Model which references it:
77
2201c2e4 78 script/myapp_create.pl model CatalystModelName DBIC::Schema \
79 MyApp::SchemaClass create=static dbi:mysql:foodb myuname mypass
80
a75b6e58 81Same, with extra connect_info args
82user and pass can be omitted for sqlite, since they are always empty
83
2201c2e4 84 script/myapp_create.pl model CatalystModelName DBIC::Schema \
a75b6e58 85 MyApp::SchemaClass create=static dbi:SQLite:foo.db \
2201c2e4 86 AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
a75b6e58 87 on_connect_do='["select 1", "select 2"]' quote_char='"'
88
c34bcab6 89If using a 2 character quote_char:
90
91 script/myapp_create.pl ... quote_char='[]'
92
a75b6e58 93B<ON WINDOWS COMMAND LINES QUOTING RULES ARE DIFFERENT>
94
95In C<cmd.exe> the above example would be:
96
97 script/myapp_create.pl model CatalystModelName DBIC::Schema \
98 MyApp::SchemaClass create=static dbi:SQLite:foo.db \
99 AutoCommit=1 cursor_class=DBIx::Class::Cursor::Cached \
100 on_connect_do="[\"select 1\", \"select 2\"]" quote_char="\""
101
102Same, but with extra Schema::Loader args (separate multiple values by commas):
0b2a7108 103
2201c2e4 104 script/myapp_create.pl model CatalystModelName DBIC::Schema \
105 MyApp::SchemaClass create=static db_schema=foodb components=Foo,Bar \
4cbe63e7 106 exclude='^(wibble|wobble)$' moniker_map='{ foo => "FOO" }' \
2201c2e4 107 dbi:Pg:dbname=foodb myuname mypass
34f036a0 108
a75b6e58 109See L<DBIx::Class::Schema::Loader::Base> for a list of options
110
111Create a dynamic DBIx::Class::Schema::Loader-based Schema,
112and a Model which references it (B<DEPRECATED>):
34f036a0 113
2201c2e4 114 script/myapp_create.pl model CatalystModelName DBIC::Schema \
115 MyApp::SchemaClass create=dynamic dbi:mysql:foodb myuname mypass
ad91060a 116
a75b6e58 117Reference an existing Schema of any kind, and provide some connection information for ->config:
118
2201c2e4 119 script/myapp_create.pl model CatalystModelName DBIC::Schema \
120 MyApp::SchemaClass dbi:mysql:foodb myuname mypass
ad91060a 121
a75b6e58 122Same, but don't supply connect information yet (you'll need to do this
123in your app config, or [not recommended] in the schema itself).
124
d89e6c8a 125 script/myapp_create.pl model ModelName DBIC::Schema My::SchemaClass
ad91060a 126
f090a149 127=cut
128
129has helper => (is => 'ro', isa => 'Catalyst::Helper', required => 1);
4cbe63e7 130has create => (is => 'rw', isa => CreateOption);
131has args => (is => 'ro', isa => ArrayRef);
c34bcab6 132has traits => (is => 'rw', isa => ArrayRef);
61ed82a5 133has schema_class => (is => 'ro', isa => Str, required => 1);
61ed82a5 134has loader_args => (is => 'rw', isa => HashRef);
135has connect_info => (is => 'rw', isa => HashRef);
61ed82a5 136has old_schema => (is => 'rw', isa => Bool, lazy_build => 1);
4cbe63e7 137has components => (is => 'rw', isa => ArrayRef);
f090a149 138
018eb0e2 139=head1 METHODS
ad91060a 140
018eb0e2 141=head2 mk_compclass
ad91060a 142
0fbbc8d5 143This is called by L<Catalyst::Helper> with the commandline args to generate the
144files.
145
ad91060a 146=cut
147
148sub mk_compclass {
2201c2e4 149 my ($package, $helper, $schema_class, @args) = @_;
150
4cbe63e7 151 my $self = $package->new(
152 helper => $helper,
153 schema_class => $schema_class,
154 args => \@args
155 );
0f2fd2c0 156
4cbe63e7 157 $self->run;
158}
159
160sub BUILD {
161 my $self = shift;
162 my $helper = $self->helper;
7dfd616a 163 my @args = @{ $self->args || [] };
4cbe63e7 164
165 $helper->{schema_class} = $self->schema_class;
c4fee9b8 166
167 @args = $self->_cleanup_args(\@args);
d89e6c8a 168
c34bcab6 169 my ($traits_idx, $traits);
170 if (($traits_idx = firstidx { ($traits) = /^traits=(\S*)\z/ } @args) != -1) {
171 my @traits = split /,/ => $traits;
2201c2e4 172
c34bcab6 173 $self->traits(\@traits);
4cbe63e7 174
c34bcab6 175 $helper->{traits} = '['
7dfd616a 176 .(join ',' => map { qq{'$_'} } @traits)
4cbe63e7 177 .']';
178
c34bcab6 179 splice @args, $traits_idx, 1, ();
4cbe63e7 180 }
181
182 if ($args[0] && $args[0] =~ /^create=(\S*)\z/) {
183 $self->create($1);
184 shift @args;
2ff00e2b 185
2201c2e4 186 if (@args) {
187 $self->_parse_loader_args(\@args);
188
4cbe63e7 189 $helper->{loader_args} = $self->_build_helper_loader_args;
ca7cf6f0 190 }
191 }
4cbe63e7 192
ca7cf6f0 193 my $dbi_dsn_part;
194 if (first { ($dbi_dsn_part) = /^(dbi):/i } @args) {
195 die
a4803ca6 196qq{DSN must start with 'dbi:' not '$dbi_dsn_part' (case matters!)}
ca7cf6f0 197 if $dbi_dsn_part ne 'dbi';
a4803ca6 198
ca7cf6f0 199 $helper->{setup_connect_info} = 1;
2201c2e4 200
ca7cf6f0 201 $helper->{connect_info} =
202 $self->_build_helper_connect_info(\@args);
2201c2e4 203
ca7cf6f0 204 $self->_parse_connect_info(\@args);
d89e6c8a 205 }
0f2fd2c0 206
2201c2e4 207 $helper->{generator} = ref $self;
208 $helper->{generator_version} = $VERSION;
4cbe63e7 209}
210
9f7cc709 211=head2 run
212
213Can be called on an instance to generate the files.
214
215=cut
216
4cbe63e7 217sub run {
218 my $self = shift;
2201c2e4 219
4cbe63e7 220 if ($self->create eq 'dynamic') {
c4fee9b8 221 $self->_print_dynamic_deprecation_warning;
2201c2e4 222 $self->_gen_dynamic_schema;
4cbe63e7 223 } elsif ($self->create eq 'static') {
2201c2e4 224 $self->_gen_static_schema;
225 }
226
227 $self->_gen_model;
228}
229
230sub _parse_loader_args {
231 my ($self, $args) = @_;
232
233 my %loader_args = $self->_read_loader_args($args);
234
235 while (my ($key, $val) = each %loader_args) {
236 next if $key =~ /^(?:components|constraint|exclude)\z/;
237
5f9a34d7 238 $loader_args{$key} = $self->_eval($val);
c4fee9b8 239 die "syntax error for loader args key '$key' with value '$val': $@"
2201c2e4 240 if $@;
241 }
242
243 my @components =
244 $self->_build_loader_components(delete $loader_args{components});
245
4cbe63e7 246 $self->components(\@components);
247
2201c2e4 248 for my $re_opt (qw/constraint exclude/) {
249 $loader_args{$re_opt} = qr/$loader_args{$re_opt}/
250 if exists $loader_args{$re_opt};
251 }
252
253 tie my %result, 'Tie::IxHash';
254
255 %result = (
256 relationships => 1,
257 (%loader_args ? %loader_args : ()),
f090a149 258 (!$self->old_schema ? (
2201c2e4 259 use_namespaces => 1
260 ) : ()),
261 (@components ? (
262 components => \@components
263 ) : ())
264 );
265
266 $self->loader_args(\%result);
267
268 wantarray ? %result : \%result;
269}
270
271sub _read_loader_args {
272 my ($self, $args) = @_;
273
274 my %loader_args;
275
a4803ca6 276 while (@$args && $args->[0] !~ /^dbi:/i) {
2201c2e4 277 my ($key, $val) = split /=/, shift(@$args), 2;
ca14239e 278
4cbe63e7 279 if ($self->_is_struct($val)) {
280 $loader_args{$key} = $val;
281 } elsif ((my @vals = split /,/ => $val) > 1) {
2201c2e4 282 $loader_args{$key} = \@vals;
ca14239e 283 } else {
2201c2e4 284 $loader_args{$key} = $val;
ca14239e 285 }
286 }
287
2201c2e4 288 wantarray ? %loader_args : \%loader_args;
289}
290
291sub _build_helper_loader_args {
292 my $self = shift;
293
294 my $args = $self->loader_args;
295
296 tie my %loader_args, 'Tie::IxHash';
297
298 while (my ($arg, $val) = each %$args) {
299 if (ref $val) {
300 $loader_args{$arg} = $self->_data_struct_to_string($val);
301 } else {
302 $loader_args{$arg} = qq{'$val'};
0b2a7108 303 }
0f2fd2c0 304 }
305
2201c2e4 306 \%loader_args
307}
308
309sub _build_loader_components {
310 my ($self, $components) = @_;
d89e6c8a 311
f090a149 312 my @components = $self->old_schema ? () : ('InflateColumn::DateTime');
d89e6c8a 313
2201c2e4 314 if ($components) {
315 $components = [ $components ] if !ref $components;
316 push @components, @$components;
d89e6c8a 317 }
2201c2e4 318
319 wantarray ? @components : \@components;
320}
321
322sub _build_helper_connect_info {
323 my ($self, $connect_info) = @_;
324
325 my @connect_info = @$connect_info;
326
a75b6e58 327 my ($dsn, $user, $password) = $self->_get_dsn_user_pass(\@connect_info);
2201c2e4 328
329 tie my %helper_connect_info, 'Tie::IxHash';
330
331 %helper_connect_info = (
332 dsn => qq{'$dsn'},
333 user => qq{'$user'},
334 password => qq{'$password'}
335 );
336
337 for (@connect_info) {
338 if (/^\s*{.*}\s*\z/) {
5f9a34d7 339 my $hash = $self->_eval($_);
c4fee9b8 340 die "Syntax errorr in connect_info hash: $_: $@" if $@;
2201c2e4 341 my %hash = %$hash;
342
343 for my $key (keys %hash) {
344 my $val = $hash{$key};
345
346 if (ref $val) {
347 $val = $self->_data_struct_to_string($val);
348 } else {
c34bcab6 349 $val = $self->_quote($val);
2201c2e4 350 }
351
352 $helper_connect_info{$key} = $val;
592cd3ae 353 }
2201c2e4 354
355 next;
592cd3ae 356 }
357
2201c2e4 358 my ($key, $val) = split /=/, $_, 2;
1d155058 359
c34bcab6 360 if ($key eq 'quote_char') {
361 $helper_connect_info{$key} = length($val) == 1 ?
362 $self->_quote($val) :
363 $self->_data_struct_to_string([split //, $val]);
364 } else {
365 $helper_connect_info{$key} = $self->_quote_unless_struct($val);
366 }
2201c2e4 367 }
1d155058 368
2201c2e4 369 \%helper_connect_info
370}
1d155058 371
61ed82a5 372sub _build_old_schema {
373 my $self = shift;
374
375 my @schema_pm = split '::', $self->schema_class;
376 $schema_pm[-1] .= '.pm';
377 my $schema_file =
378 File::Spec->catfile($self->helper->{base}, 'lib', @schema_pm);
379
380 if (-f $schema_file) {
381 my $schema_code = do { local (@ARGV, $/) = $schema_file; <> };
382 return 1 if $schema_code =~ /->load_classes/;
383 }
384
385 0;
386}
387
2201c2e4 388sub _data_struct_to_string {
389 my ($self, $data) = @_;
34f036a0 390
2201c2e4 391 local $Data::Dumper::Terse = 1;
392 local $Data::Dumper::Quotekeys = 0;
393 local $Data::Dumper::Indent = 0;
394 local $Data::Dumper::Useqq = 1;
34f036a0 395
2201c2e4 396 return Data::Dumper->Dump([$data]);
397}
34f036a0 398
a75b6e58 399sub _get_dsn_user_pass {
400 my ($self, $connect_info) = @_;
401
402 my $dsn = shift @$connect_info;
403 my ($user, $password);
404
405 if ($dsn =~ /sqlite/i) {
406 ($user, $password) = ('', '');
108a45d1 407 shift @$connect_info while @$connect_info and $connect_info->[0] eq '';
a75b6e58 408 } else {
409 ($user, $password) = splice @$connect_info, 0, 2;
410 }
411
412 ($dsn, $user, $password)
413}
414
2201c2e4 415sub _parse_connect_info {
416 my ($self, $connect_info) = @_;
417
418 my @connect_info = @$connect_info;
419
a75b6e58 420 my ($dsn, $user, $password) = $self->_get_dsn_user_pass(\@connect_info);
2201c2e4 421
422 tie my %connect_info, 'Tie::IxHash';
423 @connect_info{qw/dsn user password/} = ($dsn, $user, $password);
424
425 for (@connect_info) {
426 if (/^\s*{.*}\s*\z/) {
5f9a34d7 427 my $hash = $self->_eval($_);
c4fee9b8 428 die "Syntax errorr in connect_info hash: $_: $@" if $@;
2201c2e4 429
430 %connect_info = (%connect_info, %$hash);
ca14239e 431
2201c2e4 432 next;
ca14239e 433 }
434
2201c2e4 435 my ($key, $val) = split /=/, $_, 2;
436
c34bcab6 437 if ($key eq 'quote_char') {
438 $connect_info{$key} = length($val) == 1 ? $val : [split //, $val];
439 } elsif ($key =~ /^(?:name_sep|limit_dialect)\z/) {
3f139b02 440 $connect_info{$key} = $val;
441 } else {
5f9a34d7 442 $connect_info{$key} = $self->_eval($val);
3f139b02 443 }
444
c4fee9b8 445 die "syntax error for connect_info key '$key' with value '$val': $@"
2201c2e4 446 if $@;
d89e6c8a 447 }
202d09c8 448
2201c2e4 449 $self->connect_info(\%connect_info);
450
451 \%connect_info
452}
453
4cbe63e7 454sub _is_struct {
455 my ($self, $val) = @_;
456
457 return $val =~ /^\s*[[{]/;
458}
459
c34bcab6 460sub _quote {
461 my ($self, $val) = @_;
462
463 return 'q{'.$val.'}';
464}
465
2201c2e4 466sub _quote_unless_struct {
467 my ($self, $val) = @_;
468
c34bcab6 469 $val = $self->_quote($val) if not $self->_is_struct($val);
2201c2e4 470
c34bcab6 471 return $val;
2201c2e4 472}
473
5f9a34d7 474sub _eval {
475 my ($self, $code) = @_;
476
477 return $code if looks_like_number $code;
478
479 return $code if $code =~ m{^[\w;:/]*\z};
480
481 return eval "{no strict; $code}";
482}
483
2201c2e4 484sub _gen_dynamic_schema {
485 my $self = shift;
486
487 my $helper = $self->helper;
488
489 my @schema_parts = split(/\:\:/, $self->schema_class);
490 my $schema_file_part = pop @schema_parts;
491
492 my $schema_dir = File::Spec->catfile(
493 $helper->{base}, 'lib', @schema_parts
494 );
495 my $schema_file = File::Spec->catfile(
496 $schema_dir, $schema_file_part . '.pm'
497 );
498
499 $helper->mk_dir($schema_dir);
500 $helper->render_file('schemaclass', $schema_file);
501}
502
503sub _gen_static_schema {
504 my $self = shift;
505
c4fee9b8 506 die "cannot load schema without connect info" unless $self->connect_info;
2201c2e4 507
508 my $helper = $self->helper;
509
510 my $schema_dir = File::Spec->catfile($helper->{base}, 'lib');
511
f090a149 512 eval { Class::MOP::load_class('DBIx::Class::Schema::Loader') };
c4fee9b8 513 die "Cannot load DBIx::Class::Schema::Loader: $@" if $@;
f090a149 514
515 DBIx::Class::Schema::Loader->import(
2201c2e4 516 "dump_to_dir:$schema_dir", 'make_schema_at'
f090a149 517 );
2201c2e4 518
519 make_schema_at(
520 $self->schema_class,
521 $self->loader_args,
522 [$self->connect_info]
523 );
524}
525
2201c2e4 526sub _gen_model {
527 my $self = shift;
528 my $helper = $self->helper;
529
530 $helper->render_file('compclass', $helper->{file} );
ad91060a 531}
532
c4fee9b8 533sub _print_dynamic_deprecation_warning {
534 warn <<EOF;
535************************************ WARNING **********************************
536* create=dynamic is DEPRECATED, please use create=static instead. *
537*******************************************************************************
538EOF
539 print "Continue? [y/n]: ";
540 chomp(my $response = <STDIN>);
541 exit 0 if $response =~ /^n(o)?\z/;
542}
543
544sub _cleanup_args {
545 my ($self, $args) = @_;
546
547# remove blanks, ie. someoned doing foo \ bar
61ed82a5 548 my @res = grep !/^\s+\z/, @$args;
c4fee9b8 549
550# remove leading whitespace, ie. foo \ bar
551 s/^\s*// for @res;
552
553 @res
554}
555
ad91060a 556=head1 SEE ALSO
557
7b39f3f0 558General Catalyst Stuff:
559
ad91060a 560L<Catalyst::Manual>, L<Catalyst::Test>, L<Catalyst::Request>,
7b39f3f0 561L<Catalyst::Response>, L<Catalyst::Helper>, L<Catalyst>,
562
563Stuff related to DBIC and this Model style:
564
565L<DBIx::Class>, L<DBIx::Class::Schema>,
ef91bcf9 566L<DBIx::Class::Schema::Loader>, L<Catalyst::Model::DBIC::Schema>
ad91060a 567
568=head1 AUTHOR
569
570Brandon L Black, C<blblack@gmail.com>
571
2ff00e2b 572Contributors:
573
41bcf32f 574Rafael Kitover, C<rkitover at cpan.org>
2ff00e2b 575
ad91060a 576=head1 LICENSE
577
578This library is free software, you can redistribute it and/or modify
579it under the same terms as Perl itself.
580
581=cut
582
dce0dfe8 5831;
584
ad91060a 585__DATA__
586
5d11d759 587=begin pod_to_ignore
588
d89e6c8a 589__schemaclass__
590package [% schema_class %];
591
592use strict;
593use base qw/DBIx::Class::Schema::Loader/;
594
595__PACKAGE__->loader_options(
2201c2e4 596 [%- FOREACH key = loader_args.keys %]
597 [% key %] => [% loader_args.${key} %],
598 [%- END -%]
599
d89e6c8a 600);
601
602=head1 NAME
603
2201c2e4 604[% schema_class %] - L<DBIx::Class::Schema::Loader> class
d89e6c8a 605
606=head1 SYNOPSIS
607
608See L<[% app %]>
609
610=head1 DESCRIPTION
611
2201c2e4 612Dynamic L<DBIx::Class::Schema::Loader> schema for use in L<[% class %]>
613
614=head1 GENERATED BY
615
616[% generator %] - [% generator_version %]
d89e6c8a 617
618=head1 AUTHOR
619
2201c2e4 620[% author.replace(',+$', '') %]
d89e6c8a 621
622=head1 LICENSE
623
624This library is free software, you can redistribute it and/or modify
625it under the same terms as Perl itself.
626
627=cut
628
6291;
630
ad91060a 631__compclass__
632package [% class %];
633
634use strict;
635use base 'Catalyst::Model::DBIC::Schema';
636
637__PACKAGE__->config(
0f2fd2c0 638 schema_class => '[% schema_class %]',
c34bcab6 639 [% IF traits %]traits => [% traits %],[% END %]
2201c2e4 640 [% IF setup_connect_info %]connect_info => {
641 [%- FOREACH key = connect_info.keys %]
642 [% key %] => [% connect_info.${key} %],
643 [%- END -%]
644
645 }[% END %]
ad91060a 646);
647
648=head1 NAME
649
1aeb6e1e 650[% class %] - Catalyst DBIC Schema Model
2201c2e4 651
ad91060a 652=head1 SYNOPSIS
653
654See L<[% app %]>
655
656=head1 DESCRIPTION
657
d89e6c8a 658L<Catalyst::Model::DBIC::Schema> Model using schema L<[% schema_class %]>
ad91060a 659
2201c2e4 660=head1 GENERATED BY
661
662[% generator %] - [% generator_version %]
663
ad91060a 664=head1 AUTHOR
665
2201c2e4 666[% author.replace(',+$', '') %]
ad91060a 667
668=head1 LICENSE
669
670This library is free software, you can redistribute it and/or modify
671it under the same terms as Perl itself.
672
673=cut
674
6751;
a75b6e58 676__END__
677# vim:sts=4 sw=4: