Mojolicious::Controller - Controller Base Class


Mojolicious documentation Contained in the Mojolicious distribution.

Index


Code Index:

NAME

Top

Mojolicious::Controller - Controller Base Class

SYNOPSIS

Top

  use Mojo::Base 'Mojolicious::Controller';

DESCRIPTION

Top

Mojolicious::Controller is the base class for your Mojolicious controllers. It is also the default controller class for Mojolicious unless you set controller_class in your application.

ATTRIBUTES

Top

Mojolicious::Controller inherits all attributes from Mojo::Base and implements the following new ones.

app

  my $app = $c->app;
  $c      = $c->app(Mojolicious->new);

A reference back to the Mojolicious application that dispatched to this controller.

match

  my $m = $c->match;

A Mojolicious::Routes::Match object containing the routes results for the current request.

tx

  my $tx = $c->tx;

The transaction that is currently being processed, defaults to a Mojo::Transaction::HTTP object.

METHODS

Top

Mojolicious::Controller inherits all methods from Mojo::Base and implements the following new ones.

finish

  $c->finish;
  $c->finish('Bye!');

Gracefully end WebSocket connection or long poll stream.

flash

  my $flash = $c->flash;
  my $foo   = $c->flash('foo');
  $c        = $c->flash({foo => 'bar'});
  $c        = $c->flash(foo => 'bar');

Data storage persistent for the next request, stored in the session.

  $c->flash->{foo} = 'bar';
  my $foo = $c->flash->{foo};
  delete $c->flash->{foo};

on_finish

  $c->on_finish(sub {...});

Callback to be invoked when the transaction has been finished.

  $c->on_finish(sub {
    my $c = shift;
  });

on_message

  $c = $c->on_message(sub {...});

Callback to be invoked when new WebSocket messages arrive.

  $c->on_message(sub {
    my ($c, $message) = @_;
  });

param

  my @names = $c->param;
  my $foo   = $c->param('foo');
  my @foo   = $c->param('foo');
  $c        = $c->param(foo => 'ba;r');

Access GET/POST parameters and route captures that are not reserved stash values.

  # Only GET parameters
  my $foo = $c->req->url->query->param('foo');

  # Only GET and POST parameters
  my $foo = $c->req->param('foo');

redirect_to

  $c = $c->redirect_to('named');
  $c = $c->redirect_to('named', foo => 'bar');
  $c = $c->redirect_to('/path');
  $c = $c->redirect_to('http://127.0.0.1/foo/bar');

Prepare a 302 redirect response, takes the exact same arguments as url_for.

  return $c->redirect_to('login') unless $c->session('user');

render

  $c->render;
  $c->render(controller => 'foo', action => 'bar');
  $c->render({controller => 'foo', action => 'bar'});
  $c->render(text => 'Hello!');
  $c->render(template => 'index');
  $c->render(template => 'foo/index');
  $c->render(template => 'index', format => 'html', handler => 'epl');
  $c->render(handler => 'something');
  $c->render('foo/bar');
  $c->render('foo/bar', format => 'html');

This is a wrapper around Mojolicious::Renderer exposing pretty much all functionality provided by it. It will set a default template to use based on the controller and action name or fall back to the route name. You can call it with a hash of options which can be preceded by an optional template name. It will also run the before_render plugin hook.

render_content

  my $output = $c->render_content;
  my $output = $c->render_content('header');
  my $output = $c->render_content(header => 'Hello world!');
  my $output = $c->render_content(header => sub { 'Hello world!' });

Contains partial rendered templates, used for the renderers layout and extends features.

render_data

  $c->render_data($bits);
  $c->render_data($bits, format => 'png');

Render the given content as raw bytes, similar to render_text but data will not be encoded.

render_exception

  $c->render_exception('Oops!');
  $c->render_exception(Mojo::Exception->new('Oops!'));

Render the exception template exception.$mode.html.$handler or exception.html.$handler and set the response status code to 500.

render_json

  $c->render_json({foo => 'bar'});
  $c->render_json([1, 2, -3]);

Render a data structure as JSON.

render_later

  $c->render_later;

Disable auto rendering, especially for long polling this can be quite useful.

  $c->render_later;
  Mojo::IOLoop->timer(2 => sub {
    $c->render(text => 'Delayed by 2 seconds!');
  });

render_not_found

  $c->render_not_found;
  $c->render_not_found($resource);

