@shin1x1
2015/09/14 DevLove関西
レイヤードアーキテクチャを意識した
PHPアプリケーションの構築 ver2
http://www.slideshare.net/shin1x1/lt-up-49929619
レイヤードアーキテクチャ
レイヤードアーキテクチャ
(c) 2015 Masashi Shinbara @shin1x1
• アプリケーションをレイヤ(層)に分割
• レイヤは自身の役割を担う
• レイヤ間で協調して、処理を行う
OSI参照モデル
(c) 2015 Masashi Shinbara @shin1x1
7.Application
6. Presentation
5. Session
4.Transport
3. Network
2. Data link
1. Physical
MVC
(c) 2015 Masashi Shinbara @shin1x1
View
Controller
Model
MVCの悩み
CakePHP
(c) 2015 Masashi Shinbara @shin1x1
FatController -> FatModel
Fat Model
(c) 2015 Masashi Shinbara @shin1x1
• 1,000行を超える Model
• Model の役割が多すぎる
• DAO / バリデーション / ビジネスロジックなど
MVC
(c) 2015 Masashi Shinbara @shin1x1
View
Controller
Model
MVC + Service
(c) 2015 Masashi Shinbara @shin1x1
View
Controller
Model
Service
サービスレイヤを追加
(c) 2015 Masashi Shinbara @shin1x1
• Controller と Model の間のレイヤ
• 主にビジネスロジックとバリデーションを担う
• 1アクションメソッドに、1サービスクラス
結果
(c) 2015 Masashi Shinbara @shin1x1
• Fat(Controller¦Model) を、ある程度解消
• レイヤの責務があいまい

=> サービスが、セッションを操作等
• レイヤ間の処理の流れが統一できていない

=> サービスが、コントローラを操作等
レイヤを意識
Laravel
(c) 2015 Masashi Shinbara @shin1x1
• Laraval + AngularJS
• Laravel は、REST API の提供のみ
• UI 関連の処理は、AngularJS
• レイヤの責務と流れを意識
レイヤ構造
(c) 2015 Masashi Shinbara @shin1x1
Routing
Controller
Eloquent(ORM)
Service
レイヤの役割
(c) 2015 Masashi Shinbara @shin1x1
Routing
Controller
ORM
Service
ルーティング、認証、フィルタ
HTTPリクエスト、レスポンス
バリデーション、サービス実行
事前条件検証、ビジネスロジック
データベースアクセス、
エンティティ固有の処理
例: 書籍の予約
(c) 2015 Masashi Shinbara @shin1x1
•会員制書籍予約Webアプリケーション
•利用者は認証トークンが必要
•書籍の予約を行う
•予約する書籍と予約数を指定
例: 書籍の予約
(c) 2015 Masashi Shinbara @shin1x1
•POST /reservation
•X-Api-Token: ユーザ認証トークン
•asin=書籍コード
•quantity=予約数
Routingレイヤ
(c) 2015 Masashi Shinbara @shin1x1
•認証はミドルウェア(フィルタ)で実行

=> 未ログインなら、401を返す
•URIからコントローラを実行
Routing
Route::group(['before' => 'api_auth'],
function () {

Route::post('/reservation',
ReservationController::class.'@create');

}
); 認証フィルタ
Routing
Route::group(['before' => 'api_auth'],
function () {

Route::post('/reservation',
ReservationController::class.'@create');

}
);
URIとコントローラのマッピング
Controllerレイヤ
(c) 2015 Masashi Shinbara @shin1x1
•POSTパラメータ、セッションユーザ情報を取得
•バリデーション実行

(asinとquantityパラメータの形式チェック)
•サービスに必要なパラメータを渡して実行

(HTTPの関心事はサービスに持ち込まない)
•HTTPレスポンスを返す
Controller
public function create()

