技術メモ

プログラミングとか電子工作とか

YouTube Data API (v3)でOAuthを使ってAPIを使う準備 その1

どうも、靖宗です。

最近YouTubeAPIを触ることがあってメモ代わりに記事にします。
OAuthあたりはよく忘れるので・・・

オープンな情報ならAPI Keyを作成するだけで簡単なのですが、ユーザーに紐付いた操作などをする場合はOAuthの認証が必要です。
今回はAPI Keyの方は言及しませんがもしかしたらそのうち書くかも。

Google Cloud Platformで下準備

まずはGoogle Cloud Platform(GCP)のダッシュボードからプロジェクトを作成し、その元にアプリケーションを登録します。

console.cloud.google.com

アクセスするとログイン画面がでるので、APIで操作したいアカウントでログインします。

f:id:ysmn_deus:20190408112851p:plain

プロジェクト作成

プロジェクト毎に認証情報は異なる方が都合が良いのでプロジェクトを作成します。
左上の「プロジェクトの選択」から作成します。

f:id:ysmn_deus:20190408212505p:plain

f:id:ysmn_deus:20190408212642p:plain

f:id:ysmn_deus:20190408213001p:plain

プロジェクト名は各自好きに設定して下さい。

「作成」というボタンを押すと、ダッシュボードに戻されて右上でなんか進みます。
暫くすると「プロジェクト「○○」を作成」と表示されればプロジェクトの作成が完了していると思います。

OAuthクライアント

お次にアプリケーションの登録を行います。
左上のメニューマークから

f:id:ysmn_deus:20190408213445p:plain

f:id:ysmn_deus:20190408213602p:plain

APIとサービス」→「認証情報」を選択します。
プロジェクトが選択されてないと「このページを表示するには、プロジェクトを選択してください。」という表示が出ると思うので、右上の「プロジェクトの選択」から先ほど作成したプロジェクトを選択します。

f:id:ysmn_deus:20190408213847p:plain

プロジェクトが選択されていれば、「認証情報」という窓が表示されていると思うので、認証情報を作成していきます。

f:id:ysmn_deus:20190408214020p:plain

今回はOAuthを利用するのでOAuthクライアントIDを選択します。

f:id:ysmn_deus:20190408214059p:plain

同意画面の設定

進むと「OAuth クライアント ID を作成するには、まず同意画面でサービス名を設定する必要があります。」と警告されていると思います。
ユーザーがOAuthを利用する際に「承認しますか?」と聞かれるページに表示されている情報を設定して下さいという意図です。確かにそれがないと始まらない。
同意画面の設定ボタンを押して設定しときます。

f:id:ysmn_deus:20190408214314p:plain

設定画面が表示されますので、必要な情報を入力します。
とりあえず動かしたいって方はおそらく

  • アプリケーション名
  • サポートメール

だけ設定しておけばローカル環境では動きそうです。
もしサービスを運用するドメイン(URL)なども決まっている場合は「承認済みドメイン」の箇所を設定しておいて下さい。
たぶんここを設定してないとなにがしかのトラブルに巻き込まれます(不正な認証情報だとか云々?しらんけど)。
実際にOAuthを利用するサービスにする場合はちゃんとポリシーなんかのアドレスも設定して下さい。

f:id:ysmn_deus:20190408214903p:plain

設定できたら「保存」を押して同意画面の設定を完了します。

f:id:ysmn_deus:20190408215016p:plain

クライアントIDの作成

同意画面の設定を完了できればようやくクライアントIDの発行ができます。

f:id:ysmn_deus:20190408215210p:plain

今回はWebアプリケーションを想定して一番上の「ウェブアプリケーション」を選択します。

承認済みのJavaScript生成元

ちょっと画像に入れ忘れてたんですが、ローカル環境で開発する際は「承認済みのJavaScript生成元」に"http://localhost"や"http://127.0.0.1:8000"などを設定しておかないとGoogleにはじかれます。

承認済みのリダイレクト URI

また、認証情報を取得してそのまま情報をアプリケーションのDBに格納したいというケースも考えられますが、そういった際にリダイレクトされるURL(URI)を「承認済みのリダイレクト URI」に設定しておきます。
例えば、承認済みのリダイレクト URIを"http://localhost/oauth/"に設定しておくと、OAuthの認証が終わった後に"http://localhost/oauth/?code=xxxxxxxxxxxxxx"へリダイレクトされます。
これを利用してアプリケーションは"アプリケーションのURL/oauth/?code=[認証トークン]"というルーティングに処理を書いておくことで認証トークンを取得することができます。

あとはYouTube Data APIを有効にするだけなんですが、別記事に記載します。

Windows 10 Home (64bit)でDockerの環境を構築する

どうも、靖宗です。
Docker for Windowsが出現してからWindowsでDockerを利用するのが簡単になりました。
とはいえ自分の利用しているWindowsがHomeでDocker for Windowsが利用できない人もいるんじゃないでしょうか?ぼくもその一人です。
(アップグレードしろよという突っ込みは受け付けません)
どうやら広大なネットの海で調べたところ、Windows 10 HomeではDocker Toolboxで一応動かせるようなので、そちらのセットアップをしていきたいと思います。

基本的に下記の記事を参考にさせていただきました。

qiita.com

インストーラーを手に入れる

なんてことないのですが、Docker公式としてはあまりToolboxの方を利用して欲しくないのか分かりにくい所にインストーラーがある気がします。
DockerToolboxのリリース一覧から取得しろとのことです。

github.com

f:id:ysmn_deus:20190805123509p:plain
ここ

今回は「18.09.3」というバージョンをダウンロードしましたが、未来にこの記事を見ている人はより新しいバージョンのインストーラーがあると思います。
(上から見てって「Assets」というところにexeファイルがあるのを選んで下さい。) ダウンロードできたら基本的にはインストーラーに従って進めて行きます。

インストール

f:id:ysmn_deus:20190805123824p:plain

自分はこんなかんじですが、Gitがインストールされてない方などはチェックボックスがついてそうです。
要らないことないのでインストールしておくことをお勧めします。

f:id:ysmn_deus:20190805123936p:plain

自分はデスクトップが汚れるのは許せないので「Create a desktop shortcut」はチェックを外しておきます。
この辺はご自由にどうぞ。

DockerToolboxを起動

最後まで順調に進めば「Docker Quickstart Terminal」というショートカットが表示されると思います。
ダブルクリックで起動!

無事くじらAAが表示されればセットアップは完了です。

f:id:ysmn_deus:20190805124639p:plain

試しにコンテナを起動してみる

試しにニンジンXのコンテナを起動してみます。
Dockerの基本的な利用方法については割愛しますが、nginxというコンテナを起動します。

docker run --name ninjin -d -p 8080:80 nginx

f:id:ysmn_deus:20190805125219p:plain

