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
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
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
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']
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')
]
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>
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>
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>
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')
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')
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'}
Which will return a JSON response:
{
"Name": "Joe"
}
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')
]
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')
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
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
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'
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)