Upgrade to Pro
— share decks privately, control downloads, hide ads and more …
Speaker Deck
Sign up for free
Menu
Search
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Features
All features
Private URLs
Password Protection
Custom URLS
Scheduled publishing
Remove Branding
Restrict embedding
Deck Collections
Notes
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Explore
Featured decks
Featured speakers
Programming
Technology
Storyboards
Pricing
Search
Sign in
Sign up for free
なぜ適用するか、移行して理解するClean Architecture 〜構造を超えて設計を継承...
Search
Sponsored
·
SiteGround - Reliable hosting with speed, security, and support you can count on.
→
shiro seike
PRO
June 28, 2025
Programming
1.1k
3
Share
Embed
Copy iframe code
Copy JS code
Copy link
Start on current slide
なぜ適用するか、移行して理解するClean Architecture 〜構造を超えて設計を継承する〜 / Why Apply, Migrate and Understand Clean Architecture - Inherit Design Beyond Structure
PHP カンファレンス 2025
https://phpcon.php.gr.jp/2025/
shiro seike
PRO
June 28, 2025
More Decks by shiro seike
See All by shiro seike
Seeing Through Serverless: Observability for AWS Lambda with ADOT and CloudWatch Application Signals
seike460
PRO
1
140
Seeing Through Serverless: ADOT と CloudWatch Application Signals で実現する AWS Lambda のオブザーバビリティ(日本語版)/ Seeing Through Serverless (Japanese Edition)
seike460
PRO
1
21
モノレポの型は、そのままつながる 〜Hono RPCで型を素通しする開発体験〜 / Monorepo Types Connect As-Is: Passing Types Through with Hono RPC
seike460
PRO
1
27
OSSのコンセプトを知る / Understanding the Concept Behind an OSS
seike460
PRO
1
28
型も通る、synthも通る、それでも危ない 〜AIのCDKの権限とコストを機械で検証する〜 / It Passes Type Checks, It Passes Synth Checks, but It’s Still Risky — Automatically Verifying Permissions and Costs in AI’s CDK —
seike460
PRO
1
640
コミュニティの有益性 ~JAWS Days 2026 での体験を通して~ / The Benefits of a Community ~Through My Experience at JAWS Days 2026~
seike460
PRO
0
460
実行委員長目線で振り返る JAWS DAYS 2026 / JAWS DAYS 2026 from the Chair's Perspective
seike460
PRO
1
26
SLO から始める SRE / Starting SRE with SLO
seike460
PRO
1
100
Architecture as SteeringOn-Ramp to AI-DLC
seike460
PRO
0
64
Other Decks in Programming
See All in Programming
デプロイ直後のレイテンシスパイクを調べたら、 Railsの仕様にたどり着いた
nhsykym
0
120
Swift愛好会100回記念 第1回を振り返る
jollyjoester
0
120
GKE で Pod の見方を変えたら、スケールアウト時の挙動を真に捉えられた話
stkk
0
110
AIエージェント時代のコードレビューを設計する
nogu66
6
2.6k
Heart of Swift Concurrency
koher
0
300
選挙速報を多くのユーザーへ 届ける Live Activities 設計
hamayokokuririn
0
110
型解析で実現する Go の言語内 DSL / Conference に Go! タイムテーブルの歩き方 for Gophers
mazrean
0
140
ALB ログから Trace を気合で繋げる技術
fohte
7
890
テストを司るデーモンに会いに行く 〜隔離した仮想マシンでテストを通すまで〜
h1d3mun3
1
180
The Good Stuff, Not the Slop: Engineering High-Quality Android Apps with Modern AI Tooling
danybony
1
240
go-spidermonkeyでAIエージェントのCode Modeを実装する
syumai
3
1.5k
業務時間外もAIに働いてもらう話
colorful12
3
10k
Featured
See All Featured
Why You Should Never Use an ORM
jnunemaker
PRO
61
10k
Jamie Indigo - Trashchat’s Guide to Black Boxes: Technical SEO Tactics for LLMs
techseoconnect
PRO
0
670
YesSQL, Process and Tooling at Scale
rocio
174
15k
How to Grow Your eCommerce with AI & Automation
katarinadahlin
PRO
1
270
Digital Projects Gone Horribly Wrong (And the UX Pros Who Still Save the Day) - Dean Schuster
uxyall
1
2.8k
Distributed Sagas: A Protocol for Coordinating Microservices
caitiem20
333
23k
We Analyzed 250 Million AI Search Results: Here's What I Found
joshbly
1
1.9k
Navigating the moral maze — ethical principles for Al-driven product design
skipperchong
2
530
Hiding What from Whom? A Critical Review of the History of Programming languages for Music
tomoyanonymous
3
1.2k
The Art of Delivering Value - GDevCon NA Keynote
reverentgeek
16
2.2k
Optimising Largest Contentful Paint
csswizardry
37
4k
Evolving SEO for Evolving Search Engines
ryanjones
0
290
Transcript
None
None
None
None
None
None
None
None
None
None
None
None
None
None
None
{ return $this->posts->count() < 5; // ドメイン知識:「ユーザーは最大5件まで投稿可能」 } // この実装がLaravel→Symfony移行時に変更不要となる理由
} <?php // Domain/Entity/User.php - フレームワーク非依存 namespace App\Domain\Entity; // Clean Architectureの核心:Pure PHPで実装されたドメイン層 class User // Entityはアイデンティティを持つビジネスオブジェクト { private UserId $id; // Value Objectによる型安全性の確保 private UserName $name; // プリミティブ型ではなくドメイン概念として表現 private Email $email; // バリデーションロジックをValue Object内に委譲 private Posts $posts; // コレクションも専用のValue Objectで管理 public function __construct(UserId $id, UserName $name, Email $email) // コンストラクタでの不変条件の確立 { $this->id = $id; // Value Object を使用:IDの一意性と型安全性を保証 $this->name = $name; // バリデーションは Value Object 内で実行済み $this->email = $email; // フレームワーク依存なし:Laravel/Symfony問わず動作 $this->posts = new Posts([]); // 初期状態は空のコレクションで安全に初期化 } public function canCreateNewPost(): bool // ビジネスルールをEntityに集約
public function equals(Email $other): bool // 値による等価性:Value Objectの重要特性 { return
$this->value === $other->value; // 同じ値 = 同じオブジェクト:ビジネス的な同一性判定 } // この仕組みがLaravel/Symfony関係なく動作する理由 } <?php // Domain/ValueObject/Email.php namespace App\Domain\ValueObject; // Value ObjectもDomain層に配置:ドメイン知識の一部 class Email // 値そのものを表現するオブジェクト:メールアドレスの概念を型で表現 { private string $value; // 不変な内部状態:一度作成されたら変更されない public function __construct(string $email) // 生成時バリデーション:不正な値でのオブジェクト作成を防ぐ { if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { // ドメインルール:メールアドレス形式の検証 throw new InvalidArgumentException('Invalid email format'); // 例外による契約:不正値は作成させない } $this->value = $email; // 妥当な値のみ保持:以降は常に正しいメールアドレスであることが保証される } public function getValue(): string // 値の取得:カプセル化されたプリミティブ値へのアクセス { return $this->value; // 内部表現を返却:フレームワーク層での利用時 }
None
None
None
None
None
None
CleanArchitecture/ ├── Pure PHP実装 (src/) ├── Laravel実装 (laravel/) └── Symfony実装
(symfony/)
src/ ├── Domain/ # ビジネスルール中核 │ ├── Entity/ # User.php
- Pure PHPエンティティ │ ├── ValueObject/ # Email.php, UserId.php等 │ └── Repository/ # UserRepositoryInterface.php ├── Application/ # アプリケーション処理 │ └── UseCase/User/ # CreateUserUseCase.php等 └── Infrastructure/ # 外部技術実装 └── Repository/ # InMemoryUserRepository.php
laravel/ ├── src/ │ ├── Infrastructure/Repository/ # LaravelUserRepository.php │ ├──
Http/Controllers/ # UserController.php │ └── Models/ # EloquentUser.php └── public/ # Webインターフェース symfony/ ├── src/ │ ├── Infrastructure/Repository/ # SymfonyUserRepository.php │ ├── Controller/ # UserController.php │ └── Entity/ # DoctrineUser.php ├── config/ # services.yaml └── public/ # Webインターフェース
// Infrastructure/Repository/LaravelUserRepository.php // Infrastructure層で具体実装 class LaravelUserRepository implements UserRepositoryInterface // Interface契約の実装
{ public function findById(UserId $id): ?User // Domain Entityを返すことでDomain層との契約を守る { $eloquentUser = EloquentUser::find($id->getValue()); // Laravel固有:Eloquent Model 使用 if (!$eloquentUser) { return null; } return new User( // 重要:EloquentからDomain Entityへの変換 new UserId($eloquentUser->id), // ORMの詳細をDomain層から隠蔽 new UserName($eloquentUser->name), // フレームワーク固有データをDomain概念に変換 new Email($eloquentUser->email) // この変換層がフレームワーク移行を可能にする ); } } <?php // Domain/Repository/UserRepositoryInterface.php namespace App\Domain\Repository; // Domain層でInterfaceを定義:依存関係逆転の実現 interface UserRepositoryInterface // データアクセスの契約:フレームワーク非依存 { public function findById(UserId $id): ?User; // Domain Entity を返却:戻り値もDomain概念 public function save(User $user): void; // Domain Entity を受け取り:引数もDomain概念 } // このInterfaceがLaravel/Symfony移行の鍵 ===================================================================================
$user->getName()->getValue(), // 外部システム(Controller等)への戻り値 $user->getEmail()->getValue(), // フレームワーク固有形式への変換は不要 $user->canCreateNewPost() // ビジネスルール結果もそのまま返却 );
// このコード全体がLaravel固有クラスを一切使用していない点が重要 } } <?php // Application/UseCase/CreateUserUseCase.php namespace App\Application\UseCase; // Application層:アプリケーション固有の処理フロー class CreateUserUseCase // 1つのUseCaseは1つのビジネス機能を表現 { public function __construct( private UserRepositoryInterface $userRepository // Interface に依存:実装ではなく契約に依存 ) {} // DI(依存性注入):フレームワークが具体実装を注入 public function execute(CreateUserRequest $request): UserDto // 入力・出力はDTOで型安全に { // バリデーションは Value Object で実行済み:関心の分離 $user = new User( // Domain Entityの生成:ビジネスルールに従った正しいオブジェクト作成 $this->userRepository->nextId(), // Repository経由でID生成:永続化層の詳細を隠蔽 new UserName($request->name), // Value Object でバリデーション:不正値は例外で弾かれる new Email($request->email) // Value Object でバリデーション:メール形式チェック済み ); $this->userRepository->save($user); // Repository 経由で永続化:ORM詳細は隠蔽 return new UserDto( // DTO で結果を返却:Domain EntityをAPI用データに変換 $user->getId()->getValue(), // Value Objectからプリミティブ値を取得
public function findById(UserId $id): ?User // 戻り値は同じDomain Entity:Laravel版と完全に同じ { $doctrineUser
= $this->entityManager // Doctrine固有の取得方法 ->find(DoctrineUser::class, $id->getValue()); // Eloquentとは異なるAPI、同じ結果 if (!$doctrineUser) { return null; // 見つからない場合の処理も同じ } return new User( // 重要:全く同じDomain Entity に変換 new UserId($doctrineUser->getId()), // Doctrine→Domain変換:Eloquent版と同じ変換処理 new UserName($doctrineUser->getName()), // ORMの違いを吸収してDomain概念に統一 new Email($doctrineUser->getEmail()) // この変換があるからUseCase層は変更不要 ); // Laravel版のRepositoryと戻り値が完全に同じ:これがフレームワーク移行の鍵 <?php // Infrastructure/Repository/SymfonyUserRepository.php namespace App\Infrastructure\Repository; // Infrastructure層:Symfony/Doctrine実装 class SymfonyUserRepository implements UserRepositoryInterface // 同じInterface契約を実装 { public function __construct( private EntityManagerInterface $entityManager // Symfony固有:Doctrine ORM使用 ) {} // LaravelのEloquentとは異なるORM、しかし同じInterface } }
$this->app->bind( // Laravelコンテナへのバインド UserRepositoryInterface::class, // Interface(契約) LaravelUserRepository::class // Laravel 版実装をバインド:Eloquent使用版
); // この設定により、Interface注入時にLaravel実装が自動注入される } // Laravel - AppServiceProvider.php // Laravel固有のDI設定方法 public function register(): void // ServiceProviderでの依存関係定義 {
# Symfony - services.yaml # Symfony固有のDI設定方法 services: # サービスコンテナの定義 App\Domain\Repository\UserRepositoryInterface:
# Interface(契約)の定義 alias: App\Infrastructure\Repository\SymfonyUserRepository # Symfony実装をエイリアス:Doctrine使用版 ``` # この設定により、Interface注入時にSymfony実装が自動注入される:Laravel版と同じ効果 **結果**: 設定変更だけで移行完了!
None
None
None
CleanArchitecture/ ├── Eloquent依存実装 (eloquent-dependent/) └── 段階的移行デモ (eloquent-to-symfony/) ├── Phase 1:
Domain Entity抽出 └── Phase 2: Repository Pattern導入
eloquent-dependent/ └── src/ ├── Models/ │ └── User.php # Eloquentモデル
+ ビジネスロジック混在 └── Http/Controllers/ └── UserController.php # Model直接操作
phase1-domain-extraction/ └── src/ ├── Domain/Entity/ # User.php (Pure PHP Entity新規作成)
├── Http/Controllers/ # UserController.php (まだModel直接操作) └── Models/ └── User.php # toDomainEntity()メソッド追加 phase2-repository-pattern/ └── src/ ├── Domain/ │ ├── Entity/ # User.php │ └── Repository/ # UserRepositoryInterface.php ├── Infrastructure/Repository/ # EloquentUserRepository.php ├── Application/Service/ # UserValidationService.php ├── Http/Controllers/ # Repository経由に変更 └── Models/ # User.php (まだEloquent併存)
// 問題:バリデーションも Model 内に混在 public static function createWithValidation(array $data): self
// スタティックメソッド:テスト難しい { $validator = Validator::make($data, [ // Laravel Validator 使用:Symfonyでは全く異なるAPI 'email' => 'required|email|unique:users', // Laravel固有バリデーションルール 'name' => 'required|min:2' // この書き方はSymfonyでは使えない ]); if ($validator->fails()) { // Laravel固有のエラーハンドリング throw new ValidationException($validator); // Laravel固有例外:Symfonyでは対応不可 } <?php // App/Models/User.php - Eloquent Model use Illuminate\Database\Eloquent\Model; // Laravel固有:Eloquentへの直接依存 class User extends Model // Model継承:Laravelフレームワークと密結合 { protected $fillable = ['name', 'email']; // Laravel固有:Mass Assignment設定 // リレーション定義:Eloquent固有の機能 public function posts() // メソッド名がリレーションを表現:Eloquentの約束 { return $this->hasMany(Post::class); // Eloquent リレーション:Symfony/Doctrineでは使えない } // Laravel→Symfony移行時にこのメソッドは全書き直し必要 // 問題:ビジネスロジックが Model に混在 public function canCreateNewPost(): bool // ドメイン知識がORM層に漏出 { return $this->posts()->count() < 5; // Eloquent Query Builder 使用:フレームワーク固有処理 } // このビジネスルールがEloquentに依存:移行時の大きな障壁
'errors' => $e->validator->errors() // Validatorオブジェクトへの直接アクセス:Symfonyでは使えない ], 422); // Laravel固有HTTPステータスコード返却 }
// この全体が密結合:Symfony移行時にリファクタ必要 } } <?php // App/Http/Controllers/UserController.php use App\Models\User; // Eloquent Modelへの直接依存:コントローラーがORMを直接操作 class UserController extends Controller // Laravel固有Controller継承 { public function store(Request $request): JsonResponse // Laravel固有Request/Response使用 { try { // 例外ハンドリングでビジネスロジックを制御 // 問題:Model のスタティックメソッドを直接呼び出し $user = User::createWithValidation($request->all()); // ControllerがModelの内部実装を知っている return response()->json([ // Laravel固有のJSONレスポンス生成 'id' => $user->id, // 問題:Model の属性に直接アクセス 'name' => $user->name, // カプセル化されていないデータアクセス 'email' => $user->email, // ビジネスルールが無い直接アクセス 'can_create_post' => $user->canCreateNewPost() // Model のメソッド呼び出し:Controllerがビジネスロジックを直接利用 ], 201); } catch (ValidationException $e) { // Laravel固有例外のキャッチ return response()->json([ // エラーレスポンスもLaravel固有方式
$this->hasMany()
None
{ return count($this->posts) < 5; // Pure PHP でビジネスロジック:Eloquent非依存 }
// これでLaravel→Symfony移行時にビジネスロジックが保持される } <?php // Domain/Entity/User.php - 新規作成 namespace App\Domain\Entity; // Domain層への移行:Eloquentからの脱却 class User // Pure PHPクラス:フレームワーク非依存 { private int $id; // プリミティブ型使用:シンプルな移行例 private string $name; // EloquentのattributeではなくPHPプロパティ private string $email; // 正規化されたデータ表現 private array $posts; // リレーションではなくシンプルな配列 public function __construct(int $id, string $name, string $email, array $posts = []) // コンストラクタでの状態初期化 { $this->id = $id; // バリデーションは別途実装:関心の分離 $this->name = $name; // Eloquentの$fillableやmass assignmentではない $this->email = $email; // 直接的なプロパティ設定 $this->posts = $posts; // 簡潔な初期化 } public function canCreateNewPost(): bool // ビジネスルールの抽出成功
} return new User( // 重要:Eloquent→Domain Entity 変換 $eloquentUser->id, //
ORM固有データをドメインオブジェクトに $eloquentUser->name, // この変換層がフレームワーク依存を隠蔽 $eloquentUser->email, // UseCase層はDomain Entityしか知らない $eloquentUser->posts->toArray() // Eloquent Collectionを普通の配列に変換 ); // この設計でUseCase層がフレームワーク非依存に } } <?php // Domain/Repository/UserRepositoryInterface.php namespace App\Domain\Repository; // Repository Pattern導入:データアクセスの抽象化 interface UserRepositoryInterface // データアクセスの契約定義 { public function findById(int $id): ?User; // Domain Entityを返却:フレームワーク非依存 public function save(User $user): void; // Domain Entityを受け取り:純粋なビジネスオブジェクト } // このInterfaceがEloquentからDoctrineへの移行を可能にする // Infrastructure/Repository/EloquentUserRepository.php // Eloquent実装版Repository class EloquentUserRepository implements UserRepositoryInterface // Interfaceの実装:Eloquent版 { public function findById(int $id): ?User // Domain Entity返却の契約守る { $eloquentUser = EloquentUser::with('posts')->find($id); // まだEloquent 使用:しかし局所化済み if (!$eloquentUser) { return null; // 結果なしの場合の処理
$user->getId(), // 正規化されたデータ取得 $user->getName(), // カプセル化されたアクセサ $user->getEmail(), // フレームワーク非依存データ $user->canCreateNewPost()
// ビジネスルール結果も含めて返却 ); // このUseCaseがフレームワーク非依存なのでSymfony移行が可能 } } <?php // Application/UseCase/CreateUserUseCase.php namespace App\Application\UseCase; // UseCase層の導入:アプリケーション処理の分離 class CreateUserUseCase // ビジネス処理の結節点:Controllerからの分離成功 { public function __construct( private UserRepositoryInterface $userRepository, // Repository Interfaceに依存:実装非依存 private ValidatorInterface $validator // バリデーション分離:Laravel Validatorからの脱却 ) {} // DIでInterface注入:コンクリートクラス非依存 public function execute(CreateUserRequest $request): UserDto // ビジネス処理のカプセル化 { // バリデーション実行:Laravel固有の方法からの脱却 $this->validator->validate($request); // Interface経由でバリデーション $user = new User( // Domain Entityでビジネスオブジェクト生成 0, // Repository で ID 生成:永続化詳細の隠蔽 $request->name, // シンプルなデータ渡し $request->email // フレームワーク固有処理なし ); $this->userRepository->save($user); // Repository経由で永続化:ORM詳細隠蔽 return new UserDto( // DTOで結果返却:ドメインオブジェクトからAPI用データ変換
$doctrineUser->getName(), // メソッド名は異なるが結果は同じ $doctrineUser->getEmail(), // ORMの違いをRepository層で吸収 $doctrineUser->getPosts()->toArray() // Collection→配列変換も同じ );
// この結果、UseCase層は何も変更せずに動作:移行成功 } } <?php // Infrastructure/Repository/DoctrineUserRepository.php namespace App\Infrastructure\Repository; // Infrastructure層:Symfony移行後の実装 class DoctrineUserRepository implements UserRepositoryInterface // 同じInterface契約:移行成功の証拠 { public function __construct( private EntityManagerInterface $entityManager // Doctrine 使用:Symfony固有ORM ) {} // Eloquentとは全く異なるORM、しかし同じ目的 public function findById(int $id): ?User // Interface契約遵守:同じDomain Entity返却 { $doctrineUser = $this->entityManager // Doctrine固有のデータ取得方法 ->getRepository(DoctrineUser::class) // Eloquent::find()とは異なるAPI ->find($id); // しかし同じ結果を得る if (!$doctrineUser) { return null; // エラーハンドリングも同じ } return new User( // 最重要:全く同じDomain Entity生成 $doctrineUser->getId(), // Doctrineエンティティ→Domain Entity変換
None
None
None
None
None
None
None
None
None
None
None
None
None
None
https://recruit.fusic.co.jp/