CakePHP使いがDjangoでサイトを作ってみた 〜ミドルウェア〜

この記事はだいぶ前に書かれたものなので情報が古いかもしれません
けものフレンズ風のロゴです

この記事を三行にまとめると

beforeFilterやbeforeRenderのような処理を実装できます
ミドルウェアの中で処理を場合分けしなきゃいけない
メリットは大きいと思うので何とか使いこなせるようになりたいですね
例えばページが読み込まれた時、どのページでも共通の処理が走るようにしたい場合、CakePHPだとAppControllerのbeforeFilterやbeforeRenderに処理を書いて、各コントローラーでクラスを継承するというやり方があります。

//AppController
class AppController {
  public function beforeFilter() {
    //共通の処理
  }

  public function beforeRender() {
    //共通の処理
  }
}

//PageController
class PageController extends AppController {
  public function beforeFileter() {
    parent::beforeFilter();
  }

  public function beforeRender() {
    parent::beforeRender();
  }
}

これと同じようなことをDjangoで実現したい場合にミドルウェアという機能があります。今日はそのミドルウェアを使ってみましょう。CakePHPにもミドルウェアという機能はありますが、あっちは玉ねぎのようにアプリケーションを包み込む機能だっていう噂なので、Djangoのミドルウェアとは根本的に違……くもないのかな。似たようなもんか。

まあとにかく、そのミドルウェアを使えばbeforeFilterやbeforeRenderのような処理を実装できます。



ミドルウェアの読み込み

ミドルウェアを読み込むためにはミドルウェア用のpyファイルを作成し、settings.pyで読み込みの設定を記述する必要があります。

例えば今、アプリケーションフォルダの中に「auth.py」というミドルウェアのファイルを作成したとしましょう。

/normnois
  ├ /app
  │   ├ /middlewares
  │   │   ├ __init__.py
  │   │   └ auth.py
  │   │    
  │   └ views.py  他
  │
  └ /normnois
      └ settings.py

絶対にmiddlewaresというフォルダ名じゃないといけないわけではないですが、今回はこの構成でいきます。

auth.pyの中身はざっくりこんな感じです。

//auth.py
class AuthMiddleware() :
  def __init__(self, get_respons) :

  def __call__.py(self, request) :

  def process_view(self, request, func, args, kwargs)
  
  def process_template_response(self, request, response)

これをsettings.pyで読み込みます。

settings.pyを開くと「MIDDLEWARE」という変数があると思います。

#settings.py
MIDDLEWARE = [
  'django.middleware.security.SecurityMiddleware',
  'django.contrib.sessions.middleware.SessionMiddleware',
  'django.middleware.common.CommonMiddleware',
  'django.middleware.csrf.CsrfViewMiddleware',
  'django.contrib.auth.middleware.AuthenticationMiddleware',
  'django.contrib.messages.middleware.MessageMiddleware',
  'django.middleware.clickjacking.XFrameOptionsMiddleware',
]

ここに自作のミドルウェアを追加します。

#settings.py
MIDDLEWARE = [
  'django.middleware.security.SecurityMiddleware',
  'django.contrib.sessions.middleware.SessionMiddleware',
  'django.middleware.common.CommonMiddleware',
  'django.middleware.csrf.CsrfViewMiddleware',
  'django.contrib.auth.middleware.AuthenticationMiddleware',
  'django.contrib.messages.middleware.MessageMiddleware',
  'django.middleware.clickjacking.XFrameOptionsMiddleware',
  'app.middlewares.auth.AuthMiddleware', #これを追加
]

importする時と同じような書き方ですね。これでどのページにアクセスした時もAuthMiddlewareが読み込まれるようになります。



呼び出されるタイミング

先ほどAuthMiddlewareの中に四つほどメソッドを書きましたが、それぞれのメソッドは呼び出しのタイミングが異なります。

「__init__.py」はサーバーが起動した時に一回だけ読み込まれます。runserverで起動した時とかApacheを起動した時の最初の一回だけです。その後は何回ページを読み込み直しても__init__.pyは呼ばれない。

「__call__.py」は何かしらのページにアクセスした時に最初に読み込まれます。CakePHPで言うと……何だろ、bootstrap.phpとかが読み込まれるタイミングに似ているのかな。ちょっと違うかもしれない。

この二つは今日は置いときましょう。今回はbeforeFilterやbeforeRenderに相当するところを見たいので、残りの「process_view」や「process_template_response」を重点的に触れていきます。



process_view

process_viewはビューが読み込まれる直前に呼び出されるメソッドです。だからbeforeFilterに相当する部分がこれにあたると考えて良いと思います。

例えばGETパラメータに特定の値が入ってなかったらエラーページに飛ばすみたいな処理はここで書くことができる。

//auth.py
from django.shortcuts import redirect
class AuthMiddleware() :
  def process_view(self, request, func, args, kwargs) :
    #GETパラメータからトークンを取得
    token = request.GET.get('token')

    #トークンがなければエラーページにリダイレクト
    if not token :
      return redirect('/error')