基本的にインストールし終わってパスが通ってる状態であればDocker Quickstart Terminalのコンソールからコマンドを打たなくてもPowerShellやcmd.exeでdockerコマンドが通ると思います。

VirtualBox上のVMのポートフォワーディング

Docker for Windowsならたぶんここでブラウザに「http://localhost:8080」でnginxが表示されると思うのですが、今回はDockerToolboxを利用しているのでもう一手間要ります。
VirtualBoxに「default」という仮想マシンが作成されていると思いますが(環境によっては変わる?)、このVM上でDockerが動いています。

f:id:ysmn_deus:20190805131139p:plain

なので普通にコンテナを起動すると、ポートフォワーディングはdefaultという仮想マシンへ渡されます。
なので、なにも設定していないとWindows側からは見れません。

f:id:ysmn_deus:20190805131813p:plain

今回はnginxをdefaultというVMには8080に渡しているので、defaultからWindowsへ8080に渡す設定を追加します。
VirtualBox仮想マシンなのでVirtualBoxで設定します。

f:id:ysmn_deus:20190805132007p:plain

f:id:ysmn_deus:20190805132023p:plain

f:id:ysmn_deus:20190805132037p:plain

defaultのポートフォワーディングが設定できれば、Windowsのブラウザ上からもnginxへとアクセスできるようになります。

f:id:ysmn_deus:20190805132127p:plain

ブラウザ上で「localhost:8080」にアクセスすれば、nginxのデフォルト表記が確認できると思います。

f:id:ysmn_deus:20190805132202p:plain

FusionPCBで基板製作+部品実装(PCB+PCBA)を発注する一連の流れまとめ

f:id:ysmn_deus:20190612190424p:plain

どうも、靖宗です。
FusionPCBで基板製作から部品調達+部品実装までしてもらって楽しようという記事です。
防備録としての意味合いも込めて記事にしておきます。
部品を国内から送る方法は需要があれば別途記事にします。
※2019/06/12 現在のまとめです。ちょいちょい変わったりすると思うので、お気を付けて。
※2019/06/13 追記

準備

Gerberデータ作成

一応基板の設計は終わってるものとします。
Gerberデータの作成方法はFusionPCB公式サイトで詳しく記載がありますのでそちらをご覧下さい。
デザインルールなどをチェックするのをお忘れなく。

BOMデータ作成

基板のみ作りたいって人はこの項目を飛ばして貰って構いません。
実装の際に部品表が必要になりますので、そちらを作成していきます。
部品表のテンプレートがあるので、そちらをダウンロードして作成してください。
(リンク切れの場合はFusionPCBのサイトから直接ダウンロードしてください。)

Eagleを使っている方はPartlistなどを出力して部品表を作成するのが良いと思います。
一応PartlistからBOMの基礎となるデータを出力するPythonスクリプトを置いておきますので自己責任でご利用ください。

https://www.axfc.net/u/3985652&key=deus

実装情報(任意)

※この項目は現在調査中ですので参考程度でお願いします。

実装図などをアップロードできるようになったみたいなので、この辺の情報があるとFusionPCBの人達は楽かも。
連絡回数を少なくして迅速に実装してもらうなら準備しておいた方が良いかもしれません。
Eagleで回路を設計しているなら

  • ボード図(PDF, brdを印刷→PDFなどで対応、もしくはImageなど?)
  • 配置情報(Partlist)

ぐらいあると丁寧かも。

見積

Gerberデータアップロード

GerberデータとBOMデータができていればウェブ上から見積がすぐ取れます。
FusionPCBのPCBのページからGerberデータをアップロードします。

f:id:ysmn_deus:20190612184109p:plain

アップロードできれば、念のためガーバービューアで閲覧しておく事をオススメします。

f:id:ysmn_deus:20190612184350p:plain

基板の作成枚数が10以外の場合はパラメータのところで変更してください。

BOMデータアップロード

各パラメータを入力し終えたら「実装サービス」という欄をクリックし、実装情報やBOMデータなどをアップロードします。
実装枚数が10以外の場合は、「実装枚数」と書かれた箇所を変更してください。

f:id:ysmn_deus:20190612184654p:plain

BOMデータが問題無くアップロードできれば見積が完了します。

f:id:ysmn_deus:20190612185028p:plain

もしFusionPCB側でウェブ見積できない部品(実装実績がない部品)があれば、部品見積の一番上に表示が出てきますので、部品を調達するために必要な情報を「購入リンク」から入力しましょう。だいたいDigiKeyにある部品なら調達してくれるのでDigiKeyのリンクを張っておけば問題無いかと思います。

f:id:ysmn_deus:20190612185621p:plain

部品を国内から送るとき

この場合はカスタマーサポートへの連絡が必須です。
(調達価格と実装価格が変わるので、特殊なBOMを作って貰うのにメールでのやりとりが必要です。)
迅速丁寧に対応して貰えるので恐れずに fusion.jp@seeed.cc へ連絡を送りましょう。
この辺はまた別途詳細をまとめます。

カートに入れる+購入手続き

上記までが問題無く終わっていれば上部に価格が反映されているので、「カートに追加」ボタンでカートに追加します。
カートに追加した以降は、日本語で案内がでているので大丈夫だと思います。
PayPal支払とクレジットカード支払が使えたと思います。)

決済後

担当者とのやりとり

購入手続きが完了すると、担当者から連絡があると思います。
日本語で丁寧に対応してくださるので、指示に従って必要な追加データや修正点などあればメールベースでやりとりすることになります。

納期

おおよそ25日前後と考えておいた方が良さそうです。
これは勝手なイメージですが、おおよそ基板製造に2営業日程度、実装に3営業日程度なのですが、部品調達にどうしても時間がかかっている(税関などの影響?最近は厳しいみたいです)ようです。
なので、もし実装を急いでいる場合は事前に部品を調達しておいて、国内から部品を発送したほうが実装期間の短縮には繋がるかもしれません。その分手間は増えますが。
部品を送付する際の手順はまた気が向けば。

Phoenix入門 (第15章 Custom Errors)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
とりあえず主な項目はコレで最後です。
残るはテストの話とデプロイの話が残っていますが、残ってる話の方が重要そうな気がしますね。

今回はたぶん短めです。
PhoenixというかElixirの例外処理のお話に近いかも。

Custom Errors

Viewsの項目でErrorViewに関しては一回やりました。
基本的に400とか500のエラーはテンプレート(404.html.eexとか)を用意しておけばPhoenixのコントローラがよしなにしてくれます。
ビューのエラーはこのErrorViewで受けるとして、内部処理のエラーはPhoenixでは例外処理などが少々利用されているようです。

Custom Errors

