Share
Embed
どうしても Eloquent でリポジトリパターンをやりたいんだ #phpcon_niigata / phpcon-niigata-2026
2026年11月14日に新潟県新潟市・朱鷺メッセで開催されたPHPカンファレンス新潟2026の登壇資料です。
shogogg
November 13, 2026
Transcript
73 slides
-
どうしても Eloquent で リポジトリパターンをやりたいんだ Nov. 14 2026 PHP CONFERENCE NIIGATA
2026 Shogo Kawase / @shogogg -
自己紹介 河瀨 翔吾 / Shogo Kawase エンジニアリングマネージャー I LOVE... 妻
/ 設計 / 型安全 / アジャイル /ももクロ 麻雀 / F1 / マリオカート / ACE COMBAT shogogg shogogg -
おしながき 1 Laravel Way と Eloquent 2 Eloquent × リポジトリパターンを採用してもいいのか?
3 Eloquent × リポジトリパターン実践例 4 まとめ -
おしながき 1 Laravel Way と Eloquent 2 Eloquent × リポジトリパターンを採用してもいいのか?
3 Eloquent × リポジトリパターン実践例 4 まとめ -
Batteries Included • 元々は Python における哲学 • 個別のパッケージなしに、すぐ利用できる高機能で多目的な標準ライブラリ を備える •
Python のフレームワーク Django もこれを明示的に標榜 -
Laravel as a whole is very inspired by Ruby on
Rails more than anything. Laravel のすべてはなによりも Ruby on Rails に強い影響を受けている Taylor Otwell 2024年, Podcast「Taylor Otwell's opinions on PHP, React, Laravel and Lamborghini Memes」より -
Ruby on Rails, Django, even, like, .NET MVC, which I
was using at work at the time, it takes a lot of inspiration from those various places. Ruby on Rails, Django, そして当時職場で使っていた .NET MVC など 様々なものから多くのインスピレーションを得ています Taylor Otwell 2024年, Podcast「Taylor Otwell's opinions on PHP, React, Laravel and Lamborghini Memes」より -
Batteries Included in Laravel • Web 開発に必要なものをあらかじめ備え、豊富なエコシステムを持つ → 開発者をツール選定の苦労から解放 •
設計方針さえもフレームワークから強い意見として提示 → 開発者をアーキテクチャ検討の苦悩から解放 -
So with Laravel, a lot of those opinions are baked
in. Laravel にはそういった意見(設計方針)があらかじめ組み込まれているのです。 Taylor Otwell 2026年, Podcast「Taylor Otwell on Why Laravel + AI Pair so Well, and the Future of Programming」より -
Laravel Way • 余計なものを付け足さず、持ち込まず、Laravel の機能を使う • Migration, Controller, Model などのファイルは
artisan make:* コマンドを 使って生成する • 設定より規約 -
Follow Laravel conventions unless the domain or an existing schema
requires an override. ドメインや既存スキーマが上書きを必要としない限り、Laravelの規約に従うこと Laravel Boost 「Architecture Best Practices」より -
Eloquent? • Laravel に搭載されている強力な ORM であり Query Builder • Active
Record パターン を採用 -
Active Record? • データベースの1行=1オブジェクト • プロパティを書き換え save() で永続化(保存)する • オブジェクトがデータ表現と
DB 操作の両方を担う • データ操作とビジネスロジックが集約された Mutable(可変) なモデル -
おしながき 1 Laravel Way と Eloquent 2 Eloquent × リポジトリパターンを採用してもいいのか?
3 Eloquent × リポジトリパターン実践例 4 まとめ -
どうしても Eloquent で リポジトリパターンをやりたいんだ Nov. 14 2026 PHP CONFERENCE NIIGATA
2026 Shogo Kawase / @shogogg -
There is no right or wrong in architecture, there are
only trade-offs. アーキテクチャには正解も不正解もない。あるのはトレードオフだけだ。 Neal Ford 2020年,「ソフトウェアアーキテクチャの基礎 ― エンジニアリングに基づく体系的アプローチ」より ※リンク先は第2版(日本語版) -
いくつもの要素を踏まえてアーキテクチャは決ま る 事業ドメインの 複雑さ ビジネスの 不確実度 保守性 学習コスト 既存システム との整合性
チームのスキル 習熟度 AIにどこまで 任せるか スケジュール -
リポジトリパターンを採用したい理由 一般論 • データアクセスロジックの集約と再利用性の向上 • インターフェースによるテスト容易性の向上 • データソース変更への耐性向上 • ビジネスロジックとインフラ層の分離・疎結合化
Active Record(Eloquent)の場合 • 責務が多すぎる • Mutable だから -
それでも Laravel を選ぶ理由 • 自分がよく知っている • 経験者が多く、学習コストが低い • Laravel Way
を離れても依然として使える強力な基盤 ◦ Routing, Validation, Migration, Mailable, Queue, FileSystem, etc... • 相対的に EOL のリスクが低い • 既に Laravel を使っている • etc... -
失うものは多い • Auth 関連便利機能の数々 • Route Model Binding • Model
Factory • Eloquent Events / Observers • 公式/サードパーティによる Eloquent を前提とした便利な機能・拡張の数々 -
There is no right or wrong in architecture, there are
only trade-offs. アーキテクチャには正解も不正解もない。あるのはトレードオフだけだ。 Neal Ford 2020年,「ソフトウェアアーキテクチャの基礎 ― エンジニアリングに基づく体系的アプローチ」より ※リンク先は第2版(日本語版) -
おしながき 1 Laravel Way と Eloquent 2 Eloquent × リポジトリパターンを採用してもいいのか?
3 Eloquent × リポジトリパターン実践例 4 まとめ -
詰め替えがめんどうくさい public function toDomainModel(): Order { return new Order( //
... created_at: Carbon::make($this->created_at), updated_at: Carbon::make($this->updated_at), payment_method: PaymentMethod::from($this->payment_method), shipping_method: ShippingMethod::from($this->shipping_method), name: new StructuredName( 'givenName' => $this->given_name, 'familyName' => $this->family_name, ), items: $this->items ->map(fn (OrderItem $x): DomainOrderItem => $x->toDomainModel()) ->toArray(), // ... ); } ※コードは Laravel 13 時点でのものです。古い/新しい Laravel では異なる場合があります -
Casting /** * Get the attributes that should be cast.
* * @return array<string, string> */ protected function casts(): array { return [ // created_at, updated_at は Carbon で読み書きができる 'created_at' => 'datetime', 'updated_at' => 'datetime', ]; } ※コードは Laravel 13 時点でのものです。古い/新しい Laravel では異なる場合があります -
Enum Casting /** * Get the attributes that should be
cast. * * @return array<string, string> */ protected function casts(): array { return [ // PaymentMethod, ShippingMethod はいずれも Enum 'payment_method' => PaymentMethod::class, 'shipping_method' => ShippingMethod::class, ]; } ※コードは Laravel 13 時点でのものです。古い/新しい Laravel では異なる場合があります -
Value Object Casting /** * Get the attributes that should
be cast. * * @return array<string, string> */ protected function casts(): array { return [ // AsStructuredName は CastsAttributes を実装したクラス 'name' => AsStructuredName::class, ]; } ※コードは Laravel 13 時点でのものです。古い/新しい Laravel では異なる場合があります -
Value Object Casting class AsStructuredName implements CastsAttributes { public function
get(Model $model, string $key, mixed $value, array $attributes): Address { return new StructuredName( givenName: $attributes['given_name'], familyName: $attributes['family_name'], ); } public function set(Model $model, string $key, mixed $value, array $attributes): array { return [ 'given_name' => $value->givenName, 'family_name' => $value->familyName, ]; } } ※コードは Laravel 13 時点でのものです。古い/新しい Laravel では異なる場合があります -
Accessors // リレーションの定義 public function itemsRelation(): HasMany { return $this->hasMany(OrderItem::class,
'order_id'); } // Accessor: $this->items は get の結果を返すようになる protected function items(): Attribute { return Attribute::make( get: fn (): array => $this->itemsRelation ->map(fn (OrderItem $x): DomainOrderItem => $x->toDomainModel()) ->toArray(), ); } ※コードは Laravel 13 時点でのものです。古い/新しい Laravel では異なる場合があります -
名前付き引数( Named Arguments ) /* 位置引数(Positional Arguments)*/ echo pow(2, 6);
// 64 /* 名前付き引数(Named Arguments)※PHP 8.0以降 */ echo pow(num: 2, exponent: 7); // 128 /* 名前付き引数は順番を入れ替えてもOK */ echo pow(exponent: 8, num: 2); // 256 -
引数での ... の使用 /* Argument Unpacking */ $args = [2,
6]; echo pow(...$args); // 64 /* 順序を入れ替えると、もちろん結果が変わる */ $args = [6, 2]; echo pow(...$args); // 36 /* PHP 8.0 からは連想配列も使える */ $args = ['num' => 2, 'exponent' => 8]; echo pow(...$args); // 256 /* 順序を入れ替えるとどうなる……? */ $args = ['exponent' => 8, 'num' => 2]; echo pow(...$args); // ? -
引数での ... の使用 /* Argument Unpacking */ $args = [2,
6]; echo pow(...$args); // 64 /* 順序を入れ替えると、もちろん結果が変わる */ $args = [6, 2]; echo pow(...$args); // 36 /* PHP 8.0 からは連想配列も使える */ $args = ['num' => 2, 'exponent' => 8]; echo pow(...$args); // 256 /* 順序を入れ替えても結果は変わらない!=名前付き引数の仕組みが使われている! */ $args = ['exponent' => 8, 'num' => 2]; echo pow(...$args); // 256 -
php-src/Zend/zend_vm_def.h if (UNEXPECTED(name)) { void *cache_slot[2] = {NULL, NULL}; have_named_params
= 1; top = zend_handle_named_arg(&EX(call), name, &arg_num, cache_slot); if (UNEXPECTED(!top)) { FREE_OP1(); HANDLE_EXCEPTION(); } } else { if (have_named_params) { zend_throw_error(NULL, "Cannot use positional argument after named argument during unpacking"); FREE_OP1(); HANDLE_EXCEPTION(); } top = ZEND_CALL_ARG(EX(call), arg_num); ZEND_CALL_NUM_ARGS(EX(call))++; } php-src/Zend/zend_vm_def.h より抜粋 -
abstract class Table extends \Illuminate\Database\Eloquent\Model { protected static array $reflectionClass
= []; protected static array $domainModelAttributes = []; // Returns the domain model class name. abstract protected function getDomainModelName(): string; final protected static function getReflectionClass(string $className): ReflectionClass { // Returns the reflection class instance for domain model. } final protected static function getDomainModelAttributes(string $className): array { // Returns domain model attribute names as an array. } final protected function getDomainModelAttributeValues(): iterable { // Returns attributes for domain model. } final public function toDomainModel(): object { // Converts itself to domain model. } } final public function fromDomainModel(object $model): self { // Creates an instance from domain model. } -
ReflectionClass の生成とキャッシュ protected static array $reflectionClass = []; // Returns
the reflection class instance for domain model. final protected static function getReflectionClass(string $className): ReflectionClass { if (!isset(self::$reflectionClass[$className])) { self::$reflectionClass[$className] = new ReflectionClass($className); } return self::$reflectionClass[$className]; } -
プロパティ一覧の取得とキャッシュ protected static array $domainModelAttributes = []; // Returns domain
model attribute names as an array. final protected static function getDomainModelAttributes(string $className): array { if (!isset(self::$domainModelAttributes[$className])) { $constructor = self::getReflectionClass($className)->getConstructor(); if ($constructor === null) { throw new LogicException("There is no constructor for {$className}"); } self::$domainModelAttributes[$className] = array_map( fn (ReflectionParameter $param) => $param->getName(), $constructor->getParameters(), ); } return self::$domainModelAttributes[$className]; } -
ドメインモデルへの変換 // Returns attributes for domain model. final protected function
getDomainModelAttributeValues(): iterable { $className = $this->getDomainModelName(); foreach ($this->getDomainModelAttributes($className) as $key) { yield Str::camel($key) => $this->getAttribute($key); } } // Converts itself to domain model. final public function toDomainModel(): object { $className = $this->getDomainModelName(); $attributes = iterator_to_array($this->getDomainModelAttributeValues()); return self::getReflectionClass($className)->newInstanceArgs($attributes); } -
ドメインモデルからの生成 protected $fillable = [/* ... */]; // Creates an
instance from domain model. public function fromDomainModel(object $model): self { $attributes = []; foreach ($model->toAssoc() as $key => $value) { $attributes[Str::snake($key)] = $value; } return new static($attributes); } -
final class Order extends Table { protected $fillable = [/*
... */]; protected function getDomainModelName(): string { return DomainOrder::class; } protected function casts(): array { return [ 'created_at' => 'datetime', 'updated_at' => 'datetime', 'payment_method' => PaymentMethod::class, 'shipping_method' => ShippingMethod::class, 'name' => AsStructuredName::class, ]; } public function itemsRelation(): HasMany { return $this->hasMany(OrderItem::class, 'order_id'); } } protected function items(): Attribute { return Attribute::make( get: fn (): array => $this->itemsRelation ->map(fn (OrderItem $x): DomainOrderItem => $x->toDomainModel()) ->toArray(), ); } -
Repository final class OrderRepository { public function lookup(int $id): ?DomainOrder
{ return Order::with('itemsRelation')->find($id)?->toDomainModel(); } public function store(Order $model): void { $order = Order::fromDomainModel($model)->save(); // OrderItem を保存するコードが続く } public function remove(DomainOrder $order): void { // 必要に応じて OrderItem を削除するコードも記述する Order::fromDomainModel($order)->delete(); } } -
おしながき 1 Laravel Way と Eloquent 2 Eloquent × リポジトリパターンを採用してもいいのか?
3 Eloquent × リポジトリパターン実践例 4 まとめ -
まとめ • Laravel は “batteries included” なフレームワーク • Laravel を使い、Laravel
Way に従うことで価値に集中できる • ”アーキテクチャには正解も不正解もない。あるのはトレードオフだけ ” • Eloquent × Repository Pattern は Laravel Way から外れる茨の道 • それでも Laravel を使う価値は十分にある • Laravel Way を外れて得るものが失うものより多いのであれば、間違いではない • Eloquent と PHP 8 のパワー があれば、ボイラープレートの大分部はなくせる -
参考文献 • Do Things the Laravel Way(Laravel Boost) • Architecture
Best Practices(Laravel Boost) • Laravelは単なるバックエンドフレームワークではない。急速に進化し続けるLaravelエコシステ ム。(Ryuta Hamasaki) • Why Laravel apps break(武田 憲太郎) • Laravelへの異常な愛情 または私は如何にして心配するのを止めてEloquentを愛するようになった か(武田 憲太郎) • ソフトウェアアーキテクチャの基礎 第2版 (Mark Richards, Neal Ford 著 / 島田 浩二 訳)