DEV Community 👩‍💻👨‍💻

Joseph Mancuso for Masonite

Posted on • Updated on

Masonite Framework Tutorial Series Part 3 - Controllers

Introduction

This is part 3 of the Masonite tutorial series. In this series we will be going much more in depth on a more personal level than the documentation goes into. I’ll try to explain each part of each section in as much detail as possible while trying to stay on topic.

Masonite takes a bit of a different approach with it’s business logic than Flask or Django. Masonite sticks to conventions of a true MVC framework while Flask or Django sort of bends the interpretations a bit.

Controllers in Masonite are simple classes. They are actually so simple that they do not inherit from any class (which could change in Masonite 2.1). Controllers are just classes and methods. The class can be looked at generally as the “group” and the controller methods contain all the business logic for particular views. We’ll explain this relationship more in a bit.

For now let’s start with the beginner concepts and move to more advanced topics

Creating Controllers

Controllers can be created by running a craft command:

$ craft controller Name
Enter fullscreen mode Exit fullscreen mode

Controller Name

You can name your controllers whatever you want but, by convention, these controllers are appended with Controller. For example, the above command will create a controller named NameController instead of Name.

This will also put the class inside it’s own file inside: app/http/controllers/NameController.py. Also by convention, and maybe opinionated, is the fact that each class in Masonite is in is in it’s own file. We have seen this useful as more and more text editors and IDEs allow switching rapidly between file which hot keys that it becomes incredibly fast to swap between entire classes (because they are their file names).

Not Appending “Controller”

Something important to note is that not all controllers need to be appended with Controller so we can create a controller “exactly” as we specify it in the terminal.

If you are like me then you dislike things that are “weasel worded”. In other words a structure like app/http/controllers/NameController.py might be weird to you because it might be obvious to you that it’s a controller because it’s in the controller directory.

So we can also create controllers like so:

$ craft controller Name -e
Enter fullscreen mode Exit fullscreen mode

This simple -e flag will create the controller without the appended Controller part.

Structure

Let’s walk through the controller structure so we can familiarize ourselves with it. A controller after the command is written looks like:

''' A Module Description '''

class NameController:
    ''' Class Docstring Description '''

    def show(self):
        pass
Enter fullscreen mode Exit fullscreen mode

You’ll see we have a simple class which can be thought of as a “group of business logic” and a controller method which is the individual business logic per view.

The controller methods can be looked at as “function based views” in a Django application and will contain similar logic. The only real difference is that you can use other methods to complete and break up some of the logic. For example with this class we can now do something like:

''' A Module Description '''

class NameController:
    ''' Class Docstring Description '''

    def show(self):
        ...
        self.get_names()

    def another(self):
        self.get_names()
        ...

    def get_names(self):
        return ['Mike', 'John', 'Bob']
Enter fullscreen mode Exit fullscreen mode

We can do something similar in Django by creating functions but it won’t really have all the logic you’ll need. This is a simple example but you can imagine how your business logic might get more complex.

Container Resolving

If you haven’t learned about how objects are resolved by Masonite’s Service Container then I suggest you read that before reading this section. If you understand how containers and auto resolving dependency injection works then continue :)

Controller Methods

All methods in a class are resolved by the container if they are specified by the route. The auto resolving part is a part of the routing logic. For example we might have a route like this:

ROUTES = [
    get('/hello/@name', 'NameController@show')
]
Enter fullscreen mode Exit fullscreen mode

In this instance, the show method will be resolved when we go to render this route:

from masonite.request import Request

class NameController:

    def show(self, request: Request):
        request # <class masonite.request.Request>
Enter fullscreen mode Exit fullscreen mode

You’ll notice that we use Python annotations in order to access the class. This has several positive implications to include testability, which we will talk about later.

Controller Constructors

You can imagine that as several methods start to require the same classes, we have a bit of redundant code. For example we may have 2 methods that require the request class:

from masonite.request import Request

class NameController:

    def show(self, request: Request):
        request # <class masonite.request.Request>

    def store(self, request: Request):
        request # <class masonite.request.Request>
Enter fullscreen mode Exit fullscreen mode

This is repetitive so we can throw those classes inside the class constructor:

from masonite.request import Request

