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