Merge 'trunk' into 'current'
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / RelBuilder.pm
1 package DBIx::Class::Schema::Loader::RelBuilder;
2
3 use strict;
4 use warnings;
5 use Carp::Clan qw/^DBIx::Class/;
6 use Lingua::EN::Inflect::Number ();
7
8 our $VERSION = '0.03999_01';
9
10 =head1 NAME
11
12 DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
13
14 =head1 SYNOPSIS
15
16 See L<DBIx::Class::Schema::Loader>
17
18 =head1 DESCRIPTION
19
20 This class builds relationships for L<DBIx::Class::Schema::Loader>.  This
21 is module is not (yet) for external use.
22
23 =head1 METHODS
24
25 =head2 new
26
27 Arguments: schema_class (scalar), fk_info (hashref), inflect_plural, inflect_singular
28
29 C<$schema_class> should be a schema class name, where the source
30 classes have already been set up and registered.  Column info, primary
31 key, and unique constraints will be drawn from this schema for all
32 of the existing source monikers.
33
34 The fk_info hashref's contents should take the form:
35
36   {
37       TableMoniker => [
38           {
39               local_columns => [ 'col2', 'col3' ],
40               remote_columns => [ 'col5', 'col7' ],
41               remote_moniker => 'AnotherTableMoniker',
42           },
43           # ...
44       ],
45       AnotherTableMoniker => [
46           # ...
47       ],
48       # ...
49   }
50
51 Options inflect_plural and inflect_singular are optional, and are better documented
52 in L<DBIx::Class::Schema::Loader::Base>.
53
54 =head2 generate_code
55
56 This method will return the generated relationships as a hashref per table moniker,
57 containing an arrayref of code strings which can be "eval"-ed in the context of
58 the source class, like:
59
60   {
61       'Some::Source::Class' => [
62           "belongs_to( col1 => 'AnotherTableMoniker' )",
63           "has_many( anothers => 'AnotherTableMoniker', 'col15' )",
64       ],
65       'Another::Source::Class' => [
66           # ...
67       ],
68       # ...
69   }
70
71 You might want to use this in building an on-disk source class file, by
72 adding each string to the appropriate source class file,
73 prefixed by C<__PACKAGE__-E<gt>>.
74
75 =cut
76
77 sub new {
78     my ( $class, $schema, $fk_info, $inflect_pl, $inflect_singular ) = @_;
79
80     my $self = {
81         schema => $schema,
82         fk_info => $fk_info,
83         inflect_plural => $inflect_pl,
84         inflect_singular => $inflect_singular,
85     };
86
87     bless $self => $class;
88
89     $self;
90 }
91
92
93 # pluralize a relationship name
94 sub _inflect_plural {
95     my ($self, $relname) = @_;
96
97     if( ref $self->{inflect_plural} eq 'HASH' ) {
98         return $self->{inflect_plural}->{$relname}
99             if exists $self->{inflect_plural}->{$relname};
100     }
101     elsif( ref $self->{inflect_plural} eq 'CODE' ) {
102         my $inflected = $self->{inflect_plural}->($relname);
103         return $inflected if $inflected;
104     }
105
106     return Lingua::EN::Inflect::Number::to_PL($relname);
107 }
108
109 # Singularize a relationship name
110 sub _inflect_singular {
111     my ($self, $relname) = @_;
112
113     if( ref $self->{inflect_singular} eq 'HASH' ) {
114         return $self->{inflect_singular}->{$relname}
115             if exists $self->{inflect_singular}->{$relname};
116     }
117     elsif( ref $self->{inflect_singular} eq 'CODE' ) {
118         my $inflected = $self->{inflect_singular}->($relname);
119         return $inflected if $inflected;
120     }
121
122     return Lingua::EN::Inflect::Number::to_S($relname);
123 }
124
125 sub generate_code {
126     my $self = shift;
127
128     my $all_code = {};
129
130     foreach my $local_moniker (keys %{$self->{fk_info}}) {
131         my $local_table = $self->{schema}->source($local_moniker)->from;
132         my $local_class = $self->{schema}->class($local_moniker);
133         my $rels = $self->{fk_info}->{$local_moniker};
134         
135         my %counters;
136         foreach my $rel (@$rels) {
137             next if !$rel->{remote_source};
138             $counters{$rel->{remote_source}}++;
139         }
140
141         foreach my $rel (@$rels) {
142             next if !$rel->{remote_source};
143             my $local_cols = $rel->{local_columns};
144             my $remote_cols = $rel->{remote_columns};
145             my $remote_moniker = $rel->{remote_source};
146             my $remote_obj = $self->{schema}->source($remote_moniker);
147             my $remote_class = $self->{schema}->class($remote_moniker);
148             my $remote_table = $remote_obj->from;
149             $remote_cols ||= [ $remote_obj->primary_columns ];
150
151             if($#$local_cols != $#$remote_cols) {
152                 croak "Column count mismatch: $local_moniker (@$local_cols) "
153                     . "$remote_moniker (@$remote_cols)";
154             }
155
156             my %cond;
157             foreach my $i (0 .. $#$local_cols) {
158                 $cond{$remote_cols->[$i]} = $local_cols->[$i];
159             }
160
161             # If more than one rel between this pair of tables, use the
162             #  local col name(s) as the relname in the foreign source, instead
163             #  of the local table name.
164             my $local_relname;
165             if($counters{$remote_moniker} > 1) {
166                 $local_relname = $self->_inflect_plural(
167                     lc($local_table) . q{_} . join(q{_}, @$local_cols)
168                 );
169             } else {
170                 $local_relname = $self->_inflect_plural(lc $local_table);
171             }
172
173             # for single-column case, set the relname to the column name,
174             # to make filter accessors work
175             my $remote_relname;
176             if(scalar keys %cond == 1) {
177                 my ($col) = keys %cond;
178                 $remote_relname = $self->_inflect_singular($cond{$col});
179             }
180             else {
181                 $remote_relname = $self->_inflect_singular(lc $remote_table);
182             }
183
184             my %rev_cond = reverse %cond;
185
186             for (keys %rev_cond) {
187                 $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
188                 delete $rev_cond{$_};
189             }
190
191             push(@{$all_code->{$local_class}},
192                 { method => 'belongs_to',
193                   args => [ $remote_relname,
194                             $remote_class,
195                             \%cond,
196                   ],
197                 }
198             );
199
200             push(@{$all_code->{$remote_class}},
201                 { method => 'has_many',
202                   args => [ $local_relname,
203                             $local_class,
204                             \%rev_cond,
205                   ],
206                 }
207             );
208         }
209     }
210
211     return $all_code;
212 }
213
214 1;