Render the not found template not_found.$mode.html.$handler or not_found.html.$handler and set the response status code to 404.

render_partial

  my $output = $c->render_partial('menubar');
  my $output = $c->render_partial('menubar', format => 'txt');

Same as render but returns the rendered result.

render_static

  my $success = $c->render_static('images/logo.png');
  my $success = $c->render_static('../lib/MyApp.pm');

Render a static file using Mojolicious::Static relative to the public directory of your application.

render_text

  $c->render_text('Hello World!');
  $c->render_text('Hello World', layout => 'green');

Render the given content as Perl characters, which will be encoded to bytes. See render_data for an alternative without encoding. Note that this does not change the content type of the response, which is text/html;charset=UTF-8 by default.

  $c->render_text('Hello World!', format => 'txt');

rendered

  $c = $c->rendered;
  $c = $c->rendered(302);

Finalize response and run after_dispatch plugin hook.

req

  my $req = $c->req;

Alias for $c->tx->req. Usually refers to a Mojo::Message::Request object.

res

  my $res = $c->res;

Alias for $c->tx->res. Usually refers to a Mojo::Message::Response object.

send_message

  $c = $c->send_message('Hi there!');
  $c = $c->send_message('Hi there!', sub {...});

Send a message via WebSocket, only works if there is currently a WebSocket connection in progress.

session

  my $session = $c->session;
  my $foo     = $c->session('foo');
  $c          = $c->session({foo => 'bar'});
  $c          = $c->session(foo => 'bar');

Persistent data storage, by default stored in a signed cookie. Note that cookies are generally limited to 4096 bytes of data.

  $c->session->{foo} = 'bar';
  my $foo = $c->session->{foo};
  delete $c->session->{foo};

stash

  my $stash = $c->stash;
  my $foo   = $c->stash('foo');
  $c        = $c->stash({foo => 'bar'});
  $c        = $c->stash(foo => 'bar');

Non persistent data storage and exchange.

  $c->stash->{foo} = 'bar';
  my $foo = $c->stash->{foo};
  delete $c->stash->{foo};

ua

  my $ua = $c->ua;

A Mojo::UserAgent prepared for the current environment.

  # Blocking
  my $tx = $c->ua->get('http://mojolicio.us');
  my $tx = $c->ua->post_form('http://kraih.com/login' => {user => 'mojo'});

  # Non-blocking
  $c->ua->get('http://mojolicio.us' => sub {
    my $tx = pop;
    $c->render_data($tx->res->body);
  });

url_for

  my $url = $c->url_for;
  my $url = $c->url_for(controller => 'bar', action => 'baz');
  my $url = $c->url_for('named', controller => 'bar', action => 'baz');
  my $url = $c->url_for('/perldoc');
  my $url = $c->url_for('http://mojolicio.us/perldoc');

Generate a portable Mojo::URL object with base for a route, path or URL.

write

  $c->write;
  $c->write('Hello!');
  $c->write(sub {...});
  $c->write('Hello!', sub {...});

Write dynamic content chunk wise, the optional drain callback will be invoked once all data has been written to the kernel send buffer or equivalent.

  # Keep connection alive (with Content-Length header)
  $c->res->headers->content_length(6);
  $c->write('Hel', sub {
    my $c = shift;
    $c->write('lo!')
  });

  # Close connection when done (without Content-Length header)
  $c->write('Hel', sub {
    my $c = shift;
    $c->write('lo!', sub {
      my $c = shift;
      $c->finish;
    });
  });

write_chunk

  $c->write_chunk;
  $c->write_chunk('Hello!');
  $c->write_chunk(sub {...});
  $c->write_chunk('Hello!', sub {...});

Write dynamic content chunk wise with the chunked Transfer-Encoding which doesn't require a Content-Length header, the optional drain callback will be invoked once all data has been written to the kernel send buffer or equivalent.

  $c->write_chunk('He', sub {
    my $c = shift;
    $c->write_chunk('ll', sub {
      my $c = shift;
      $c->finish('o!');
    });
  });

You can call finish at any time to end the stream.

  2
  He
  2
  ll
  2
  o!
  0

SEE ALSO

Top

Mojolicious, Mojolicious::Guides, http://mojolicio.us.


Mojolicious documentation Contained in the Mojolicious distribution.

package Mojolicious::Controller;
use Mojo::Base -base;

