このモジュールの目的は、 多様なコンテンツ(記事・診断・ギャラリー・ミニゲームなど)を
再利用可能かつ、分類可能な構造で管理する
ための仕組みを構築することです。
今回の進捗
前回の記事では「こんな構想を考えている」という構想段階の共有
が中心でした。
今回は、実際に以下のような動作する構成
が形になっています。
- コンテンツのデータ保存(
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、他サービス連携などにも容易に拡張できます。
今回実装した内容
仕様ポイント
- 疎結合:コアは純粋な
ContentService
/Entity
、永続化や分類同期は 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
主要コード抜粋
ContentService::create()
1. // 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;
}
ContentEntity
の toArray/fromModel
2. // modules/ContentModule/src/Domain/Entities/ContentEntity.php
public static function fromModel(ContentModel $m): self { /* ... */ }
public function toArray(): array { /* return DB保存用の連想配列 */ }
EloquentContentRepository
3. // modules/.../EloquentContentRepository.php
public function save(ContentEntity $c): ContentEntity
{
$model = ContentModel::updateOrCreate(
['id' => $c->id],
$c->toArray()
);
return ContentEntity::fromModel($model);
}
EloquentTaxonomySyncService
4. // 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で扱える
分類可能
・複製可能
・汎用的
なコンテンツ管理のためのベースパッケージ
です。
「記事」だけでなく「フォーム」、「ギャラリー」、「ゲーム」など、
あらゆる“出せるコンテンツ”を構造的に扱えるのが強みです。
今後も段階的にこのモジュールの発展を記録していきます。
気になる方は、また覗いてみてくださいね。✍️✨