このモジュールの目的は、 多様なコンテンツ(記事・診断・ギャラリー・ミニゲームなど)を
再利用可能かつ、分類可能な構造で管理するための仕組みを構築することです。


今回の進捗

前回の記事では「こんな構想を考えている」という構想段階の共有が中心でした。
今回は、実際に以下のような動作する構成が形になっています。

  • コンテンツのデータ保存(contents テーブル)
  • 分類情報との同期(content_taxonomy_term
  • アプリ層・永続化層の分離
  • JSONでの動作確認が可能

実装の構成と責務

以下のように、各レイヤーが役割分担しています。

レイヤ クラス 説明
Entity ContentEntity データ状態を保持。配列化やModel変換も担当
DTO ContentData 入力情報の受け皿
Repository ContentRepositoryInterface
EloquentContentRepository
保存・取得ロジック
Service ContentService 作成/分類同期の統括
分類同期 TaxonomySyncServiceInterface
EloquentTaxonomySyncService
タームとの紐付け管理

具体的に何ができる?

// テストエンドポイントで動作確認済
$dto = ContentData::fromArray([
  'site_id' => 1,
  'title' => 'テスト記事',
  'slug' => 'test-001',
  'content_type' => 'article',
  'status' => 'draft'
]);

app(ContentService::class)->create($dto, [3, 5]);

→ contents に1レコード挿入され、3,5 のタームIDと紐付けられます。
ルート /develop/content/test から確認も可能。


なぜこういう仕組みにしているのか?

この構成は、以下のような汎用性・拡張性を重視しています。

  • タイプ別にコンテンツのロジックを分けたい(記事・診断・フォームなど)
  • タクソノミーで複数軸から分類・絞り込みしたい
  • テンプレート化・複製による共有・再利用も可能にしたい

このベースを押さえておくことで、
あとは View(表示)や UI、他サービス連携などにも容易に拡張できます。


今回実装した内容

仕様ポイント

  • 疎結合:コアは純粋な ContentServiceEntity、永続化や分類同期は Interface 経由で差し替え可能
  • 拡張性:タイプ別ロジックやルールは別クラスに切り出し、あとからアプリケーション層に注入
  • モジュール単位:マイグレーションも含めすべて modules/ContentModule 配下に集約し、ServiceProvider でアプリ本体へ統合

フォルダ構成(モジュール内)

modules/ContentModule/
├─ composer.json
└─ src/
   ├─ Application/
   │  ├─ DTOs/
   │  │  └─ ContentData.php
   │  └─ Services/
   │     └─ ContentService.php
   │     └─ ContentCloneService.php   # (未実装)
   │     └─ PackageService.php        # (未実装)
   │
   ├─ Domain/
   │  ├─ Entities/
   │  │  └─ ContentEntity.php
   │  └─ Repositories/
   │     ├─ ContentRepositoryInterface.php
   │     └─ TaxonomySyncServiceInterface.php
   │
   ├─ Infrastructure/
   │  ├─ Eloquent/
   │  │  ├─ Models/
   │  │  │  └─ ContentModel.php
   │  │  └─ Repositories/
   │  │     ├─ EloquentContentRepository.php
   │  │     └─ EloquentTaxonomySyncService.php
   │  ├─ Migrations/
   │  │  ├─ 2025_04_23_000000_create_contents_table.php
   │  │  └─ 2025_04_23_000001_create_content_taxonomy_term_table.php
   │  └─ Providers/
   │     └─ ContentModuleServiceProvider.php
   └─ tests/                            # (今後ユニットテスト配置予定)

主なクラス図(Mermaid)

classDiagram
    %% Domain層
    class ContentEntity {
        +int id
        +int site_id
        +string title
        +string slug
        +... (他フィールド)
        +static fromData(obj)
        +static fromModel(ContentModel)
        +toArray()
    }
    class ContentRepositoryInterface
    class TaxonomySyncServiceInterface

    %% Infrastructure層
    class ContentModel
    class EloquentContentRepository
    class EloquentTaxonomySyncService
    class ContentModuleServiceProvider

    %% Application層
    class ContentData
    class ContentService

    %% 依存関係
    ContentService --> ContentRepositoryInterface
    ContentService --> TaxonomySyncServiceInterface
    ContentRepositoryInterface <|-- EloquentContentRepository
    TaxonomySyncServiceInterface <|-- EloquentTaxonomySyncService
    EloquentContentRepository --> ContentModel
    ContentEntity ..> ContentModel : uses

主要コード抜粋

1. ContentService::create()

// modules/ContentModule/src/Application/Services/ContentService.php
public function create(ContentData $data, array $taxonomyIds): ContentEntity
{
    // DTO→Entity
    $entity = ContentEntity::fromData($data);

    // 永続化(insert or update)
    $saved = $this->repository->save($entity);

    // 分類同期
    $this->taxonomySync->sync($saved, $taxonomyIds);

    return $saved;
}

2. ContentEntity の toArray/fromModel

// modules/ContentModule/src/Domain/Entities/ContentEntity.php
public static function fromModel(ContentModel $m): self { /* ... */ }
public function toArray(): array { /* return DB保存用の連想配列 */ }

3. EloquentContentRepository

// modules/.../EloquentContentRepository.php
public function save(ContentEntity $c): ContentEntity
{
    $model = ContentModel::updateOrCreate(
        ['id' => $c->id],
        $c->toArray()
    );
    return ContentEntity::fromModel($model);
}

4. EloquentTaxonomySyncService

// modules/.../EloquentTaxonomySyncService.php
public function sync(ContentEntity $c, array $ids): void
{
    DB::table('content_taxonomy_term')->where('content_id', $c->id)->delete();
    $insert = array_map(fn($i, $term) => [
        'content_id'=>$c->id,'taxonomy_term_id'=>$term,
        'sort_order'=>$i,'created_at'=>now(),'updated_at'=>now()
    ], array_keys($ids), $ids);
    DB::table('content_taxonomy_term')->insert($insert);
}

次にやること

今後のステップとしては:

  • 一覧取得・フィルター(type/taxonomy)
  • 更新・削除・スラッグ重複バリデーション
  • ContentTypeごとのふるまい切り替え(Strategyパターン)
  • 複製・パッケージングのサービス層設計

ここまで来ると「CMS」としての骨格が完成します。


まとめ

このモジュールは、Laravelで扱える
分類可能複製可能汎用的コンテンツ管理のためのベースパッケージです。

「記事」だけでなく「フォーム」、「ギャラリー」、「ゲーム」など、
あらゆる“出せるコンテンツ”を構造的に扱えるのが強みです。

今後も段階的にこのモジュールの発展を記録していきます。
気になる方は、また覗いてみてくださいね。✍️✨