use Mojo::Asset::File;
use Mojo::ByteStream;
use Mojo::Cookie::Response;
use Mojo::Exception;
use Mojo::Transaction::HTTP;
use Mojo::URL;
use Mojo::Util;

require Carp;
require File::Basename;
require File::Spec;

# "Scalpel... blood bucket... priest."
has [qw/app match/];
has tx => sub { Mojo::Transaction::HTTP->new };

# Template directory
my $T = File::Spec->catdir(File::Basename::dirname(__FILE__), 'templates');

# Exception template
our $EXCEPTION =
  Mojo::Asset::File->new(path => File::Spec->catfile($T, 'exception.html.ep'))
  ->slurp;

# Exception template (development)
our $DEVELOPMENT_EXCEPTION =
  Mojo::Asset::File->new(
  path => File::Spec->catfile($T, 'exception.development.html.ep'))->slurp;

# Not found template
our $NOT_FOUND =
  Mojo::Asset::File->new(path => File::Spec->catfile($T, 'not_found.html.ep'))
  ->slurp;

# Not found template (development)
our $DEVELOPMENT_NOT_FOUND =
  Mojo::Asset::File->new(
  path => File::Spec->catfile($T, 'not_found.development.html.ep'))->slurp;

# Reserved stash values
my @RESERVED = (
  qw/action app cb class controller data exception extends format handler/,
  qw/json layout method namespace partial path status template text/
);
my %RESERVED;
$RESERVED{$_}++ for @RESERVED;