Elixirの標準機能として、例外を定義するためのdefexceptionを利用してエラー処理を行っています。
エラーを実装したいモジュール内部にモジュールを更に定義し、内部でdefexceptionを定義するように実装するそうです。
router.exが利用しているPhoenix.Routerの内部実装が例としてドキュメントに記載されています。

defmodule Phoenix.Router do
  defmodule NoRouteError do
    @moduledoc """
    Exception raised when no route is found.
    """
    defexception plug_status: 404, message: "no route found", conn: nil, router: nil

    def exception(opts) do
      conn   = Keyword.fetch!(opts, :conn)
      router = Keyword.fetch!(opts, :router)
      path   = "/" <> Enum.join(conn.path_info, "/")

      %NoRouteError{message: "no route found for #{conn.method} #{path} (#{inspect router})",
      conn: conn, router: router}
    end
  end
...
end

Elixirの例外処理の記事では取り扱ってなかったのですが、defexceptionを宣言したモジュール内でdef exceptionとして関数を実装すると、エラー処理の関数になるようです。
(詳細はElixirのExceptionのビヘイビアを確認)

Plugを実装する際にはPlug.Exceptionというプロトコルがあるようなのでそちらを利用するのが良いかと思います。
例なども示されてますが、特殊ケースな気がするので、また利用機会があった際に追記したいと思います。

今回は短めですがこんなもんで。

Phoenix入門 (第14章 Mix Tasks その2)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
引き続きMix Tasksの項目を見ていきます。
今回はEcto関連のコマンドから。

Ecto Specific Mix Tasks

このEcto周辺のコマンドですが、当たり前ですが--no-ectoとかしてない場合が対象です。

mix ecto.create

データベースの作成を行うコマンドです。デフォルトならconfigやlib/hello/repo.exに記載したデータベースに作成するはずです。
デフォルトでいいなら引数は要らないのでシンプルです。

$ mix ecto.create
The database for Hello.Repo has been created.

別の名前で定義してたりするなら、-rオプションでモジュール名を指定します。

$ mix ecto.create -r OurCustom.Repo
The database for OurCustom.Repo has been created.

PostgreSQLのユーザー周りの説明は割愛します。
(要望があれば書きますが)

ecto.drop

純粋にecto.createの逆だと思えば良さそうで、指定したデータベースからデータを消去します。
使い方もほぼcreateと同じで、デフォルトなら引数は不要。

$ mix ecto.drop
The database for Hello.Repo has been dropped.

-rでモジュール名を指定することも可能。

$ mix ecto.drop -r OurCustom.Repo
The database for OurCustom.Repo has been dropped.

mix ecto.gen.repo

データストアが1個じゃないケースもあると思います。そういうときにこのecto.gen.repoで新しいリポジトリを作成できるそうです。
たとえば、デフォルトのリポジトリHello.Repoで、別のリポジトリOurCustom.Repoであるばあい、下記のようにして作成するようです。

$ mix ecto.gen.repo -r OurCustom.Repo
* creating lib/our_custom
* creating lib/our_custom/repo.ex
* updating config/config.exs
Don't forget to add your new repo to your supervision tree
(typically in lib/hello.ex):

worker(OurCustom.Repo, [])

* updating config/config.exsとあるので、configも修正されています。

...
config :hello, OurCustom.Repo,
database: "hello_repo",
username: "user",
password: "pass",
hostname: "localhost"
...

これは各環境で合わせて下さい。
場合によってはdev.exsprod.exsに記載してもいいかもしれません。

ファイルの生成などは自動でやってくれましたが、リポジトリのワーカーをSupervisorで管理する必要があります。
ドキュメントではlib/hello.exを編集、と書いてありますがたぶんlib/hello/application.exの間違いです。

...
children = [
  # Start the Ecto repository
  Hello.Repo,
  # Start the endpoint when the application starts
  HelloWeb.Endpoint,
  # Starts a worker by calling: Hello.Worker.start_link(arg)
  # {Hello.Worker, arg},
  # Here you could define other workers and supervisors as children
  OurCustom.Repo
]
...

これで大丈夫そうです。

mix ecto.gen.migration

マイグレーションする準備をするのがecto.gen.migrationです。こういうフレームワークにはよくある奴だと思います。
Contextの章でもいじったファイルを作成するやつで、マイグレーションも実際の所はElixirのスクリプトを実行して行っていますのでそのスクリプトを生成するコマンドということです。
基本的に何かのコマンドで自動生成されるのでそこまで出番があるように思えませんが、スキームの変更などをした場合には手動で作成する必要がありそうです。
コマンドにマイグレーションファイル名を引数として渡します。

mix ecto.gen.migration add_comments_table
* creating priv/repo/migrations
* creating priv/repo/migrations/20150318001628_add_comments_table.exs

マイグレーション用のファイルがpriv/repo/migrationsに作成されます。おそらくタイムスタンプ_指定した名前.exsというファイルが作成されます。
ファイルの中身は下記のようになっているそうです。

defmodule Hello.Repo.Migrations.AddCommentsTable do
  use Ecto.Migration

  def change do
  end
end

基本的にコマンドで生成した場合は特に何も書かれていないはずです。
このchange/0に記載された仕様をもとに、データベースを編集したりロールバックしたりします。
たとえば、commentsというテーブルを作成し、bodyword_countというフィールド+タイムスタンプがあるスキームを追加したとします。
このとき、マイグレーションファイルは下記のように編集します。

...
def change do
  create table(:comments) do
    add :body,       :string
    add :word_count, :integer
    timestamps()
  end
end
...

ちなみに、今はcreateを使いましたが、スキームを追加するのではなく変更する場合などはalterを利用します。

基本的な使い方は上記の通りですが、デフォルトではないリポジトリを対象としたマイグレーションのファイルは、ecto.createなどと同様-rオプションでリポジトリ名を指定してやる必要があります。

$ mix ecto.gen.migration -r OurCustom.Repo add_users
* creating priv/repo/migrations
* creating priv/repo/migrations/20150318172927_add_users.exs

mix ecto.migrate

マイグレーションファイルを作成したらすることは決まっています。マイグレーションです。
何も考えずにmix ecto.migrateすることが多いのではないでしょうか。

$ mix ecto.migrate
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 forward
[info] create table comments
[info] == Migrated in 0.1s

そこまで開発にかかわってこない可能性がありますが、mix ecto.migrateをするとデータベース上のschema_migrationsというテーブルにマイグレーション日時のタイムスタンプが作成されるようです。

hello_dev=# select * from schema_migrations;
version     |     inserted_at
----------------+---------------------
20150317170448 | 2015-03-17 21:07:26
20150318001628 | 2015-03-18 01:45:00
(2 rows)

あとで見てみますが、ecto.rollbackなどでロールバックするときはこの辺の情報を利用してロールバックするようです。

