RailsのConcernsとLaravelのTraits | コード再利用の文化の違い

RailsからLaravelを眺める

※本記事は「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_manyvalidatesといったクラスマクロを自然に書けます。

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');
    }
}

比較ポイント

Railsdefault_scope をConcern内で自然に書けます。 included ブロックのおかげで、クラス定義と同じように書けるのが特徴です。

LaravelbootSoftDeletable でグローバルスコープを追加します。Traitの命名規則に従った自動起動を活用しています。

どちらも機能的には同等ですが、書き方の「文化」が異なります。

ベストプラクティス

Railsでの使い方

Railsでは app/models/concernsapp/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の規約を組み合わせます。

どちらのアプローチも、「コードの再利用性」と「保守性」という同じゴールを目指しています。ただ、そこに至る道筋が違うのです。

コメント

タイトルとURLをコピーしました