Once I have a reusable controller, how do I extend it? Object-oriented programming gives me a couple ways of extending a controller through code: Inheritance and composition. But, we need to write our controller so that it's easy to inherit or compose.

Don't Render, Stash

First, this means we shouldn't call the render method ourselves (unless we have a good reason, but we'll get to that later). The render method can only ever be called once, so we should only call it after we've gathered all the data we want.

# This method cannot easily be used by a subclass, since it explicitly
# calls render()
sub list {
    my ( $c ) = @_;
    my $resultset_class = $c->stash( 'resultset' );
    my $resultset = $c->schema->resultset( $resultset_class );
    $c->render(
        resultset => $resultset,
    );
}

So, to make sure I don't call render too early, and to make sure subclasses can use the data from my superclass, I instead put all the data directly in to the stash with the stash() method.

Remember that $c->render( %stash ); is the same as $c->stash( %stash ); $c->render();. And, if we never call render() ourselves, that's okay, as Mojolicious will call it for us (unless we call render_later, which we won't).

# This method can be used by a subclass, which can get
# the ResultSet object out of the stash
sub list {
    my ( $c ) = @_;
    my $resultset_class = $c->stash( 'resultset' );
    my $resultset = $c->schema->resultset( $resultset_class );
    $c->stash(
        resultset => $resultset,
    );
}

Return True to Continue

Since there are times where we do want to render a response in the superclass (in the case of a 404 not found error, for example), we need to be able to tell our caller that we did.

# How can I tell that a 404 error is already rendered?
sub get {
    my ( $c ) = @_;
    my $resultset_class = $c->stash( 'resultset' );
    my $id = $c->stash( 'id' );
    my $resultset = $c->schema->resultset( $resultset_class );
    my $row = $resultset->find( $id );
    if ( !$row ) {
        $c->reply->not_found();
    }
    else {
        $c->stash(
            row => $row,
        );
    }
}

We can do so with a simple convention: Return true to continue the dispatch, and return false to stop. This is the same as under route callbacks. Since we're returning, we can also simplify the code a little bit to remove the need for the else block:

sub get {
    my ( $c ) = @_;
    my $resultset_class = $c->stash( 'resultset' );
    my $id = $c->stash( 'id' );
    my $resultset = $c->schema->resultset( $resultset_class );
    my $row = $resultset->find( $id );
    if ( !$row ) {
        $c->reply->not_found();
        # stop dispatch
        return;
    }
    # continue dispatch
    return $c->stash(
        row => $row,
    );
}

Inheritance

With our superclass controller ready, I can now write a subclass. I have a section of my site that's dedicated to user content, so I'll filter the list ResultSet to only those results for the current user, and make sure that get only returns content for the current user (displaying a "not found" response if it's the wrong user).

Remember our superclass method will return true if we're okay to continue working. So, we need to check the return value.

package My::Controller::DBIC::UserContent;
use Mojo::Base 'My::Controller::DBIC';
sub list {
    my ( $c ) = @_;
    $c->SUPER::list || return;
    my $rs = $c->stash( 'resultset' );
    $rs = $rs->search( { user_id => $c->current_user->id } );
    $c->stash( resultset => $rs );
}
sub get {
    my ( $c ) = @_;
    $c->SUPER::get || return;
    my $row = $c->stash( 'row' );
    if ( $row->user_id ne $c->current_user->id ) {
        $c->reply->not_found;
        return;
    }
}

With reusable controllers, we can greatly reduce the amount of code we need to write. Less code means fewer bugs and more time spent writing new features and doing useful things!

Original artwork by Doug Bell, released under CC-BY-SA 4.0.

Tagged in : development, controller, DBIx::Class

author image
Doug Bell

Doug (preaction) is a long time Perl user. He is the current maintainer of CPAN Testers and the author of many CPAN modules including the Statocles blog engine that powers this site.