{

$validator = (new ReservationValidatorBuilder())->create(Input::all());

if ($validator->fails()) {

return $this->responseValidationError($validator->messages());

}



$reservation = $this->service->book($this->getUser(), Input::all());



return $this->responseCreated($reservation);

}
バリデーション
Controller
public function create()

{

$validator = (new ReservationValidatorBuilder())->create(Input::all());

if ($validator->fails()) {

return $this->responseValidationError($validator->messages());

}



$reservation = $this->service->book($this->getUser(), Input::all());



return $this->responseCreated($reservation);

}
サービスの実行
Serviceレイヤ
(c) 2015 Masashi Shinbara @shin1x1
•事前条件の検証

(書籍の在庫が足りているか?等)
•予約処理を実行
•DB操作をEloquent(ORM)で行う
Service
public function book(User $user, array $inputs)

{

$book = Book::where('asin', $inputs['asin'])->first();

if (empty($book)) {

throw new PreconditionException('book_not_found');

}

if ($book->inventory < $inputs['quantity']) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation = new Reservation();

DB::transaction(function () use ($user, $book, &$reservation, $inputs) {

$affectedRows = $book->decrementInventory($inputs['quantity']);

if ($affectedRows !== 1) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation->user_id = $user->id;

$reservation->book_id = $book->id;

$reservation->quantity = $inputs['quantity'];

$reservation->reservation_code = $reservation->generateReservationCode();

$reservation->save();

});



return $reservation;

}
Service
public function book(User $user, array $inputs)

{

$book = Book::where('asin', $inputs['asin'])->first();

if (empty($book)) {

throw new PreconditionException('book_not_found');

}

if ($book->inventory < $inputs['quantity']) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation = new Reservation();

DB::transaction(function () use ($user, $book, &$reservation, $inputs) {

$affectedRows = $book->decrementInventory($inputs['quantity']);

if ($affectedRows !== 1) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation->user_id = $user->id;

$reservation->book_id = $book->id;

$reservation->quantity = $inputs['quantity'];

$reservation->reservation_code = $reservation->generateReservationCode();

$reservation->save();

});



return $reservation;

}
必要なパラメータ
Service
public function book(User $user, array $inputs)

{

$book = Book::where('asin', $inputs['asin'])->first();

if (empty($book)) {

throw new PreconditionException('book_not_found');

}

if ($book->inventory < $inputs['quantity']) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation = new Reservation();

DB::transaction(function () use ($user, $book, &$reservation, $inputs) {

$affectedRows = $book->decrementInventory($inputs['quantity']);

if ($affectedRows !== 1) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation->user_id = $user->id;

$reservation->book_id = $book->id;

$reservation->quantity = $inputs['quantity'];

$reservation->reservation_code = $reservation->generateReservationCode();

$reservation->save();

});



return $reservation;

}
事前条件の検証
Service
public function book(User $user, array $inputs)

{

$book = Book::where('asin', $inputs['asin'])->first();

if (empty($book)) {

throw new PreconditionException('book_not_found');

}

if ($book->inventory < $inputs['quantity']) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation = new Reservation();

DB::transaction(function () use ($user, $book, &$reservation, $inputs) {

$affectedRows = $book->decrementInventory($inputs['quantity']);

if ($affectedRows !== 1) {

throw new PreconditionException('not_enough_book_inventory');

}



$reservation->user_id = $user->id;

$reservation->book_id = $book->id;

$reservation->quantity = $inputs['quantity'];

$reservation->reservation_code = $reservation->generateReservationCode();

$reservation->save();

});



return $reservation;

}
ビジネスロジックの実行
レイヤの依存、処理の流れ
(c) 2015 Masashi Shinbara @shin1x1
Routing
Controller
Eloquent(ORM)
Service
レイヤの依存、処理の流れ
(c) 2015 Masashi Shinbara @shin1x1
Routing
Controller
Eloquent(ORM)
Service
流れを一方向に固定
レイヤをまたぐのはアリ
(c) 2015 Masashi Shinbara @shin1x1
Routing
Controller
Eloquent(ORM)
Service
レイヤをまたぐのはアリ
(c) 2015 Masashi Shinbara @shin1x1
Routing
Controller
Eloquent(ORM)
Service
方向は変えない
サービスを中心に考える
(c) 2015 Masashi Shinbara @shin1x1
•サービス(ビジネスドメイン)が中心
•事前条件検証とビジネスロジック
•HTTPの関心事は持ち込まない

