Completed
Pull Request — experimental/sf (#3318)
by
unknown
212:51 queued 199:08
created

ProductClassController   C

Complexity

Total Complexity 54

Size/Duplication

Total Lines 418
Duplicated Lines 5.98 %

Coupling/Cohesion

Components 1
Dependencies 13

Test Coverage

Coverage 85.71%

Importance

Changes 0
Metric Value
dl 25
loc 418
ccs 156
cts 182
cp 0.8571
rs 6.4799
c 0
b 0
f 0
wmc 54
lcom 1
cbo 13

8 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 13 1
C index() 25 97 13
B clearProductClasses() 0 36 9
A createProductClasses() 0 28 5
B mergeProductClassess() 0 26 9
C saveProductClasses() 0 89 14
A createMatrixForm() 0 15 1
A findProduct() 0 20 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

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 Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like ProductClassController 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 ProductClassController, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
/*
4
 * This file is part of EC-CUBE
5
 *
6
 * Copyright(c) LOCKON CO.,LTD. All Rights Reserved.
7
 *
8
 * http://www.lockon.co.jp/
9
 *
10
 * For the full copyright and license information, please view the LICENSE
11
 * file that was distributed with this source code.
12
 */
13
14
namespace Eccube\Controller\Admin\Product;
15
16
use Doctrine\ORM\NoResultException;
17
use Eccube\Controller\AbstractController;
18
use Eccube\Entity\ClassName;
19
use Eccube\Entity\Product;
20
use Eccube\Entity\ProductClass;
21
use Eccube\Entity\ProductStock;
22
use Eccube\Entity\TaxRule;
23
use Eccube\Form\Type\Admin\ProductClassMatrixType;
24
use Eccube\Repository\BaseInfoRepository;
25
use Eccube\Repository\ClassCategoryRepository;
26
use Eccube\Repository\ProductClassRepository;
27
use Eccube\Repository\ProductRepository;
28
use Eccube\Repository\TaxRuleRepository;
29
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
30
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
31
use Symfony\Component\Form\Extension\Core\Type\FormType;
32
use Symfony\Component\HttpFoundation\Request;
33
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
34
35
class ProductClassController extends AbstractController
0 ignored issues
show
introduced by
Missing class doc comment
Loading history...
36
{
37
    /**
38
     * @var ProductRepository
39
     */
40
    protected $productRepository;
41
42
    /**
43
     * @var ProductClassRepository
44
     */
45
    protected $productClassRepository;
46
47
    /**
48
     * @var ClassCategoryRepository
49
     */
50
    protected $classCategoryRepository;
51
52
    /**
53
     * @var BaseInfoRepository
54
     */
55
    protected $baseInfoRepository;
56
57
    /**
58
     * @var TaxRuleRepository
59
     */
60
    protected $taxRuleRepository;
61
62
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$productRepository" missing
Loading history...
introduced by
Doc comment for parameter "$baseInfoRepository" missing
Loading history...
introduced by
Doc comment for parameter "$taxRuleRepository" missing
Loading history...
63
     * ProductClassController constructor.
64
     *
65
     * @param ProductClassRepository $productClassRepository
0 ignored issues
show
introduced by
Expected 2 spaces after parameter type; 1 found
Loading history...
introduced by
Doc comment for parameter $productClassRepository does not match actual variable name $productRepository
Loading history...
66
     * @param ClassCategoryRepository $classCategoryRepository
0 ignored issues
show
introduced by
Doc comment for parameter $classCategoryRepository does not match actual variable name $productClassRepository
Loading history...
67
     */
68 13
    public function __construct(
69
        ProductRepository $productRepository,
70
        ProductClassRepository $productClassRepository,
71
        ClassCategoryRepository $classCategoryRepository,
72
        BaseInfoRepository $baseInfoRepository,
73
        TaxRuleRepository $taxRuleRepository
74
    ) {
75 13
        $this->productRepository = $productRepository;
76 13
        $this->productClassRepository = $productClassRepository;
77 13
        $this->classCategoryRepository = $classCategoryRepository;
78 13
        $this->baseInfoRepository = $baseInfoRepository;
79 13
        $this->taxRuleRepository = $taxRuleRepository;
80
    }
81
82
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$request" missing
Loading history...
introduced by
Doc comment for parameter "$id" missing
Loading history...
83
     * 商品規格が登録されていなければ新規登録, 登録されていれば更新画面を表示する
84
     *
85
     * @Route("/%eccube_admin_route%/product/product/class/{id}", requirements={"id" = "\d+"}, name="admin_product_product_class")
86
     * @Template("@admin/Product/product_class.twig")
87
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
88 13
    public function index(Request $request, $id)
89
    {
90 13
        $Product = $this->findProduct($id);
91 13
        if (!$Product) {
92
            throw new NotFoundHttpException();
93
        }
94
95 13
        $ClassName1 = null;
96 13
        $ClassName2 = null;
97
98 13
        if ($Product->hasProductClass()) {
99
            // 規格ありの商品は編集画面を表示する.
100 11
            $ProductClasses = $Product->getProductClasses()
101 11
                ->filter(function ($pc) {
102 11
                    return $pc->getClassCategory1() !== null;
103 11
                });
104
105
            // 設定されている規格名1, 2を取得(商品規格の規格分類には必ず同じ値がセットされている)
106 11
            $FirstProductClass = $ProductClasses->first();
107 11
            $ClassName1 = $FirstProductClass->getClassCategory1()->getClassName();
108 11
            $ClassCategory2 = $FirstProductClass->getClassCategory2();
109 11
            $ClassName2 = $ClassCategory2 ? $ClassCategory2->getClassName() : null;
110
111
            // 規格名1/2から組み合わせを生成し, DBから取得した商品規格とマージする.
112 11
            $ProductClasses = $this->mergeProductClassess(
113 11
                $this->createProductClasses($ClassName1, $ClassName2),
114 11
                $ProductClasses);
115
116
            // 組み合わせのフォームを生成する.
117 11
            $form = $this->createMatrixForm($ProductClasses, $ClassName1, $ClassName2,
118 11
                ['product_classes_exist' => true]);
119 11
            $form->handleRequest($request);
120
121 11 View Code Duplication
            if ($form->isSubmitted() && $form->isValid()) {
122
                // フォームではtokenを無効化しているのでここで確認する.
123 5
                $this->isTokenValid();
124
125 5
                $this->saveProductClasses($Product, $form['product_classes']->getData());
126
127 5
                $this->addSuccess('admin.product.product_class.update.complete', 'admin');
128
129 5
                if ($request->get('return')) {
130
                    return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return' => $request->get('return')]);
131
                }
132
133 11
                return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
134
            }
135
        } else {
136
            // 規格なし商品
137 5
            $form = $this->createMatrixForm();
138 5
            $form->handleRequest($request);
139
140 5
            if ($form->isSubmitted() && $form->isValid()) {
141
                // フォームではtokenを無効化しているのでここで確認する.
142 4
                $this->isTokenValid();
143
144
                // 登録,更新ボタンが押下されたかどうか.
145 4
                $isSave = $form['save']->isClicked();
146
147
                // 規格名1/2から商品規格の組み合わせを生成する.
148 4
                $ClassName1 = $form['class_name1']->getData();
149 4
                $ClassName2 = $form['class_name2']->getData();
150 4
                $ProductClasses = $this->createProductClasses($ClassName1, $ClassName2);
151
152
                // 組み合わせのフォームを生成する.
153
                // class_name1, class_name2が取得できるのがsubmit後のため, フォームを再生成して組み合わせ部分を構築している
154
                // submit後だと, フォーム項目の追加やデータ変更が許可されないため.
155 4
                $form = $this->createMatrixForm($ProductClasses, $ClassName1, $ClassName2,
156 4
                    ['product_classes_exist' => true]);
157
158
                // 登録ボタン押下時
159 4
                if ($isSave) {
160 3
                    $form->handleRequest($request);
161 3 View Code Duplication
                    if ($form->isSubmitted() && $form->isValid()) {
162 3
                        $this->saveProductClasses($Product, $form['product_classes']->getData());
163
164 3
                        $this->addSuccess('admin.product.product_class.save.complete', 'admin');
165
166 3
                        if ($request->get('return')) {
167
                            return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return' => $request->get('return')]);
168
                        }
169
170 3
                        return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
171
                    }
172
                }
173
            }
174
        }
175
176
        return [
177 13
            'Product' => $Product,
178 13
            'form' => $form->createView(),
179 13
            'clearForm' => $this->createForm(FormType::class)->createView(),
180 13
            'ClassName1' => $ClassName1,
181 13
            'ClassName2' => $ClassName2,
182 13
            'return_product' => $request->get('return'),
183
        ];
184
    }
185
186
    /**
0 ignored issues
show
introduced by
Doc comment for parameter "$request" missing
Loading history...
introduced by
Doc comment for parameter "$Product" missing
Loading history...
187
     * 商品規格を初期化する.
188
     *
189
     * @Route("/%eccube_admin_route%/product/product/class/{id}/clear", requirements={"id" = "\d+"}, name="admin_product_product_class_clear")
190
     */
0 ignored issues
show
introduced by
Missing @return tag in function comment
Loading history...
191
    public function clearProductClasses(Request $request, Product $Product)
192
    {
193
        if (!$Product->hasProductClass()) {
194
            return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
195
        }
196
197
        $form = $this->createForm(FormType::class);
198
        $form->handleRequest($request);
199
200
        if ($form->isSubmitted() && $form->isValid()) {
201
            $ProductClasses = $this->productClassRepository->findBy([
202
                'Product' => $Product,
203
            ]);
204
205
            // デフォルト規格のみ有効にする
206
            foreach ($ProductClasses as $ProductClass) {
207
                $ProductClass->setVisible(false);
208
            }
209
            foreach ($ProductClasses as $ProductClass) {
210
                if (null === $ProductClass->getClassCategory1() && null === $ProductClass->getClassCategory2()) {
211
                    $ProductClass->setVisible(true);
212
                    break;
213
                }
214
            }
215
216
            $this->entityManager->flush();
217
218
            $this->addSuccess('admin.product.product_class.clear.complete', 'admin');
219
        }
220
221
        if ($request->get('return')) {
222
            return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return' => $request->get('return')]);
223
        }
224
225
        return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
226
    }
227
228
    /**
229
     * 規格名1/2から, 商品規格の組み合わせを生成する.
230
     *
231
     * @param ClassName $ClassName1
232
     * @param ClassName|null $ClassName2
233
     *
234
     * @return array|ProductClass[]
235
     */
236 12
    protected function createProductClasses(ClassName $ClassName1, ClassName $ClassName2 = null)
237
    {
238 12
        $ProductClasses = [];
239 12
        $ClassCategories1 = $this->classCategoryRepository->findBy(['ClassName' => $ClassName1], ['sort_no' => 'DESC']);
240 12
        $ClassCategories2 = [];
241 12
        if ($ClassName2) {
242 8
            $ClassCategories2 = $this->classCategoryRepository->findBy(['ClassName' => $ClassName2],
243 8
                ['sort_no' => 'DESC']);
244
        }
245 12
        foreach ($ClassCategories1 as $ClassCategory1) {
246
            // 規格1のみ
247 12
            if (!$ClassName2) {
248 4
                $ProductClass = new ProductClass();
249 4
                $ProductClass->setClassCategory1($ClassCategory1);
250 4
                $ProductClasses[] = $ProductClass;
251 4
                continue;
252
            }
253
            // 規格1/2
254 8
            foreach ($ClassCategories2 as $ClassCategory2) {
255 8
                $ProductClass = new ProductClass();
256 8
                $ProductClass->setClassCategory1($ClassCategory1);
257 8
                $ProductClass->setClassCategory2($ClassCategory2);
258 8
                $ProductClasses[] = $ProductClass;
259
            }
260
        }
261
262 12
        return $ProductClasses;
263
    }
264
265
    /**
266
     * 商品規格の配列をマージする.
267
     *
268
     * @param $ProductClassessForMatrix
269
     * @param $ProductClasses
270
     *
271
     * @return array|ProductClass[]
272
     */
273 11
    protected function mergeProductClassess($ProductClassessForMatrix, $ProductClasses)
274
    {
275 11
        $mergedProductClasses = [];
276 11
        foreach ($ProductClassessForMatrix as $pcfm) {
277 11
            foreach ($ProductClasses as $pc) {
278 11
                if ($pcfm->getClassCategory1()->getId() === $pc->getClassCategory1()->getId()) {
279 11
                    $cc2fm = $pcfm->getClassCategory2();
280 11
                    $cc2 = $pc->getClassCategory2();
281
282 11
                    if (null === $cc2fm && null === $cc2) {
283 3
                        $mergedProductClasses[] = $pc;
284 3
                        continue 2;
285
                    }
286
287 8
                    if ($cc2fm && $cc2 && $cc2fm->getId() === $cc2->getId()) {
288 8
                        $mergedProductClasses[] = $pc;
289 11
                        continue 2;
290
                    }
291
                }
292
            }
293
294 6
            $mergedProductClasses[] = $pcfm;
295
        }
296
297 11
        return $mergedProductClasses;
298
    }
299
300
    /**
301
     * 商品規格を登録, 更新する.
302
     *
303
     * @param Product $Product
304
     * @param array|ProductClass[] $ProductClasses
305
     */
306 8
    protected function saveProductClasses(Product $Product, $ProductClasses = [])
307
    {
308 8
        foreach ($ProductClasses as $pc) {
309
            // 新規登録時、チェックを入れていなければ更新しない
310 8
            if (!$pc->getId() && !$pc->isVisible()) {
311 4
                continue;
312
            }
313
314
            // 無効から有効にした場合は, 過去の登録情報を検索.
315 8
            if (!$pc->getId()) {
316
                /** @var ProductClass $ExistsProductClass */
317 4
                $ExistsProductClass = $this->productClassRepository->findOneBy([
318 4
                    'Product' => $Product,
319 4
                    'ClassCategory1' => $pc->getClassCategory1(),
320 4
                    'ClassCategory2' => $pc->getClassCategory2(),
321
                ]);
322
323
                // 過去の登録情報があればその情報を復旧する.
324 4
                if ($ExistsProductClass) {
325 1
                    $ExistsProductClass->copyProperties($pc, [
326 1
                        'id',
327
                        'price01_inc_tax',
328
                        'price02_inc_tax',
329
                        'create_date',
330
                        'update_date',
331
                        'Creator',
332
                    ]);
333 1
                    $pc = $ExistsProductClass;
334
                }
335
            }
336
337
            // 更新時, チェックを外した場合はPOST内容を破棄してvisibleのみ更新する.
338 8
            if ($pc->getId() && !$pc->isVisible()) {
339 2
                $this->entityManager->refresh($pc);
340 2
                $pc->setVisible(false);
341 2
                continue;
342
            }
343
344 8
            $pc->setProduct($Product);
345 8
            $this->entityManager->persist($pc);
346
347
            // 在庫の更新
348 8
            $ProductStock = $pc->getProductStock();
349 8
            if (!$ProductStock) {
350 4
                $ProductStock = new ProductStock();
351 4
                $ProductStock->setProductClass($pc);
352 4
                $this->entityManager->persist($ProductStock);
353
            }
354 8
            $ProductStock->setStock($pc->isStockUnlimited() ? null : $pc->getStock());
355
356 8
            if ($this->baseInfoRepository->get()->isOptionProductTaxRule()) {
357 6
                $rate = $pc->getTaxRate();
358 6
                $TaxRule = $pc->getTaxRule();
359 6
                if (is_numeric($rate)) {
360 5
                    if ($TaxRule) {
361
                        $TaxRule->setTaxRate($rate);
362
                    } else {
363
                        // 初期税率設定の計算方法を設定する
364 5
                        $RoundingType = $this->taxRuleRepository->find(TaxRule::DEFAULT_TAX_RULE_ID)
365 5
                            ->getRoundingType();
366
367 5
                        $TaxRule = new TaxRule();
368 5
                        $TaxRule->setProduct($Product);
369 5
                        $TaxRule->setProductClass($pc);
370 5
                        $TaxRule->setTaxRate($rate);
371 5
                        $TaxRule->setRoundingType($RoundingType);
372 5
                        $TaxRule->setTaxAdjust(0);
373 5
                        $TaxRule->setApplyDate(new \DateTime());
374 5
                        $this->entityManager->persist($TaxRule);
375
                    }
376
                } else {
377 4
                    if ($TaxRule) {
378
                        $this->taxRuleRepository->delete($TaxRule);
379 8
                        $pc->setTaxRule(null);
380
                    }
381
                }
382
            }
383
        }
384
385
        // デフォルト規格を非表示にする.
386 8
        $DefaultProductClass = $this->productClassRepository->findOneBy([
387 8
            'Product' => $Product,
388
            'ClassCategory1' => null,
389
            'ClassCategory2' => null,
390
        ]);
391 8
        $DefaultProductClass->setVisible(false);
392
393 8
        $this->entityManager->flush();
394
    }
395
396
    /**
397
     * 商品規格登録フォームを生成する.
398
     *
399
     * @param array $ProductClasses
400
     * @param ClassName|null $ClassName1
401
     * @param ClassName|null $ClassName2
402
     * @param array $options
403
     *
404
     * @return \Symfony\Component\Form\FormInterface
405
     */
406 13
    protected function createMatrixForm(
407
        $ProductClasses = [],
408
        ClassName $ClassName1 = null,
409
        ClassName $ClassName2 = null,
410
        array $options = []
411
    ) {
412 13
        $options = array_merge(['csrf_protection' => false], $options);
413 13
        $builder = $this->formFactory->createBuilder(ProductClassMatrixType::class, [
414 13
            'product_classes' => $ProductClasses,
415 13
            'class_name1' => $ClassName1,
416 13
            'class_name2' => $ClassName2,
417 13
        ], $options);
418
419 13
        return $builder->getForm();
420
    }
421
422
    /**
423
     * 商品を取得する.
424
     * 商品規格はvisible=trueのものだけを取得し, 規格分類はsort_no=DESCでソートされている.
425
     *
426
     * @param $id
427
     *
428
     * @return Product|null
429
     *
430
     * @throws \Doctrine\ORM\NonUniqueResultException
431
     */
432 13
    protected function findProduct($id)
433
    {
434 13
        $qb = $this->productRepository->createQueryBuilder('p')
435 13
            ->addSelect(['pc', 'cc1', 'cc2'])
436 13
            ->leftJoin('p.ProductClasses', 'pc')
437 13
            ->leftJoin('pc.ClassCategory1', 'cc1')
438 13
            ->leftJoin('pc.ClassCategory2', 'cc2')
439 13
            ->where('p.id = :id')
440 13
            ->andWhere('pc.visible = :pc_visible')
441 13
            ->setParameter('id', $id)
442 13
            ->setParameter('pc_visible', true)
443 13
            ->orderBy('cc1.sort_no', 'DESC')
444 13
            ->addOrderBy('cc2.sort_no', 'DESC');
445
446
        try {
447 13
            return $qb->getQuery()->getSingleResult();
448
        } catch (NoResultException $e) {
449
            return null;
450
        }
451
    }
452
}
453