diff --git a/lib/Dancer2/Manual.pod b/lib/Dancer2/Manual.pod index 58e3d72cc..8f471b3be 100644 --- a/lib/Dancer2/Manual.pod +++ b/lib/Dancer2/Manual.pod @@ -452,1195 +452,1179 @@ can use lexical prefix like this: get '/page1' => sub {}; # will match /page1 -=head2 Delayed responses (Async/Streaming) - -L can provide delayed (otherwise known as I) responses -using the C keyword. These responses are streamed, although you can -set the content all at once, if you prefer. - - get '/status' => sub { - delayed { - response_header 'X-Foo' => 'Bar'; - - # flush headers (in case of streaming) - flush; +=head2 Action Skipping - # send content to the user - content 'Hello, world!'; +An action can choose not to serve the current request and ask Dancer2 to +process the request with the next matching route. - # you can write more content - # all streaming - content 'Hello, again!'; +This is done with the B keyword, like in the following example - # when done, close the connection - done; + get '/say/:word' => sub { + pass if route_parameters->get('word') =~ /^\d+$/; + "I say a word: " . route_parameters->get('word'); + }; - # do whatever you want else, asynchronously - # the user socket closed by now - ... - }; + get '/say/:number' => sub { + "I say a number: " . route_parameters->get('number'); }; -If you are streaming (calling C several times), you must call -C first. If you're sending only once, you don't need to call C. -Here is an example of using delayed responses with L: +=head1 HANDLERS - use Dancer2; - use AnyEvent; +=head2 File Handler - my %timers; - my $count = 5; - get '/drums' => sub { - delayed { - print "Stretching...\n"; - flush; # necessary, since we're streaming +Whenever a content is produced out of the parsing of a static file, the +L component is used. This component provides two +hooks, C and C. - $timers{'Snare'} = AE::timer 1, 1, delayed { - $timers{'HiHat'} ||= AE::timer 0, 0.5, delayed { - content "Tss...\n"; - }; +C hooks are called just before starting to parse the +file, the hook receives as its first argument the file path that is going to +be processed. - content "Bap!\n"; + hook before_file_render => sub { + my $path = shift; + }; - if ( $count-- == 0 ) { - %timers = (); - content "Tugu tugu tugu dum!\n"; - done; +C hooks are called after the file has been parsed and the +response content produced. It receives the response object +(L) produced. - print "\n\n"; - $timers{'Applause'} = AE::timer 3, 0, sub { - # the DSL will not available here - # because we didn't call the "delayed" keyword - print "\n"; - }; - } - }; - }; + hook after_file_render => sub { + my $response = shift; }; -If an error happens during a write operation, a warning will be issued -to the logger. - -You can handle the error yourself by providing an C handler: +=head2 Auto page - get '/' => sub { - delayed { - flush; - content "works"; +Whenever a page that matches an existing template needs to be served, the +L component is used. - # ... user disconnected here ... - content "fails"; +=head1 ERRORS - # ... error triggered ... +=head2 Error Pages - done; # doesn't even get run - } on_error => sub { - # delayed{} not needed, DSL already available - my ($error) = @_; - # do something with $error - }; - }; +When an HTTP error occurs (i.e. the action responds with a status code other +than 200), this is how Dancer2 determines what page to display. -Here is an example that asynchronously streams the contents of a CSV file: +=over - use Dancer2; - use Text::CSV_XS qw< csv >; - use Path::Tiny qw< path >; - use JSON::MaybeXS qw< encode_json >; - # Create CSV parser - my $csv = Text::CSV_XS->new({ - binary => 1, - auto_diag => 1, - }); - get '/' => sub { - # delayed response: - delayed { - # streaming content - flush; - # Read each row and stream it in JSON - my $fh = path('filename.csv')->openr_utf8; - while ( my $row = $csv->getline($fh) ) { - content encode_json $row; - } - # close user connection - done; - } on_error => sub { - my ($error) = @_; - warning 'Failed to stream to user: ' . request->remote_address; - }; - }; +=item * Looks in the C directory for a corresponding template file +matching the error code (e.g. C<500.tt> or C<404.tt>). If such a file exists, +it's used to report the error. -B If you just want to send a file's contents asynchronously, -use C instead of C, as it will -automatically take advantage of any asynchronous capability. +=item * Next, looks in the C directory for a corresponding HTML file +matching the error code (e.g. C<500.html> or C<404.html>). If such a file +exists, it's used to report the error. (Note, however, that if B +is set to true, in the case of a 500 error the static HTML page will not be +shown, but will be replaced with a default error page containing more +informative diagnostics. For more information see L.) -=head2 Action Skipping +(In older versions, B was used instead of B. +Both are supported, but B is deprecated.) -An action can choose not to serve the current request and ask Dancer2 to -process the request with the next matching route. +=item * As default, render a generic error page on the fly. -This is done with the B keyword, like in the following example +=back - get '/say/:word' => sub { - pass if route_parameters->get('word') =~ /^\d+$/; - "I say a word: " . route_parameters->get('word'); - }; +=head2 Execution Errors - get '/say/:number' => sub { - "I say a number: " . route_parameters->get('number'); - }; +When an error occurs during the route execution, Dancer2 will render an +error page with the HTTP status code 500. -=head1 HOOKS +It's possible either to display the content of the error message or to hide +it with a generic error page. This is a choice left to the end-user and can +be controlled with the B setting (see above). -Hooks are code references (or anonymous subroutines) that are triggered at -specific moments during the resolution of a request. They are set up using the -L keyword. +=head2 Error Hooks -Many of them are provided by Dancer2's core, but plugins and engines can also -define their own. +When an error is caught by Dancer2's core, an exception object is built (of +the class L). This class provides a hook to let the +user alter the error workflow if needed. -=over 4 +C hooks are called whenever an error object is built, the object +is passed to the hook. -=item * C hooks + hook init_error => sub { + my $error = shift; + # do something with $error + }; -C hooks are evaluated before each request within the context of the -request and receives as argument the app (a L object). +I in Dancer, both names currently +are synonyms for backward-compatibility.> -It's possible to define variables which will be accessible in the action -blocks with the L. +C hooks are called whenever an error is going to be thrown, it +receives the error object as its sole argument. - hook before => sub { - var note => 'Hi there'; + hook before_error => sub { + my $error = shift; + # do something with $error }; - get '/foo/*' => sub { - my ($match) = splat; # 'oversee'; - vars->{note}; # 'Hi there' - }; +I in Dancer, both names currently +are synonyms for backward-compatibility.> -For another example, this can be used along with session support to easily -give non-logged-in users a login page: +C hooks are called whenever an error object has been thrown, it +receives a L object as its sole argument. - hook before => sub { - if (!session('user') && request->path !~ m{^/login}) { - # Pass the original path requested along to the handler: - forward '/login', { requested_path => request->path }; - } + hook after_error => sub { + my $response = shift; }; -The request keyword returns the current L object -representing the incoming request. +I in Dancer, both names currently +are synonyms for backward-compatibility.> -=item * C hooks +C is called when an exception has been caught, at the +route level, just before rethrowing it higher. This hook receives a +L and the error as arguments. -C hooks are evaluated after the response has been built by a route -handler, and can alter the response itself, just before it's sent to the -client. + hook on_route_exception => sub { + my ($app, $error) = @_; + }; -This hook runs after a request has been processed, but before the response -is sent. +=head1 SESSIONS -It receives a L object, which it can modify if it -needs to make changes to the response which is about to be sent. +=head2 Handling sessions -The hook can use other keywords in order to do whatever it wants. +It's common to want to use sessions to give your web applications state; for +instance, allowing a user to log in, creating a session, and checking that +session on subsequent requests. - hook after => sub { - response->content( - q{The "after" hook can alter the response's content here!} - ); - }; +By default Dancer 2 has L sessions enabled. +It implements a very simple in-memory session storage. This will be fast and +useful for testing, but such sessions will not persist between restarts of +your app. -=back +If you'd like to use a different session engine you must declare it in the +configuration file. -=head2 Templates +For example to use YAML file base sessions you need to add the following +to your F: -=over 4 + session: YAML -=item * C +Or, to enable session support from within your code, -C hooks are called whenever a template is going to -be processed, they are passed the tokens hash which they can alter. + set session => 'YAML'; - hook before_template_render => sub { - my $tokens = shift; - $tokens->{foo} = 'bar'; - }; +(However, controlling settings is best done from your config file.) -The tokens hash will then be passed to the template with all the -modifications performed by the hook. This is a good way to setup some -global vars you like to have in all your templates, like the name of the -user logged in or a section name. +The L backend implements a file-based YAML session +storage to help with debugging, but shouldn't be used on production systems. -=item * C +There are other session backends, such as L, +which are recommended for production use. -C hooks are called after the view has been rendered. -They receive as their first argument the reference to the content that has -been produced. This can be used to post-process the content rendered by the -template engine. +You can then use the L keyword to manipulate the +session: - hook after_template_render => sub { - my $ref_content = shift; - my $content = ${$ref_content}; +=head3 Storing data in the session - # do something with $content - ${$ref_content} = $content; - }; +Storing data in the session is as easy as: -=item * C + session varname => 'value'; -C hooks are called whenever the layout is going to be -applied to the current content. The arguments received by the hook are the -current tokens hashref and a reference to the current content. +=head3 Retrieving data from the session - hook before_layout_render => sub { - my ($tokens, $ref_content) = @_; - $tokens->{new_stuff} = 42; - $ref_content = \"new content"; - }; +Retrieving data from the session is as easy as: -=item * C + session('varname') -C hooks are called once the complete content of the -view has been produced, after the layout has been applied to the content. -The argument received by the hook is a reference to the complete content -string. +Or, alternatively, - hook after_layout_render => sub { - my $ref_content = shift; - # do something with ${ $ref_content }, which reflects directly - # in the caller - }; + session->read("varname") -=back +=head3 Controlling where sessions are stored -=head2 Error Handling +For disc-based session backends like L, +session files are written to the session dir specified by +the C setting, which defaults to C<./sessions> +if not specifically set. -Refer to L -for details about the following hooks: +If you need to control where session files are created, you can do so +quickly and easily within your config file, for example: -=over 4 + session: YAML + engines: + session: + YAML: + session_dir: /tmp/dancer-sessions -=item * C +If the directory you specify does not exist, Dancer2 will attempt to create +it for you. -=item * C +=head3 Changing session ID -=item * C +If you wish to change the session ID (for example on privilege level change): -=item * C + my $new_session_id = app->change_session_id -=back +=head3 Destroying a session -=head2 File Rendering +When you're done with your session, you can destroy it: -Refer to L -for details on the following hooks: + app->destroy_session -=over 4 +=head2 Sessions and logging in -=item * C +A common requirement is to check the user is logged in, and, if not, require +them to log in before continuing. -=item * C +This can easily be handled using a before hook to check their session: -=back + use Dancer2; + set session => "Simple"; -=head2 Logging + hook before => sub { + if (!session('user') && request->path !~ m{^/login}) { + forward '/login', { requested_path => request->path }; + } + }; -Logging hooks do not allow you to alter log message content, but they do -give you a place to perform extra actions. For example, if you want to send -an email for error messages, but you don't want to use a heavier logger -such as L or L: + get '/' => sub { return "Home Page"; }; - hook 'engine.logger.after' => sub { - my ( $logger, $level, $message ) = @_; + get '/secret' => sub { return "Top Secret Stuff here"; }; - if( $level eq 'error' ) { - # Send an email with the message content - } + get '/login' => sub { + # Display a login page; the original URL they requested is available as + # query_parameters->get('requested_path'), so could be put in a hidden field in the form + template 'login', { path => query_parameters->get('requested_path') }; }; -There are two hooks available for logging: + post '/login' => sub { + # Validate the username and password they supplied + if (body_parameters->get('user') eq 'bob' && body_parameters->get('pass') eq 'letmein') { + session user => body_parameters->get('user'); + redirect body_parameters->get('path') || '/'; + } else { + redirect '/login?failed=1'; + } + }; -=over 4 + dance(); -=item * engine.logger.before +Here is what the corresponding C file should look like. You should +place it in a directory called C: -This hook is called before a log message is produced. + + + Session and logging in + + +
+ User Name : + Password: -=item * engine.logger.after + + -This hook is called after a log message is produced. + +
+ + -=back +Of course, you'll probably want to validate your users against a database +table, or maybe via IMAP/LDAP/SSH/POP3/local system accounts via PAM etc. +L is probably a good starting point here! -=head2 Serializers +A simple working example of handling authentication against a database table +yourself (using L which provides the C +keyword, and L to handle salted hashed passwords (well, +you wouldn't store your users passwords in the clear, would you?)) follows: -=over 4 + post '/login' => sub { + my $user_value = body_parameters->get('user'); + my $pass_value = body_parameters->get('pass'); -=item * C is called before serializing the content, and receives -the content to serialize as an argument. + my $user = database->quick_select('users', + { username => $user_value } + ); + if (!$user) { + warning "Failed login for unrecognised user $user_value"; + redirect '/login?failed=1'; + } else { + if (Crypt::SaltedHash->validate($user->{password}, $pass_value)) + { + debug "Password correct"; + # Logged in successfully + session user => $user; + redirect body_parameters->get('path') || '/'; + } else { + debug("Login failed - password incorrect for " . $user_value); + redirect '/login?failed=1'; + } + } + }; - hook before_serializer => sub { - my $content = shift; - ... - }; +=head3 Retrieve complete hash stored in session -=item * C is called after the payload has been serialized, and -receives the serialized content as an argument. +Get complete hash stored in session: - hook after_serializer => sub { - my $serialized_content = shift; - ... - }; + my $hash = session; -=back +=head3 The Session keyword +Dancer2 maintains two session layers. -=head1 HANDLERS +The first layer, L provides a session object +which represents the current session. You can read from it as many +times as you want, and write to it as many times as you want. -=head2 File Handler +The second layer is the session engine (L +is one example), which is used in order to implement the reading and +writing from the actual storage. This is read only once, when a request +comes in (using a cookie whose value is C by default). +At the end of a request, all the data you've written will be flushed +to the engine itself, which will do the actual write to the storage +(whether it's in a hash in memory, in Memcache, or in a database). -Whenever a content is produced out of the parsing of a static file, the -L component is used. This component provides two -hooks, C and C. +=head1 TEMPLATES -C hooks are called just before starting to parse the -file, the hook receives as its first argument the file path that is going to -be processed. +Returning plain content is all well and good for examples or trivial apps, +but soon you'll want to use templates to maintain separation between your +code and your content. Dancer2 makes this easy. - hook before_file_render => sub { - my $path = shift; - }; +Your route handlers can use the L keyword +to render templates. -C hooks are called after the file has been parsed and the -response content produced. It receives the response object -(L) produced. +=head2 Views - hook after_file_render => sub { - my $response = shift; - }; +In Dancer2, a file which holds a template is called a I. Views are +located in the C directory. -=head2 Auto page +You can change this location by changing the setting 'views'. For instance +if your templates are located in the 'templates' directory, do the +following: -Whenever a page that matches an existing template needs to be served, the -L component is used. + set views => path( app->location , 'templates' ); +By default, the internal template engine L is +used, but you may want to upgrade to L