※本記事は「RailsからLaravelを眺める」シリーズの第14回です。Rails出身の私がLaravelを触りながら、Railsと比較して違いを整理していく連載になります。
はじめに
Webアプリケーションを開発していると、複数のモデルで同じような機能を実装したくなる場面は頻繁に訪れます。「記事も商品も公開/非公開の状態を持つ」「ユーザーも投稿もタグ付けできる」といった横断的な機能をどう実装するか。
RailsにはConcerns、LaravelにはTraitsという仕組みがあります。一見似ているこの2つですが、実は設計思想やアプローチに興味深い違いがあります。今回はこの「コード再利用」の文化の違いを掘り下げていきましょう。
RailsのConcernsとは
RailsのConcernsは、ActiveSupport::Concernを使って実装するモジュールです。複数のモデルやコントローラーで共通する機能を切り出すために使います。
# app/models/concerns/publishable.rb
module Publishable
extend ActiveSupport::Concern
included do
scope :published, -> { where(published: true) }
scope :draft, -> { where(published: false) }
validates :published_at, presence: true, if: :published?
end
def publish!
update!(published: true, published_at: Time.current)
end
def unpublish!
update!(published: false)
end
end
モデルで使う際は:
class Article < ApplicationRecord
include Publishable
end
class Product < ApplicationRecord
include Publishable
end
Concernsの特徴
included
ブロック内でクラスマクロ(scope
,validates
など)を実行できるclass_methods
ブロックでクラスメソッドを定義できる- モジュール間の依存関係を自動解決してくれる
- Railsのフレームワーク機能として提供
LaravelのTraitsとは
一方、LaravelのTraitsはPHP言語の機能をそのまま活用しています。PHP 5.4から導入されたTraitsは、クラスに機能を追加するための仕組みです。
<?php
// app/Traits/Publishable.php
namespace App\Traits;
use Carbon\Carbon;
trait Publishable
{
public function publish(): bool
{
return $this->update([
'published' => true,
'published_at' => Carbon::now(),
]);
}
public function unpublish(): bool
{
return $this->update([
'published' => false,
]);
}
public function scopePublished($query)
{
return $query->where('published', true);
}
public function scopeDraft($query)
{
return $query->where('published', false);
}
}
モデルで使う際は:
<?php
namespace App\Models;
use App\Traits\Publishable;
use Illuminate\Database\Eloquent\Model;
class Article extends Model
{
use Publishable;
}
class Product extends Model
{
use Publishable;
}
Traitsの特徴
- PHP言語レベルの機能
- メソッドをクラスに「混ぜ込む」シンプルな仕組み
- Laravelのマジックメソッド(
scopeXxx
)を活用してスコープを定義 - フレームワークに依存しない
決定的な違い:設計思想
1. 言語機能 vs フレームワーク機能
PHP Traitsは言語仕様です。フレームワークに関係なく、どのPHPプロジェクトでも使えます。一方、Rails ConcernsはActiveSupportが提供するフレームワーク機能です。
この違いは、Laravelが「PHPの標準的な機能を活用する」という思想を持っていることの表れです。Railsは「Ruby on Rails流のやり方」を提供することで、開発者体験を最適化しようとします。
2. includedブロックの有無
Concernsの最大の特徴はincluded
ブロックです:
module Taggable
extend ActiveSupport::Concern
included do
has_many :tags, as: :taggable, dependent: :destroy
validates :tags, length: { minimum: 1 }
before_save :normalize_tags
end
private
def normalize_tags
# タグの正規化処理
end
end
このincluded
ブロック内では、includeされたクラスのコンテキストでコードが実行されます。つまり、has_many
やvalidates
といったクラスマクロを自然に書けます。
Laravelでは、この部分をboot
メソッドや直接モデルに書く必要があります:
<?php
namespace App\Traits;
use App\Models\Tag;
trait Taggable
{
public static function bootTaggable()
{
static::saving(function ($model) {
// タグの正規化処理
});
}
public function tags()
{
return $this->morphMany(Tag::class, 'taggable');
}
}
LaravelのTraitではboot{TraitName}
という命名規則のメソッドが自動的に呼ばれます。これはEloquentの機能で、Trait専用のブートストラップ処理を書けます。
3. 依存関係の解決
RailsのActiveSupport::Concern
は、Concern同士の依存関係を自動的に解決してくれます:
module Taggable
extend ActiveSupport::Concern
include Searchable # Searchableに依存
end
module Searchable
extend ActiveSupport::Concern
# 検索機能の実装
end
PHPのTraitsでは、依存関係は明示的に管理する必要があります:
trait Taggable
{
use Searchable; // 明示的に宣言
}
これは一見手間に見えますが、依存関係が明確になるというメリットもあります。
実践例:SoftDelete機能の比較
それぞれでSoftDelete(論理削除)機能を実装してみましょう。
Rails版
# app/models/concerns/soft_deletable.rb
module SoftDeletable
extend ActiveSupport::Concern
included do
scope :active, -> { where(deleted_at: nil) }
scope :deleted, -> { where.not(deleted_at: nil) }
default_scope { active }
end
def soft_delete
update(deleted_at: Time.current)
end
def restore
update(deleted_at: nil)
end
def deleted?
deleted_at.present?
end
end
Laravel版
<?php
// app/Traits/SoftDeletable.php
namespace App\Traits;
use Carbon\Carbon;
use Illuminate\Database\Eloquent\Builder;
trait SoftDeletable
{
public static function bootSoftDeletable()
{
static::addGlobalScope('active', function (Builder $builder) {
$builder->whereNull('deleted_at');
});
}
public function softDelete(): bool
{
return $this->update(['deleted_at' => Carbon::now()]);
}
public function restore(): bool
{
return $this->update(['deleted_at' => null]);
}
public function isDeleted(): bool
{
return !is_null($this->deleted_at);
}
public function scopeWithDeleted($query)
{
return $query->withoutGlobalScope('active');
}
public function scopeOnlyDeleted($query)
{
return $query->withoutGlobalScope('active')
->whereNotNull('deleted_at');
}
}
比較ポイント
Rails は default_scope をConcern内で自然に書けます。 included ブロックのおかげで、クラス定義と同じように書けるのが特徴です。
Laravelは bootSoftDeletable でグローバルスコープを追加します。Traitの命名規則に従った自動起動を活用しています。
どちらも機能的には同等ですが、書き方の「文化」が異なります。
ベストプラクティス
Railsでの使い方
Railsでは app/models/concerns と app/controllers/concerns にConcernを配置するのが慣例です。
ディレクトリ構成例:
app/
models/
concerns/
publishable.rb
taggable.rb
searchable.rb
controllers/
concerns/
authenticatable.rb
authorizable.rb
避けるべきパターン:
- Concernに詰め込みすぎる(God Object化)
- 複雑な依存関係を作る
- ビジネスロジックを全てConcernに押し込む
Laravelでの使い方
Laravelには公式の配置場所はありませんが、 app/Traits が一般的です。モデル専用なら app/Models/Concerns という配置も見かけます。
ディレクトリ構成例:
app/
Traits/
Publishable.php
Taggable.php
Models/
Concerns/
Searchable.php
避けるべきパターン:
- 状態を持つTraitの作成(Traitはステートレスに)
- メソッド名の衝突(同じ名前のメソッドを持つTraitを複数use)
- 過度な抽象化
まとめ:文化の違いを楽しむ
RailsのConcernsとLaravelのTraits、どちらが優れているという話ではありません。
Railsの哲学は「Rails Way」に沿った、統一された開発体験の提供です。ActiveSupport::Concern
という抽象化を提供することで、誰が書いても同じような構造になります。
Laravelの哲学は、PHPの言語機能を最大限活用しつつ、Laravelらしい便利さを追加することです。Traitsという標準機能に、boot{TraitName}
やscopeXxx
といったLaravelの規約を組み合わせます。
どちらのアプローチも、「コードの再利用性」と「保守性」という同じゴールを目指しています。ただ、そこに至る道筋が違うのです。
コメント