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