基本的に存在している全てのマイグレーションファイルを実行してマイグレートするコマンドですが、一応実行する個数を指定出来るようです(順番はタイムスタンプ依存?)
-n--stepのオプションを使用して実行するマイグレーションの個数を指定します。

$ mix ecto.migrate -n 2
[info] == Running Hello.Repo.Migrations.CreatePost.change/0 forward
[info] create table posts
[info] == Migrated in 0.0s
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 forward
[info] create table comments
[info] == Migrated in 0.0s

一応下記も同様

$ mix ecto.migrate --step 2

-vのオプションを使用すると、指定したタイムスタンプのマイグレーションファイルのみを実行できるようです。

$ mix ecto.migrate -v 20150317170448

--toも同じ働き。

$ mix ecto.migrate --to 20150317170448

mix ecto.rollback

ロールバック、詰まり先祖返りできます。スキーム変更したけどやり直したい!とかいう場合に活用できそうです。
それもコレもマイグレーションファイルにきっちり変更の仕様が記載されているおかげでしょう。

$ mix ecto.rollback
[info] == Running Hello.Repo.Migrations.AddCommentsTable.change/0 backward
[info] drop table comments
[info] == Migrated in 0.0s

何も指定しなければ全部戻ってしまいそうです。

rollbackmigrationと同様のオプションが取れます。例えば-nでn個マイグレーションファイルを遡る、などでしょうか。
こちらはオプションを大いに活用しそうです。

Creating Our Own Mix Tasks

おおよそ見ていったmixコマンドで事足りそうですが、「もうちょっとやってくれよ」「ここ毎回同じ事してる」という場合はかゆいところに手が届くmixコマンドが欲しくなると思います。
そういった需要に応えるがごとく、mixコマンドを作成できるそうです。

まずはlib/フォルダにmix/tasksディレクトリを作成していきます。

$ mkdir -p lib/mix/tasks

試しにhello.greeting.exというファイルを作成するとします。

defmodule Mix.Tasks.Hello.Greeting do
  use Mix.Task

  @shortdoc "Sends a greeting to us from Hello Phoenix"

  @moduledoc """
    This is where we would put any long form documentation or doctests.
  """

  def run(_args) do
    Mix.shell.info("Greetings from the Hello Phoenix Application!")
  end

  # We can define other functions as needed here.
end

慣習なのか知りませんが、基本的にElixirはディレクトリ名をモジュール名に適応するようです。
(そもそもコンパイル時にチェックされる?すみません、この辺は勉強不足です。)
なので、lib/ディレクトリ以下のmix/tasksディレクトリ下にあるhello.greeting.exなので、モジュール名はMix.Tasks.Hello.Greetingになります。

お次にuse Mix.Taskが宣言されています。たぶんこれでmixのコマンドとして機能するのでしょう。

@shortdocのモジュール属性ですが、これはmix help字に表示される説明文です。

@moduledocはモジュールのドキュメントです。コレに関しては割愛します。

run/1mixで呼ばれたときに実行される関数です。ここではmixで実行されたときにシェルに文字列がプリントされるだけのようです。

以上の仕様で、利用する為にはまずコンパイルします。

$ mix compile
Compiled lib/tasks/hello.greeting.ex
Generated hello.app

コンパイルすると、mix helpにも表示されるようです。

$ mix help | grep hello
mix hello.greeting # Sends a greeting to us from Hello Phoenix

実際に利用する場合は、ファイル名で実行するようです。

$ mix hello.greeting
Greetings from the Hello Phoenix Application!

run/1では今のところ文字列を出力するMix.shell.info/1しか利用していませんが、アプリケーションを実行するにはMix.Task.run/1を利用すればいいようです。

...
  def run(_args) do
    Mix.Task.run("app.start")
    Mix.shell.info("Now I have access to Repo and other goodies!")
  end
...

もうちょっといろんな機能を実装するにはMixの仕様をより深掘りする必要がありそうです・・・

Phoenix入門 (第14章 Mix Tasks その1)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
今回はMix Tasksということでmix phx.hogehogeのコマンドあたりの説明でしょうか。

Mix Tasks

Phoenix Specific Mix Tasks

Phoenixで使いそうなコマンド一覧があります。

mix local.phx          # Updates the Phoenix project generator locally
mix phx                # Prints Phoenix help information
mix phx.digest         # Digests and compresses static files
mix phx.digest.clean   # Removes old versions of static assets.
mix phx.gen.cert       # Generates a self-signed certificate for HTTPS testing
mix phx.gen.channel    # Generates a Phoenix channel
mix phx.gen.context    # Generates a context with functions around an Ecto schema
mix phx.gen.embedded   # Generates an embedded Ecto schema file
mix phx.gen.html       # Generates controller, views, and context for an HTML resource
mix phx.gen.json       # Generates controller, views, and context for a JSON resource
mix phx.gen.presence   # Generates a Presence tracker
mix phx.gen.schema     # Generates an Ecto schema and migration file
mix phx.gen.secret     # Generates a secret
mix phx.new            # Creates a new Phoenix application
mix phx.new.ecto       # Creates a new Ecto project within an umbrella project
mix phx.new.web        # Creates a new Phoenix web project within an umbrella project
mix phx.routes         # Prints all routes
mix phx.server         # Starts applications and their servers

細かい機能はPhoenix公式ドキュメントの「MIX TASKS」を見れば良さそうです。
幾つか簡単に取り上げてくれているので、一応目を通しておきます。

mix phx.new

言わずと知れたPhoenixプロジェクトを生成する際に使うコマンドです。
--no-ecto--no-webpackというオプションをつける事でEctoの実装を無くしたり、Webpackの実装を無くしたりできるそうですが、基本的には必要なんじゃないでしょうか。
APIサーバーとして機能するときはWebpackは不要?)

実際に生成するプロジェクト名はスネークケースがいいっぽいです。

> mix phx.new task_tester

相対パスでも絶対パスでもヨシ。

> mix phx.new ../task_tester
> mix phx.new /Users/me/work/task_tester

mix phx.newでプロジェクトを生成するとアプリケーション名はプロジェクト名(上記だとtask_tester)になります。
もしアプリケーション名を変更したい場合はmix phx.new task_tester --app hello--appオプションを利用するそうです。

mix.exsでアプリケーション名が変更されています。

