Skip to content

Instantly share code, notes, and snippets.

@nanasess
Last active February 28, 2017 01:15
Show Gist options
  • Save nanasess/6bdb9e8d497460c3db7d879fb2a1a6e5 to your computer and use it in GitHub Desktop.
Save nanasess/6bdb9e8d497460c3db7d879fb2a1a6e5 to your computer and use it in GitHub Desktop.

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

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 を使用できるようになりました。 例えば、 商品(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 プロパティを使用しています。
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment