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