パーフェクトRuby on Rails 2章 メモ・雑感

# 2章 Ruby on RailsとMVC

## 雑感

この章では基本的なMVCの仕組みを押さえつつ、今まで抜けていたより詳細な基本的な知識を再確認することができました。特にjbuilder周りは知っていれば個人開発もっと楽にできたなあと感じたのと、業務でもすぐに使いそうなのでしっかり押さえていきたいと思います。以下、初見の知識とメモしたかった箇所を中心とした忘備録です。

:::note warn
1章でもそうだったのですが、私の環境(以下参照）で`scaffold`するとエラーとなりました。解決策は以下の記事を参考にしております。次章以降も毎回`rails new`実行時に同様の操作をしています。

```
$ ruby -v
ruby 2.7.8p225 (2023-03-30 revision 1f4d455848) [x86_64-darwin23]
$ bin/rails -v
Rails 6.0.6.1
```

[「Webpacker::Manifest::MissingEntryError in Tasks#index」エラーが発生する](https://zenn.dev/oneopeadoauo_dv/scraps/dda4f660bfa3bb)
:::

## 単数系のresourceでルーティングを作成

複数存在しない単一のリソースは`resources`の単数系の`resource`でルーティングを定義でき、以下のような違いがある。

- 基本のルーティングから`index`を除いたルーティングが定義される
- 単一のリソースなのでURLに`:id`が含まれない

```ruby:config/routes.rb
resource :profile
```

```
 new_profile GET    /profile/new(.:format)      profiles#new
edit_profile GET    /profile/edit(.:format)     profiles#edit
     profile GET    /profile(.:format)          profiles#show
             POST   /profile(.:format)          profiles#create
             PATCH  /profile(.:format)          profiles#update
             PUT    /profile(.:format)          profiles#update
             DELETE /profile(.:format)          profiles#destroy
```

## rescue_fromによってRailsが自動対応するもの以外の例外に対する挙動を指定

- Railsはいくつかの特定の例外クラスに関してはそれぞれに対応したエラーコードを返す
- それ以外の例外が発生した場合は`rescue_from`というクラスメソッドを用いることでアプリケーション全体の振る舞いとして特定のレスポンスを返却することができる

```rb
class LoginFailed < StandardError
end
```

with:オプションで例外発生時に呼ばれるアクションを指定する。

```rb
class ApplicationController < ActionController::Base
  rescue_from LoginFailed, with: :login_failed

  def login_failed
    render template: "shared/login_failed", status: 401
  end
end
```

ログインが失敗した場合に定義した例外を発生させる。

```rb
class LoginController < ApplicationController
  def create
    @user = User.where(name: params[:name], password: params[:password]).find
    raise LoginFailed unless @user # ログイン失敗時に例外発生
  end
```

## variantsによるテンプレートの切り替え

コントローラ内で`request.variant`に`:tablet`や`:mobile`の値を入力することで展開されるビューテンプレートファイルを`index.html.erb`から`index.html+mobile.erb`へ、といった具合に、"+"と`request.variant`の値を合わせた名称のテンプレートへと変更できる。

```app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  before_action :detect_mobile_variant

  private

  def detect_mobile_variant
    request.variant = :mobile if request.user_agent =~ /iPhone/
  end
end
```

:::note info
### <補足> chromeでPCからUserAgentをiPhoneとしてアクセスする方法

```
1. Chrome開発者ツールを開く（F12 or Command + Option + I）
2. 開発者ツールの「...」（その他）メニューをクリック
3. More tools → Network conditions を選択
4. User agentセクションで「Custom」を選択
5. 以下のようなiPhoneのUser-Agentを入力：

Mozilla/5.0 (iPhone; CPU iPhone OS 15_0 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Mobile/15E148 Safari/604.1
```
:::

## コンソールでURLとパスの取得をする

- ルーティング表示した時の各Prefixへ`_path`をつけるとドメインやポートを除いた`/`から始まるパス部分を、`_url`とすると完全なURLを取得できる
- コントローラーやビューでは単に`edit_profile_url`と記述するとURLを取得できるがコンソール上では`app`オブジェクトを経由することで取得可能

```
irb(main):001:0> app.edit_profile_url
=> "http://www.example.com/profile/edit"
irb(main):002:0> app.edit_profile_path
=> "/profile/edit"
irb(main):003:0> app.publisher_url(id: 1)
=> "http://www.example.com/publishers/1"
irb(main):004:0> app.publisher_path(id: 1)
=> "/publishers/1"
```

## 独自のヘルパーメソッドの定義とコンソールでの動作確認

- HTMLタグの付与や整形などビューに特有の加工をする場合はモデルクラスに処理を書くよりヘルパーメソッドを利用するのが一般的
- 独自のヘルパーメソッドの定義は`app/helpers`配下に定義する

```app/helpers/application_helper.rb
module ApplicationHelper
  def to_hankaku(str)
    str.tr("Ａ－Ｚａ－ｚ", "A-Za-z")
  end
end
```

- ヘルパーメソッドをコンソールで利用する場合は、`helper`オブジェクトを呼び出す

```
irb(main):002:0> helper.to_hankaku("Ａ-Ｚ")
=> "A-Z"
```

## jbuilderでAPIとして返却するJSONデータを整形する

コントローラーの`rebder`メソッドはjsonへオブジェクトを変換できるが、より複雑なJSONデータを構築する場合は、Rails組み込みのjbuilderを利用する。その場合、コントローラー内では`render`メソッドの呼び出しを止める。

```diff_ruby:app/controllers/books_controller.rb
class BooksController < ApplicationController
  def show
    respond_to do |format|
      format.html
-     format.json { render json: @book } # 単純なJSONを返却
+     format.json # jbuilderを利用する場合
    end
  end
end
```

対応するアクションごとの`.json.jbuilder`という拡張子のファイルを用意する。

```app/views/books/show.json.jbuilder
# 指定したパラメータのJSONデータを構築できる
json.extract! @book, :id, :name, :price

# !で終わらないメソッドはそのままJSONのキーとなる
json.name_with_id "#{@book.id} - #{@book.name}" 

# ブロックを渡すことでネストしたJSONデータを構築
json.publisher do 
  json.name @book.publisher.name
  json.address @book.publisher.address
end

# Rubyのコードなのでifやunlessで制御できる
unless @book.high_price?
  json.low_price true
end
```
上記の出力は以下のようになる。

```url:http://localhost:3000/books/1.json
{
  "id": 1,
  "name": "Book 1",
  "price": 1000,
  "name_with_id": "1 - Book 1",
  "publisher": {
    "name": "Gihyo inc",
    "address": "Ichigaya"
  },
  "low_price": true
}
```