Given a syntax like
$c->routes->get($path)->to("$controller#$sub");
I would like to know which sub $controller#$sub resolves to on dispatch. Is there a simple method to get the ref of the sub? You can hard-set ->namespaces() so I assume it's not always as simple as $controller::$sub because you could have namespace::$controller::$sub.
I could not find a way to do this using the api, but there is a private method _class() that will give the controller object that contains the sub. Here is an example:
./lib/MyApp/Controller/Foo.pm:
package MyApp::Controller::Foo;
use Mojo::Base 'Mojolicious::Controller';
sub welcome {
my $self = shift;
$self->render(text => 'Hello there.');
}
1;
./myapp.pl:
use strict;
use warnings;
use Mojolicious::Lite;
use lib './lib';
get '/' => sub {
my $c = shift;
$c->render(text => 'Hello World!');
};
my $namespaces = app->routes->namespaces;
push #$namespaces, 'MyApp::Controller';
app->routes->get('/welcome')->to('foo#welcome');
app->hook(
before_dispatch => sub {
my $c = shift;
my $field = { action => "welcome", controller => "foo" };
my $obj = $c->app->routes->_class( $c, $field );
my $method = $field->{action};
my $subref = sub { $obj->$method( #_ ) };
}
);
app->start;
Related
In this example:
$logger->debug({
filter => \&Data::Dumper::Dumper,
value => $ref
});
I can pretty print my references instead of ARRAY(0xFFDFKDJ). But it's too boring to type that long code every time. I just want:
$logger->preprocessor({
filter => \&Data::Dumper::Dumper,
value => $ref
});
$logger->debug( $ref, $ref2 );
$logger->info( $array );
And $ref, $ref2, and $array will be dumped by Data::Dumper.
It there a way to do this?
UPD
With help of your answers I do the patch
Now you just:
log4perl.appender.A1.layout=FallbackLayout
log4perl.appender.A1.layout.chain=PatternLayout
log4perl.appender.A1.layout.chain.ConversionPattern=%m%n
log4perl.appender.A1.warp_message = sub { $#_ = 2 if #_ > 3; \
return #_; }
# OR
log4perl.appender.A1.warp_message = main::warp_my_message
sub warp_my_message {
my( #chunks ) = #_;
use Data::Dump qw/ pp /;
for my $msg ( #chunks ) {
$msg = pp $msg if ref $msg;
}
return #chunks;
}
UPD2
Or you can use this small module
log4perl.appender.SomeAPP.warp_message = Preprocess::Messages::msg_filter
log4perl.appender.SomeAPP.layout = Preprocess::Messages
package Preprocess::Messages;
sub msg_filter {
my #chunks = #_;
for my $msg ( #chunks ) {
$msg = pp $msg if ref $msg;
}
return #chunks;
};
sub render {
my $self = shift;
my $layout = Log::Log4perl::Layout::PatternLayout->new(
'%d %P %p> %c %F:%L %M%n %m{indent=2}%n%n'
);
$_[-1] += 1; # increase level of the caller
return $layout->render( join $Log::Log4perl::JOIN_MSG_ARRAY_CHAR, #{ shift() }, #_ );
}
sub new {
my $class = shift;
$class = ref ($class) || $class;
return bless {}, $class;
}
1;
Yes, of course you can set 'warp_message = 0' and combine msg_filter and render together.
log4perl.appender.SomeAPP.warp_message = 0
log4perl.appender.SomeAPP.layout = Preprocess::Messages
sub render {
my($self, $message, $category, $priority, $caller_level) = #_;
my $layout = Log::Log4perl::Layout::PatternLayout->new(
'%d %P %p> %c %F:%L %M%n %m{indent=2}%n%n'
);
for my $item ( #{ $message } ) {
$item = pp $item if ref $item;
}
$message = join $Log::Log4perl::JOIN_MSG_ARRAY_CHAR, #$message;
return $layout->render( $message, $category, $priority, $caller_level+1 );
}
The easy way: use warp_message
The easiest way to do this is to create a custom appender and set the warp_message parameter so you can get the original references that were passed to the logger:
package DumpAppender;
use strict;
use warnings;
use Data::Dumper;
$Data::Dumper::Indent = 0;
$Data::Dumper::Terse = 1;
sub new {
bless {}, $_[0];
}
sub log {
my($self, %params) = #_;
print ref($_) ? Dumper($_) : $_ for #{ $params{message} };
print "\n";
}
package main;
use strict;
use warnings;
use Log::Log4perl;
Log::Log4perl->init(\q{
log4perl.rootLogger=DEBUG,Dump
log4perl.appender.Dump=DumpAppender
log4perl.appender.Dump.layout=NoopLayout
log4perl.appender.Dump.warp_message=0
});
my $logger = Log::Log4perl->get_logger;
$logger->debug(
'This is a string, but this is a reference: ',
{ foo => 'bar' },
);
Output:
This is a string, but this is a reference: {'foo' => 'bar'}
Unfortunately, if you take this approach, you're stuck writing your own code to handle layouts, open files, etc. I wouldn't take this approach except for very simple projects that only need to print to screen.
A better way: composite appender
A better approach is to write your own composite appender. A composite appender forwards messages on to another appender after manipulating them somehow, e.g. filtering or caching them. With this approach, you can write only the code for dumping the references and let an existing appender do the heavy lifting.
The following shows how to write a composite appender. Some of this is explained in the docs for Log::Log4perl::Appender, but I copied much of it from Mike Schilli's Log::Log4perl::Appender::Limit:
package DumpAppender;
use strict;
use warnings;
our #ISA = qw(Log::Log4perl::Appender);
use Data::Dumper;
$Data::Dumper::Indent = 0;
$Data::Dumper::Terse = 1;
sub new {
my ($class, %options) = #_;
my $self = {
appender => undef,
%options
};
# Pass back the appender to be limited as a dependency to the configuration
# file parser.
push #{ $options{l4p_depends_on} }, $self->{appender};
# Run our post_init method in the configurator after all appenders have been
# defined to make sure the appenders we're connecting to really exist.
push #{ $options{l4p_post_config_subs} }, sub { $self->post_init() };
bless $self, $class;
}
sub log {
my ($self, %params) = #_;
# Adjust call stack so messages are reported with the correct caller and
# file
local $Log::Log4perl::caller_depth = $Log::Log4perl::caller_depth + 2;
# Dump all references with Data::Dumper
$_ = ref($_) ? Dumper($_) : $_ for #{ $params{message} };
$self->{app}->SUPER::log(
\%params,
$params{log4p_category},
$params{log4p_level}
);
}
sub post_init {
my ($self) = #_;
if(! exists $self->{appender}) {
die "No appender defined for " . __PACKAGE__;
}
my $appenders = Log::Log4perl->appenders();
my $appender = Log::Log4perl->appenders()->{$self->{appender}};
if(! defined $appender) {
die "Appender $self->{appender} not defined (yet) when " .
__PACKAGE__ . " needed it";
}
$self->{app} = $appender;
}
package main;
use strict;
use warnings;
use Log::Log4perl;
Log::Log4perl->init(\q{
log4perl.rootLogger=DEBUG, Dump
log4perl.appender.Dump=DumpAppender
log4perl.appender.Dump.appender=SCREEN
log4perl.appender.SCREEN=Log::Log4perl::Appender::Screen
log4perl.appender.SCREEN.layout=PatternLayout
log4perl.appender.SCREEN.layout.ConversionPattern=%d %p %m%n
});
my $logger = Log::Log4perl->get_logger;
$logger->debug(
'This is a string, but this is a reference: ',
{ foo => 'bar' },
);
Output:
2015/09/14 13:38:47 DEBUG This is a string, but this is a reference: {'foo' => 'bar'}
Note that you have to take some extra steps if you initialize Log::Log4perl via the API instead of via a file. This is documented in the composite appenders section of the Log::Log4perl::Appender documentation.
I have a super class called Response :
package Response;
use strict;
use warnings;
use HTML::Template;
sub response {
my ( $class, $request ) = #_;
return $request->new_response( $class->status, $class->headers, $class->body );
}
sub body {
my $class = shift;
my $template = HTML::Template->new( 'filehandle' => eval("$class::DATA") );
return $template->output() . $class;
}
sub status {
return 200;
}
sub headers {
return [ 'Content-Type' => 'text/html' ];
}
1;
__DATA__
Default content
and a subclass called URIError :
package URIError;
use strict;
use warnings;
use Response;
our #ISA = qw(Response);
1;
__DATA__
Invalid URI
When URIError->response is called, line
my $template = HTML::Template->new( 'filehandle' => eval("$class::DATA") );
in Response class does not takes DATA section content from URIError class.
What's the syntax to achieve this ?
Your code will work if you change the body method like this. There is no need for eval: all you have to do is disable strict 'refs' and dereference the string "${class}::DATA"
sub body {
my $class = shift;
my $data_fh = do {
no strict 'refs';
*{"${class}::DATA"};
};
my $template = HTML::Template->new( filehandle => $data_fh );
$template->output . $class;
}
We have created the following simple Mojolicious controller:
package SampleApp::Pages;
# $Id$
use strict;
use warnings;
our $VERSION = '0.01';
use Mojo::Base 'Mojolicious::Controller';
sub home {
my $self = shift;
$self->render( 'title' => 'Home' );
return;
}
sub contact {
my $self = shift;
$self->render( 'title' => 'Contact' );
return;
}
sub about {
my $self = shift;
$self->render( 'title' => 'About' );
return;
}
1;
The corresponding unit tests look as follows:
package Test::SampleApp::Pages;
# $Id$
use strict;
use warnings;
our $VERSION = '0.01';
use Carp;
use English '-no_match_vars';
use Readonly;
use Test::Mojo;
use Test::Most;
use base 'Test::Class';
Readonly my $SERVER_OK => 200;
sub startup : Tests(startup) {
eval {
require SampleApp;
SampleApp->import;
1;
} or Carp::croak($EVAL_ERROR);
return;
}
sub get_home : Tests(4) {
my $test = shift;
my $mojo = $test->mojo;
$mojo->get_ok('/pages/home')->status_is($SERVER_OK);
$mojo->text_is(
'title',
$test->base_title . ' | Home',
'... and should have the right title'
);
$mojo->content_like(
qr/<body>(?:\s*\S+\s*)+<\/body>/msx,
'... and should have a non-blank body'
);
return;
}
sub get_contact : Tests(3) {
my $test = shift;
my $mojo = $test->mojo;
$mojo->get_ok('/pages/contact')->status_is($SERVER_OK);
$mojo->text_is(
'title',
$test->base_title . ' | Contact',
'... and should have the right title'
);
return;
}
sub get_about : Tests(3) {
my $test = shift;
my $mojo = $test->mojo;
$mojo->get_ok('/pages/about')->status_is($SERVER_OK);
$mojo->text_is(
'title',
$test->base_title . ' | About',
'... and should have the right title'
);
return;
}
sub base_title {
my ( $self, $base_title ) = #_;
if ( defined $base_title ) {
$self->{base_title} = $base_title;
}
return $self->{base_title};
}
sub mojo {
my ( $self, $mojo ) = #_;
if ( defined $mojo ) {
$self->{mojo} = $mojo;
}
return $self->{mojo};
}
sub setup : Tests(setup) {
my $test = shift;
$test->base_title('Mojolicious Sample App');
$test->mojo( Test::Mojo->new( app => 'SampleApp', max_redirects => 1 ) );
return;
}
1;
To us, this is more like functionality testing rather than unit testing
Is there a way to call the home method of the controller and test its output that doesn't require starting up a server instance via Test::Mojo?
To test your controller's wiring, use code such as the following.
We begin t/pages.t with familiar front matter.
use Mojolicious;
use Test::More;
Now create a testing subclass of SampleApp::Pages that records calls to render.
package TestingPages;
use Mojo::Base 'SampleApp::Pages';
has 'render_called';
has 'render_arg';
sub render {
my($self,%arg) = #_;
$self->render_called(1);
$self->render_arg({ %arg });
}
Your question used Test::Class, so continue with that theme.
package Test::SampleApp::Pages;
use base 'Test::Class';
use Test::More;
Note that die with no arguments propagates the most recent exception, so you don't have to write $# explicitly.
sub startup : Test(startup) {
eval { require SampleApp::Pages; SampleApp::Pages->import; 1 } or die;
}
In setup, instantiate the testing subclass, connect it to a Mojolicious instance, and turn off logging.
sub setup : Test(setup) {
my($self) = #_;
my $c = TestingPages->new(app => Mojolicious->new);
$c->app->log->path(undef);
$c->app->log->level('fatal');
$self->{controller} = $c;
}
In the home test, call the controller's home method and inspect the results.
sub home : Tests(2) {
my($self) = #_;
my $c = $self->{controller};
$c->home;
is $c->render_called, 1, "render called";
is $c->render_arg->{title}, "Home", "correct title arg";
}
Finally, run your tests.
package main;
Test::SampleApp::Pages->runtests;
Output:
$ ./sampleapp.pl test
Running tests from '/tmp/sampleapp/t'.
t/pages.t .. ok
All tests successful.
Files=1, Tests=2, 1 wallclock secs ( 0.03 usr 0.02 sys + 0.24 cusr 0.03 csys = 0.32 CPU)
Result: PASS
Now that you see how to do it, the question is whether it's worth all the trouble. Controllers should be simple wiring. Consider whether any complexity in a controller really belongs in a model where testing is much more straightforward.
I have set of files that needs to either emailed or FTPed(read from config). Before doing either of these I need to so some common operation on the files, like changing filenames, sanity check, so on.
package Class::Foo::Partners;
use Carp;
use Data::Dumper;
# Sanity check and Blessing
sub new ($) {
my $class = shift;
my %attr = #_;
Carp::confess('Config undefined') unless defined $attr{cfg};
my $self = bless({}, $class);
%$self = #_;
return $self;
}
sub process {
my $self = shift;
my %filestoupload = ();
if ($self->{dbh}->sql($sql, \%filestoupload)) {
my $stats;
if (defined $self->{cfg}->{$self->{section}}->{pdf_email_rcpt}) {
$stats = Class::Foo::Email->new(section => $self->{cfg}->{$self->{section}}, filestoupload => \%filestoupload);
$stats->sendfiles;
} else {
$stats = Class::Foo::FTP->new(section => $self->{cfg}->{$self->{section}}, filestoupload => \%filestoupload);
$stats->sendfiles;
}
} elsif ($self->{dbh}->{_error}) {
Carp::confess($self->{dbh}->{_error});
} else {
print "NO FILES";
}
}
package Class::Foo::FTP;
use Carp;
use Data::Dumper;
use POSIX qw( strftime );
use File::Temp qw (tempdir) ;
use File::Copy;
use Net::FTP;
# Sanity check and Blessing
sub new ($) {
my $class = shift;
my %attr = #_;
Carp::confess('Section undefined') unless defined $attr{section};
Carp::confess('undefined ftp_host') unless defined $attr{section}->{ftp_host};
my $self = bless({}, $class);
%$self = #_;
return $self;
}
sub sendfiles {
my $self = shift;
return unless(keys %{$self->{filestoupload}});
#DO SOME COMMON TASK
..
$self->ftp_connect();
..
..
}
package Class::Foo::Email;
use Data::Dumper;
use Mail::Sender;
use POSIX qw( strftime );
use File::Temp qw (tempdir) ;
use File::Copy;
sub new ($) {
my $class = shift;
my %attr = #_;
Carp::confess('Config: undefined pdf_email_subject') unless defined $attr{section}->{pdf_email_subject};
Carp::confess('Config: undefined pdf_email_from') unless defined $attr{section}->{pdf_email_from};
my $self = bless({}, $class);
%$self = #_;
return $self;
}
sub sendfiles {
my $self = shift;
return unless(keys %{$self->{filestoupload}});
#DO SOME COMMON TASK
..
my $mailrcpt = $self->{section}->{pdf_email_rcpt};
my $sender = new Mail::Sender {smtp => 'localhost', from => $self->{section}->{pdf_email_from}};
$sender->MailFile({ to => $mailrcpt,
subject => $self->{section}->{pdf_email_subject},
msg => "Attached is A1 of today's WSJE. ",
ctype => 'application/pdf',
file => #files } );
$self->{uploaded_count} = #files;
}
Where to do the common operation and when and how to call respective child classes?
Should I use abstraction?
thanks for your help
Check out the implementation of MT::FileMgr:
https://github.com/openmelody/melody/tree/master/lib/MT
It should give you a lot of ideas on how to do Perl OOP for something like this.
I have the following code in my class :
sub new {
my $class = shift;
my %args = #_;
my $self = {};
bless( $self, $class );
if ( exists $args{callback} ) {
$self->{callback} = $args{callback};
}
if ( exists $args{dir} ) {
$self->{dir} = $args{dir};
}
return $self;
}
sub test {
my $self = shift;
my $arg = shift;
&$self->{callback}($arg);
}
and a script containing the following code :
use strict;
use warnings;
use MyPackage;
my $callback = sub {
my $arg = shift;
print $arg;
};
my $obj = MyPackage->new(callback => $callback);
but I receive the following error:
Not a CODE reference ...
What am I missing? Printing ref($self->{callback}) shows CODE. It works if I use $self->{callback}->($arg), but I would like to use another way of invoking the code ref.
The ampersand is binding just to $self and not the whole thing. You can do curlies around the part that returns the reference:
&{$self->{callback}}($arg);
But the
$self->{callback}->($arg);
is generally considered cleaner, why don't you want to use it?