Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.
Common duplication problems, and corresponding solutions are:
Complex classes like ShoppingController often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.
Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.
While breaking up the class, it is a good idea to analyze how other classes use ShoppingController, and based on these observations, apply Extract Interface, too.
1 | <?php |
||
57 | class ShoppingController extends AbstractShoppingController |
||
58 | { |
||
59 | /** |
||
60 | * @var BaseInfo |
||
61 | */ |
||
62 | protected $BaseInfo; |
||
63 | |||
64 | /** |
||
65 | * @var OrderHelper |
||
66 | */ |
||
67 | protected $orderHelper; |
||
68 | |||
69 | /** |
||
70 | * @var CartService |
||
71 | */ |
||
72 | protected $cartService; |
||
73 | |||
74 | /** |
||
75 | * @var ShoppingService |
||
76 | */ |
||
77 | protected $shoppingService; |
||
78 | |||
79 | /** |
||
80 | * @var CustomerAddressRepository |
||
81 | */ |
||
82 | protected $customerAddressRepository; |
||
83 | |||
84 | /** |
||
85 | * @var ParameterBag |
||
86 | */ |
||
87 | protected $parameterBag; |
||
88 | |||
89 | /** |
||
90 | * ShoppingController constructor. |
||
91 | * |
||
92 | * @param BaseInfo $BaseInfo |
||
|
|||
93 | * @param OrderHelper $orderHelper |
||
94 | * @param CartService $cartService |
||
95 | * @param ShoppingService $shoppingService |
||
96 | * @param CustomerAddressRepository $customerAddressRepository |
||
97 | * @param ParameterBag $parameterBag |
||
98 | */ |
||
99 | 36 | public function __construct( |
|
114 | |||
115 | /** |
||
116 | * 購入画面表示 |
||
117 | * |
||
118 | * @Route("/shopping", name="shopping") |
||
119 | * @Template("Shopping/index.twig") |
||
120 | */ |
||
121 | 28 | public function index(Request $request) |
|
157 | |||
158 | /** |
||
159 | * 購入確認画面から, 他の画面へのリダイレクト. |
||
160 | * 配送業者や支払方法、お問い合わせ情報をDBに保持してから遷移する. |
||
161 | * |
||
162 | * @Route("/shopping/redirect", name="shopping_redirect_to") |
||
163 | * @Template("Shopping/index.twig") |
||
164 | */ |
||
165 | 10 | View Code Duplication | public function redirectTo(Request $request) |
197 | |||
198 | /** |
||
199 | * 購入処理 |
||
200 | * |
||
201 | * @Route("/shopping/confirm", name="shopping_confirm") |
||
202 | * @Method("POST") |
||
203 | * @Template("Shopping/confirm.twig") |
||
204 | */ |
||
205 | 2 | View Code Duplication | public function confirm(Request $request) |
206 | { |
||
207 | // カートチェック |
||
208 | 2 | $response = $this->forwardToRoute('shopping_check_to_cart'); |
|
209 | 2 | if ($response->isRedirection() || $response->getContent()) { |
|
210 | return $response; |
||
211 | } |
||
212 | |||
213 | // 受注の存在チェック |
||
214 | 2 | $response = $this->forwardToRoute('shopping_exists_order'); |
|
215 | 2 | if ($response->isRedirection() || $response->getContent()) { |
|
216 | return $response; |
||
217 | } |
||
218 | |||
219 | // フォームの生成 |
||
220 | 2 | $this->forwardToRoute('shopping_create_form'); |
|
221 | 2 | $form = $this->parameterBag->get(OrderType::class); |
|
222 | 2 | $form->handleRequest($request); |
|
223 | |||
224 | 2 | $form = $this->parameterBag->get(OrderType::class); |
|
225 | 2 | $Order = $this->parameterBag->get('Order'); |
|
226 | |||
227 | 2 | $flowResult = $this->executePurchaseFlow($Order); |
|
228 | 2 | if ($flowResult->hasWarning() || $flowResult->hasError()) { |
|
229 | return $this->redirectToRoute('shopping_error'); |
||
230 | } |
||
231 | |||
232 | return [ |
||
233 | 2 | 'form' => $form->createView(), |
|
234 | 2 | 'Order' => $Order, |
|
235 | ]; |
||
236 | } |
||
237 | |||
238 | /** |
||
239 | * 購入処理 |
||
240 | * |
||
241 | * @Route("/shopping/order", name="shopping_order") |
||
242 | * @Method("POST") |
||
243 | * @Template("Shopping/index.twig") |
||
244 | */ |
||
245 | 2 | public function order(Request $request) |
|
246 | { |
||
247 | // カートチェック |
||
248 | 2 | $response = $this->forwardToRoute('shopping_check_to_cart'); |
|
249 | 2 | if ($response->isRedirection() || $response->getContent()) { |
|
250 | return $response; |
||
251 | } |
||
252 | |||
253 | // 受注の存在チェック |
||
254 | 2 | $response = $this->forwardToRoute('shopping_exists_order'); |
|
255 | 2 | if ($response->isRedirection() || $response->getContent()) { |
|
256 | return $response; |
||
257 | } |
||
258 | |||
259 | // form作成 |
||
260 | // FIXME イベントハンドラを外から渡したい |
||
261 | 2 | $this->forwardToRoute('shopping_create_form'); |
|
262 | |||
263 | 2 | $form = $this->parameterBag->get(OrderType::class); |
|
264 | 2 | $Order = $this->parameterBag->get('Order'); |
|
265 | 2 | $usePoint = $Order->getUsePoint(); |
|
266 | |||
267 | 2 | $form->handleRequest($request); |
|
268 | 2 | $Order->setUsePoint($usePoint); |
|
269 | |||
270 | // 受注処理 |
||
271 | 2 | $response = $this->forwardToRoute('shopping_complete_order'); |
|
272 | 2 | if ($response->isRedirection() || $response->getContent()) { |
|
273 | 2 | return $response; |
|
274 | } |
||
275 | |||
276 | log_info('購入チェックエラー', [$Order->getId()]); |
||
277 | |||
278 | return [ |
||
279 | 'form' => $form->createView(), |
||
280 | 'Order' => $Order, |
||
281 | ]; |
||
282 | } |
||
283 | |||
284 | /** |
||
285 | * 支払方法バーリデト |
||
286 | */ |
||
287 | private function isValidPayment(Application $app, $form) |
||
312 | |||
313 | /** |
||
314 | * 購入完了画面表示 |
||
315 | * |
||
316 | * @Route("/shopping/complete", name="shopping_complete") |
||
317 | * @Template("Shopping/complete.twig") |
||
318 | */ |
||
319 | 1 | public function complete(Request $request) |
|
353 | |||
354 | /** |
||
355 | * お届け先の設定一覧からの選択 |
||
356 | * |
||
357 | * @Route("/shopping/shipping/{id}", name="shopping_shipping", requirements={"id" = "\d+"}) |
||
358 | * @Template("Shopping/shipping.twig") |
||
359 | */ |
||
360 | 2 | public function shipping(Request $request, $id) |
|
361 | { |
||
362 | // カートチェック |
||
363 | 2 | $response = $this->forwardToRoute('shopping_check_to_cart'); |
|
364 | 2 | if ($response->isRedirection() || $response->getContent()) { |
|
365 | return $response; |
||
366 | } |
||
367 | |||
368 | 2 | if ('POST' === $request->getMethod()) { |
|
369 | 2 | $address = $request->get('address'); |
|
370 | |||
371 | 2 | if (is_null($address)) { |
|
372 | // 選択されていなければエラー |
||
373 | 2 | log_info('お届け先入力チェックエラー'); |
|
374 | |||
375 | return [ |
||
376 | 2 | 'Customer' => $this->getUser(), |
|
377 | 2 | 'shippingId' => $id, |
|
378 | 'error' => true, |
||
379 | ]; |
||
380 | } |
||
381 | |||
382 | // 選択されたお届け先情報を取得 |
||
383 | $CustomerAddress = $this->customerAddressRepository->findOneBy( |
||
384 | [ |
||
385 | 'Customer' => $this->getUser(), |
||
386 | 'id' => $address, |
||
387 | ] |
||
388 | ); |
||
389 | if (is_null($CustomerAddress)) { |
||
390 | throw new NotFoundHttpException(trans('shoppingcontroller.text.error.selected_address')); |
||
391 | } |
||
392 | |||
393 | /** @var Order $Order */ |
||
394 | $Order = $this->shoppingService->getOrder(OrderStatus::PROCESSING); |
||
395 | if (!$Order) { |
||
396 | log_info('購入処理中の受注情報がないため購入エラー'); |
||
397 | $this->addError('front.shopping.order.error'); |
||
398 | |||
399 | return $this->redirectToRoute('shopping_error'); |
||
400 | } |
||
401 | |||
402 | $Shipping = $Order->findShipping($id); |
||
403 | if (!$Shipping) { |
||
404 | throw new NotFoundHttpException(trans('shoppingcontroller.text.error.address')); |
||
405 | } |
||
406 | |||
407 | log_info('お届先情報更新開始', [$Shipping->getId()]); |
||
408 | |||
409 | // お届け先情報を更新 |
||
410 | $Shipping->setFromCustomerAddress($CustomerAddress); |
||
411 | |||
412 | // 配送料金の設定 |
||
413 | $this->shoppingService->setShippingDeliveryFee($Shipping); |
||
414 | |||
415 | // 合計金額の再計算 |
||
416 | $flowResult = $this->executePurchaseFlow($Order); |
||
417 | if ($flowResult->hasWarning() || $flowResult->hasError()) { |
||
418 | return $this->redirectToRoute('shopping_error'); |
||
419 | } |
||
420 | |||
421 | // 配送先を更新 |
||
422 | $this->entityManager->flush(); |
||
423 | |||
424 | $event = new EventArgs( |
||
425 | [ |
||
426 | 'Order' => $Order, |
||
427 | 'shippingId' => $id, |
||
428 | ], |
||
429 | $request |
||
430 | ); |
||
431 | $this->eventDispatcher->dispatch(EccubeEvents::FRONT_SHOPPING_SHIPPING_COMPLETE, $event); |
||
432 | |||
433 | log_info('お届先情報更新完了', [$Shipping->getId()]); |
||
434 | |||
435 | return $this->redirectToRoute('shopping'); |
||
436 | } |
||
437 | |||
438 | return [ |
||
439 | 1 | 'Customer' => $this->getUser(), |
|
440 | 1 | 'shippingId' => $id, |
|
441 | 'error' => false, |
||
442 | ]; |
||
443 | } |
||
444 | |||
445 | /** |
||
446 | * お届け先の設定(非会員でも使用する) |
||
447 | * |
||
448 | * @Route("/shopping/shipping_edit/{id}", name="shopping_shipping_edit", requirements={"id" = "\d+"}) |
||
449 | * @Template("Shopping/shipping_edit.twig") |
||
450 | */ |
||
451 | public function shippingEdit(Request $request, $id) |
||
560 | |||
561 | /** |
||
562 | * ログイン |
||
563 | * |
||
564 | * @Route("/shopping/login", name="shopping_login") |
||
565 | * @Template("Shopping/login.twig") |
||
566 | */ |
||
567 | 2 | public function login(Request $request, AuthenticationUtils $authenticationUtils) |
|
602 | |||
603 | /** |
||
604 | * 購入エラー画面表示 |
||
605 | * |
||
606 | * @Route("/shopping/error", name="shopping_error") |
||
607 | * @Template("Shopping/shopping_error.twig") |
||
608 | */ |
||
609 | 1 | public function shoppingError(Request $request) |
|
623 | |||
624 | /** |
||
625 | * カート画面のチェック |
||
626 | * |
||
627 | * @ForwardOnly |
||
628 | * @Route("/shopping/check_to_cart", name="shopping_check_to_cart") |
||
629 | */ |
||
630 | 32 | public function checkToCart(Request $request) |
|
650 | |||
651 | /** |
||
652 | * 受注情報を初期化する. |
||
653 | * |
||
654 | * @ForwardOnly |
||
655 | * @Route("/shopping/initialize_order", name="shopping_initialize_order") |
||
656 | */ |
||
657 | 25 | public function initializeOrder(Request $request) |
|
707 | |||
708 | /** |
||
709 | * フォームを作成し, イベントハンドラを設定する |
||
710 | * |
||
711 | * @ForwardOnly |
||
712 | * @Route("/shopping/create_form", name="shopping_create_form") |
||
713 | */ |
||
714 | 25 | public function createShoppingForm(Request $request) |
|
735 | |||
736 | /** |
||
737 | * mode に応じて各変更ページへリダイレクトする. |
||
738 | * |
||
739 | * @ForwardOnly |
||
740 | * @Route("/shopping/redirect_to_change", name="shopping_redirect_to_change") |
||
741 | */ |
||
742 | 10 | public function redirectToChange(Request $request) |
|
778 | |||
779 | /** |
||
780 | * 複数配送時のエラーを表示する |
||
781 | * |
||
782 | * @ForwardOnly |
||
783 | * @Route("/shopping/handle_multiple_errors", name="shopping_handle_multiple_errors") |
||
784 | */ |
||
785 | 20 | public function handleMultipleErrors(Request $request) |
|
806 | |||
807 | /** |
||
808 | * 受注の存在チェック |
||
809 | * |
||
810 | * @ForwardOnly |
||
811 | * @Route("/shopping/exists_order", name="shopping_exists_order") |
||
812 | */ |
||
813 | 12 | public function existsOrder(Request $request) |
|
826 | |||
827 | /** |
||
828 | * 受注完了処理 |
||
829 | * |
||
830 | * @ForwardOnly |
||
831 | * @Route("/shopping/complete_order", name="shopping_complete_order") |
||
832 | */ |
||
833 | 2 | public function completeOrder(Request $request) |
|
834 | { |
||
835 | 2 | $form = $this->parameterBag->get(OrderType::class); |
|
836 | |||
837 | 2 | if ($form->isSubmitted() && $form->isValid()) { |
|
838 | /** @var Order $Order */ |
||
839 | 2 | $Order = $form->getData(); |
|
840 | 2 | log_info('購入処理開始', [$Order->getId()]); |
|
841 | |||
842 | // トランザクション制御 |
||
843 | 2 | $em = $this->entityManager; |
|
844 | 2 | $em->getConnection()->beginTransaction(); |
|
845 | try { |
||
846 | // お問い合わせ、配送時間などのフォーム項目をセット |
||
847 | // FormTypeで更新されるため不要 |
||
848 | //$app['eccube.service.shopping']->setFormData($Order, $data); |
||
849 | |||
850 | 2 | $flowResult = $this->executePurchaseFlow($Order); |
|
851 | 2 | if ($flowResult->hasWarning() || $flowResult->hasError()) { |
|
852 | // TODO エラーメッセージ |
||
853 | throw new ShoppingException(); |
||
854 | } |
||
855 | try { |
||
856 | 2 | $this->purchaseFlow->purchase($Order, new PurchaseContext($Order, $Order->getCustomer())); // TODO 変更前の Order を渡す必要がある? |
|
857 | } catch (PurchaseException $e) { |
||
858 | $this->addError($e->getMessage(), 'front'); |
||
859 | } |
||
860 | |||
861 | // 購入処理 |
||
862 | 2 | $this->shoppingService->processPurchase($Order); // XXX フロント画面に依存してるので管理画面では使えない |
|
863 | |||
864 | // Order も引数で渡すのがベスト?? |
||
865 | 2 | $paymentService = $this->createPaymentService($Order); |
|
866 | 2 | $paymentMethod = $this->createPaymentMethod($Order, $form); |
|
867 | |||
868 | // 必要に応じて別のコントローラへ forward or redirect(移譲) |
||
869 | // forward の処理はプラグイン内で書けるようにしておく |
||
870 | // dispatch をしたら, パスを返して forwardする |
||
871 | // http://silex.sensiolabs.org/doc/cookbook/sub_requests.html |
||
872 | // 確認画面も挟める |
||
873 | // Request をセッションに入れるべし |
||
874 | 2 | $dispatcher = $paymentService->dispatch($paymentMethod); // 決済処理中. |
|
875 | // 一旦、決済処理中になった後は、購入処理中に戻せない。キャンセル or 購入完了の仕様とする |
||
876 | // ステータス履歴も保持しておく? 在庫引き当ての仕様もセットで。 |
||
877 | 2 | if ($dispatcher instanceof Response |
|
878 | 2 | && ($dispatcher->isRedirection() || $dispatcher->getContent()) |
|
879 | ) { // $paymentMethod->apply() が Response を返した場合は画面遷移 |
||
880 | return $dispatcher; // 画面遷移したいパターンが複数ある場合はどうする? 引数で制御? |
||
881 | } |
||
882 | 2 | $PaymentResult = $paymentService->doCheckout($paymentMethod); // 決済実行 |
|
883 | 2 | if (!$PaymentResult->isSuccess()) { |
|
884 | $this->entityManager->getConnection()->rollback(); |
||
885 | |||
886 | return $this->redirectToRoute('shopping_error'); |
||
887 | } |
||
888 | |||
889 | 2 | $this->entityManager->flush(); |
|
890 | 2 | $this->entityManager->getConnection()->commit(); |
|
891 | |||
892 | 2 | log_info('購入処理完了', [$Order->getId()]); |
|
893 | } catch (ShoppingException $e) { |
||
894 | log_error('購入エラー', [$e->getMessage()]); |
||
895 | |||
896 | $this->entityManager->getConnection()->rollback(); |
||
897 | |||
898 | $this->log($e); |
||
899 | $this->addError($e->getMessage()); |
||
900 | |||
901 | return $this->redirectToRoute('shopping_error'); |
||
902 | } catch (\Exception $e) { |
||
903 | log_error('予期しないエラー', [$e->getMessage()]); |
||
904 | |||
905 | $this->entityManager->getConnection()->rollback(); |
||
906 | |||
907 | $this->addError('front.shopping.system.error'); |
||
908 | |||
909 | return $this->redirectToRoute('shopping_error'); |
||
910 | } |
||
911 | |||
912 | 2 | return $this->forwardToRoute('shopping_after_complete'); |
|
913 | } |
||
914 | |||
915 | return new Response(); |
||
916 | } |
||
917 | |||
918 | /** |
||
919 | * 受注完了の後処理 |
||
920 | * |
||
921 | * @ForwardOnly |
||
922 | * @Route("/shopping/after_complete", name="shopping_after_complete") |
||
923 | */ |
||
924 | 2 | public function afterComplete(Request $request) |
|
925 | { |
||
926 | 2 | $form = $this->parameterBag->get(OrderType::class); |
|
927 | 2 | $Order = $this->parameterBag->get('Order'); |
|
928 | |||
929 | // カート削除 |
||
930 | 2 | $this->cartService->clear()->save(); |
|
931 | |||
932 | 2 | $event = new EventArgs( |
|
933 | [ |
||
934 | 2 | 'form' => $form, |
|
935 | 2 | 'Order' => $Order, |
|
936 | ], |
||
937 | 2 | $request |
|
938 | ); |
||
939 | 2 | $this->eventDispatcher->dispatch(EccubeEvents::FRONT_SHOPPING_CONFIRM_PROCESSING, $event); |
|
940 | |||
941 | 2 | View Code Duplication | if ($event->getResponse() !== null) { |
942 | log_info('イベントレスポンス返却', [$Order->getId()]); |
||
943 | |||
944 | return $event->getResponse(); |
||
945 | } |
||
946 | |||
947 | // 受注IDをセッションにセット |
||
948 | 2 | $this->session->set($this->sessionOrderKey, $Order->getId()); |
|
949 | |||
950 | // メール送信 |
||
951 | 2 | $MailHistory = $this->shoppingService->sendOrderMail($Order); |
|
952 | |||
953 | 2 | $event = new EventArgs( |
|
954 | [ |
||
955 | 2 | 'form' => $form, |
|
956 | 2 | 'Order' => $Order, |
|
957 | 2 | 'MailHistory' => $MailHistory, |
|
958 | ], |
||
959 | 2 | $request |
|
960 | ); |
||
961 | 2 | $this->eventDispatcher->dispatch(EccubeEvents::FRONT_SHOPPING_CONFIRM_COMPLETE, $event); |
|
962 | |||
963 | 2 | View Code Duplication | if ($event->getResponse() !== null) { |
964 | log_info('イベントレスポンス返却', [$Order->getId()]); |
||
965 | |||
966 | return $event->getResponse(); |
||
967 | } |
||
968 | |||
969 | // 完了画面表示 |
||
970 | 2 | return $this->redirectToRoute('shopping_complete'); |
|
971 | } |
||
972 | |||
973 | 2 | private function createPaymentService(Order $Order) |
|
974 | { |
||
975 | 2 | $serviceClass = $Order->getPayment()->getServiceClass(); |
|
976 | 2 | $paymentService = new $serviceClass($this->container->get('request_stack')); |
|
977 | |||
978 | 2 | return $paymentService; |
|
979 | } |
||
980 | |||
981 | 2 | private function createPaymentMethod(Order $Order, $form) |
|
982 | { |
||
983 | 2 | $methodClass = $Order->getPayment()->getMethodClass(); |
|
984 | 2 | $PaymentMethod = new $methodClass(); |
|
985 | 2 | $PaymentMethod->setFormType($form); |
|
986 | 2 | $PaymentMethod->setRequest($this->container->get('request_stack')->getCurrentRequest()); |
|
987 | |||
988 | 2 | return $PaymentMethod; |
|
989 | } |
||
990 | |||
991 | /** |
||
992 | * 非会員でのお客様情報変更時の入力チェック |
||
993 | * |
||
994 | * TODO https://github.com/EC-CUBE/ec-cube/issues/565 |
||
995 | * |
||
996 | * @param Application $app |
||
997 | * @param array $data リクエストパラメータ |
||
998 | * |
||
999 | * @return array |
||
1000 | */ |
||
1001 | private function customerValidation(Application $app, array &$data) |
||
1089 | } |
||
1090 |