EC-CUBE 3.1 開発方針説明
機能カスタマイズ編機能カスタマイズのためのアーキテクチャ
概要
#1985 で、 v3.1 に向けた実験的な実装を行っています。詳細は主に
ShoppingControllerのソースコメントに記載しています。
• forward(Sub Request) を使用して、 Controller の処理を抽象化。継承
を使用せず、処理をオーバーライドできるようにした。
• Order 関連の FormType の抽象化
• 単価集計を CalculateService にまとめて、 Strategy パターンを適用
• 支払を PaymentService にまとめて、 Adapter パターンを適用
概要
その他、以下アーキテクチャの変更をしています。
• Symfony3.2
• v3.1 では Symfony3.4 LTS を採用予定
• Silex2.0
• Pimple3.0
• Doctrine2.5
• SensioFrameworkExtraBundle
• Inheritance Mapping
カスタマイズ方法の改善
• アノテーションの採用
• forward(Sub Request) の使用
• Inheritance Mappingの採用
• 単価集計や支払いなどの処理にデザインパターンを適用
• プラグインを使用しないカスタマイズ
アノテーションの採用
•新たに、 Doctrine アノテーション、 SensioFrameworkExtraBundle アノ
テーションが使用できるようになりました。
•Entity の定義や、 コントローラのルーティング設定をアノテーションで
記述できるようになり、より簡易に拡張が可能になりました。
※現在、既存のエンティティや、ルーティングは従来の Yaml や PHP での定義となっていますが、
将来的にはすべてアノテーションに置き換えられる予定です。
サポートされているアノテーション
•Doctrineアノテーション
•SensioFrameworkExtraBundleアノテーション
サポートされているアノテーション
Doctrine アノテーション
http://docs.doctrine-project.org/projects/doctrine-
orm/en/latest/reference/annotations-reference.html
@Column, @Entity, @GeneratedValue, @Index, @Id, @InheritanceType,
@JoinColumn, @JoinColumns, @JoinTable, @ManyToOne,
@ManyToMany, @MappedSuperclass, @OneToOne, @OneToMany,
@OrderBy, @SequenceGenerator, @Table, @UniqueConstraint...
サポートされているアノテーション
SensioFrameworkExtraBundle アノテーション
http://symfony.com/doc/current/bundles/SensioFrameworkExtraBundle/index
.html
• @Route 及び @Method
• @Template
• @Security
forward(Sub Request) の使用
従来、本体に手を入れずに、コントローラの処理を拡張する場合は、主に
以下のような方法がありました。
• 継承して別のインスタンスへ置き変える
• イベントハンドラで頑張る
これらの方法は、プラグインとコントローラの結合度が強くなり、開発効
率を下げる要因となっていました。
今回、カテゴリなどのブロックの処理に使用している Sub Request を流用
し、コントローラ内の処理を簡便に、他のコントローラへ移譲できるよう
になりました。
forward(Sub Request) の使用
ApplicationTrait::forward($path, $requestParameters)というメソッドが追加
されており、 $path で指定したコントローラへ処理を移譲することができ
ます。
このメソッドは、Response を返します。
コントローラ内で、この Response を return すると、レスポンスが出力さ
れます。
return しなければ、内部の処理のみ実行されます。
コントローラのメソッドは、ルーティングを介して、緩く結合しているイ
メージです。
forward(Sub Request) の使用
例として、 ShoppingController::index() メソッドは以下のような実装になっ
ています。
src/Eccube/Controller/ShoppingController.php
/**
* 購入画面表示
*
* @Route("/", name="shopping")
* @Template("Shopping/index.twig")
*/
public function index(Application $app, Request $request)
{
// カートチェック
$response = $app->forward($app->path("shopping/checkToCart"));
if ($response->isRedirection() || $response->getContent()) {
return $response;
}
// 受注情報を初期化
$response = $app->forward($app->path("shopping/initializeOrder"));
if ($response->isRedirection() || $response->getContent()) {
return $response;
}
// 単価集計し, フォームを生成する
$app->forwardChain($app->path("shopping/calculateOrder"))
->forwardChain($app->path("shopping/createForm"));
src/Eccube/Controller/ShoppingController.php
// 受注のマイナスチェック
$response = $app->forward(
$app->path("shopping/checkToMinusPrice"));
if ($response->isRedirection() || $response->getContent()) {
return $response;
}
// 複数配送の場合、エラーメッセージを一度だけ表示
$app->forward($app->path("shopping/handleMultipleErrors"));
$Order = $app['request_scope']->get('Order');
$form = $app['request_scope']->get(OrderType::class);
return [
'form' => $form->createView(),
'Order' => $Order
];
}
forward(Sub Request) の使用
例えば、カートチェックの振舞いを変更したい場合は、
shopping/checkToCart のルーティングをオーバーライドしたメソッドを作
成します。この処理は、 app/Acme/Controller 以下や、プラグインなどで拡
張できます。
app/Acme/Controller/ExampleController.php
/**
* @Route("/shopping")
*/
class ExampleController
{
/**
* カート画面のチェック
*
* @Route("/checkToCart", name="shopping/checkToCart")
*/
public function checkToCart(Application $app, Request $request)
{
$cartService = $app['eccube.service.cart'];
// カートチェック
if (!$cartService->isLocked()) {
log_info('カートが存在しません');
// カートが存在しない、カートがロックされていない時はエラー
return $app->redirect($app->url('cart'));
}
app/Acme/Controller/ExampleController.php
// 独自の処理を記述
log_info('カートの内容をチェックしました');
// 各コントローラ間の値の受け渡しには $app['request_scope'] を使用可能
$Order = $app['request_scope']->get('Order');
if ($Order) {
$Order->setNote('独自カスタマイズ処理を通過しました');
$app['orm.em']->flush($Order);
}
return new Response();
}
}
forward(Sub Request) の使用
forwardChain を使用することで、複数の forward を連続してつなげること
も可能です。
forward を活用することにより、各ルーティングの処理をコンパクトにまと
めることができます。
依存するクラスも少ないため、簡単にテストを記述することが可能です。
ExampleTest
public function testCheckToCart()
{
$Controller = new ¥Eccube¥Controller¥ShoppingController();
$this->assertInstanceOf('¥Eccube¥Controller¥ShoppingController', $Controller);
$Request = Request::create($this->app->path('shopping/checkToCart'), 'GET');
$Response = $Controller->checkToCart($this->app, $Request);
$this->assertInstanceOf('¥Symfony¥Component¥HttpFoundation¥RedirectResponse', $Response);
$this->assertTrue($Response->isRedirect($this->app->url('cart')), $this->app->url('cart').'へリダイレクト');
}
ExampleTest
public function testCheckToCartIn()
{
$Controller = new ¥Eccube¥Controller¥ShoppingController();
// カートに商品を投入
$cartService = $this->app['eccube.service.cart'];
$cartService->addProduct(1);
$cartService->lock();
$this->assertInstanceOf('¥Eccube¥Controller¥ShoppingController', $Controller);
$Request = Request::create($this->app->path('shopping/checkToCart'), 'GET');
$Response = $Controller->checkToCart($this->app, $Request);
$this->assertInstanceOf('¥Symfony¥Component¥HttpFoundation¥Response', $Response);
$this->assertEmpty($Response->getContent(), '空のレスポンスを返却');
}
Inheritance Mappingの採用
Inheritance Mapping
http://docs.doctrine-project.org/projects/doctrine-
orm/en/latest/reference/inheritance-mapping.html
データベースのテーブルに新たなカラムを追加したい場合に Inheritance
Mapping を使用できるようになりました。
例えば、 商品(Product)に `ExampleField` という項目を追加したい場合は、
以下のようなクラスを作成し、 schema-toolでUPDATEするだけです!
※http://ec-cube.github.io/collaboration_migration#db-1
Acme/Entity/ExamplePayment.php
/**
* Product の拡張
* @Entity
* @Table(name="example_product")
*/
class ExamplePayment extends ¥Eccube¥Entity¥Product
{
/**
* @Column(name="example_field", type="string")
*/
public $ExampleField;
}
単価集計や、支払いなどの処理にデザインパターンを適用
一部のビジネスロジックにデザインパターンを適用し、柔軟かつ効率的に
カスタマイズできるようになりました。
以下、 ShoppingController の一部です。
プラグイン側では、 CalculateStrategy や PaymentMethod クラスを実装す
ることで、独自の決済手段を実装可能です。
現在は、商品購入処理のみとなっていますが、商品管理など他の機能にも
適用していく予定です。
src/Eccube/Controller/ShoppingController.php
// 購入処理
// 集計は,この1行で実行可能
// プラグインで CalculateStrategy をセットしたりしてアルゴリズムの変更が可能
// 集計はステートレスな実装とし、再計算時に状態を考慮しなくても良いようにする
$app['eccube.service.calculate']($Order, $Order->getCustomer())->calculate();
// 支払処理
$paymentService = $app['eccube.service.payment']($Order->getPayment()->getServiceClass());
// PaymentMethod クラスは、 Cash(銀行振込)、 CreditCard(クレジットカード)などを取得する
$paymentMethod = $app['payment.method.request']($Order->getPayment()->getMethodClass(), $form, $request);
// PaymentMethod 内の処理で、必要に応じて別のコントローラへ forward(移譲)可能
$dispatcher = $paymentService->dispatch($paymentMethod); // 決済処理中.
if ($dispatcher instanceof Response
&& ($dispatcher->isRedirection() || $dispatcher->getContent())) { // $paymentMethod->apply() が Response を返した場合は画面遷移
return $dispatcher;
}
$PaymentResult = $paymentService->doCheckout($paymentMethod); // 決済実行
if (!$PaymentResult->isSuccess()) {
$em->getConnection()->rollback();
return $app->redirect($app->url('shopping_error'));
}
プラグインを使用しないカスタマイズ
新たに `app/Acme` 以下に、カスタマイズ用のプログラムを置けるようにな
りました。
• プラグインにするまでもないような、ちょっとしたカスタマイズ
• 既存のプラグインの振舞いを変更したい場合
• プラグインでは対応しにくい大規模カスタマイズ
などに利用できます。
※`Acme` という namespace は、任意のものに変更可能です。
参考実装
• プラグインの参考実装
• プラグインを使用しないカスタマイズの参考実装
プラグインの参考実装
https://github.com/nanasess/ec-
cube/tree/CalculateStrategy/app/Plugin/ExamplePlugin
• Plugin¥ExamplePlugin¥Controller¥ExampleController -
ShoppingController をオーバーライドし、独自の決済ボタンを実装して
います。
• Plugin¥ExamplePlugin¥Payment¥Method¥ExamplePaymentCreditCard
- 独自の決済処理を実装しています。
• Plugin¥ExamplePlugin¥Entity¥ExamplePayment - dtb_payment に独自
のカラムを追加しています。
プラグインを使用しないカスタマイズの参考実装
https://github.com/nanasess/ec-cube/tree/CalculateStrategy/app/Acme
• Acme¥Controller¥TestController - 独自コントローラの作成例です。
• Acme¥Controller¥AController - 上記 TestController の拡張例です。
• Acme¥Controller¥RoutingTestController - 管理画面, user_data の拡張例
です。
• Acme¥Entity¥ExtendedProduct - エンティティの拡張例です。 public プ
ロパティを使用しています。
thanks.

201703 EC-CUBE 3.1開発方針説明会:機能カスタマイズ編 02_機能カスタマイズのためのアーキテクチャ