Added missing space in error message
[dbsrgits/DBIx-Class-Schema-Loader.git] / lib / DBIx / Class / Schema / Loader / Generic.pm
index 373c725..a2fbd21 100644 (file)
@@ -6,28 +6,30 @@ use base qw/Class::Accessor::Fast/;
 use Class::C3;
 use Carp;
 use Lingua::EN::Inflect;
-require DBIx::Class::Core;
+use UNIVERSAL::require;
+require DBIx::Class;
 
 # The first group are all arguments which are may be defaulted within,
 # The last two (classes, monikers) are generated locally:
 
 __PACKAGE__->mk_ro_accessors(qw/
                                 schema
-                                dsn
-                                user
-                                password
-                                options
+                                connect_info
                                 exclude
                                 constraint
                                 additional_classes
                                 additional_base_classes
                                 left_base_classes
+                                components
+                                resultset_components
                                 relationships
-                                inflect
+                                inflect_map
+                                moniker_map
                                 db_schema
                                 drop_db_schema
                                 debug
 
+                                _tables
                                 classes
                                 monikers
                              /);
@@ -49,6 +51,16 @@ classes, and implements the common functionality between them.
 
 Available constructor options are:
 
+=head2 connect_info
+
+Identical to the connect_info arguments to C<connect> and C<connection>
+that are mentioned in L<DBIx::Class::Schema>.
+
+An arrayref of connection information.  For DBI-based Schemas,
+this takes the form:
+
+  connect_info => [ $dsn, $user, $pass, { AutoCommit => 1 } ],
+
 =head2 additional_base_classes
 
 List of additional base classes your table classes will use.
@@ -61,6 +73,18 @@ List of additional base classes, that need to be leftmost.
 
 List of additional classes which your table classes will use.
 
+=head2 components
+
+List of additional components to be loaded into your table classes.
+A good example would be C<ResultSetManager>.
+
+=head2 resultset_components
+
+List of additional resultset components to be loaded into your table
+classes.  A good example would be C<AlwaysRS>.  Component
+C<ResultSetManager> will be automatically added to the above
+C<components> list if this option is set.
+
 =head2 constraint
 
 Only load tables matching regex.
@@ -73,27 +97,56 @@ Exclude tables matching regex.
 
 Enable debug messages.
 
-=head2 dsn
+=head2 relationships
 
-DBI Data Source Name.
+Try to automatically detect/setup has_a and has_many relationships.
 
-=head2 password
+=head2 moniker_map
 
-Password.
+Overrides the default tablename -> moniker translation.  Can be either
+a hashref of table => moniker names, or a coderef for a translator
+function taking a single scalar table name argument and returning
+a scalar moniker.  If the hash entry does not exist, or the function
+returns a false/undef value, the code falls back to default behavior
+for that table name.
 
-=head2 relationships
+=head2 inflect_map
 
-Try to automatically detect/setup has_a and has_many relationships.
+Just like L</moniker_map> above, but for inflecting (pluralizing)
+relationship names.
 
 =head2 inflect
 
-An hashref, which contains exceptions to Lingua::EN::Inflect::PL().
-Useful for foreign language column names.
+Deprecated.  Equivalent to L</inflect_map>, but previously only took
+a hashref argument, not a coderef.  If you set C<inflect> to anything,
+that setting will be copied to L</inflect_map>.
+
+=head2 dsn
+
+DEPRECATED, use L</connect_info> instead.
+
+DBI Data Source Name.
 
 =head2 user
 
+DEPRECATED, use L</connect_info> instead.
+
 Username.
 
+=head2 password
+
+DEPRECATED, use L</connect_info> instead.
+
+Password.
+
+=head2 options
+
+DEPRECATED, use L</connect_info> instead.
+
+DBI connection options hashref, like:
+
+  { AutoCommit => 1 }
+
 =head1 METHODS
 
 =cut
@@ -126,14 +179,29 @@ sub new {
 
     $self->{db_schema}  ||= '';
     $self->{constraint} ||= '.*';
-    $self->{inflect}    ||= {};
     $self->_ensure_arrayref(qw/additional_classes
                                additional_base_classes
-                               left_base_classes/);
+                               left_base_classes
+                               components
+                               resultset_components
+                               connect_info/);
+
+    push(@{$self->{components}}, 'ResultSetManager')
+        if @{$self->{resultset_components}};
 
     $self->{monikers} = {};
     $self->{classes} = {};
 
+    # Support deprecated argument name
+    $self->{inflect_map} ||= $self->{inflect};
+
+    # Support deprecated connect_info args, even mixed
+    #  with a valid partially-filled connect_info
+    $self->{connect_info}->[0] ||= $self->{dsn};
+    $self->{connect_info}->[1] ||= $self->{user};
+    $self->{connect_info}->[2] ||= $self->{password};
+    $self->{connect_info}->[3] ||= $self->{options};
+
     $self;
 }
 