必要なもの引数で渡す(scalar, array, object)
ドメインをサービスで表現
(c) 2015 Masashi Shinbara @shin1x1
•クラス名、メソッド名はドメインの用語で

(ユビキタス言語)
•ユースケースをメソッドに実装
•ex) 書籍の予約

=> ReservationService#book()
サービスから作る
(c) 2015 Masashi Shinbara @shin1x1
• サービスとテストを先に実装
• サービスの最初の利用者は、テスト
• サービスを Web に結ぶのが、コントローラ
• サービスは、バッチ処理等からも使える
ドメインごとに名前空間を分ける
[package]
+ [AcmeReservation]
+ [Controller]
+ [Service]
+ [Model]
+ [Validation]
[AcmeUser]
+ [Controller]
+ [Service]
+ [Model]
+ [Validation]
PSR-4
結果
(c) 2015 Masashi Shinbara @shin1x1
• レイヤの役割に専念できる
• 流れが一方向なので、依存の方向が明確に
• サービス(ドメイン)に集中
• サービスをどう分割していくかが課題
まとめ
(c) 2015 Masashi Shinbara @shin1x1
• MVCだけじゃない
• 自分でレイヤ構造を考える
• レイヤの責務と処理の流れ
@shin1x1
(c) 2015 Masashi Shinbara @shin1x1

