Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v3.n に向けてのアーキテクチャ変更 #2127

Closed
nanasess opened this issue Feb 20, 2017 · 0 comments
Closed

v3.n に向けてのアーキテクチャ変更 #2127

nanasess opened this issue Feb 20, 2017 · 0 comments
Labels
document Improvements or additions to documentation experimental
Milestone

Comments

@nanasess
Copy link
Contributor

nanasess commented Feb 20, 2017

v3.n への課題として、 #1985 の内容を issues としておきます。

experimental/3.1 ブランチ は、 v3.1 向けた実験的な実装です。詳細は ShoppingController のソースコメントに記載

  • 機能カスタマイズ性の向上に向けた機構改善 #1984
  • forward(Sub Request) を使用して、 Controller の処理を抽象化。継承を使用せず、処理をオーバーライドできるようにした。
  • Order 関連の FormType の抽象化
  • 単価集計を CalculateService にまとめて、 Strategy パターンを適用
  • 支払を PaymentService にまとめて、 Adapter パターンを適用

その他、以下アーキテクチャの変更をしています。

  • Symfony3.2
    • v3.n では Symfony3.4 LTS を採用予定
  • Silex2.0
  • Doctrine2.5
  • SensioFrameworkExtraBundle
  • Inheritance Mapping

カスタマイズ方法の改善

アノテーションの採用

新たに、 Doctrine アノテーション、 SensioFrameworkExtraBundle アノテーションが使用できるようになりました。
Entity の定義や、 コントローラのルーティング設定をアノテーションで記述できるようになり、より簡易に拡張が可能になりました。

  • 現在、既存のエンティティや、ルーティングは従来の Yaml や PHP での定義となっていますが、将来的にはすべてアノテーションに置き換えられる予定です。

サポートされているアノテーション

Doctrine アノテーション
SensioFrameworkExtraBundle アノテーション

forward(Sub Request) の使用

従来、本体に手を入れずに、コントローラの処理を拡張する場合は、主に以下のような方法がありました。

  • 継承して別のインスタンスへ置き変える
  • イベントハンドラで頑張る

これらの方法は、プラグインとコントローラの結合度が強くなり、開発効率を下げる要因となっていました。

今回、カテゴリなどのブロックの処理に使用している Sub Request を流用し、コントローラ内の処理を簡便に、他のコントローラへ移譲できるようになりました。

ApplicationTrait::forward($path, $requestParameters) というメソッドが追加されており、 $path で指定したコントローラへ処理を移譲することができます。
このメソッドは、Response を返します。
コントローラ内で、この Responsereturn すると、レスポンスが出力されます。
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 の採用

データベースのテーブルに新たなカラムを追加したい場合に 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 プロパティを使用しています。

その他

  • カートを親子構造にして、セット商品や、アップセル、クロスセル等を実装しやすくする
  • @ParamConverter アノテーションも使いたい
@Yangsin Yangsin added this to the 3.1.0 milestone Feb 20, 2017
@Yangsin Yangsin changed the title v3.1 に向けてのアーキテクチャ変更 v3.n に向けてのアーキテクチャ変更 Jul 18, 2018
@okazy okazy added the document Improvements or additions to documentation label Sep 5, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
document Improvements or additions to documentation experimental
Projects
None yet
Development

No branches or pull requests

4 participants