v3.1 への課題として、 #1985 の内容を issues としておきます。
#1985 は、 v3.1 向けた実験的な実装です。詳細は ShoppingController のソースコメントに記載
- #1984
- forward(Sub Request) を使用して、 Controller の処理を抽象化。継承を使用せず、処理をオーバーライドできるようにした。
- Order 関連の FormType の抽象化
- 単価集計を CalculateService にまとめて、 Strategy パターンを適用
- 支払を PaymentService にまとめて、 Adapter パターンを適用
その他、以下アーキテクチャの変更をしています。
- Symfony3.2
- v3.1 では Symfony3.4 LTS を採用予定
- Silex2.0
- Doctrine2.5
- SensioFrameworkExtraBundle
- Inheritance Mapping
新たに、 Doctrine アノテーション、 SensioFrameworkExtraBundle アノテーションが使用できるようになりました。 Entity の定義や、 コントローラのルーティング設定をアノテーションで記述できるようになり、より簡易に拡張が可能になりました。
- 現在、既存のエンティティや、ルーティングは従来の Yaml や PHP での定義となっていますが、将来的にはすべてアノテーションに置き換えられる予定です。
- @Column
- @Entity
- @GeneratedValue
- @Index
- @Id
- @InheritanceType
- @JoinColumn
- @JoinColumns
- @JoinTable
- @ManyToOne
- @ManyToMany
- @MappedSuperclass
- @OneToOne
- @OneToMany
- @OrderBy
- @SequenceGenerator
- @Table
- @UniqueConstraint
- @Route 及び @Method
- @Template
- @Security
従来、本体に手を入れずに、コントローラの処理を拡張する場合は、主に以下のような方法がありました。
- 継承して別のインスタンスへ置き変える
- イベントハンドラで頑張る
これらの方法は、プラグインとコントローラの結合度が強くなり、開発効率を下げる要因となっていました。
今回、カテゴリなどのブロックの処理に使用している Sub Request を流用し、コントローラ内の処理を簡便に、他のコントローラへ移譲できるようになりました。
ApplicationTrait::forward($path, $requestParameters)
というメソッドが追加されており、 $path
で指定したコントローラへ処理を移譲することができます。
このメソッドは、Response
を返します。
コントローラ内で、この Response
を return
すると、レスポンスが出力されます。
return
しなければ、内部の処理のみ実行されます。
コントローラのメソッドは、ルーティングを介して、緩く結合しているイメージです。
例として、 ShoppingController::index()
メソッドは以下のような実装になっています。
/**
* 購入画面表示
*
* @Route("/", name="shopping")
* @Template("Shopping/index.twig")
*
* @param Application $app
* @param Request $request
* @return array
*/
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"));
// 受注のマイナスチェック
$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
];
}
例えば、カートチェックの振舞いを変更したい場合は、 shopping/checkToCart
のルーティングをオーバーライドしたメソッドを作成します。
この処理は、 app/Acme/Controller
以下や、プラグインなどで拡張できます。
/**
* @Route("/shopping")
*/
class ExampleController
{
/**
* カート画面のチェック
*
* @Route("/checkToCart", name="shopping/checkToCart")
* @param Application $app
* @param Request $request
* @return \Symfony\Component\HttpFoundation\RedirectResponse|Response
*/
public function checkToCart(Application $app, Request $request)
{
$cartService = $app['eccube.service.cart'];
// カートチェック
if (!$cartService->isLocked()) {
log_info('カートが存在しません');
// カートが存在しない、カートがロックされていない時はエラー
return $app->redirect($app->url('cart'));
}
// 独自の処理を記述
log_info('カートの内容をチェックしました');
// 各コントローラ間の値の受け渡しには $app['request_scope'] を使用可能
$Order = $app['request_scope']->get('Order');
if ($Order) {
$Order->setNote('独自カスタマイズ処理を通過しました');
$app['orm.em']->flush($Order);
}
return new Response();
}
}
forwardChain を使用することで、複数の forward を連続してつなげることも可能です。
forward を活用することにより、各ルーティングの処理をコンパクトにまとめることができます。 依存するクラスも少ないため、簡単にテストを記述することが可能です。
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').'へリダイレクト');
}
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 を使用できるようになりました。
例えば、 商品(Product)に ExampleField
という項目を追加したい場合は、以下のようなクラスを作成し、 schema-tool で UPDATE するだけです!
/**
* Product の拡張
* @Entity
* @Table(name="example_product")
*/
class ExamplePayment extends \Eccube\Entity\Product
{
/**
* @Column(name="example_field", type="string")
*/
public $ExampleField;
}
従来は、 OneToOne や OneToMany のリレーションを作成し、 JOIN で頑張るしかありませんでした。
一部のビジネスロジックにデザインパターンを適用し、柔軟かつ効率的にカスタマイズできるようになりました。 以下、 ShoppingController の一部です。 プラグイン側では、 CalculateStrategy や PaymentMethod クラスを実装することで、独自の決済手段を実装可能です。
// 購入処理
// 集計は,この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'));
}
$em->flush();
$em->getConnection()->commit();
log_info('購入処理完了', array($Order->getId()));
現在は、商品購入処理のみとなっていますが、商品管理など他の機能にも適用していく予定です。
新たに 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
に独自のカラムを追加しています。
本プラグインは、以下のコマンドでインストール可能です。
php app/console plugin:develop --code ExamplePlugin install
php app/console plugin:develop --code ExamplePlugin enable
- 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 プロパティを使用しています。