use warnings; use strict; package Jifty::ClassLoader; =head1 NAME Jifty::ClassLoader - Loads the application classes =head1 DESCRIPTION C loads all of the application's model and action classes, generating classes on the fly for Collections of pre-existing models. =head2 new Returns a new ClassLoader object. Doing this installs a hook into C<@INC> that allows L to dynamically create needed classes if they do not exist already. This works because if use/require encounters a blessed reference in C<@INC>, it will invoke the INC method with the name of the module it is searching for on the reference. Takes one mandatory argument, C, which should be the the application's base path; all of the classes under this will be automatically loaded. =cut sub new { my $class = shift; my %args = @_; my @exist = grep {ref $_ eq $class and $_->{base} eq $args{base}} @INC; return $exist[0] if @exist; my $self = bless {%args}, $class; push @INC, $self; return $self; } =head2 INC The hook that is called when a module has been C'd that cannot be found on disk. The following stub classes are auto-generated: =over =item I An empty application base class is created that doen't provide any methods or inherit from anything. =item I::Record An empty class that descends from L is created. =item I::Event An empty class that descends from L is created. =item I::Collection An empty class that descends from L is created. =item I::Notification An empty class that descends from L. =item I::Dispatcher An empty class that descends from L. =item I::Handle An empty class that descends from L is created. =item I::Bootstrap An empty class that descends from L. =item I::Upgrade An empty class that descends from L. =item I::CurrentUser An empty class that descends from L. =item I::Model::ICollection If C::Model::I> is a valid model class, then it creates a subclass of L whose C is C::Model::I>. =item I::Action::(Create or Update or Delete or Search)I If C::Model::I> is a valid model class, then it creates a subclass of L, L, L or L whose I is C::Model::I>. =back =cut # This subroutine's name is fully qualified, as perl will ignore a 'sub INC' sub Jifty::ClassLoader::INC { my ( $self, $module ) = @_; my $base = $self->{base}; return undef unless ( $module and $base ); # Canonicalize $module to :: style rather than / and .pm style; $module =~ s/.pm$//; $module =~ s{/}{::}g; # The quick check. We only want to handle things for our app return undef unless $module =~ /^$base/; if ( $module =~ /^(?:$base)$/ ) { return $self->return_class( "package " . $base . ";\n"); } elsif ( $module =~ /^(?:$base)::(Record|Collection|Notification| Dispatcher|Bootstrap|Upgrade| Handle|Event|Event::Model|Action| Action::Record::\w+)$/x ) { return $self->return_class( "package $module;\n" . "use base qw/Jifty::$1/; sub _autogenerated { 1 };\n" ); } elsif ( $module =~ /^(?:$base)::View/ ) { return $self->return_class( "package $module;\n" . "use Jifty::View::Declare -base; sub _autogenerated { 1 };\n" ); } elsif ( $module =~ /^(?:$base)::CurrentUser$/ ) { return $self->return_class( "package $module;\n" . "use base qw/Jifty::CurrentUser/; sub _autogenerated { 1 };\n" ); } elsif ( $module =~ /^(?:$base)::Model::(\w+)Collection$/ ) { return $self->return_class( "package $module;\n" . "use base qw/@{[$base]}::Collection/;\n" . "sub record_class { '@{[$base]}::Model::$1' }\n" ); } elsif ( $module =~ /^(?:$base)::Event::Model::([^\.]+)$/ ) { my $modelclass = $base . "::Model::" . $1; Jifty::Util->require($modelclass); return undef unless eval { $modelclass->table }; return $self->return_class( "package $module;\n" . "use base qw/${base}::Event::Model/;\n" . "sub record_class { '$modelclass' };\n" . "sub autogenerated { 1 };\n" ); } elsif ( $module =~ /^(?:$base)::Action:: (Create|Update|Delete|Search)([^\.]+)$/x ) { my $modelclass = $base . "::Model::" . $2; Jifty::Util->require($modelclass); local $@; eval { $modelclass->table } ; if(!$@) { return $self->return_class( "package $module;\n" . "use base qw/$base\::Action::Record::$1/;\n" . "sub record_class { '$modelclass' };\n" . "sub autogenerated { 1 };\n" ); } } # This is if, not elsif because we might have $base::Action::Deleteblah # that matches that last elsif clause but loses on the eval. if ( $module =~ /^(?:$base)::(Action|Notification)::(.*)$/x and not grep {$_ eq $base} map {ref} Jifty->plugins ) { my $type = $1; my $item = $2; # If we don't have the action in our own app, let's try the plugins # the app has loaded. foreach my $plugin (map {ref} Jifty->plugins) { next if ($plugin eq $base); my $class = $plugin."::".$type."::".$item; if (Jifty::Util->try_to_require($class) ) { return $self->return_class( "package $module;\n" . "use base qw/$class/;\n" . "sub autogenerated { 1 };\n" ); } } } # Didn't find a match return undef; } =head2 return_class CODE A helper method; takes CODE as a string and returns an open filehandle containing that CODE. =cut sub return_class { my $self = shift; my $content = shift; $content = "use warnings; use strict; ". $content . "\n1;"; open my $fh, '<', \$content; return $fh; } =head2 require Loads all of the application's Actions and Models. It additionally C's all Collections and Create/Update actions for each Model base class -- which will auto-create them using the above code if they do not exist on disk. =cut sub require { my $self = shift; my $base = $self->{base}; # if we don't even have an application class, this trick will not work return unless ($base); Jifty::Util->require($base); Jifty::Util->require($base."::CurrentUser"); my %models; Jifty::Module::Pluggable->import( # $base goes last so we pull in the view class AFTER the model classes search_path => [map { $base . "::" . $_ } ('Model', 'Action', 'Notification', 'Event')], require => 1, except => qr/\.#/, inner => 0 ); $models{$_} = 1 for grep {/^($base)::Model::(.*)$/ and not /Collection$/} $self->plugins; $self->models(sort keys %models); for my $full ($self->models) { $self->_require_model_related_classes($full); } } sub _require_model_related_classes { my $self = shift; my $full = shift; my $base = $self->{base}; my($short) = $full =~ /::Model::(.*)/; Jifty::Util->require($full . "Collection"); Jifty::Util->require($base . "::Action::" . $_ . $short) for qw/Create Update Delete Search/; } =head2 require_classes_from_database Jifty supports model classes that aren't files on disk but instead records in your database. It's a little bit mind bending, but basically, you can build an application entirely out of the database without ever writing a line of code(*). * As of early 2007, this forward looking statement is mostly a lie. But we're working on it. This method finds all database-backed models and instantiates jifty classes for them it returns a list of classnames of the models it created. =cut sub require_classes_from_database { my $self = shift; my @instantiated; require Jifty::Model::ModelClassCollection; require Jifty::Model::ModelClass; my $models = Jifty::Model::ModelClassCollection->new(current_user => Jifty::CurrentUser->superuser); $models->unlimit(); while (my $model = $models->next) { $model->instantiate(); $self->_require_model_related_classes($model->qualified_class); } } =head2 require_views Load up C<$appname::View>, the view class for the application. =cut sub require_views { my $self = shift; my $base = $self->{base}; # if we don't even have an application class, this trick will not work return unless ($base); Jifty::Util->require($base."::View"); } =head2 models Accessor to the list of models this application has loaded. In scalar context, returns a mutable array reference; in list context, return the content of the array. =cut sub models { my $self = shift; if (@_) { $self->{models} = ref($_[0]) ? $_[0] : \@_; } wantarray ? @{ $self->{models} ||= [] } : $self->{models}; } =head2 DESTROY When the ClassLoader gets garbage-collected, its entry in @INC needs to be removed. =cut # The entries in @INC end up having SvTYPE == SVt_RV, but SvRV(sv) == # 0x0 and !SvROK(sv) (!?) This may be something that perl should cope # with more cleanly. # # We call this explictly in an END block in Jifty.pm, because # otherwise the DESTROY block gets called *after* there's already a # bogus entry in @INC # This bug manifests itself as warnings that look like this: # Use of uninitialized value in require at /tmp/7730 line 9 during global destruction. sub DESTROY { my $self = shift; @INC = grep {defined $_ and $_ ne $self} @INC; } =head1 Writing your own classes If you require more functionality than is provided by the classes created by ClassLoader then you should create a class with the appropriate name and add your extra logic to it. For example you will almost certainly want to write your own dispatcher, so something like: package MyApp::Dispatcher; use Jifty::Dispatcher -base; If you want to add some application specific behaviour to a model's collection class, say for the User model, create F in your applications Model directory. package MyApp::Model::UserCollection; use base 'MyApp::Collection'; =cut 1;