59a5638cd8e3a2cb940c227667cb79889b418199
[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.04999_07';
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), 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 Options inflect_plural and inflect_singular are optional, and are better documented
35 in L<DBIx::Class::Schema::Loader::Base>.
36
37 =head2 generate_code
38
39 Arguments: local_moniker (scalar), fk_info (arrayref)
40
41 This generates the code for the relationships of a given table.
42
43 C<local_moniker> is the moniker name of the table which had the REFERENCES
44 statements.  The fk_info arrayref's contents should take the form:
45
46     [
47         {
48             local_columns => [ 'col2', 'col3' ],
49             remote_columns => [ 'col5', 'col7' ],
50             remote_moniker => 'AnotherTableMoniker',
51         },
52         {
53             local_columns => [ 'col1', 'col4' ],
54             remote_columns => [ 'col1', 'col2' ],
55             remote_moniker => 'YetAnotherTableMoniker',
56         },
57         # ...
58     ],
59
60 This method will return the generated relationships as a hashref keyed on the
61 class names.  The values are arrayrefs of hashes containing method name and
62 arguments, like so:
63
64   {
65       'Some::Source::Class' => [
66           { method => 'belongs_to', arguments => [ 'col1', 'Another::Source::Class' ],
67           { method => 'has_many', arguments => [ 'anothers', 'Yet::Another::Source::Class', 'col15' ],
68       ],
69       'Another::Source::Class' => [
70           # ...
71       ],
72       # ...
73   }
74
75 =cut
76
77 sub new {
78     my ( $class, $schema, $inflect_pl, $inflect_singular ) = @_;
79
80     my $self = {
81         schema => $schema,
82         inflect_plural => $inflect_pl,
83         inflect_singular => $inflect_singular,
84     };
85
86     bless $self => $class;
87
88     $self;
89 }
90
91
92 # pluralize a relationship name
93 sub _inflect_plural {
94     my ($self, $relname) = @_;
95
96     if( ref $self->{inflect_plural} eq 'HASH' ) {
97         return $self->{inflect_plural}->{$relname}
98             if exists $self->{inflect_plural}->{$relname};
99     }
100     elsif( ref $self->{inflect_plural} eq 'CODE' ) {
101         my $inflected = $self->{inflect_plural}->($relname);
102         return $inflected if $inflected;
103     }
104
105     return Lingua::EN::Inflect::Number::to_PL($relname);
106 }
107
108 # Singularize a relationship name
109 sub _inflect_singular {
110     my ($self, $relname) = @_;
111
112     if( ref $self->{inflect_singular} eq 'HASH' ) {
113         return $self->{inflect_singular}->{$relname}
114             if exists $self->{inflect_singular}->{$relname};
115     }
116     elsif( ref $self->{inflect_singular} eq 'CODE' ) {
117         my $inflected = $self->{inflect_singular}->($relname);
118         return $inflected if $inflected;
119     }
120
121     return Lingua::EN::Inflect::Number::to_S($relname);
122 }
123
124 sub _array_eq {
125     my ($a, $b) = @_;
126
127     return unless @$a == @$b;
128
129     for (my $i = 0; $i < @$a; $i++) {
130         return unless $a->[$i] eq $b->[$i];
131     }
132     return 1;
133 }
134
135 sub _uniq_fk_rel {
136     my ($self, $local_moniker, $local_relname, $local_cols, $uniqs) = @_;
137
138     my $remote_method = 'has_many';
139
140     # If the local columns have a UNIQUE constraint, this is a one-to-one rel
141     my $local_source = $self->{schema}->source($local_moniker);
142     if (_array_eq([ $local_source->primary_columns ], $local_cols) ||
143             grep { _array_eq($_->[1], $local_cols) } @$uniqs) {
144         $remote_method = 'might_have';
145         $local_relname = $self->_inflect_singular($local_relname);
146     }
147
148     return ($remote_method, $local_relname);
149 }
150
151 sub _remote_attrs {
152         my ($self, $local_moniker, $local_cols) = @_;
153
154         # If the referring column is nullable, make 'belongs_to' an outer join:
155         my $nullable = grep { $self->{schema}->source($local_moniker)->column_info($_)->{is_nullable} }
156                 @$local_cols;
157
158         return $nullable ? { join_type => 'LEFT' } : ();
159 }
160
161 sub generate_code {
162     my ($self, $local_moniker, $rels, $uniqs) = @_;
163
164     my $all_code = {};
165
166     my $local_table = $self->{schema}->source($local_moniker)->from;
167     my $local_class = $self->{schema}->class($local_moniker);
168         
169     my %counters;
170     foreach my $rel (@$rels) {
171         next if !$rel->{remote_source};
172         $counters{$rel->{remote_source}}++;
173     }
174
175     foreach my $rel (@$rels) {
176         next if !$rel->{remote_source};
177         my $local_cols = $rel->{local_columns};
178         my $remote_cols = $rel->{remote_columns};
179         my $remote_moniker = $rel->{remote_source};
180         my $remote_obj = $self->{schema}->source($remote_moniker);
181         my $remote_class = $self->{schema}->class($remote_moniker);
182         my $remote_table = $remote_obj->from;
183         $remote_cols ||= [ $remote_obj->primary_columns ];
184
185         if($#$local_cols != $#$remote_cols) {
186             croak "Column count mismatch: $local_moniker (@$local_cols) "
187                 . "$remote_moniker (@$remote_cols)";
188         }
189
190         my %cond;
191         foreach my $i (0 .. $#$local_cols) {
192             $cond{$remote_cols->[$i]} = $local_cols->[$i];
193         }
194
195         my $local_relname;
196         my $remote_relname;
197
198         # for single-column case, set the remote relname to the column
199         # name, to make filter accessors work, but strip trailing _id
200         if(scalar keys %cond == 1) {
201             my ($col) = values %cond;
202             $col =~ s/_id$//;
203             $remote_relname = $self->_inflect_singular($col);
204         }
205         else {
206             $remote_relname = $self->_inflect_singular(lc $remote_table);
207         }
208
209         # If more than one rel between this pair of tables, use the local
210         # col names to distinguish
211         if($counters{$remote_moniker} > 1) {
212             my $colnames = q{_} . join(q{_}, @$local_cols);
213             $local_relname = $self->_inflect_plural(
214                 lc($local_table) . $colnames
215             );
216             $remote_relname .= $colnames if keys %cond > 1;
217         } else {
218             $local_relname = $self->_inflect_plural(lc $local_table);
219         }
220
221         my %rev_cond = reverse %cond;
222
223         for (keys %rev_cond) {
224             $rev_cond{"foreign.$_"} = "self.".$rev_cond{$_};
225             delete $rev_cond{$_};
226         }
227
228         my ($remote_method);
229
230         ($remote_method, $local_relname) = $self->_uniq_fk_rel($local_moniker, $local_relname, $local_cols, $uniqs);
231
232         push(@{$all_code->{$local_class}},
233             { method => 'belongs_to',
234               args => [ $remote_relname,
235                         $remote_class,
236                         \%cond,
237                         $self->_remote_attrs($local_moniker, $local_cols),
238               ],
239             }
240         );
241
242         push(@{$all_code->{$remote_class}},
243             { method => $remote_method,
244               args => [ $local_relname,
245                         $local_class,
246                         \%rev_cond,
247               ],
248             }
249         );
250     }
251
252     return $all_code;
253 }
254
255 1;