レイヤードアーキテクチャを意識した PHPアプリケーションの構築 ver2

  • 1.
  • 2.
  • 3.
  • 4.
    レイヤードアーキテクチャ (c) 2015 MasashiShinbara @shin1x1 • アプリケーションをレイヤ(層)に分割 • レイヤは自身の役割を担う • レイヤ間で協調して、処理を行う
  • 5.
    OSI参照モデル (c) 2015 MasashiShinbara @shin1x1 7.Application 6. Presentation 5. Session 4.Transport 3. Network 2. Data link 1. Physical
  • 6.
    MVC (c) 2015 MasashiShinbara @shin1x1 View Controller Model
  • 7.
  • 8.
    CakePHP (c) 2015 MasashiShinbara @shin1x1 FatController -> FatModel
  • 9.
    Fat Model (c) 2015Masashi Shinbara @shin1x1 • 1,000行を超える Model • Model の役割が多すぎる • DAO / バリデーション / ビジネスロジックなど
  • 10.
    MVC (c) 2015 MasashiShinbara @shin1x1 View Controller Model
  • 11.
    MVC + Service (c)2015 Masashi Shinbara @shin1x1 View Controller Model Service
  • 12.
    サービスレイヤを追加 (c) 2015 MasashiShinbara @shin1x1 • Controller と Model の間のレイヤ • 主にビジネスロジックとバリデーションを担う • 1アクションメソッドに、1サービスクラス
  • 13.
    結果 (c) 2015 MasashiShinbara @shin1x1 • Fat(Controller¦Model) を、ある程度解消 • レイヤの責務があいまい
 => サービスが、セッションを操作等 • レイヤ間の処理の流れが統一できていない
 => サービスが、コントローラを操作等
  • 14.
  • 15.
    Laravel (c) 2015 MasashiShinbara @shin1x1 • Laraval + AngularJS • Laravel は、REST API の提供のみ • UI 関連の処理は、AngularJS • レイヤの責務と流れを意識
  • 16.
    レイヤ構造 (c) 2015 MasashiShinbara @shin1x1 Routing Controller Eloquent(ORM) Service
  • 17.
    レイヤの役割 (c) 2015 MasashiShinbara @shin1x1 Routing Controller ORM Service ルーティング、認証、フィルタ HTTPリクエスト、レスポンス バリデーション、サービス実行 事前条件検証、ビジネスロジック データベースアクセス、 エンティティ固有の処理
  • 18.
    例: 書籍の予約 (c) 2015Masashi Shinbara @shin1x1 •会員制書籍予約Webアプリケーション •利用者は認証トークンが必要 •書籍の予約を行う •予約する書籍と予約数を指定
  • 19.
    例: 書籍の予約 (c) 2015Masashi Shinbara @shin1x1 •POST /reservation •X-Api-Token: ユーザ認証トークン •asin=書籍コード •quantity=予約数
  • 20.
    Routingレイヤ (c) 2015 MasashiShinbara @shin1x1 •認証はミドルウェア(フィルタ)で実行
 => 未ログインなら、401を返す •URIからコントローラを実行
  • 21.
    Routing Route::group(['before' => 'api_auth'], function() {
 Route::post('/reservation', ReservationController::class.'@create');
 } ); 認証フィルタ
  • 22.
    Routing Route::group(['before' => 'api_auth'], function() {
 Route::post('/reservation', ReservationController::class.'@create');
 } ); URIとコントローラのマッピング
  • 23.
    Controllerレイヤ (c) 2015 MasashiShinbara @shin1x1 •POSTパラメータ、セッションユーザ情報を取得 •バリデーション実行
 (asinとquantityパラメータの形式チェック) •サービスに必要なパラメータを渡して実行
 (HTTPの関心事はサービスに持ち込まない) •HTTPレスポンスを返す
  • 24.
    Controller public function create()
 {
 $validator= (new ReservationValidatorBuilder())->create(Input::all());
 if ($validator->fails()) {
 return $this->responseValidationError($validator->messages());
 }
 
 $reservation = $this->service->book($this->getUser(), Input::all());
 
 return $this->responseCreated($reservation);
 } バリデーション
  • 25.
    Controller public function create()
 {
 $validator= (new ReservationValidatorBuilder())->create(Input::all());
 if ($validator->fails()) {
 return $this->responseValidationError($validator->messages());
 }
 
 $reservation = $this->service->book($this->getUser(), Input::all());
 
 return $this->responseCreated($reservation);
 } サービスの実行
  • 26.
    Serviceレイヤ (c) 2015 MasashiShinbara @shin1x1 •事前条件の検証
 (書籍の在庫が足りているか?等) •予約処理を実行 •DB操作をEloquent(ORM)で行う
  • 27.
    Service public function book(User$user, array $inputs)
 {
 $book = Book::where('asin', $inputs['asin'])->first();
 if (empty($book)) {
 throw new PreconditionException('book_not_found');
 }
 if ($book->inventory < $inputs['quantity']) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation = new Reservation();
 DB::transaction(function () use ($user, $book, &$reservation, $inputs) {
 $affectedRows = $book->decrementInventory($inputs['quantity']);
 if ($affectedRows !== 1) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation->user_id = $user->id;
 $reservation->book_id = $book->id;
 $reservation->quantity = $inputs['quantity'];
 $reservation->reservation_code = $reservation->generateReservationCode();
 $reservation->save();
 });
 
 return $reservation;
 }
  • 28.
    Service public function book(User$user, array $inputs)
 {
 $book = Book::where('asin', $inputs['asin'])->first();
 if (empty($book)) {
 throw new PreconditionException('book_not_found');
 }
 if ($book->inventory < $inputs['quantity']) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation = new Reservation();
 DB::transaction(function () use ($user, $book, &$reservation, $inputs) {
 $affectedRows = $book->decrementInventory($inputs['quantity']);
 if ($affectedRows !== 1) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation->user_id = $user->id;
 $reservation->book_id = $book->id;
 $reservation->quantity = $inputs['quantity'];
 $reservation->reservation_code = $reservation->generateReservationCode();
 $reservation->save();
 });
 
 return $reservation;
 } 必要なパラメータ
  • 29.
    Service public function book(User$user, array $inputs)
 {
 $book = Book::where('asin', $inputs['asin'])->first();
 if (empty($book)) {
 throw new PreconditionException('book_not_found');
 }
 if ($book->inventory < $inputs['quantity']) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation = new Reservation();
 DB::transaction(function () use ($user, $book, &$reservation, $inputs) {
 $affectedRows = $book->decrementInventory($inputs['quantity']);
 if ($affectedRows !== 1) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation->user_id = $user->id;
 $reservation->book_id = $book->id;
 $reservation->quantity = $inputs['quantity'];
 $reservation->reservation_code = $reservation->generateReservationCode();
 $reservation->save();
 });
 
 return $reservation;
 } 事前条件の検証
  • 30.
    Service public function book(User$user, array $inputs)
 {
 $book = Book::where('asin', $inputs['asin'])->first();
 if (empty($book)) {
 throw new PreconditionException('book_not_found');
 }
 if ($book->inventory < $inputs['quantity']) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation = new Reservation();
 DB::transaction(function () use ($user, $book, &$reservation, $inputs) {
 $affectedRows = $book->decrementInventory($inputs['quantity']);
 if ($affectedRows !== 1) {
 throw new PreconditionException('not_enough_book_inventory');
 }
 
 $reservation->user_id = $user->id;
 $reservation->book_id = $book->id;
 $reservation->quantity = $inputs['quantity'];
 $reservation->reservation_code = $reservation->generateReservationCode();
 $reservation->save();
 });
 
 return $reservation;
 } ビジネスロジックの実行
  • 31.
    レイヤの依存、処理の流れ (c) 2015 MasashiShinbara @shin1x1 Routing Controller Eloquent(ORM) Service
  • 32.
    レイヤの依存、処理の流れ (c) 2015 MasashiShinbara @shin1x1 Routing Controller Eloquent(ORM) Service 流れを一方向に固定
  • 33.
    レイヤをまたぐのはアリ (c) 2015 MasashiShinbara @shin1x1 Routing Controller Eloquent(ORM) Service
  • 34.
    レイヤをまたぐのはアリ (c) 2015 MasashiShinbara @shin1x1 Routing Controller Eloquent(ORM) Service 方向は変えない
  • 35.
    サービスを中心に考える (c) 2015 MasashiShinbara @shin1x1 •サービス(ビジネスドメイン)が中心 •事前条件検証とビジネスロジック •HTTPの関心事は持ち込まない
 必要なもの引数で渡す(scalar, array, object)
  • 36.
    ドメインをサービスで表現 (c) 2015 MasashiShinbara @shin1x1 •クラス名、メソッド名はドメインの用語で
 (ユビキタス言語) •ユースケースをメソッドに実装 •ex) 書籍の予約
 => ReservationService#book()
  • 37.
    サービスから作る (c) 2015 MasashiShinbara @shin1x1 • サービスとテストを先に実装 • サービスの最初の利用者は、テスト • サービスを Web に結ぶのが、コントローラ • サービスは、バッチ処理等からも使える
  • 38.
    ドメインごとに名前空間を分ける [package] + [AcmeReservation] + [Controller] +[Service] + [Model] + [Validation] [AcmeUser] + [Controller] + [Service] + [Model] + [Validation] PSR-4
  • 39.
    結果 (c) 2015 MasashiShinbara @shin1x1 • レイヤの役割に専念できる • 流れが一方向なので、依存の方向が明確に • サービス(ドメイン)に集中 • サービスをどう分割していくかが課題
  • 40.
    まとめ (c) 2015 MasashiShinbara @shin1x1 • MVCだけじゃない • 自分でレイヤ構造を考える • レイヤの責務と処理の流れ
  • 41.
    @shin1x1 (c) 2015 MasashiShinbara @shin1x1