Merge 'trunk' into 'current'
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / RelBuilder.pm
CommitLineData
996be9ee 1package DBIx::Class::Schema::Loader::RelBuilder;
2
3use strict;
4use warnings;
fa994d3c 5use Carp::Clan qw/^DBIx::Class/;
996be9ee 6use Lingua::EN::Inflect::Number ();
7
32f784fc 8our $VERSION = '0.03999_01';
9
996be9ee 10=head1 NAME
11
12DBIx::Class::Schema::Loader::RelBuilder - Builds relationships for DBIx::Class::Schema::Loader
13
14=head1 SYNOPSIS
15
16See L<DBIx::Class::Schema::Loader>
17
18=head1 DESCRIPTION
19
20This class builds relationships for L<DBIx::Class::Schema::Loader>. This
21is module is not (yet) for external use.
22
23=head1 METHODS
24
25=head2 new
26
27Arguments: schema_class (scalar), fk_info (hashref), inflect_plural, inflect_singular
28
29C<$schema_class> should be a schema class name, where the source
30classes have already been set up and registered. Column info, primary
31key, and unique constraints will be drawn from this schema for all
32of the existing source monikers.
33
34The 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
51Options inflect_plural and inflect_singular are optional, and are better documented
52in L<DBIx::Class::Schema::Loader::Base>.
53
54=head2 generate_code
55
56This method will return the generated relationships as a hashref per table moniker,
57containing an arrayref of code strings which can be "eval"-ed in the context of
58the 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 }
8f9d7ce5 70
996be9ee 71You might want to use this in building an on-disk source class file, by
72adding each string to the appropriate source class file,
73prefixed by C<__PACKAGE__-E<gt>>.
74
75=cut
76
77sub 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
94sub _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
d3af7a07 106 return Lingua::EN::Inflect::Number::to_PL($relname);
996be9ee 107}
108
109# Singularize a relationship name
110sub _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
d3af7a07 122 return Lingua::EN::Inflect::Number::to_S($relname);
996be9ee 123}
124
125sub 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,
f5fc2ee8 194 $remote_class,
996be9ee 195 \%cond,
196 ],
197 }
198 );
199
200 push(@{$all_code->{$remote_class}},
201 { method => 'has_many',
202 args => [ $local_relname,
f5fc2ee8 203 $local_class,
996be9ee 204 \%rev_cond,
205 ],
206 }
207 );
208 }
209 }
210
211 return $all_code;
212}
213
2141;