defmodule Hello.MixProject do
  use Mix.Project

  def project do
    [app: :hello,
     version: "0.1.0",
...

このアプリケーション名を変更するとモジュール名などの接頭辞が変わるようです。

defmodule HelloWeb.PageController do
  use HelloWeb, :controller
...

--appオプションでhelloを設定したのでモジュール名がHelloから始まってます。
lib/フォルダ以下のディレクトリやファイル名なども変わります。

基本は--appでいいような気はしますが、「モジュール名の接頭辞だけ変えたい!」という場合には--moduleオプションで指定出来るようです。

$  mix phx.new task_tester --module Hello
* creating task_tester/config/config.exs
* creating task_tester/config/dev.exs
* creating task_tester/config/prod.exs
* creating task_tester/config/prod.secret.exs
* creating task_tester/config/test.exs
* creating task_tester/lib/task_tester/application.ex
* creating task_tester/lib/task_tester.ex
* creating task_tester/lib/task_tester_web/channels/user_socket.ex
* creating task_tester/lib/task_tester_web/views/error_helpers.ex
* creating task_tester/lib/task_tester_web/views/error_view.ex
* creating task_tester/lib/task_tester_web/endpoint.ex
* creating task_tester/lib/task_tester_web/router.ex
* creating task_tester/lib/task_tester_web.ex
* creating task_tester/mix.exs
* creating task_tester/README.md
* creating task_tester/.gitignore
* creating task_tester/test/support/channel_case.ex
* creating task_tester/test/support/conn_case.ex
* creating task_tester/test/test_helper.exs
* creating task_tester/test/task_tester_web/views/error_view_test.exs
* creating task_tester/lib/task_tester_web/gettext.ex
* creating task_tester/priv/gettext/en/LC_MESSAGES/errors.po
* creating task_tester/priv/gettext/errors.pot
* creating task_tester/lib/task_tester/repo.ex

mix.exsは下のようなかんじ。

defmodule Hello.MixProject do
  use Mix.Project

  def project do
    [app: :task_tester,
...

アプリケーション名はtask_testerのままで、接頭辞のみがHelloとなっています。
ややこしいことになりそうなので、変えたいなら--appを使った方が良さそうです。

mix phx.gen.html

これはContextあたりで使いました。HTMLでのresourcerouter.exresource指定する奴)を生成する際に便利なコマンドです。
Webレイヤーの実装(コントローラ、ビュー、テンプレート)はもちろん、EctoのマイグレーションファイルやContextも生成してくれます。

このコマンドは引数が多くてややこしいですが、その中身は至ってシンプルで
mix phx.gen.html Context名 スキーマ名 リソース名 スキーマのリスト(項目名:型)
です。

$ mix phx.gen.html Blog Post posts body:string word_count:integer
* creating lib/hello_web/controllers/post_controller.ex
* creating lib/hello_web/templates/post/edit.html.eex
* creating lib/hello_web/templates/post/form.html.eex
* creating lib/hello_web/templates/post/index.html.eex
* creating lib/hello_web/templates/post/new.html.eex
* creating lib/hello_web/templates/post/show.html.eex
* creating lib/hello_web/views/post_view.ex
* creating test/hello_web/controllers/post_controller_test.exs
* creating lib/hello/blog/post.ex
* creating priv/repo/migrations/20170906150129_create_posts.exs
* creating lib/hello/blog/blog.ex
* injecting lib/hello/blog/blog.ex
* creating test/hello/blog/blog_test.exs
* injecting test/hello/blog/blog_test.exs

mix系のコマンドに一般的に言えることですが、次に何やったらいいか注釈がでてくれます。

Add the resource to your browser scope in lib/hello_web/router.ex:

    resources "/posts", PostController

Remember to update your repository by running migrations:

    $ mix ecto.migrate

ルーティングの設定とマイグレーションしろよ!と出ています。基本これだけでリソースが作成できます。

大体無いとは思うんですが、Contextいらないよ!って時(Bootstrapのテストなど、なんらかのテストに使う?)は--no-contextというオプションがあるそうです。
活用法があまり思い浮かばないので省略。 同様に--no-schemaオプションもあり。

mix phx.gen.json

こちらは使ったことはないですが、見た目からして上記のJSON版といったところでしょうか。

$ mix phx.gen.json Blog Post posts title:string content:string
* creating lib/hello_web/controllers/post_controller.ex
* creating lib/hello_web/views/post_view.ex
* creating test/hello_web/controllers/post_controller_test.exs
* creating lib/hello_web/views/changeset_view.ex
* creating lib/hello_web/controllers/fallback_controller.ex
* creating lib/hello/blog/post.ex
* creating priv/repo/migrations/20170906153323_create_posts.exs
* creating lib/hello/blog/blog.ex
* injecting lib/hello/blog/blog.ex
* creating test/hello/blog/blog_test.exs
* injecting test/hello/blog/blog_test.exs

コマンドの仕様もほぼhtmlと変わらないようです。
コマンド実行後の注釈はちょっと変わってきています。

Add the resource to your :api scope in lib/hello_web/router.ex:

    resources "/posts", PostController, except: [:new, :edit]


Remember to update your repository by running migrations:

    $ mix ecto.migrate

:new:editのアクションを除外しています。
JSONなので新規生成画面と編集画面がないためでしょう。HTTPリクエストなどで直接:create:updateを実行するということでしょう。

htmlと同様に--no-context--no-schemaオプションもあり。

mix phx.gen.context

Contextの章でAccountsにCredentialを追加するときに使いました。
追加するように使った場合はinjecting lib/hello/accounts.exというように既存のContextに追記する形で生成されましたが、Contextのみ生成したい場合にも利用できるようです。
主な使用方法はhtmljsonの時と同じ。

$ mix phx.gen.context Accounts User users name:string age:integer
* creating lib/hello/accounts/user.ex
* creating priv/repo/migrations/20170906161158_create_users.exs
* creating lib/hello/accounts/accounts.ex
* injecting lib/hello/accounts/accounts.ex
* creating test/hello/accounts/accounts_test.exs
* injecting test/hello/accounts/accounts_test.exs

mix phx.gen.schema

HTMLやJSONのリソースも作らず、Contextも不要というレアケース用?でもコマンドが用意されてるってことは必要なタイミングがあるのかも。
コマンド引数はhtmljsonからContext名を省略したようなかんじ。

$ mix phx.gen.schema Accounts.Credential credentials email:string:unique user_id:references:users
* creating lib/hello/accounts/credential.ex
* creating priv/repo/migrations/20170906162013_create_credentials.exs

mix phx.gen.channel

Channelを生成するためのコマンド。Channelの章では手動で作成しましたが、コマンドでも生成してくれるようです。
引数は純粋にチャンネル名のみ。

$ mix phx.gen.channel Room
* creating lib/hello_web/channels/room_channel.ex
* creating test/hello_web/channels/room_channel_test.exs

ちゃんとメッセージも出してくれます。

Add the channel to your `lib/hello_web/channels/user_socket.ex` handler, for example:

    channel "rooms:lobby", HelloWeb.RoomChannel

mix phx.gen.presence

これはPresenceの章で使いました。
一応モジュール名を指定できますが、省略すればPresenceで作成されます。

$ mix phx.gen.presence
* creating lib/hello_web/channels/presence.ex

こちらもきちんと注釈を付けてくれます。

Add your new module to your supervision tree,
in lib/hello/application.ex:

    children = [
      ...
      HelloWeb.Presence
    ]

You're all set! See the Phoenix.Presence docs for more details:
http://hexdocs.pm/phoenix/Phoenix.Presence.html

mix phx.routes

Routingの章で利用しました。
コマンドを実行すればルーティングルールが表示されます。

$ mix phx.routes
page_path  GET  /  TaskTester.PageController.index/2

ルーティングファイルが2個以上あるときは、ルーティングファイル名を指定するとソイツだけみれるようです。

$ mix phx.routes TaskTesterWeb.Router
page_path  GET  /  TaskTesterWeb.PageController.index/2

mix phx.server

サーバーを立ち上げるコマンド。DoesNotExistオプションとかもあるけどよくわかんない。(本来は「DoesNotExist」が必要だけど省略されてる?)
iexで立ち上げたい時はiex -S mix phx.serverを実行してやればよいそうで。

$ iex -S mix phx.server
Erlang/OTP 17 [erts-6.4] [source] [64-bit] [smp:8:8] [async-threads:10] [hipe] [kernel-poll:false] [dtrace]

[info] Running TaskTesterWeb.Endpoint with Cowboy on port 4000 (http)
Interactive Elixir (1.0.4) - press Ctrl+C to exit (type h() ENTER for help)
iex(1)>

これ地味に便利そうですね。

mix phx.digest

静的ファイル(static assets)の名前にダイジェスト(MD5)を付与し、圧縮するコマンドだそうです。
機能的にほぼデプロイ用(改ざん防止、転送量軽減)と考えて問題無さそうです。

assets/ディレクトリにあるファイルに処理を行い、デフォルトではpriv/staticにコピーするようです。
出力されるファイルの場所や処理するファイルの場所を指定したいときは引数で調整可能。

$ mix phx.digest priv/static -o www/public
Check your digested files at 'www/public'.

コンフィグに:を指定すると圧縮するファイルの対象を変更できるようです。

config :phoenix, :gzippable_exts, ~w(.js .css)

役割的にconfig/config.exsあたりにでも書けば良いのでしょうか。

デプロイは別の章でやるので、そのときにもう一度確認します。

飛ばし飛ばしやってるつもりが結構長くなったのでまた次回!

Phoenix入門 (第13章 Contexts その5)

f:id:ysmn_deus:20190219130922p:plain

どうも、靖宗です。
流石に今回で終わらす!

Cross-context data

CMSとAccountsというContextが介在していますが、場合によっては1個のContextで表現した方がシンプルになり得ます。(規模が小さいときとか)
この辺は各自の判断だとは思うんですが、個人的には今回みたいに細かくContextを作成し、繋がりを最小限にとどめる設計が拡張しやすいんじゃないかとは思います。

依存関係の編集(CMS

なにはともあれ、CMSのContext内でもまだ依存関係を追記していません。その辺を修正していきます。
まずはlib/hello/cms/page.exから。

defmodule Hello.CMS.Page do
  use Ecto.Schema
  import Ecto.Changeset
  alias Hello.CMS.Author # 追記

  schema "pages" do
    field :body, :string
    field :title, :string
    field :views, :integer
    belongs_to :author, Author # 追記
...

PageはAuthorに対して従属関係なのでこれで良さそうです。
お次はlib/hello/cms/author.ex

defmodule Hello.CMS.Author do
  use Ecto.Schema
  import Ecto.Changeset
  alias Hello.CMS.Page # 追記

  schema "authors" do
    field :bio, :string
    field :genre, :string
    field :role, :string
#    field :user_id, :id #消去
    has_many :pages, Page # 追記
    belongs_to :user, Hello.Accounts.User # 追記
...

pagesに対してAuthorは一対多です。なのでhas_manyを指定します。
userに対しては一対一の従属関係なのでbelongs_toです。
belongs_toの項目があるので:user_idの行は不要です。

ContextのAPI編集

preloadの追加

お次にContextのlist_pagesとかを編集していきます。
Accounts.Credentialを追加したときのように、Pageがロードされる前に従属関係であるAuthorをpreloadしておく必要があります。
lib/hello/cms.exを編集します。

defmodule Hello.CMS do

...

  alias Hello.CMS.{Page, Author}
  alias Hello.Accounts

...

  def list_pages do
    Page# Repoにしててエラーが起こった。詳細はもうちょっと先の項目で。
    |> Repo.all()
    |> Repo.preload(author: [user: :credential])
  end

...

  def get_page!(id) do
    Page
    |> Repo.get!(id)
    |> Repo.preload(author: [user: :credential])
  end

...

#  alias Hello.CMS.Author # 消去(上でaliasしてるので)

...

  def get_author!(id) do
    Author
    |> Repo.get!(id)
    |> Repo.preload(user: :credential)
  end

...

どうやらRepo.preload(author: [user: :credential])でAuthor、User、Credentialがロードされるようです。

ページを生成するときや編集する時のAuthorの取り扱い

先ほどはデータアクセス(読み込み)の際に必要なpreloadを追加しました。
ではここでは書き込みの際に必要な処理を追記していきます。
同様にlib/hello/cms.exを編集していきます。

...

  def create_page(%Author{} = author, attrs \\ %{}) do
    %Page{}
    |> Page.changeset(attrs)
    |> Ecto.Changeset.put_change(:author_id, author.id)
    |> Repo.insert()
  end

  def ensure_author_exists(%Accounts.User{} = user) do
    %Author{user_id: user.id}
    |> Ecto.Changeset.change()
    |> Ecto.Changeset.unique_constraint(:user_id)
    |> Repo.insert()
    |> handle_existing_author()
  end
  defp handle_existing_author({:ok, author}), do: author
  defp handle_existing_author({:error, changeset}) do
    Repo.get_by!(Author, user_id: changeset.data.user_id)
  end

...

まずはcreate_pageの変更点ですが、引数にAuthorの構造体が必要になりました。
Ecto.Changeset.put_change(:author_id, author.id)でPageの構造体にAuthorのIDを追加し、リポジトリに保存という流れです。

次にensure_author_exists以降の箇所ですが、まず前提を整理します。
このCMSはページを作る前には作成者(Author)の情報が必要(生成するページ情報に紐付くので)です。ですので、もしページを作ろうとしているユーザーがAuthorに登録されていなければ登録するというような処理が必要になります。
フォームを用意してバリデーションではじくという方針も考えられそうですがいちいち手間ですし、バックエンドで処理するべきです。ですので、そういった処理用の関数ensure_author_existsを作成しているのでしょう。

内容は、アカウント情報を渡してAuthorとしてリポジトリに保存し、その後handle_exisiting_authorリポジトリに挿入できたか否かをチェックしているようです。
Ecto.Changeset.unique_constraint(:user_id)でAuthor中に同じuser_idのAuthorがいないかチェックする情報を乗せて、Repo.insert()の際に判断されるようです。既に存在している場合はRepo.get_by!でAuthorの情報を返すようになっているようです。

以上でCMSのContextはだいたいおっけーのはずです。

Webレイヤーの実装

お次はCMSのページを作成したりする画面を編集していきます。
ログインできるかどうかは前回確認しましたが、ページを生成するあたりはスルーしました。(リストは表示されてましたが。)
ページを生成するにあたっては、上の項目で作成したensure_author_existsを利用してユーザーがAuthorに登録されているかどうか、されていなければ登録するPlugを作成してこのPlugを通過するように編集するのが良さそうです。

作成者関係のPlugを作成する

これはコントローラ内に実装します。
lib/hello_web/controllers/cms/page_controller.exを編集します。

defmodule HelloWeb.CMS.PageController do
  use HelloWeb, :controller

  alias Hello.CMS
  alias Hello.CMS.Page

  plug :require_existing_author # 追加
  plug :authorize_page when action in [:edit, :update, :delete] # 追加

...

# 以下追加
  defp require_existing_author(conn, _) do
    author = CMS.ensure_author_exists(conn.assigns.current_user)
    assign(conn, :current_author, author)
  end

  defp authorize_page(conn, _) do
    page = CMS.get_page!(conn.params["id"])

    if conn.assigns.current_author.id == page.author_id do
      assign(conn, :page, page)
    else
      conn
      |> put_flash(:error, "You can't modify that page")
      |> redirect(to: Routes.cms_page_path(conn, :index))
      |> halt()
    end
  end # 忘れてた
end

require_existing_authorauthorize_pageという2種類のPlugを作成しました。
require_existing_authorは分かりやすく、ensure_author_existsを利用して接続情報にAuthorの情報を載せています。(無ければ作ってくれる)
authorize_pageは対象のページの編集者と閲覧者が一致する場合のみ正常に動作し、一致しない場合はフラッシュメッセージを送出してPlugの処理を止めています。このプラグは編集と削除の時だけでいいのでwhen action in~~のオプションを使用しております。
ついでにこのプラグ内でページのIDを取得し、ページ情報も接続情報に登録(assign)しています。

Plugや他の変更に合わせてコントローラを修正する

これらを踏まえてcreateeditupdatedeleteを編集します。

...

  def create(conn, %{"page" => page_params}) do
    # create_pageは上の項目でAuthorの情報が必要になった
    # あと, page_paramsを忘れていた
    case CMS.create_page(conn.assigns.current_author, page_params) do
      {:ok, page} ->
        conn
        |> put_flash(:info, "Page created successfully.")
        |> redirect(to: Routes.cms_page_path(conn, :show, page))

      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

...

  # idの情報はconnに載ってるので冗長
  def edit(conn, _) do
    # connからページ情報を取り出す
    changeset = CMS.change_page(conn.assigns.page)
    # ページ情報はconnに載ってるので冗長
    render(conn, "edit.html", changeset: changeset)
  end

...

  # idの情報はconnに載ってるので冗長
  def update(conn, %{"page" => page_params}) do
    # connからページ情報を取り出す
    case CMS.update_page(conn.assigns.page, page_params) do
      {:ok, page} ->
        conn
        |> put_flash(:info, "Page updated successfully.")
        |> redirect(to: Routes.cms_page_path(conn, :show, page))

      {:error, %Ecto.Changeset{} = changeset} ->
        # ページ情報はconnに載ってるので冗長
        render(conn, "edit.html", changeset: changeset)
    end
  end

  # idの情報はconnに載ってるので冗長
  def delete(conn, _) do
    # connからページ情報を取り出す
    {:ok, _page} = CMS.delete_page(conn.assigns.page)

    conn
    |> put_flash(:info, "Page deleted successfully.")
    |> redirect(to: Routes.cms_page_path(conn, :index))
  end

...

大量に修正点がありますが、主にauthorize_pageで既にページ情報はPlug.Connにロードしてるのでそこから取り出すように変更したところです。

作成者名を表示する

ついでにページの表示に作成者を表示するように修正しておきます。
まずはビューのlib/hello_web/views/cms/page_view.exを編集します。

defmodule HelloWeb.CMS.PageView do
  use HelloWeb, :view

  alias Hello.CMS

  def author_name(%CMS.Page{author: author}) do
    author.user.name
  end
end

ページ情報に基づき、作成者の名前情報を返す関数を作成しました。
これをテンプレートで呼びます。lib/hello_web/templates/cms/page/show.html.eexを編集します。

...

  <li>
    <strong>Views:</strong>
    <%= @page.views %>
  </li>

  <li>
    <strong>Author:</strong>
    <%= author_name(@page) %>
  </li>

</ul>

...

他の項目と並べるように作成者の表示を追加しました。これでOKな筈です。

mix phx.serverで試す(間違い探し)

それではPageは閲覧数以外もう大丈夫な筈なのでチェックしてみます。
mix phx.serverで実行します。

PS \hello> mix phx.server
Compiling 2 files (.ex)

== Compilation error in file lib/hello_web/controllers/cms/page_controller.ex ==
** (TokenMissingError) lib/hello_web/controllers/cms/page_controller.ex:79: missing terminator: end (for "do" starting at line 1)
    (elixir) lib/kernel/parallel_compiler.ex:208: anonymous fn/4 in Kernel.ParallelCompiler.spawn_workers/6

lib/hello_web/controllers/cms/page_controller.exの最後の箇所でエラーが出ています。
end忘れでした。再実行。
サーバーは立ち上がったのでhttp://localhost:4000/cms/pagesにアクセス。

Request: GET /cms/pages
** (exit) an exception was raised:
    ** (Protocol.UndefinedError) protocol Ecto.Queryable not implemented for Hello.Repo, the
given module does not provide a schema. This protocol is implemented for: Atom, BitString, Ecto.Query, Ecto.SubQuery, Tuple
        (ecto) lib/ecto/queryable.ex:40: Ecto.Queryable.Atom.to_query/1
        (ecto) lib/ecto/repo/queryable.ex:14: Ecto.Repo.Queryable.all/3
        (hello) lib/hello/cms.ex:23: Hello.CMS.list_pages/0
...

('ω')。o(????????????)
Ecto絡みということはWebレイヤー側の実装ミスでは無い筈。
Context周りを確認します。list_pagesって書いてあるしそのへん?

...

  def list_pages do
    Repo # ←Pageやんけ・・・
    |> Repo.all()
    |> Repo.preload(author: [user: :credential])
  end

...

寝てたんかな。PageをRepoとタイポ。
修正して再アクセス。

f:id:ysmn_deus:20190404125044p:plain

表示はできたけどセッションが生きてる?
ログアウトしたらCSRFと勘違いされたので、サーバーを再起動。
の後再ログイン。

f:id:ysmn_deus:20190404125339p:plain

作成ページは問題無さそう。作成してみる。

f:id:ysmn_deus:20190404125445p:plain

タイトルと本文ちゃんと入力したのに怒られた・・・
まずはコントローラーを疑ってみる。

...

  def create(conn, %{"page" => page_params}) do
    # , page_params ないやん ↓
    case CMS.create_page(conn.assigns.current_author) do
      {:ok, page} ->

...

そらそうなるわ。修正してもう一度。

f:id:ysmn_deus:20190404130230p:plain

こんどはうまくいった!
一応別のユーザーを作成して編集できないかチェック。

f:id:ysmn_deus:20190404130546p:plain

authorize_pageのPlugもよさそうです。

Adding CMS functions

AccountsのContextにauthenticate_by_email_password/2という関数を作成して機能を拡張したようにCMSにも機能を追加で来ます。Contextを使って閲覧数をカウントアップする機能を追加していきます。

機能の仕様を考える

編集したときと同じ様にCMS.update_pageで実装するのは色々と問題があります。
まず第一に競合が起こりやすくなります。PV数がかなり少ないようなショボいケースであればほぼ問題無いのですが、複数人が同時にアクセスした場合にカウントが正しくないケースが考えられます。
例えば

  1. User 1がカウント13のページをロードする
  2. User 1がページのカウントを14にする
  3. User 2がカウント14のページをロードする
  4. User 2がページのカウントを15にする

こうなれば良いんですが、同時アクセスなどがある場合下記が考えられます。

  1. User 1がカウント13のページをロードする
  2. User 2がカウント13のページをロードする
  3. User 1がページのカウントを14にする
  4. User 2がカウント14のページをロードする
  5. User 2がページのカウントを14にする

ではどのような仕様が望ましいか。
ページ情報のロードの際にインクリメントして呼び出されるのが望ましいです。つまり

page = CMS.inc_page_views(page)

となるようなinc_page_views/1を定義して、Page情報取得の箇所にパイプラインとして渡せば綺麗におさまりそうです。

CMSに実装する

それではCMSのContextに実装していきます。
lib/hello/cms.exを編集します。

...

  def inc_page_views(%Page{} = page) do
    {1, [%Page{views: views}]} =
      Repo.update_all(
        from(p in Page, where: p.id == ^page.id),
        [inc: [views: 1]], returning: [:views])
  
    put_in(page.views, views)
  end

...

場所はどこでも良いと思いますが、一応Context内の整理を考慮してchange_page関数の次に記載しました。
Ectoのクエリを詳しく見る必要がありそうですが、Repo.update_allの箇所はfrom(p in Page, where: p.id == ^page.id)で該当ページをヒットさせ、[inc: [views: 1]]でヒットしたPageのviewsを1インクリメントして保存し、返値にviewsを要求する、という処理のようです。
PageのIDはユニークなので成功すれば、帰ってくるタプルは必ず{1(updateした項目数), hogehoge}となるというところでしょう。
put_inpage.viewsviewsの値を代入し、pageを返すといった流れでしょうか。

これでinc_page_viewsの実装は良さそうです。
いつものごとくWebレイヤーを修正していきます。

コントローラを修正する

HTTPアクセスの度に呼び出される処理はコントローラの役目です。lib/hello_web/controllers/cms/page_controller.exを編集します。

...

  def show(conn, %{"id" => id}) do
    page = 
    id
    |> CMS.get_page!()
    |> CMS.inc_page_views()
    
    render(conn, "show.html", page: page)
  end

...

閲覧した時にインクリメントされて欲しいのでshowに先ほどの機能を実装します。
これで実装は完了されたはずです。サーバーを起動して確認してみます。

f:id:ysmn_deus:20190404134021p:plain
1回目

f:id:ysmn_deus:20190404134039p:plain
2回目

インクリメントされてます!完成です!

パスワード認証などははしょりましたが、これでCMSまでをも作る事ができました。
割とこの辺は他のシステムなどにも応用が利きそうです。

FAQ

Returning Ecto structures from context APIs

(Contextはカプセル化するための概念なのになんでcreate_user/1みたいな関数は失敗したときにEcto.Changesetを生で返すねん!)

Phoenixでは%Ecto.Changeset{}は一般的な構造体として考えられているそうで、Phoenix.ParamPhoenix.HTML.FormDataでハンドリングできるそうです。あと、エラーメッセージとかバリデーションのどこがアカンかったとか見やすいからだそうで。

Strategies for cross-context workflows

今回はページを生成する際にAuthorをバックエンドで作成してましたが、これは必ずしも全ユーザーがAuthorである必要性がない前提です。もしユーザーがAuthorのデータを必要とするときはどのような依存関係になるのでしょうか。

アカウントを生成する際に依存関係を結ぶようなケースを考えると、たとえばAccountsのContextのcreate_user

def create_user(attrs) do
  %User{}
  |> User.changeset(attrs)
  |> Ecto.Changeset.cast_assoc(:credential, with: &Credential.changeset/2)
  |> Ecto.Changeset.put_assoc(:author, %Author{...}) # これ
  |> Repo.insert()
end

となります。
一見問題無さそうに見えますが、この設計だとCMSはAccountsの構造に依存しており、AccountsもまたCMSの構造に依存することになります。
AccountsはCMSから完全に独立しているからこそ拡張性が高いのですが、上記のような循環参照みたいな関係になってしまうと拡張性も糞も無くなります。

ただ、よくある事のようなので、こういうケースは新しくContextを生成するのがベストプラクティスとされているようです。

例えば、今回であればAccountsとCMSはそのままで、UserRegistrationというContextを新しく作ります。このContextからAccountsとCMSを呼び出し、CMSのAuthorの関連付けを行います。こうすることでAccountsとCMSのそれぞれの結びつきを最小限にできるだけでなく、APIとしても明瞭(Context名から何を意味しているのかが推測できたりとか?)になるはずです。このアプローチを採用する際にはEcto.Multiが有用だそうです。
例を見ます。

defmodule Hello.UserRegistration do
  alias Ecto.Multi
  alias Hello.{Accounts, CMS}

  def register_user(params) do
    Multi.new()
    |> Multi.run(:user, fn _ -> Accounts.create_user(params) end)
    |> Multi.run(:author, fn %{user: user} ->
      {:ok, CMS.ensure_author_exists(user)}
    end)
    |> Repo.transaction()
  end
end

Multi.new()から続くパイプラインでそれぞれの依存関係の処理を行い、最後にトランザクション処理がなされる流れです。
もしどこかの処理で失敗すれば、全てがロールバックする仕組みだそうです。

おわりに

Context長かったのですが、割とPhoenixの中枢の話な気はします。
また復習してMulti.new()あたりも活用できるサンプルを公開できればなぁと思います。