# "Is all the work done by the children?
#  No, not the whipping."
sub AUTOLOAD {
  my $self = shift;

  # Method
  my ($package, $method) = our $AUTOLOAD =~ /^([\w\:]+)\:\:(\w+)$/;

  # Call helper
  Carp::croak(qq/Can't locate object method "$method" via package "$package"/)
    unless my $helper = $self->app->renderer->helpers->{$method};
  $self->$helper(@_);
}

sub DESTROY { }

# "For the last time, I don't like lilacs!
#  Your first wife was the one who liked lilacs!
#  She also liked to shut up!"
sub cookie {
  my ($self, $name, $value, $options) = @_;
  return unless $name;

  # Response cookie
  if (defined $value) {

    # Cookie too big
    $self->app->log->error(qq/Cookie "$name" is bigger than 4096 bytes./)
      if length $value > 4096;

    # Create new cookie
    $options ||= {};
    my $cookie = Mojo::Cookie::Response->new(
      name  => $name,
      value => $value,
      %$options
    );
    $self->res->cookies($cookie);
    return $self;
  }

  # Request cookie
  unless (wantarray) {
    return unless my $cookie = $self->req->cookie($name);
    return $cookie->value;
  }

  # Request cookies
  my @cookies = $self->req->cookie($name);
  map { $_->value } @cookies;
}

# "Something's wrong, she's not responding to my poking stick."
sub finish {
  my ($self, $chunk) = @_;

  # WebSocket
  my $tx = $self->tx;
  return $tx->finish if $tx->is_websocket;

  # Chunked stream
  if ($tx->res->is_chunked) {
    $self->write_chunk($chunk) if defined $chunk;
    return $self->write_chunk('');
  }

  # Normal stream
  $self->write($chunk) if defined $chunk;
  $self->write('');
}

# "You two make me ashamed to call myself an idiot."
sub flash {
  my $self = shift;

  # Get
  my $session = $self->stash->{'mojo.session'};
  if ($_[0] && !defined $_[1] && !ref $_[0]) {
    return unless $session && ref $session eq 'HASH';
    return unless my $flash = $session->{flash};
    return unless ref $flash eq 'HASH';
    return $flash->{$_[0]};
  }

  # Initialize
  $session = $self->session;
  my $flash = $session->{new_flash};
  $flash = {} unless $flash && ref $flash eq 'HASH';
  $session->{new_flash} = $flash;

  # Hash
  return $flash unless @_;

  # Set
  my $values = exists $_[1] ? {@_} : $_[0];
  $session->{new_flash} = {%$flash, %$values};

  $self;
}

# "My parents may be evil, but at least they're stupid."
sub on_finish {
  my ($self, $cb) = @_;
  $self->tx->on_finish(sub { shift and $self->$cb(@_) });
}

sub on_message {
  my $self = shift;

  my $tx = $self->tx;
  Carp::croak('No WebSocket connection to receive messages from')
    unless $tx->is_websocket;
  my $cb = shift;
  $tx->on_message(sub { shift and $self->$cb(@_) });
  $self->rendered(101);

  $self;
}

# "Just make a simple cake. And this time, if someone's going to jump out of
#  it make sure to put them in *after* you cook it."
sub param {
  my $self = shift;
  my $name = shift;

  # List
  my $p = $self->stash->{'mojo.captures'} || {};
  unless (defined $name) {
    my %seen;
    my @keys = grep { !$seen{$_}++ } $self->req->param;
    push @keys, grep { !$RESERVED{$_} && !$seen{$_}++ } keys %$p;
    return sort @keys;
  }

  # Override value
  if (@_) {
    $p->{$name} = $_[0];
    return $self;
  }

  # Captured unreserved value
  return $p->{$name} if !$RESERVED{$name} && exists $p->{$name};

  # Param value
  $self->req->param($name);
}

# "Is there an app for kissing my shiny metal ass?
#  Several!
#  Oooh!"
sub redirect_to {
  my $self = shift;

  my $headers = $self->res->headers;
  $headers->location($self->url_for(@_)->to_abs);
  $headers->content_length(0);
  $self->rendered(302);

  $self;
}

# "Mamma Mia! The cruel meatball of war has rolled onto our laps and ruined
#  our white pants of peace!"
sub render {
  my $self = shift;

  # Recursion
  my $stash = $self->stash;
  if ($stash->{'mojo.rendering'}) {
    $self->app->log->debug(qq/Can't render in "before_render" hook./);
    return '';
  }

  # Template may be first argument
  my $template;
  $template = shift if @_ % 2 && !ref $_[0];
  my $args = ref $_[0] ? $_[0] : {@_};

  # Template
  $args->{template} = $template if $template;
  unless ($stash->{template} || $args->{template}) {

    # Default template
    my $controller = $args->{controller} || $stash->{controller};
    my $action     = $args->{action}     || $stash->{action};

    # Normal default template
    if ($controller && $action) {
      $self->stash->{template} = join('/', split(/-/, $controller), $action);
    }

    # Try the route name if we don't have controller and action
    elsif ($self->match && $self->match->endpoint) {
      $self->stash->{template} = $self->match->endpoint->name;
    }
  }

  # Render
  my $app = $self->app;
  {
    local $stash->{'mojo.rendering'} = 1;
    $app->plugins->run_hook_reverse(before_render => $self, $args);
  }
  my ($output, $type) = $app->renderer->render($self, $args);
  return unless defined $output;
  return $output if $args->{partial};

  # Prepare response
  my $res = $self->res;
  $res->body($output) unless $res->body;
  my $headers = $res->headers;
  $headers->content_type($type) unless $headers->content_type;
  $self->rendered($stash->{status});

  1;
}

sub render_content {
  my $self    = shift;
  my $name    = shift;
  my $content = pop;

  # Initialize
  my $stash = $self->stash;
  my $c = $stash->{'mojo.content'} ||= {};
  $name ||= 'content';

  # Set
  if (defined $content) {

    # Reset with multiple values
    if (@_) {
      $c->{$name} = '';
      for my $part (@_, $content) {
        $c->{$name} .= ref $part eq 'CODE' ? $part->() : $part;
      }
    }

    # First come
    else {
      $c->{$name} ||= ref $content eq 'CODE' ? $content->() : $content;
    }
  }

  # Get
  $content = $c->{$name};
  $content = '' unless defined $content;
  Mojo::ByteStream->new("$content");
}

sub render_data { shift->render(data => shift, @_) }

# "The path to robot hell is paved with human flesh.
#  Neat."
sub render_exception {
  my ($self, $e) = @_;
  $e = Mojo::Exception->new($e);
  $self->app->log->error($e);

  # Recursion
  return if $self->stash->{'mojo.exception'};

  # Filtered stash snapshot
  my $snapshot = {};
  my $stash    = $self->stash;
  for my $key (keys %$stash) {
    next if $key =~ /^mojo\./;
    next unless defined(my $value = $stash->{$key});
    $snapshot->{$key} = $value;
  }

  # Mode specific template
  my $mode    = $self->app->mode;
  my $options = {
    template         => "exception.$mode",
    format           => 'html',
    handler          => undef,
    status           => 500,
    snapshot         => $snapshot,
    exception        => $e,
    'mojo.exception' => 1
  };
  unless ($self->render($options)) {

    # Template
    $options->{template} = 'exception';
    unless ($self->render($options)) {

      # Inline template
      delete $stash->{layout};
      delete $stash->{extends};
      delete $options->{template};
      $options->{inline} =
        $mode eq 'development' ? $DEVELOPMENT_EXCEPTION : $EXCEPTION;
      $options->{handler} = 'ep';
      $self->render($options);
    }
  }
}

# DEPRECATED in Smiling Face With Sunglasses!
sub render_inner {
  warn <<EOF;
Mojolicious::Controller->render_inner is DEPRECATED in favor of
Mojolicious::Controller->render_content!!!
EOF
  shift->render_content(@_);
}

# "If you hate intolerance and being punched in the face by me,
#  please support Proposition Infinity."
sub render_json {
  my $self = shift;
  my $json = shift;
  my $args = ref $_[0] ? $_[0] : {@_};
  $args->{json} = $json;
  $self->render($args);
}

sub render_later { shift->stash->{'mojo.rendered'} = 1 }

# "Excuse me, sir, you're snowboarding off the trail.
#  Lick my frozen metal ass."
sub render_not_found {
  my ($self, $resource) = @_;
  $self->app->log->debug(qq/Resource "$resource" not found./) if $resource;

  # Recursion
  my $stash = $self->stash;
  return if $stash->{'mojo.exception'};
  return if $stash->{'mojo.not_found'};

  # Check for POD plugin
  my $guide =
      $self->app->renderer->helpers->{pod_to_html}
    ? $self->url_for('/perldoc')
    : 'http://mojolicio.us/perldoc';

  # Mode specific template
  my $mode    = $self->app->mode;
  my $options = {
    template         => "not_found.$mode",
    format           => 'html',
    status           => 404,
    guide            => $guide,
    'mojo.not_found' => 1
  };
  unless ($self->render($options)) {

    # Template
    $options->{template} = 'not_found';
    unless ($self->render($options)) {

      # Inline template
      delete $options->{layout};
      delete $options->{extends};
      delete $options->{template};
      $options->{inline} =
        $mode eq 'development' ? $DEVELOPMENT_NOT_FOUND : $NOT_FOUND;
      $options->{handler} = 'ep';
      $self->render($options);
    }
  }
}

# "You called my thesis a fat sack of barf, and then you stole it?
#  Welcome to academia."
sub render_partial {
  my $self     = shift;
  my $template = @_ % 2 ? shift : undef;
  my $args     = {@_};

  $args->{template} = $template if defined $template;
  $args->{partial} = 1;

  Mojo::ByteStream->new($self->render($args));
}

sub render_static {
  my ($self, $file) = @_;

  my $app = $self->app;
  unless ($app->static->serve($self, $file)) {
    $app->log->debug(
      qq/Static file "$file" not found, public directory missing?/);
    return;
  }
  $self->rendered;

  1;
}

sub render_text { shift->render(text => shift, @_) }

# "On the count of three, you will awaken feeling refreshed,
#  as if Futurama had never been canceled by idiots,
#  then brought back by bigger idiots. One. Two."
sub rendered {
  my ($self, $status) = @_;

  # Disable auto rendering
  $self->render_later;

  # Make sure we have a status
  my $res = $self->res;
  $res->code($status) if $status;

  # Finish transaction
  my $stash = $self->stash;
  unless ($stash->{'mojo.finished'}) {
    $res->code(200) unless $res->code;
    my $app = $self->app;
    $app->plugins->run_hook_reverse(after_dispatch => $self);
    $app->sessions->store($self);
    $stash->{'mojo.finished'} = 1;
  }
  $self->tx->resume;

  $self;
}

sub req { shift->tx->req }
sub res { shift->tx->res }

sub send_message {
  my ($self, $message, $cb) = @_;

  my $tx = $self->tx;
  Carp::croak('No WebSocket connection to send message to')
    unless $tx->is_websocket;
  $tx->send_message($message, sub { shift and $self->$cb(@_) if $cb });
  $self->rendered(101);

  $self;
}

# "Why am I sticky and naked? Did I miss something fun?"
sub session {
  my $self = shift;

  # Get
  my $stash   = $self->stash;
  my $session = $stash->{'mojo.session'};
  if ($_[0] && !defined $_[1] && !ref $_[0]) {
    return unless $session && ref $session eq 'HASH';
    return $session->{$_[0]};
  }

  # Hash
  $session = {} unless $session && ref $session eq 'HASH';
  $stash->{'mojo.session'} = $session;
  return $session unless @_;

  # Set
  my $values = exists $_[1] ? {@_} : $_[0];
  $stash->{'mojo.session'} = {%$session, %$values};

  $self;
}

sub signed_cookie {
  my ($self, $name, $value, $options) = @_;
  return unless $name;

  # Response cookie
  my $secret = $self->app->secret;
  if (defined $value) {

    # Sign value
    my $signature = Mojo::Util::hmac_md5_sum $value, $secret;
    $value = $value .= "--$signature";

    # Create cookie
    my $cookie = $self->cookie($name, $value, $options);
    return $cookie;
  }

  # Request cookies
  my @values = $self->cookie($name);
  my @results;
  for my $value (@values) {

    # Check signature
    if ($value =~ s/\-\-([^\-]+)$//) {
      my $signature = $1;
      my $check = Mojo::Util::hmac_md5_sum $value, $secret;

      # Verified
      if ($signature eq $check) { push @results, $value }

      # Bad cookie
      else {
        $self->app->log->debug(
          qq/Bad signed cookie "$name", possible hacking attempt./);
      }
    }

    # Not signed
    else { $self->app->log->debug(qq/Cookie "$name" not signed./) }
  }

  wantarray ? @results : $results[0];
}

# "All this knowledge is giving me a raging brainer."
sub stash {
  my $self = shift;

  # Hash
  $self->{stash} ||= {};
  return $self->{stash} unless @_;

  # Get
  return $self->{stash}->{$_[0]} unless @_ > 1 || ref $_[0];

  # Set
  my $values = ref $_[0] ? $_[0] : {@_};
  for my $key (keys %$values) {
    $self->app->log->debug(qq/Careful, "$key" is a reserved stash value./)
      if $RESERVED{$key};
    $self->{stash}->{$key} = $values->{$key};
  }

  $self;
}

sub ua { shift->app->ua }

# "Behold, a time traveling machine.
#  Time? I can't go back there!
#  Ah, but this machine only goes forward in time.
#  That way you can't accidentally change history or do something disgusting
#  like sleep with your own grandmother.
#  I wouldn't want to do that again."
sub url_for {
  my $self = shift;
  my $target = shift || '';

  # Absolute URL
  return Mojo::URL->new($target) if $target =~ /^\w+\:\/\//;

  # Make sure we have a match for named routes
  my $match;
  unless ($match = $self->match) {
    $match = Mojolicious::Routes::Match->new(get => '/');
    $match->root($self->app->routes);
  }

  # Base
  my $url = Mojo::URL->new;
  my $req = $self->req;
  $url->base($req->url->base->clone);
  my $base = $url->base;
  $base->userinfo(undef);

  # Relative URL
  my $path = $url->path;
  if ($target =~ /^\//) {
    if (my $e = $self->stash->{path}) {
      my $real = $req->url->path->to_abs_string;
      Mojo::Util::url_unescape($real);
      my $backup = $real;
      Mojo::Util::decode('UTF-8', $real);
      $real = $backup unless defined $real;
      $real =~ s/\/?$e$/$target/;
      $target = $real;
    }
    $url->parse($target);
  }

  # Route
  else {
    my ($p, $ws) = $match->path_for($target, @_);
    $path->parse($p) if $p;

    # Fix scheme for WebSockets
    $base->scheme(($base->scheme || '') eq 'https' ? 'wss' : 'ws') if $ws;
  }

  # Make path absolute
  my $base_path = $base->path;
  unshift @{$path->parts}, @{$base_path->parts};
  $base_path->parts([]);

  $url;
}

# "I wax my rocket every day!"
sub write {
  my ($self, $chunk, $cb) = @_;

  if (ref $chunk && ref $chunk eq 'CODE') {
    $cb    = $chunk;
    $chunk = undef;
  }
  $self->res->write($chunk, sub { shift and $self->$cb(@_) if $cb });
  $self->rendered;

  $self;
}

sub write_chunk {
  my ($self, $chunk, $cb) = @_;

  if (ref $chunk && ref $chunk eq 'CODE') {
    $cb    = $chunk;
    $chunk = undef;
  }
  $self->res->write_chunk($chunk, sub { shift and $self->$cb(@_) if $cb });
  $self->rendered;

  $self;
}

1;

__END__