@@ -147,14 +215,14 @@ L<DBIx::Class::Schema::Loader> right after object construction.
 sub load {
     my $self = shift;
 
-    $self->schema->connection($self->dsn, $self->user,
-                              $self->password, $self->options);
+    $self->schema->connection(@{$self->connect_info});
 
     warn qq/\### START DBIx::Class::Schema::Loader dump ###\n/
         if $self->debug;
 
     $self->_load_classes;
     $self->_load_relationships if $self->relationships;
+    $self->_load_external;
 
     warn qq/\### END DBIx::Class::Schema::Loader dump ###\n/
         if $self->debug;
@@ -163,15 +231,38 @@ sub load {
     $self;
 }
 
+sub _load_external {
+    my $self = shift;
+
+    foreach my $table_class (map { $self->classes->{$_} } $self->tables) {
+        $table_class->require;
+        if($@ && $@ !~ /^Can't locate /) {
+            croak "Failed to load external class definition"
+                  . " for '$table_class': $@";
+        }
+        elsif(!$@) {
+            warn qq/# Loaded external class definition for '$table_class'\n/
+                if $self->debug;
+        }
+    }
+}
+
 # Overload in your driver class
 sub _db_classes { croak "ABSTRACT METHOD" }
 
 # Inflect a relationship name
-#   XXX (should pluralize, but currently also tends to de-pluralize plurals)
 sub _inflect_relname {
     my ($self, $relname) = @_;
 
-    return $self->inflect->{$relname} if exists $self->inflect->{$relname};
+    if( ref $self->{inflect_map} eq 'HASH' ) {
+        return $self->inflect_map->{$relname}
+            if exists $self->inflect_map->{$relname};
+    }
+    elsif( ref $self->{inflect_map} eq 'CODE' ) {
+        my $inflected = $self->inflect_map->($relname);
+        return $inflected if $inflected;
+    }
+
     return Lingua::EN::Inflect::PL($relname);
 }
 
@@ -223,6 +314,11 @@ sub _make_cond_rel {
 
     my $rev_cond = { reverse %$cond };
 
+    for (keys %$rev_cond) {
+        $rev_cond->{"foreign.$_"} = "self.".$rev_cond->{$_};
+        delete $rev_cond->{$_};
+    }
+
     my $cond_printable = _stringify_hash($cond)
         if $self->debug;
     my $rev_cond_printable = _stringify_hash($rev_cond)
@@ -272,43 +368,63 @@ sub _inject {
 sub _load_classes {
     my $self = shift;
 
-    my @tables     = $self->_tables();
     my @db_classes = $self->_db_classes();
     my $schema     = $self->schema;
 
-    foreach my $table (@tables) {
-        my $constraint = $self->constraint;
-        my $exclude = $self->exclude;
+    my $constraint = $self->constraint;
+    my $exclude = $self->exclude;
+    my @tables = sort grep
+        { /$constraint/ && (!$exclude || ! /$exclude/) }
+            $self->_tables_list;
 
-        next unless $table =~ /$constraint/;
-        next if defined $exclude && $table =~ /$exclude/;
+    $self->{_tables} = \@tables;
 
+    foreach my $table (@tables) {
         my ($db_schema, $tbl) = split /\./, $table;
-        my $tablename = lc $table;
         if($tbl) {
-            $tablename = $self->drop_db_schema ? $tbl : lc $table;
+            $table = $self->drop_db_schema ? $tbl : $table;
         }
-        my $lc_tblname = lc $tablename;
+        my $lc_table = lc $table;
 
         my $table_moniker = $self->_table2moniker($db_schema, $tbl);
         my $table_class = $schema . q{::} . $table_moniker;
 
-        $self->_inject($table_class, 'DBIx::Class::Core');
-        $self->_inject($table_class, @db_classes);
-        $self->_inject($table_class, @{$self->additional_base_classes});
+        $self->classes->{$lc_table} = $table_class;
+        $self->monikers->{$lc_table} = $table_moniker;
+        $self->classes->{$table} = $table_class;
+        $self->monikers->{$table} = $table_moniker;
+
+        no warnings 'redefine';
+        local *Class::C3::reinitialize = sub { };
+        use warnings;
+
+        { no strict 'refs';
+          @{"${table_class}::ISA"} = qw/DBIx::Class/;
+        }
         $self->_use   ($table_class, @{$self->additional_classes});
+        $self->_inject($table_class, @{$self->additional_base_classes});
+        $table_class->load_components(@{$self->components}, @db_classes, 'Core');
+        $table_class->load_resultset_components(@{$self->resultset_components})
+            if @{$self->resultset_components};
         $self->_inject($table_class, @{$self->left_base_classes});
+    }
 
-        warn qq/\# Initializing table "$tablename" as "$table_class"\n/
+    Class::C3::reinitialize;
+
+    foreach my $table (@tables) {
+        my $table_class = $self->classes->{$table};
+        my $table_moniker = $self->monikers->{$table};
+
+        warn qq/\# Initializing table "$table" as "$table_class"\n/
             if $self->debug;
-        $table_class->table($lc_tblname);
+        $table_class->table($table);
 
         my ( $cols, $pks ) = $self->_table_info($table);
         carp("$table has no primary key") unless @$pks;
         $table_class->add_columns(@$cols);
         $table_class->set_primary_key(@$pks) if @$pks;
 
-        warn qq/$table_class->table('$tablename');\n/ if $self->debug;
+        warn qq/$table_class->table('$table');\n/ if $self->debug;
         my $columns = join "', '", @$cols;
         warn qq/$table_class->add_columns('$columns')\n/ if $self->debug;
         my $primaries = join "', '", @$pks;
@@ -316,15 +432,13 @@ sub _load_classes {
             if $self->debug && @$pks;
 
         $schema->register_class($table_moniker, $table_class);
-        $self->classes->{$lc_tblname} = $table_class;
-        $self->monikers->{$lc_tblname} = $table_moniker;
     }
 }
 
 =head2 tables
 
 Returns a sorted list of loaded tables, using the original database table
-names.  Actually generated from the keys of the C<monikers> hash below.
+names.
 
   my @tables = $schema->loader->tables;
 
@@ -333,7 +447,7 @@ names.  Actually generated from the keys of the C<monikers> hash below.
 sub tables {
     my $self = shift;
 
-    return sort keys %{ $self->monikers };
+    return @{$self->_tables};
 }
 
 # Find and setup relationships
@@ -348,10 +462,10 @@ sub _load_relationships {
             $self->db_schema, '', '', '', $table );
         next if !$sth;
         while(my $raw_rel = $sth->fetchrow_hashref) {
-            my $uk_tbl  = lc $raw_rel->{UK_TABLE_NAME};
+            my $uk_tbl  = $raw_rel->{UK_TABLE_NAME};
             my $uk_col  = lc $raw_rel->{UK_COLUMN_NAME};
             my $fk_col  = lc $raw_rel->{FK_COLUMN_NAME};
-            my $relid   = lc $raw_rel->{UK_NAME};
+            my $relid   = $raw_rel->{UK_NAME};
             $uk_tbl =~ s/$quoter//g;
             $uk_col =~ s/$quoter//g;
             $fk_col =~ s/$quoter//g;
@@ -383,21 +497,34 @@ sub _table2moniker {
         $table = $db_schema;
     }
 
-    my $moniker = join '', map ucfirst, split /[\W_]+/, lc $table;
+    my $moniker;
+
+    if( ref $self->moniker_map eq 'HASH' ) {
+        $moniker = $self->moniker_map->{$table};
+    }
+    elsif( ref $self->moniker_map eq 'CODE' ) {
+        $moniker = $self->moniker_map->($table);
+    }
+
+    $moniker ||= join '', map ucfirst, split /[\W_]+/, lc $table;
+
     $moniker = $db_schema_ns ? $db_schema_ns . $moniker : $moniker;
 
     return $moniker;
 }
 
 # Overload in driver class
-sub _tables { croak "ABSTRACT METHOD" }
+sub _tables_list { croak "ABSTRACT METHOD" }
 
 sub _table_info { croak "ABSTRACT METHOD" }
 
 =head2 monikers
 
 Returns a hashref of loaded table-to-moniker mappings for the original
-database table names.
+database table names.  In cases where the database driver returns table
+names as uppercase or mixed case, there will also be a duplicate entry
+here in all lowercase.  Best practice would be to use lower-case table
+names when accessing this.
 
   my $monikers = $schema->loader->monikers;
   my $foo_tbl_moniker = $monikers->{foo_tbl};
@@ -408,7 +535,9 @@ database table names.
 =head2 classes
 
 Returns a hashref of table-to-classname mappings for the original database
-table names.  You probably shouldn't be using this for any normal or simple
+table names.  Same lowercase stuff as above applies here. 
+
+You probably shouldn't be using this for any normal or simple
 usage of your Schema.  The usual way to run queries on your tables is via
 C<$schema-E<gt>resultset('FooTbl')>, where C<FooTbl> is a moniker as
 returned by C<monikers> above.