「https://norm-nois.com/users/index?token=sampletoken」みたいな感じでアクセスした時にパラメータのtokenをチェックするみたいな処理です。

たぶん「__call__.py」でも同じような動きを実装できると思うんですが、じゃあ__call__.pyとの使い方は何が違うのよって言うと、僕も正直あまりよく分かってないんですが一つは「kwargs」などの引数が使えるっていう点ですかね。

例えば「https://norm-nois.com/novels/short/t/853」というURLにアクセスできるように設定を行なったとする。

#urls.py
urlpatterns = [
  path('novels/short/t/<int:id>', novels.detail.as_view()),
]

#auth.py
class AuthMiddleware() :
  def process_view(self, request, func, args, kwargs) :
    print(kwargs)

#出力結果
{'id': 853}

こんな感じでkwargsでURLのIDを取れたりします。なのでこのIDを使って何か処理を行なったり、あるいはIDがなかった時にエラーページに飛ばしたりできます。



process_template_response

process_template_responseはビューが実行された後に読み込まれるので、beforeRenderに近いタイミングと考えて良いと思います。

#auth.py
class AuthMiddleware() :
  def process_template_response(self, request, response) :
    response.context_data['title'] = '共通タイトル'
    return response

#views.py
from django.template.response import TemplateResponse
class index(TemplateView) :
  def get(self, request, *args, **kwargs) :
    context = super().get_context_data(**kwargs)
    return TemplateResponse(request,'index.html', context)

#index.html
{{title}}

こんな感じですね。process_template_responseはTemplateResponseでページを出力している場合に有効なメソッドになるようです。ドキュメントを見るとrender関数で読み込んでる場合なども有効だーみたいなことが書いてあるんですが、僕がやった限りではrenderだと上手く動かなかったです。

#renderを使った場合
from django.shortcuts import render
class index(TemplateView) :
  def get(self, request, *args, **kwargs) :
    context = super().get_context_data(**kwargs)
    return render(request,'index.html', context)

#HttpResponseを使った場合
from django.http import HttpResponse
class index(TemplateView) :
  def get(self, request, *args, **kwargs) :
    context = super().get_context_data(**kwargs)
    return HttpResponse()

この書き方の場合はいずれもprocess_template_responseが機能しませんでした。まあHttpResponseの方はテンプレートをレンダリングしてないんで動かないのはドキュメント通りなのですが。

じゃあこういう場合にbeforeRender的な処理を呼び出したい場合はどうすれば良いんだって言うと……どうしたら良いんだろう? すまん、今の俺には分からん。



特定のURLの時だけ処理を実行したい

CakePHPの場合、共通の処理を行いたくないコントローラーでAppControllerを継承しない、あるいはbeforeFilterやbeforeRenderでAppControllerのそれらを呼び出さないようにすれば回避することができるんですが、Djangoのミドルウェアの場合は特定のURLでだけ読み込まないようにすると言った処理はできないみたいです。

それをやりたければミドルウェアの中で処理を場合分けしなきゃいけない。

#auth.py
class AuthMiddleware() :
  def process_view(self, request, func, args, kwargs) :
    #GETパラメータからトークンを取得
    token = request.GET.get('token')

    #トークンがないURLでは処理を実行しない
    if not token :
      return None

    #トークンチェック処理を入れる

こんな風にURLにトークンがあるかどうかを見て、ある場合は認証が必要だと判断する。ただしURLの場合はユーザーが簡単に書き換えられてしまうんで、本当は認証が必要なページなのに意図的にそれを回避されてしまうかもしれない。

なのでいろんなやり方が考えられるとは思いますが、例えばurls.pyでURLに必ず特定のパラメータを渡せるようにする方法が考えられます。

#urls.py
urlpatterns = [
  path('novels/index/', novels.index.as_view()),
  path('novels/short/', novels.detail.as_view(), kwargs = {'auth': 'check'}),
]

#auth.py
class AuthMiddleware() :
  def process_view(self, request, func, args, kwargs) :
    if 'auth' in kwargs :
      #認証チェックを行う   

urls.pyでURLにauthというパラメータを渡せるように設定しています。あくまでも一例ですが、こうすれば「https://norm-nois.com/novels/index」の方ではauthパラメータを渡してないので認証は行われず、「https://norm-nois.com/novels/short」の方のみ認証チェックの処理を行うようにできます。






CakePHPのbeforeFilterやbeforeRenderに比べると若干クセがあるような気もしますが、でも処理が共通化できるメリットは大きいと思うので、何とか使いこなせるようになりたいですね。

今回は触れませんでしたが、エラーが発生した時の「process_exception」などもあります。詳細はこちら(↓)にあるのでもう少し情報が欲しいという場合はぜひに。

ミドルウェア
 もしかしたら何か関連しているかも? 
 質問や感想などお気軽にコメントしてください