rubyonrails.org
rails/rails - github.comRuby 製の MVC WEB アプリケーションフレームワーク。アジャイル開発に適しスタートアップ系企業の採用例が多く、クックパッドや食べログなどでも利用されている。「設定より規約」で DRY 重視。RESTful なインターフェイスや HTML 5 / モダン JavaScript への追従も早く WEB の流行をいち早く取り込んでいて、バージョンアップは割と早めなのでメジャーバージョンの移行は結構大変みたい。
- Ruby on Rails Tutorial
- API リファレンス - api.rubyonrails.org
- 日本語ガイド - railsguides.jp
- その他サードパーティリファレンス
- 学習
- eliotsykes/real-world-rails
- Rails の実装例リポジトリ、参考に
- eliotsykes/real-world-rails
肥大化したActiveRecordモデルをリファクタリングする7つの方法(翻訳)
中規模Web開発のためのMVC分割とレイヤアーキテクチャ
Railsで導入してよかったデザインパターンと各クラスの役割について
Rails は基本的には「シンプルな MVC パターン」を提供しており、Laravel のように「いろんなデザインパターンに使えるクラスは俺が用意しておいたぜ」って感じではない。また結構歴史が古いため Rails 開発者間では「あって当然」な各種デザインパターン系クラスが存在することが多いみたい。基本の MVC や Rails が ( 多くは ActiveSupport で ) 提供する Concern など以外も把握しておくこと。また、実際には Uploader とかシステムの Feature に応じたわかりやすい名前のクラスも app 下でクラス化されていることが多い。
# Base
- ActiveModel
- ActionController
- Helpers
- ActionView
- FormHelper
- FormBuilder ヘルパーにより生成されるインスタンスで拡張も可能
- ActionMailer
- ActiveResource 外部 API やサービス連携
- ActiveJob 非同期処理やジョブ実行
# ActiveSupport
- Concern
- Model / Controller の共通処理 ( 関心 ) 共通化のためのモジュールで鬼よく使う
# Design Pattern Classes
- Assert
- フロントエンドのアサーションに利用するらしい
* Decorator
- Model の状態に応じた View ロジックや文字列フォーマットに利用
- 一般的に MVC パターンで Fat Model を避けるための ViewModel レイヤとして非常によく利用される
- 単一 Model や単一インスタンスに対するロジック ... という文脈が多いみたい
- draper gem で実装することが多いが同じくサードパーティーの active_decorator とかいう紛らわしい gem もある
- Exception
- よくある独自例外つかいたいときのやつ
- raise 'hoge' で RuntimeError でおk ... ってとき以外に
- Factory
- いわゆるファクトリパターンで以下のようなインスタンス生成に利用
- 複数オブジェクトから単一オブジェクトを生成
- 非構造化データからデータオブジェクトを生成 CSV → DataFrame
- 共通インタフェースを持つオブジェクトの透過的生成
* Form
- ある Model がいくつかのフォームに依るロジックを持つ場合に便利なやつ
- パラメータ毎のバリデーションや異常系処理などを詰め込むことが多いみたい
- Rails 的に言うと FormBuilder からの受け処理全般を保持することが多い
- Model インスタンスを引数に new され params を引数に build され valid? とか聞かれる感じ
- Valid 時の永続化トランザクションもそのまま内部に持ったりすることも結構あるみたい
- メソッドとしては create や update で登場することになる
- 後述する ViewObject が無いパターンでは ViewObject として new や edit で View に Passing したいデータや入力項目など「フォーム準備」の役割を担うこともある
- Job/Worker
- ActiveJob や Sidekiq による非同期処理を担当させるクラス
- Notifier
- 通知系おまとめ ActionMailer をラップすることも
- Presenter
- 複数 Model を跨ぐような複雑な View ロジックを担当させる ViewModel レイヤ
- Repository
- データアクセス抽象化クラス、鬼複雑な検索処理とか扱う場合に。Concerns で Module として実装して Model から include するのがいい?
- Resource
- ActiveResource をラップするときにつかうとか?
* Service
- 他クラスで吸収できない「ビジネスロジック」特化のクラス
- Task
- Rake タスクで実行するやつのラッパ作ったり?
- Util
- 専門性の低い雑多な共通処理を扱うやつ
- Validator
- 独自バリデーション詰め込みクラス
* ViewObject
- Form と混同しやすいが View に Passing したいデータに関する責務持ちクラス
- 基本的には所謂 ViewModel レイヤとして View にロジックを入れたくなるようなケースで利用
- Rails 的に言うと `@hoge` など View へ渡すデータをどんな風に Build するかを管理する
- つまりメソッドとしては new や edit で登場することになる
新規Railsプロジェクトの作成手順まとめ
Bundlerの使い方
ライブラリ? gem? bundler? -- Rubyのgem管理に関するあれこれまとめ
Rails は gem なので Ruby の gem パッケージ管理ライブラリ Bundler で Rails 本体、および依存パッケージ群をプロジェクトローカルへインストールし、プロジェクト毎に管理するのが一般的。因みに Bundler も gem なので、こいつだけはグローバルにぶち込む。
# Ruby 実行環境 ( ruby や gem コマンド実行可能 ) 前提
# Bundler のグローバルインストール
#
$ gem install bundler
# プロジェクトディレクトリの作成
#
$ mkdir my_project
$ cd my_project
# Gemfile に依存パッケージを記載
# ( まずは ruby と rails 本体 )
#
$ vi Gemfile
> source 'https://rubygems.org'
> git_source(:github) { |repo| "https://github.com/#{repo}.git" }
>
> ruby '2.5.3' # このプロジェクトで使う ruby のバージョン
>
> gem 'rails', '~> 5.2.2' # このプロジェクトで使う rails バージョン
# Bundler で rails gem をローカルインストール
# `--path` なしだとグローバルインストール扱いになる
#
# Bundler 経由でローカルインストールした gem のバイナリを実行するときは
# 通常 bundler exec 経由で実行しないと PATH 解決できないが、これを省略するため
# --binstubs で bin を指定ディレクトリにまとめ export で PATH を通している
# Docker 環境ならコンテナ毎に隔離されているのでこの --path --binstubs は不要
#
$ bundle install --path=vendor/bundle --binstubs=vendor/bin
$ export PATH=./vendor/bin:$PATH
# Rails アプリケーションを新規作成し FW スケルトンを生成
# Rails の依存パッケージが Gemfile に追加されるため再度 bundle install
# 一度 bundle install --path したら次から --path は省略してもいいみたい
# ちなみにここで `--api` にすると Rails が API モードに ( ビューなし )
#
$ rails new .
$ bundle install --path vendor/bundle --binstubs=vendor/bin
# DB 設定 / 作成
$ vi config/database.yml
$ rake db:create
# 開発用サーバ起動
$ rails server
# モデル生成
$ rails generate model User email:string:uniq password:string
# アソシエーション/バリデーションをごにょごにょ
$ vi app/models/user.rb
# データ型とかインデックスとかごにょごにょ
$ vi db/migrate/20180101000000_create_users.rb
# マイグレート
$ rake db:migrate
# DB ドロップ
$ rake db:drop
# セットアップ ( db:create / db:schema:load / db:seed )
$ rake db:setup
# キャッシュ削除
$ rake tmp:cache:clear # ファイルだけ削除
$ bundle exec rails r 'Rails.cache.clear' # Redis にいれた動的キャッシュも削除
# クエリキャッシュだけ削除
$ bundle exec rails r 'ActiveRecord::Base.connection.query_cache.clear'
# ログ削除
$ rake log:clear
# コンソールで確認
$ rails c
$ irb> User.all
> User Load (1.0ms) SELECT "users".* FROM "users" LIMIT $1 [["LIMIT", 11]]
> => #<ActiveRecord::Relation []>
# コード数などのプロファイリング
#
# https://easyramble.com/check-loc-on-rails.html
#
$ rake stats
Apache x Passenger x Rails - redmine.jp
Rails5.2+Puma
Rails + Puma + Nginx on CentOS 7 の 環境構築メモ
Ruby アプリケーションは Apache や nginx とは別に、Ruby アプリサーバが必要。Rails 5 系からは標準で Puma という Ruby アプリサーバを兼ねた Web サーバがビルトインされており rails server
で即立ち上がる。開発時はこれを利用するのが一番楽。本番環境では前段に別途 WEB サーバをかますとよろしい。
# 初回は起動に少しかかるから待っててね
$ rails c
> Running via Spring preloader in process 58
> Loading development environment (Rails 5.2.1)
# 実 DB を動かしたくないときは sandbox モードで
$ rails c --sandbox
# 環境指定も可能
$ rails c -e development
# こんな感じで動かせる
pry(main)> user = User.find(1)
# View 系のヘルパーは app レシーバが必要
pry(main)> app.url_for(action: :show, controller: 'users', id: 1)
pry(main)> app.users_path(id: 1)
# 終了
pry(main)> quit
# Rails.application.credentials.secret_key_base の確認
$ rails c
irb> Rails.application.credentials.secret_key_base
irb> => xxxxxxx...
Rails では控えめな JavaScript 連携のための JS パッケージ rails-ujs を利用することで Ajax や form_tag との JS 連携を強化することができる。5系より前は jQuery 依存だったが、5.1 系より依存を取り除いて npm パッケージになった。
$ npm i -S rails-ujs
import Rails from 'rails-ujs'
Rails.start()
<%=
button_to 'Terminate Account', delete_user_path(@user),
method: :delete, class: 'btn btn-danger',
data: { confirm: 'Are you sure?' } # こいつを動かすのに必要
%>
Rails プロジェクトでは設定ファイルや定数の取り扱いについて、別途 gem を入れてることが多いみたい。
$ vi Gemfile
> gem 'config'
$ bundle install
# インストールしつつ環境毎の設定 YAML を生成
$ rails g config:install
> config/settings.yml
> config/settings/development.yml
> config/settings/production.yml
> config/settings/test.yml
# Config 定数名で各環境の YAML にオブジェクト的にアクセス可能に
$ vi config/initializers/config.rb
> Config.setup do |config|
> config.const_name = 'Config'
> end
ディレクトリ | ファイル | クラス |
---|---|---|
app/controllers/ | users_controller.rb | UsersController |
app/models/ | user.rb | User |
app/views/users/ | index.html.erb | - |
app/views/layouts/ | user.html.erb | - |
app/helpers/ | users_helper.rb | - |
db/migrate/ | 20080906120000_create_users.rb | CreateUsers |
test/fixtures/ | user.yml | - |
- テーブル名は複数形
users
- 単語区切りは
_
- 対応モデルクラス名はテーブル名アッパーキャメル
- 単語区切りは
- Primary Key カラム名は
id
- Foreign Key カラム名は
テーブル名の単数_id
- Foreign Key カラム名は
- DATE 型のカラム名は
受動態_on
published_on
- TIMESTAMP / DATETIME 型のカラム名は
受動態_at
updated_at / created_at
- アソシエーション
- 関連させたいテーブル名をくっつけた名前にする
users
とtokens
の交差はusers_tokens
id
を作らず、関連する 2 つの Foreign Key セットを Primary key にする
- 関連させたいテーブル名をくっつけた名前にする
Rails には 3 つのモード ( 実行環境 ) があり、WEB / アプリサーバ実行時に環境変数を設定することで切り替える。デフォルトでは development モードで立ち上がる。
- production
- ログレベル info
- キャッシュ有効
- 更新には再起動が必要
- development
- ログレベル debug
- キャッシュ無効
- VM 環境下だとがっつりキャッシュ効いちゃう 対策
- 更新を即時反映
- test
- ログレベル debug
- キャッシュ有効
- 自動テスト用
$ vi Gemfile
> gem "rails_12factor", group: :production
$ bundle install
Heroku などへのデプロイの際に「開発環境はローカル DB だけど本番環境 ( Heroku ) では Heroku Postgres のようなクラウドサービス」というのがよくある。Rails アプリは config/database.yml よりも環境変数 DATABASE_URL
を接続先として優先する ので、割と楽に対応できる。
# 開発環境
DATABASE_URL=postgres://db # 開発時は別途立ち上げている DB コンテナを参照
# 本番環境 ( 実際には .env に書かずに Heroku 管理画面で設定するなど )
DATABASE_URL=postgres://xxx:yyy@zzz/hoge # 本番ではサービスの URL を参照
# host や url 指定をしなくても .env の DATABASE_URL が採用される
default: &default
adapter: postgresql
encoding: utf8
username: <%= ENV.fetch('POSTGRES_USER') %>
password: <%= ENV.fetch('POSTGRES_PASSWORD') %>
pool: <%= ENV.fetch('RAILS_MAX_THREADS') { 5 } %>
development:
<<: *default
database: app_development
test:
<<: *default
database: app_test
production:
<<: *default
database: app_production
/
├─ /bin - シェル用バイナリ
├─ /tmp
│ ├─ restart.txt 再起動用ファイル ( touch で再起動 )
| └─ /storage アップロードファイル入れ
├─ /log
├─ /public 公開ファイル入れ ( favicon / html )
├─ /vendor
├─ /config
│ ├─ /environments 実行環境設定ファイル入れ
│ ├─ /locales ロケールファイル入れ
│ ├─ application.rb アプリのセットアップファイル
│ ├─ routes.rb ルーティング設定ファイル
│ ├─ storage.yml クラウドストレージサービス設定ファイル
│ └─ database.yml DB 設定ファイル
├─ /db
│ ├─ /migrate マイグレーションファイル入れ
│ └─ seeds.rb シード
├─ /app
│ ├─ /assets
│ │ ├─ /javascripts JS のアセットパイプライン
│ │ └─ /stylesheets CSS のアセットパイプライン
│ ├─ /models
│ │ └─ user.rb
│ ├─ /controllers
│ │ └─ users_controller.rb
│ ├─ /views
│ │ ├─ /shared 共通パーシャル入れ
│ │ ├─ /layouts レイアウト入れ
│ │ └─ /users 各コントローラ対応ビュー入れ
│ │ └─ /index.html.erb
│ └─ /helpers コントローラ固有のビューヘルパー入れ
│ └─ users_helper.rb
└─ /test
├─ /controllers
│ └─ users_controller_test.rb
└─ /helpers
└─ users_helper_test.rb
Railsで「Cannot render console from Allowed networks」と言われたら
Rails5.2のdevelopment環境でControllerの変更が反映されないときはRails の開発用コンソールは localhost アクセスしか想定していないので Docker や VM を介してアクセスする場合はホワイトリストへ自身 IP を登録する必要がある。面倒なら開発環境限定で 0.0.0.0/0 で全許可するのがよい。また、 Rails は development 時にファイル変更イベントを探知して内部でキャッシュを削除しているみたい。だが Vagrant + VirtualBox で rsync などを利用せず同期フォルダー下で動作させているとき、ファイル変更のイベントを探知できずキャッシュが production と同様に残ってしまう。回避するためにファイルの WATCH 方法を変更してやる。
# config/environments/development.rb
# config.file_watcher = ActiveSupport::EventedFileUpdateChecker # 殺す
config.file_watcher = ActiveSupport::FileUpdateChecker # 追記
config.web_console.whitelisted_ips = '0.0.0.0/0' # 追記
Docker などを利用している場合は Docker 経由で .env
から環境変数をセットできるが、Rails でも dotenv という gem を導入することでアプリケーションルートの .env
から環境変数をロードできる。
# Gemfile
gem 'dotenv-rails'
stdlib や外部ライブラリの読み込みは個別に行っても良いが、アプリケーション全体に跨る設定などは config/application.rb
や、環境毎で異なる場合は config/environments
下に書いちゃうのが良いみたい。
# config/application.rb
require 'rails/all'
require 'uri' # 適当に使う stdlib とか読み込んどく
module App
class Application < Rails::Application
config.load_defaults 5.2
config.i18n.default_locale = :en # デフォルトロケール
config.time_zone = 'UTC' # Time / TimeWithZone が参照し created_at / updated_at に影響
config.active_record.default_timezone = :utc # DB の without timezone なカラム ( datetime 型等 ) の読み書きに影響
config.action_controller.default_url_options = { # URL メソッド設定
host: ENV.fetch('DOMAIN'),
protocol: 'https',
}
config.action_mailer.raise_delivery_errors = true # メール設定
config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
address: ENV.fetch('SMTP_HOST'),
port: ENV.fetch('SMTP_PORT'),
user_name: ENV.fetch('SMTP_USERNAME'),
password: ENV.fetch('SMTP_PASSWORD'),
authentication: ENV.fetch('SMTP_TYPE'),
enable_starttls_auto: ENV.fetch('SMTP_TLS'),
}
config.action_mailer.default_url_options = {
host: ENV.fetch('DOMAIN'),
protocol: 'https',
}
end
end
# config/application.rb
module App
class Application < Rails::Application
# Rails 標準以外のデザパタ系や独自ユーティリティクラスの読み込みはこのへんでやることが多いみたい
config.autoload_paths += %W(#{config.root}/app/services)
config.autoload_paths += %W(#{config.root}/app/forms)
config.autoload_paths += %W(#{config.root}/app/factories)
config.autoload_paths += %W(#{config.root}/app/view_objects)
config.autoload_paths += %W(#{config.root}/app/validators)
config.autoload_paths += %W(#{config.root}/app/libraies)
config.autoload_paths += %W(#{config.root}/app/utilities)
end
end
# config/routes.rb
namespace :admin do
resource :user
get '/login', to: 'users#login'
get '/logout', to: 'users#logout'
end
__END__
# クラス/ディレクトリ構成は以下のようになる
/app
├─ /controllers
│ ├─ application_controller.rb # ApplicationController < ActionController::Base
│ └─ /brands
│ ├─ application_controller.rb # Admin::ApplicationController < ApplicationController
│ └─ users_controller.rb # Admin::UsersController < Admin::ApplicationController
└─ /views
├─ /layouts
| ├─ application.html.erb
| └─ /admin
| └─ application.html.erb # layouts/application.html.erb からコピって OK です
└─ /brands
└─ /users
├─ login.html.erb # layouts/admin/application.html.erb が適用される
└─ logout.html.erb # コントローラは Admin::UsersController が適用される
namespace :admin do
# ...
namespace :api do
# ...
end
end
__END__
/app
├─ /controllers
├─ application_controller.rb # ApplicationController < ActionController::Base
└─ /admin
├─ application_controller.rb # Admin::ApplicationController < ApplicationController
└─ users_controller.rb # Admin::UsersController < Admin::ApplicationController
└─ /api
├─ application_controller.rb # Admin::Api::ApplicationController < Admin::ApplicationController
└─ users_controller.rb # Admin::Api::UsersController < Admin::Api::ApplicationController
rails コマンド
いつも忘れる「Railsのgenerateコマンド」の備忘録
Railsコマンドでgenerateしたのを取り消したい場合(メモ)
CakePHP でいうところの bake
で Laravel でいうところの artisan
コマンド。ご利用は計画的に。
# CRUD の MVC ぜんぶ ( -p: ドライラン / --assets=false: アセット抜き )
$ rails g scaffold article title:string body:text published_at:datetime
# Scaffold モデル抜き
$ rails g scaffold_controller articles
# Scaffold モデル抜き ( skip test / assets / helper )
$ rails g scaffold_controller articles --skip-helper --skip-assets --skip-test-framework
# モデルのみ
$ rails g model user name:string age:integer is_admin:boolean
# コントローラとビューのみ
$ rails g controller tags index add edit delete
# コントローラとビューのみ ( skip test / assets / helper )
$ rails g controller users --skip-helper --skip-assets --skip-test-framework
# コントローラとビューのみ ( 名前空間つき )
$ rails g controller admin/users
# jbuilder ( json 返却用ビュー )
$ rails g jbuilder article
# mailer ( メール本文用ビュー )
$ rails g mailer article
# task ( CLI タスク )
$ rails g task article
# マイグレーションのみ
$ rails g migration 名前 [カラム名:型] [オプション]
# Generate したファイルの削除
$ rails destroy 生成したファイルの種類 [削除するファイル名] [オプション]
# 現在のルーティングを確認できる
# Prefix は redirect_to や link_to で _url や _path のように利用可能
# Prefix が root なら root_url や root_path となる
# _url は絶対パスでリダイレクトで利用する / _path は相対パスで link_to なんかで利用
$ rake routes
> Prefix Verb URI Pattern Controller#Action
> root GET / users#login
> ...
# config/routes.rb
# root 指定 ( 最上位に記載 )
root 'articles#index' # '/' で記事一覧へ
# 通常のルーティング
get '/articles/:id', to 'articles#show'
# 引数省略可パターン
get '/articles/(:page)', to 'articles#show', defaults: {page: 1}
# as で名前付け
get '/login', to: 'users#login', as: :login
get '/logout', to: 'users#logout', as: :logout
# match + via
match "user/account" => "user#account", as: :user_account, via: [:get, :post]
# リソースベースルーティング
# GET /photos photos#index すべての写真の一覧を表示
# GET /photos/new photos#new 写真を1つ作成するためのフォームを返す
# POST /photos photos#create 写真を1つ作成する
# GET /photos/:id photos#show 特定の写真を表示する
# GET /photos/:id/edit photos#edit 写真編集用のフォームを返す
# PUT /photos/:id photos#update 特定の写真を更新する
# DELETE /photos/:id photos#destroy 特定の写真を削除する
resources :photos, :books, :videos
# 単数形リソースルーティング
# GET /user/new user#new ユーザを1つ作成するためのフォームを返す
# POST /user user#create ユーザを1つ作成する
# GET /user user#show 1つしかないユーザを表示する
# GET /user/edit user#edit ユーザ編集用のフォームを返す
# PUT /user/ user#update 1つしかないユーザを更新する
# DELETE /user/ user#destroy ユーザを削除する
resource :user
get '/profile', to: 'users#show' # /profile = users#show はログインユーザ専用となる
# リソースベースルーティング + 除外
resources :attachments, except: [:show]
# 名前空間
namespace :admin do
resources :users, :tweets
end
# ワイルドカードとパスパラメータ
get 'books/*section/:title', to: 'books#show' # params[:section] や params[:title] がとれる
# フォーマット ( ファイル や JSON 返却 REST API とか )
get 'photos/:id', to: 'photos#show', defaults: { format: 'jpg' }
defaults format: :json do
resources :feeds
end
# リダイレクト
get '/stories', to: redirect('/articles')
# config/routes.rb
get '/articles/:id', to: 'articles#show', as: 'articles'
# app/controllers/articles_controller.rb
def index
@articles = Article.all
end
# app/views/articles/index.rb
<ul>
<% @articles&.each do |article| %>
<li>
<!-- :as で指定した prefix を用いた動的リンク生成 -->
<%= link_to @article.title, article_path(@article) %>
</li>
<% end %>
</ul>
http://weblog.jamisbuck.org/2007/2/5/nesting-resources
リソースのネスティングは、ぜひとも1回にとどめて下さい。決して2回以上ネストするべきではありません。
Railsのルーティングは、ルーティングファイルの「上からの記載順に」マッチします。
→ よってarticles/:id
をarticles/new
より前に記載すると articles#new にたどり着かなくなる。
ActiveSupport::Rescuable::ClassMethods
rescue_from
Railsアプリケーションにおけるエラー処理(例外設計)の考え方
Railsアプリの例外構造パターン
Railsでrescue_fromメソッドを使ってエラーハンドリングをする方法
Rack mountable ErrorsController
Railsアプリの例外ハンドリングとエラーページの表示についてまとめてみた
他の FW 同様、Rails では各所で raise された例外について上位クラス ( ミドルウェア層 / MVC 層 ) で rescue してエラーハンドリングを行っている。Rails には下位クラスで起きた例外をキャッチしてディスパッチするための rescue_from メソッドがあるため、最上位の ApplicationController でコントローラ以下で発生する例外について拾って、カスタムエラー画面のレンダリング ... みたいなことができる。
またカスタム例外を作成したい場合は StandardError を継承して、同じく最上位の ApplicationController あたりで定義してあげればよろし。Rails には 403 を流す例外がデフォルトで定義されていないので適当に ForbiddenError とか作った方が楽かも。
但しこの実装は、コントローラより上位の Rails の内部的な例外や StandardError や基底 Exception はここで拾うべきではなかったり ( マニュアル参照 ) と拡張性に乏しい。もう少し固くするなら別途 gem を入れるなりミドルウェア系クラスを拡張する必要があるみたい。
# 500 Internal Server Error
#
# raise 時のデフォルト例外 RuntimeError が
# 500 Internal Server Error として rescue され
# デフォルトでは public/500.html がレンダリングされる
#
if ENV.fetch('DB_HOST').empty?
raise 'Missing required environment variables of DB.'
end
# 400 Bad Request
#
# ActionController::BadRequest 例外を raise すると
# 400 Bad Request エラーとして rescue され
# デフォルトではレンダリングなし ( HTTP ステータスコード返却のみ )
#
if params[:user_id] != current_user.id
raise ActionController::BadRequest, 'Detected illegal params!'
end
# 404 Not Found
#
# コントローラ内なら ActionController::RoutingError
# モデルロジック内なら ActiveRecord::RecordNotFound
# これらを raise すれば 404 Not Found として rescue され
# デフォルトでは public/404.html がレンダリングされる
#
raise ActionController::RoutingError, 'Missing parameters!'
raise ActiveRecord::RecordNotFound, 'Results not found...'
# config/initializers/cheap_error_handling.rb
# Cheap error handling.
# @see https://gist.github.com/yano3nora/3dadc56b5970a23a0528d9d4066b2110#cheap-error-handling
# Custom error classes.
class ForbiddenError < StandardError; end
# Handling exceptions at ErrorsController.
Rails.application.configure do
config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
end
# controllers/errors_controller.rb
class ErrorsController < ActionController::Base # Not ApplicationController.
layout 'error' # Should logic less template.
# Rescue exception show method raised.
rescue_from RuntimeError, with: :handle_500
rescue_from StandardError, with: :handle_500
rescue_from ForbiddenError, with: :handle_403
rescue_from ActiveRecord::RecordNotFound, with: :handle_404
rescue_from ActionController::RoutingError, with: :handle_404
rescue_from ActionController::BadRequest, with: :handle_400
# config.exceptions_app = ->(env) { ErrorsController.action(:show).call(env) }
def show
raise request.env["action_dispatch.exception"]
end
private def handle_400(e = nil)
@status_line = '400 Bad Request.'
@exception = e
render template: 'errors/error',
status: 400,
layout: 'error',
content_type: 'text/html'
end
private def handle_403(e = nil)
@status_line = '403 Forbidden.'
@exception = e
render template: 'errors/error',
status: 403,
layout: 'error',
content_type: 'text/html'
end
private def handle_404(e = nil)
@status_line = '404 Not found.'
@exception = e
render template: 'errors/error',
status: 404,
layout: 'error',
content_type: 'text/html'
end
private def handle_500(e = nil)
@status_line = '500 Internal server error.'
@exception = e
render template: 'errors/error',
status: 500,
layout: 'error',
content_type: 'text/html'
end
end
<!-- views/layouts/error.html.erb -->
<!-- ErrorController は ActionController::Base 継承なのでロジック控えめで -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<title>
<%= @status_line %> | <%= t 'words.app_name' %>
</title>
<%= csrf_meta_tags %>
<%= csp_meta_tag %>
<link rel="stylesheet" type="text/css" href="/css/style.css">
<script src="/js/bundle.js" defer></script>
</head>
<body>
<header>
<!-- メニューなしヘッダとかてきとうに -->
</header>
<main>
<%= yield %>
</main>
<footer>
<!-- てきとうに -->
</footer>
</body>
</html>
# views/errors/error.html.erb
<article class="container">
<h2 class="heading">Error <small class="text-muted">/ <%= @status_line %></small></h2>
<blockquote class="blockquote"><%= @exception.message %></blockquote>
<div class="m-3"><%= link_to 'Back', :back, class: 'btn btn-light' %></div>
</article>
# 因みに RAILS_ENV=development で本番と同じエラー画面を出すなら
#
# config/environments/development.rb
#
config.consider_all_requests_local = false
Rails 上の ERB で出力を行う場合、自動的に HTML エンティティ ( HTML entities and special characters ) がエスケープされ XSS 対策となる。ただし、以下メソッドではエスケープをキャンセルするので注意。
<!-- raw -->
<%= raw "<b>hanako</b>" %>
<!-- '==' -->
<%== "<b>hanako</b>" %>
<!-- html_safe -->
<p><%= "<script>alert('a');</script>".html_safe %></p>
# 平文のぶち込みは NG name: "' or 1=1 --'" などで死ぬ
User.where("name='#{params[:name]}'")
# 基本プレースホルダーを利用していれば安全
User.where(name: params[:name])
User.where('name = ?', params[:name]) # 同義
User.where('name = :name', {name: params[:name]}) # 同義
CSRF への対応策 - Rails セキュリティガイド
リクエストフォージェリからの保護
protect_from_forgery
ActionController :: RequestForgeryProtection
Rails の CSRF 対策のやつ。コントローラでこいつをコールするとビューに Token 吐いてくれて、フォームの送信時に検証してくれる。Rails はデフォルトで、GET 以外のリクエストを受ける際に X-CSRF-Token リクエストヘッダに ↑ で吐いてる Token がセットされていないと、これを Token Invalid として弾くようになっている。
↑ GET 以外と書いたが、実際は XmlHttpRequest ( 要は Ajax ) のような js 起点のリクエストでは、GET も検証されるぽい。2020 年現在だいたいの web サーバでは、XHR リクエストは同じ Origin からのみ許可される仕様なので、CSRF 保護から除外することも検討して良い。但し、サーバ設定の CORF で *
みたいに全許可してたら前述した Origin 保障は当然ないので、必ず CORF 設定を確認しておくこと。
Rails の JavaScript ユーティリティ @rails/ujs ( rails-ujs ) を使うと、このリクエストヘッダに Token を埋め込んでくれる ( 多分ページから要素取得 ) ので意識する必要はそこまでない。またこのライブラリを使って Ajax する際もヘッダを自動でうにょってくれるみたい。これを使わず jQuery など別ライブラリで Ajax する際には、自分でリクエストヘッダをこねる必要があるので注意すること。
また外部サービスの Webhook リクエストを受けたり、Rails が API サーバとして機能するようなシステムでは Token を含んだビューのレンダリングが行われないため with: :null_session
で例外スローを抑止したり、except
や only
を使って認証を部分的に外すなどの措置が必要。その際は別途認証を必ず設けること。
ハックな手段だが verify_authenticity_token
をオーバライドして独自認証を設ける手段もあるみたい ref 。
# application_controller.rb
class ApplicationController < ActionController::Base
# 以下にプラスして form_helper 使っとけばトークン認証が走る
protect_from_forgery with: :exception # with: :exception がデフォルト
# with: :null_session だと検証失敗時にセッションを空にしてくれる
# 利用ケースとしては、API のリクエストを受けるエンドポイントなど
protect_from_forgery with: :null_session
# rails 5 以降は prepend ( 先行実行 ) false デフォルトなので
# 何らかのフックの後続に書いた場合はかならず prepend: true にしておく
protect_from_forgery prepend: true
end
Ruby 2.3 からの ぼっち演算子 - Safe Navigation Operator である &.
に似た挙動をする。nil チェックをすっ飛ばして「メソッドが呼び出せるときだけ呼び出す」という便利なエラー握り潰しメソッド。ロジック層ではガツガツ使うべきでない。
# try
@person.try(:non_existing_method) # nil となり NoMethodError を回避
# try!
@person.try!(:non_existing_method) # メソッドが定義すらされていなければ NoMethodError
<ul>
<% @user.try(:articles).try(:each) do |article| %>
<li><%= article.title %></li>
<% end %>
</ul>
<!-- &. で書くとこうかな ... ? -->
<ul>
<% @user&.articles&.each do |article| %>
<li><%= article.title %></li>
<% end %>
</ul>
ruby にはもとから object.nil?
メソッドがあるが、Rails にはこれに似た存在確認用メソッドが標準で備わっている。
obj.nil? # nil なら true
obj.empty? # nil ではないが中が falsy なら true
obj.blank? # nil または falsy なら true
obj.present? # nil でないかつ中身が falsy でないなら true
<%= debug @user %>
Rails.logger.debug user
# オブジェクトを展開するなら .inspect メソッドを利用
Rails.logger.debug user.inspect
Ruby の Date や DateTime は require しないと使えないので Rails 上ではこっち使ったほうが良いかも。
Time.current.class # ActiveSupport::TimeWithZone
Time.current # Fri, 18 Aug 2017 18:12:21 JST +09:00
Time.current - 1.days # Thu, 17 Aug 2017 18:12:26 JST +09:00
Time.new(2017, 9, 1).to_s(:default) # 2017-09-01 00:00:00 +0900
Time.new(2017, 9, 1).to_s(:long) # September 01, 2017 00:00
Time.new(2017, 9, 1).to_s(:short) # 01 Sep 00:00
Time.new(2017, 9, 1).to_s(:db) # 2017-09-01 00:00:00
# 文字列パース ( 以下は同義みたい )
Time.zone.parse('2016-11-11T12:23:42+0000')
'2016-11-11T12:23:42+0000'.in_time_zone
# user.timezone に依存させる
Time.now.in_time_zone(current_user.time_zone)
# config/initializers/time_formats.rb で追加可能
# Time::DATE_FORMATS[:ja] = "%Y年%m月%d日
# Time::DATE_FORMATS[:ymd] = "%Y%m%d"
Time.new(2017, 9 ,1).to_s(:ja) # 2017年9月1日
# config/locales/ja.yml による追加ならこう
I18n.l(Time.current, format: :long)
ActiveSupport::Durationの期間処理メソッド(1)演算、比較など
Rails の便利なメソッド(数値オブジェクト編)
Rails 内での数値リテラル ( Integer オブジェクト ) は ActiveSupport::Integer
を継承しており ActiveSupport::Duration
系の便利な日時変換のメソッドが数値リテラルから直接叩ける。ぎょっとするけど便利。
# 今日が 2019-12-09 だとして、前日の 00:00:00 時点を呼び出す。
1.days.ago.to_time.beginning_of_day # 2019-12-08 00:00:00 +0900
# 月、年もある。
1.months.ago.to_time.beginning_of_day # 2019-11-09 00:00:00 +0900
1.years.ago.to_time.beginning_of_day # 2018-11-09 00:00:00 +0900
# 翌日ならこう。
1.days.since.to_time.beginning_of_day # 2019-12-10 00:00:00 +0900
# 当然、当日の 00:00:00 時点ならこう。
Time.now.beginning_of_day # 2019-12-09 00:00:00 +0900
# 23:59:59 ならこう。
Time.now.end_of_day # 2019-12-09 23:59:59 +0900
# n 日間を秒で返すみたいなこともできる
seconds = 5.days.to_i # 432000
Rails.env == 'production'
と Rails.env.production?
は等価。これは Rails.env が ActiveSupport::StringInquirer という String を継承したクラスに変換されているため。
String#inquiry
で String => ActiveSupport::StringInquirer への変換が可能。
s = "hoge".inquiry # => "hoge"
s.class # => ActiveSupport::StringInquirer
s.hoge? # => true
s.fuga? # => false