class NameController:

    def __init__(self, request: Request):
        self.request = request

    def show(self):
        self.request # <class masonite.request.Request>

    def store(self):
        self.request # <class masonite.request.Request>
Enter fullscreen mode Exit fullscreen mode

You’ll notice here we cleaned up the request class to just get resolved once. This makes our code much more DRY.

Returning Things

We can return a few things in our controller methods. The most obvious thing to return is a view.

In Django, a view is a function and more similar to a controller method above. In Masonite, a view is the actual HTML template instead. We will talk about views in a future tutorial series.

Since we will be returning views so often in Masonite, we have added a “builtin” helper function. This function is added via a Service Provider called HelpersProvider and is already added for you inside the PROVIDERS list. If you are not a fan of these “magical” functions then you can simply remove that Service Provider and it will remove all helper functions from the framework.

Returning a View

All views are inside the resources/templates directory. We can render a view in 2 ways:

We can use the builtin helper function:


class NameController:

    def show(self):
        return view('index')
Enter fullscreen mode Exit fullscreen mode

Notice that we didn’t import anything here. These are called builtin helper functions (because they utilize Python 3 builtins).

Some people do not like this so we can more explicitly define our view class:

from masonite.view import View

class NameController:

    def show(self, view: View):
        return view.render('index')
Enter fullscreen mode Exit fullscreen mode

The helper function just points to this render method on the view class so they both perform exactly the same.

Most people like helper functions because it allows them mock up functionality very quickly. Once everything works, you can go back and refactor into more explicit imports.

Returning a dictionary

Something else convenient about controller methods is you can return a dictionary and it will return a JSON response automatically:

from masonite.view import View

class NameController:

    def show(self):
        return {'name': 'Joe'}
Enter fullscreen mode Exit fullscreen mode

Which will return a JSON response:

{
  "Name": "Joe"
}
Enter fullscreen mode Exit fullscreen mode

This is convenient for building quick API endpoints.

Getting Route Parameters

In the previous tutorial we explained that we can have routes with dynamic properties like an id or a name and they looked like this:

ROUTES = [
    get('/dashboard/user/@id', 'NameController@dashboard'),
    get('/hello/@name', 'NameController@hello')
]
Enter fullscreen mode Exit fullscreen mode

We have these @id and @name fields here. We can fetch these in a controller with the request class:

from masonite.request import Request

class NameController:

    def dashboard(self, request: Request):
        request.param('id')

    def hello(self, request: Request):
        request.param('name')
Enter fullscreen mode Exit fullscreen mode

Input Data

We can also get input parameters such as form data on a POST request or part of the query string on a GET request. Masonite is smart enough to know which one. It never made sense to me on why I should specify exactly which request I am in in order to get specific input data so we can get either one with the .input() method:

from masonite.request import Request

class NameController:

    # GET /dashboard/route?firstname=Joe
    def show(self, request: Request):
        request.input('firstname') # returns Joe
Enter fullscreen mode Exit fullscreen mode

Testing

If I haven’t sold you on the usefulness of Masonite’s controllers then maybe explaining the testability of these controllers is worth while. I’ll only go into a little bit of detail here because this section can be an entire article on its own so let’s give just a little tease.

Notice that we are just passing in our request and view as parameters. So we can now mock them.

Let’s say we have a class like this:

from masonite.request import Request

class NameController:

    def show(self, request: Request):
        return request.input('name') # returns Mike
Enter fullscreen mode Exit fullscreen mode

How might we test this? I’ll walk through testing in greater detail in another series but our test might look something like:

from app.http.controllers.NameController import NameController

class MockRequest:
    def input(self, name):
        return 'Mike'

class TestNameController:

    def test_name_controller(self):
        assert NameController().show(MockRequest()) == 'Mike'
Enter fullscreen mode Exit fullscreen mode

There are a lot of great testing tips which I’ll go through in much more detail than I can here. Let’s keep this more about the basics of controllers.


Well that’s the basics of controllers for this tutorial. If you like what you’ve seen be sure to follow or leave a comment below. You can also find Masonite here

Top comments (0)

We want your help! Become a Tag Moderator.
Fill out this survey and help us moderate our community by becoming a tag moderator here at DEV.