Upgrade to Pro — share decks privately, control downloads, hide ads and more …

独立したコアレイヤパターンによる PHP アプリケーションの実装 / phpcon2018-independent-core-layer-pattern

shin1x1
December 15, 2018

独立したコアレイヤパターンによる PHP アプリケーションの実装 / phpcon2018-independent-core-layer-pattern

2018/12/15 PHP カンファレンス 2018

shin1x1

December 15, 2018
Tweet

More Decks by shin1x1

Other Decks in Technology

Transcript

  1. A. 対象ドメインが違うから フレームワーク = Web アプリケーション開発 アプリケーション = EC サイト、チャットサービ

    ス、業務システム等々 ドメインは重なるが一致するわけではない Web アプリケーションは効率的に作れるが、 EC サイトを効率化するのではない ドメイン特化のパッケージなどの方が近い 7
  2. 14

  3. 15

  4. 16

  5. 実装 API 顧客サービスのポイント加算 API PUT /customers/add_point customer_id = 会員ID add_point

    = 加算ポイント 顧客ポイントが加算される 更新後のポイントを JSON で返す 26
  6. Laravel アプリケーション app/ <-- for Laravel packages/ <-- Point application

    Acme/ Point/ Core/ <--- コアレイヤ Application/ <--- アプリケーションレイヤ 独自ディレクトリに配置 29
  7. [Core] AddPointUseCaseQuery interface AddPointUseCaseQuery { public function existsCustomerId( int $customerId):

    bool; public function findPoint(int $customerId): int; } 顧客 ID の存在確認 顧客ポイント取得 34
  8. [Core] AddPointUseCase インターフェイスを満たすインスタンスを コンストラクタで受け取る final class AddPointUseCase { private $query;

    private $command; public function __construct( AddPointUseCaseQuery $query, AddPointUseCaseCommand $command ) { $this->query = $query; $this->command = $command; } 36
  9. public function run(int $customerId, int $addPoint): int { // 事前条件の検証

    if ($addPoint <= 0) { throw new DomainRuleException(); } if (!$this->query->existsCustomerId($customerId)) { throw new DomainRuleException(); } // 顧客ポイントの加算 $this->command->addCustomerPoint( $customerId, $addPoint); // 加算後ポイントの取得 return $this->query->findPoint($customerId); } 37
  10. AppAddPointAdapter Query と Command を 1 クラスで実装 $this->customer = Eloquent

    final class AppAddPointAdapter implements AddPointUseCaseQuery, AddPointUseCaseCommand { // (snip) public function existsCustomerId( int $customerId): bool { return $this->customer->existsId($customerId); } // (snip) } 40
  11. ユースケース実行 Action クラスでユースケース実行 $customerId = filter_var( $request->json('customer_id'), FILTER_VALIDATE_INT); $addPoint =

    filter_var( $request->json('add_point'), FILTER_VALIDATE_INT); // ユースケースクラスを実行 $customerPoint = $this->useCase->run( $customerId, $addPoint ); return response()->json( ['customer_point' => $customerPoint]); 42
  12. curl で API 実行 $ curl "http://localhost:8000/api/customers/add_point" -X PUT -d

    '{"customer_id": 1, "add_point": 1}' -H "Content-Type: application/json" -H "Accept: application/json" | jq . { "customer_point": 101 } 43
  13. コアレイヤでドメインモデルを活用 アプリケーションドメインをモデルで表現 顧客 ID = CustomerId 加算ポイント = AddPoint 顧客ポイント

    = Point コアレイヤにドメインモデルを実装 レイヤ間のデータ受け渡しもモデルを利用 45
  14. 46

  15. ユースケース public function run(CustomerId $customerId , AddPoint $addPoint): Point {

    if (!$this->query->existsCustomerId($customerId)) { throw new DomainRuleException(); } $this->command->addCustomerPoint( $customerId, $addPoint); return $this->query->findPoint($customerId); } 47
  16. インターフェイス interface AddPointUseCaseQuery { public function existsCustomerId( CustomerId $customerId): bool;

    public function findPoint( CustomerId $customerId): Point; } interface AddPointUseCaseCommand { public function addCustomerPoint( CustomerId $customerId, AddPoint $addPoint): void; } 48
  17. Action public function __invoke(AddPointRequest $request): JsonResponse { $customerId = filter_var(

    $request->json('customer_id'), FILTER_VALIDATE_INT); $addPoint = filter_var( $request->json('add_point'), FILTER_VALIDATE_INT); $customerPoint = $this->useCase->run( CustomerId::of($customerId), // ドメインモデル AddPoint::of($addPoint) // ドメインモデル ); return response()->json( ['customer_point' => $customerPoint]); } 49
  18. 56

  19. フレームワークとの分離例 PHP 5.6 -> 7.3 / Laravel 4.2 -> 5.5

    (LTS) MVC + Service 効果的だった施策 独自ディレクトリにアプリケーション配置 Service に HTTP は持ち込まない Service は Eloquent に依存 Eloquent はそれほど大きく変わってなかった 変更箇所が多いと大変だった 58