new dev release
[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 Class::C3;
6 use Carp::Clan qw/^DBIx::Class/;
7 use Lingua::EN::Inflect::Number ();
8
9 our $VERSION = '0.04999_13';
10
11 =head1 NAME
12
13 DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
14
15 =head1 SYNOPSIS
16
17 See L<DBIx::Class::Schema::Loader>
18
19 =head1 DESCRIPTION
20
21 This class builds relationships for L<DBIx::Class::Schema::Loader>.  This
22 is module is not (yet) for external use.
23
24 =head1 METHODS
25
26 =head2 new
27
28 Arguments: schema_class (scalar), inflect_plural, inflect_singular
29
30 C<$schema_class> should be a schema class name, where the source
31 classes have already been set up and registered.  Column info, primary
32 key, and unique constraints will be drawn from this schema for all
33 of the existing source monikers.
34
35 Options inflect_plural and inflect_singular are optional, and are better documented
36 in L<DBIx::Class::Schema::Loader::Base>.
37
38 =head2 generate_code
39
40 Arguments: local_moniker (scalar), fk_info (arrayref)
41
42 This generates the code for the relationships of a given table.
43
44 C<local_moniker> is the moniker name of the table which had the REFERENCES
45 statements.  The fk_info arrayref's contents should take the form:
46
47     [
48         {
49             local_columns => [ 'col2', 'col3' ],
50             remote_columns => [ 'col5', 'col7' ],
51             remote_moniker => 'AnotherTableMoniker',
52         },
53         {
54             local_columns => [ 'col1', 'col4' ],
55             remote_columns => [ 'col1', 'col2' ],
56             remote_moniker => 'YetAnotherTableMoniker',
57         },
58         # ...
59     ],
60
61 This method will return the generated relationships as a hashref keyed on the
62 class names.  The values are arrayrefs of hashes containing method name and
63 arguments, like so:
64
65   {
66       'Some::Source::Class' => [
67           { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
68           { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
69       ],
70       'Another::Source::Class' => [
71           # ...
72       ],
73       # ...
74   }
75
76 =cut
77
78 sub new {
79
80     my ( $class, $schema, $inflect_pl, $inflect_singular, $rel_attrs ) = @_;
81
82     my $self = {
83         schema => $schema,
84         inflect_plural => $inflect_pl,
85         inflect_singular => $inflect_singular,
86         relationship_attrs => $rel_attrs,
87     };
88
89     # validate the relationship_attrs arg
90     if( defined $self->{relationship_attrs} ) {
91         ref($self->{relationship_attrs}) eq 'HASH'
92             or croak "relationship_attrs must be a hashref";
93     }
94
95     return bless $self => $class;
96 }
97
98
99 # pluralize a relationship name
100 sub _inflect_plural {
101     my ($self, $relname) = @_;
102
103     if( ref $self->{inflect_plural} eq 'HASH' ) {
104         return $self->{inflect_plural}->{$relname}
105             if exists $self->{inflect_plural}->{$relname};
106     }
107     elsif( ref $self->{inflect_plural} eq 'CODE' ) {
108         my $inflected = $self->{inflect_plural}->($relname);
109         return $inflected if $inflected;
110     }
111
112     return Lingua::EN::Inflect::Number::to_PL($relname);
113 }
114
115 # Singularize a relationship name
116 sub _inflect_singular {
117     my ($self, $relname) = @_;
118
119     if( ref $self->{inflect_singular} eq 'HASH' ) {
120         return $self->{inflect_singular}->{$relname}
121             if exists $self->{inflect_singular}->{$relname};
122     }
123     elsif( ref $self->{inflect_singular} eq 'CODE' ) {
124         my $inflected = $self->{inflect_singular}->($relname);
125         return $inflected if $inflected;
126     }
127
128     return Lingua::EN::Inflect::Number::to_S($relname);
129 }
130
131 # accessor for options to be passed to each generated relationship
132 # type.  take single argument, the relationship type name, and returns
133 # either a hashref (if some options are set), or nothing
134 sub _relationship_attrs {
135     my ( $self, $reltype ) = @_;
136     my $r = $self->{relationship_attrs};
137     return unless $r && ( $r->{all} || $r->{$reltype} );
138
139     my %composite = %{ $r->{all} || {} };
140     if( my $specific = $r->{$reltype} ) {
141         while( my ($k,$v) = each %$specific ) {
142             $composite{$k} = $v;
143         }
144     }
145     return \%composite;
146 }
147
148 sub _array_eq {
149     my ($a, $b) = @_;
150
151     return unless @$a == @$b;
152
153     for (my $i = 0; $i < @$a; $i++) {
154         return unless $a->[$i] eq $b->[$i];
155     }
156     return 1;
157 }
158
159 sub _uniq_fk_rel {
160     my ($self, $local_moniker, $local_relname, $local_cols, $uniqs) = @_;
161
162     my $remote_method = 'has_many';
163
164     # If the local columns have a UNIQUE constraint, this is a one-to-one rel
165     my $local_source = $self->{schema}->source($local_moniker);
166     if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
167             grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
168         $remote_method = 'might_have';
169         $local_relname = $self->_inflect_singular($local_relname);
170     }
171
172     return ($remote_method, $local_relname);
173 }
174
175 sub _remote_attrs {
176         my ($self, $local_moniker, $local_cols) = @_;
177
178         # get our base set of attrs from _relationship_attrs, if present
179         my $attrs = $self->_relationship_attrs('belongs_to') || {};
180
181         # If the referring column is nullable, make 'belongs_to' an
182         # outer join, unless explicitly set by relationship_attrs
183         my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} }
184                 @$local_cols;
185         $attrs->{join_type} = 'LEFT'
186             if $nullable && !defined $attrs->{join_type};
187
188         return $attrs;
189 }
190
191 sub _remote_relname {
192     my ($self, $remote_table, $cond) = @_;
193
194     my $remote_relname;
195     # for single-column case, set the remote relname to the column
196     # name, to make filter accessors work, but strip trailing _id
197     if(scalar keys %{$cond} == 1) {
198         my ($col) = values %{$cond};
199         $col =~ s/_id$//;
200         $remote_relname = $self->_inflect_singular($col);
201     }
202     else {
203         $remote_relname = $self->_inflect_singular(lc $remote_table);
204     }
205
206     return $remote_relname;
207 }
208
209 sub generate_code {
210     my ($self, $local_moniker, $rels, $uniqs) = @_;
211
212     my $all_code = {};
213
214     my $local_table = $self->{schema}->source($local_moniker)->from;
215     my $local_class = $self->{schema}->class($local_moniker);
216         
217     my %counters;
218     foreach my $rel (@$rels) {
219         next if !$rel->{remote_source};
220         $counters{$rel->{remote_source}}++;
221     }
222
223     foreach my $rel (@$rels) {
224         next if !$rel->{remote_source};
225         my $local_cols = $rel->{local_columns};
226         my $remote_cols = $rel->{remote_columns};
227         my $remote_moniker = $rel->{remote_source};
228         my $remote_obj = $self->{schema}->source($remote_moniker);
229         my $remote_class = $self->{schema}->class($remote_moniker);
230         my $remote_table = $remote_obj->from;
231         $remote_cols ||= [ $remote_obj->primary_columns ];
232
233         if($#$local_cols != $#$remote_cols) {
234             croak "Column count mismatch: $local_moniker (@$local_cols) "
235                 . "$remote_moniker (@$remote_cols)";
236         }
237
238         my %cond;
239         foreach my $i (0 .. $#$local_cols) {
240             $cond{$remote_cols->[$i]} = $local_cols->[$i];
241         }
242
243         my $local_relname;
244         my $remote_relname = $self->_remote_relname($remote_table, \%cond);
245
246         # If more than one rel between this pair of tables, use the local
247         # col names to distinguish
248         if($counters{$remote_moniker} > 1) {
249             my $colnames = q{_} . join(q{_}, @$local_cols);
250             $local_relname = $self->_inflect_plural(
251                 lc($local_table) . $colnames
252             );
253             $remote_relname .= $colnames if keys %cond > 1;
254         } else {
255             $local_relname = $self->_inflect_plural(lc $local_table);
256         }
257
258         my %rev_cond = reverse %cond;
259
260         for (keys %rev_cond) {
261             $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
262             delete $rev_cond{$_};
263         }
264
265         my ($remote_method);
266
267         ($remote_method, $local_relname) = $self->_uniq_fk_rel($local_moniker, $local_relname, $local_cols, $uniqs);
268
269         push(@{$all_code->{$local_class}},
270             { method => 'belongs_to',
271               args => [ $remote_relname,
272                         $remote_class,
273                         \%cond,
274                         $self->_remote_attrs($local_moniker, $local_cols),
275               ],
276             }
277         );
278
279         push(@{$all_code->{$remote_class}},
280             { method => $remote_method,
281               args => [ $local_relname,
282                         $local_class,
283                         \%rev_cond,
284                         $self->_relationship_attrs($remote_method),
285               ],
286             }
287         );
288     }
289
290     return $all_code;
291 }
292
293 =head1 AUTHOR
294
295 See L<DBIx::Class::Schema::Loader/AUTHOR> and L<DBIx::Class::Schema::Loader/CONTRIBUTORS>.
296
297 =head1 LICENSE
298
299 This library is free software; you can redistribute it and/or modify it under
300 the same terms as Perl itself.
301
302 =cut
303
304 1;