Completed
Push — dev/store-tests ( 3e9e56...714865 )
by Kiyotaka
05:47
created

ProductClassController::mergeProductClasses()   B

Complexity

Conditions 9
Paths 6

Size

Total Lines 26

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 9
nc 6
nop 2
dl 0
loc 26
rs 8.0555
c 0
b 0
f 0
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\Template;
30
use Symfony\Component\Form\Extension\Core\Type\FormType;
31
use Symfony\Component\HttpFoundation\Request;
32
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
33
use Symfony\Component\Routing\Annotation\Route;
34
35
class ProductClassController extends AbstractController
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
    /**
63
     * ProductClassController constructor.
64
     *
65
     * @param ProductClassRepository $productClassRepository
66
     * @param ClassCategoryRepository $classCategoryRepository
67
     */
68
    public function __construct(
69
        ProductRepository $productRepository,
70
        ProductClassRepository $productClassRepository,
71
        ClassCategoryRepository $classCategoryRepository,
72
        BaseInfoRepository $baseInfoRepository,
73
        TaxRuleRepository $taxRuleRepository
74
    ) {
75
        $this->productRepository = $productRepository;
76
        $this->productClassRepository = $productClassRepository;
77
        $this->classCategoryRepository = $classCategoryRepository;
78
        $this->baseInfoRepository = $baseInfoRepository;
79
        $this->taxRuleRepository = $taxRuleRepository;
80
    }
81
82
    /**
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
     */
88
    public function index(Request $request, $id)
89
    {
90
        $Product = $this->findProduct($id);
91
        if (!$Product) {
92
            throw new NotFoundHttpException();
93
        }
94
95
        $ClassName1 = null;
96
        $ClassName2 = null;
97
98
        if ($Product->hasProductClass()) {
99
            // 規格ありの商品は編集画面を表示する.
100
            $ProductClasses = $Product->getProductClasses()
101
                ->filter(function ($pc) {
102
                    return $pc->getClassCategory1() !== null;
103
                });
104
105
            // 設定されている規格名1, 2を取得(商品規格の規格分類には必ず同じ値がセットされている)
106
            $FirstProductClass = $ProductClasses->first();
107
            $ClassName1 = $FirstProductClass->getClassCategory1()->getClassName();
108
            $ClassCategory2 = $FirstProductClass->getClassCategory2();
109
            $ClassName2 = $ClassCategory2 ? $ClassCategory2->getClassName() : null;
110
111
            // 規格名1/2から組み合わせを生成し, DBから取得した商品規格とマージする.
112
            $ProductClasses = $this->mergeProductClasses(
113
                $this->createProductClasses($ClassName1, $ClassName2),
114
                $ProductClasses);
115
116
            // 組み合わせのフォームを生成する.
117
            $form = $this->createMatrixForm($ProductClasses, $ClassName1, $ClassName2,
118
                ['product_classes_exist' => true]);
119
            $form->handleRequest($request);
120
121 View Code Duplication
            if ($form->isSubmitted() && $form->isValid()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
122
                // フォームではtokenを無効化しているのでここで確認する.
123
                $this->isTokenValid();
124
125
                $this->saveProductClasses($Product, $form['product_classes']->getData());
126
127
                $this->addSuccess('admin.common.save_complete', 'admin');
128
129
                if ($request->get('return')) {
130
                    return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return' => $request->get('return')]);
131
                }
132
133
                return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
134
            }
135
        } else {
136
            // 規格なし商品
137
            $form = $this->createMatrixForm();
138
            $form->handleRequest($request);
139
140
            if ($form->isSubmitted() && $form->isValid()) {
141
                // フォームではtokenを無効化しているのでここで確認する.
142
                $this->isTokenValid();
143
144
                // 登録,更新ボタンが押下されたかどうか.
145
                $isSave = $form['save']->isClicked();
146
147
                // 規格名1/2から商品規格の組み合わせを生成する.
148
                $ClassName1 = $form['class_name1']->getData();
149
                $ClassName2 = $form['class_name2']->getData();
150
                $ProductClasses = $this->createProductClasses($ClassName1, $ClassName2);
151
152
                // 組み合わせのフォームを生成する.
153
                // class_name1, class_name2が取得できるのがsubmit後のため, フォームを再生成して組み合わせ部分を構築している
154
                // submit後だと, フォーム項目の追加やデータ変更が許可されないため.
155
                $form = $this->createMatrixForm($ProductClasses, $ClassName1, $ClassName2,
156
                    ['product_classes_exist' => true]);
157
158
                // 登録ボタン押下時
159
                if ($isSave) {
160
                    $form->handleRequest($request);
161 View Code Duplication
                    if ($form->isSubmitted() && $form->isValid()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
162
                        $this->saveProductClasses($Product, $form['product_classes']->getData());
163
164
                        $this->addSuccess('admin.common.save_complete', 'admin');
165
166
                        if ($request->get('return')) {
167
                            return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId(), 'return' => $request->get('return')]);
168
                        }
169
170
                        return $this->redirectToRoute('admin_product_product_class', ['id' => $Product->getId()]);
171
                    }
172
                }
173
            }
174
        }
175
176
        return [
177
            'Product' => $Product,
178
            'form' => $form->createView(),
179
            'clearForm' => $this->createForm(FormType::class)->createView(),
180
            'ClassName1' => $ClassName1,
181
            'ClassName2' => $ClassName2,
182
            'return_product' => $request->get('return'),
183
        ];
184
    }
185
186
    /**
187
     * 商品規格を初期化する.
188
     *
189
     * @Route("/%eccube_admin_route%/product/product/class/{id}/clear", requirements={"id" = "\d+"}, name="admin_product_product_class_clear")
190
     */
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.reset_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
    protected function createProductClasses(ClassName $ClassName1, ClassName $ClassName2 = null)
237
    {
238
        $ProductClasses = [];
239
        $ClassCategories1 = $this->classCategoryRepository->findBy(['ClassName' => $ClassName1], ['sort_no' => 'DESC']);
240
        $ClassCategories2 = [];
241
        if ($ClassName2) {
242
            $ClassCategories2 = $this->classCategoryRepository->findBy(['ClassName' => $ClassName2],
243
                ['sort_no' => 'DESC']);
244
        }
245
        foreach ($ClassCategories1 as $ClassCategory1) {
246
            // 規格1のみ
247
            if (!$ClassName2) {
248
                $ProductClass = new ProductClass();
249
                $ProductClass->setClassCategory1($ClassCategory1);
250
                $ProductClasses[] = $ProductClass;
251
                continue;
252
            }
253
            // 規格1/2
254
            foreach ($ClassCategories2 as $ClassCategory2) {
255
                $ProductClass = new ProductClass();
256
                $ProductClass->setClassCategory1($ClassCategory1);
257
                $ProductClass->setClassCategory2($ClassCategory2);
258
                $ProductClasses[] = $ProductClass;
259
            }
260
        }
261
262
        return $ProductClasses;
263
    }
264
265
    /**
266
     * 商品規格の配列をマージする.
267
     *
268
     * @param $ProductClassesForMatrix
269
     * @param $ProductClasses
270
     *
271
     * @return array|ProductClass[]
272
     */
273
    protected function mergeProductClasses($ProductClassesForMatrix, $ProductClasses)
274
    {
275
        $mergedProductClasses = [];
276
        foreach ($ProductClassesForMatrix as $pcfm) {
277
            foreach ($ProductClasses as $pc) {
278
                if ($pcfm->getClassCategory1()->getId() === $pc->getClassCategory1()->getId()) {
279
                    $cc2fm = $pcfm->getClassCategory2();
280
                    $cc2 = $pc->getClassCategory2();
281
282
                    if (null === $cc2fm && null === $cc2) {
283
                        $mergedProductClasses[] = $pc;
284
                        continue 2;
285
                    }
286
287
                    if ($cc2fm && $cc2 && $cc2fm->getId() === $cc2->getId()) {
288
                        $mergedProductClasses[] = $pc;
289
                        continue 2;
290
                    }
291
                }
292
            }
293
294
            $mergedProductClasses[] = $pcfm;
295
        }
296
297
        return $mergedProductClasses;
298
    }
299
300
    /**
301
     * 商品規格を登録, 更新する.
302
     *
303
     * @param Product $Product
304
     * @param array|ProductClass[] $ProductClasses
305
     */
306
    protected function saveProductClasses(Product $Product, $ProductClasses = [])
307
    {
308
        foreach ($ProductClasses as $pc) {
309
            // 新規登録時、チェックを入れていなければ更新しない
310
            if (!$pc->getId() && !$pc->isVisible()) {
311
                continue;
312
            }
313
314
            // 無効から有効にした場合は, 過去の登録情報を検索.
315
            if (!$pc->getId()) {
316
                /** @var ProductClass $ExistsProductClass */
317
                $ExistsProductClass = $this->productClassRepository->findOneBy([
318
                    'Product' => $Product,
319
                    'ClassCategory1' => $pc->getClassCategory1(),
320
                    'ClassCategory2' => $pc->getClassCategory2(),
321
                ]);
322
323
                // 過去の登録情報があればその情報を復旧する.
324
                if ($ExistsProductClass) {
325
                    $ExistsProductClass->copyProperties($pc, [
326
                        'id',
327
                        'price01_inc_tax',
328
                        'price02_inc_tax',
329
                        'create_date',
330
                        'update_date',
331
                        'Creator',
332
                    ]);
333
                    $pc = $ExistsProductClass;
334
                }
335
            }
336
337
            // 更新時, チェックを外した場合はPOST内容を破棄してvisibleのみ更新する.
338
            if ($pc->getId() && !$pc->isVisible()) {
339
                $this->entityManager->refresh($pc);
340
                $pc->setVisible(false);
341
                continue;
342
            }
343
344
            $pc->setProduct($Product);
345
            $this->entityManager->persist($pc);
346
347
            // 在庫の更新
348
            $ProductStock = $pc->getProductStock();
349
            if (!$ProductStock) {
350
                $ProductStock = new ProductStock();
351
                $ProductStock->setProductClass($pc);
352
                $this->entityManager->persist($ProductStock);
353
            }
354
            $ProductStock->setStock($pc->isStockUnlimited() ? null : $pc->getStock());
355
356
            if ($this->baseInfoRepository->get()->isOptionProductTaxRule()) {
357
                $rate = $pc->getTaxRate();
358
                $TaxRule = $pc->getTaxRule();
359
                if (is_numeric($rate)) {
360
                    if ($TaxRule) {
361
                        $TaxRule->setTaxRate($rate);
362
                    } else {
363
                        // 初期税率設定の計算方法を設定する
364
                        $RoundingType = $this->taxRuleRepository->find(TaxRule::DEFAULT_TAX_RULE_ID)
365
                            ->getRoundingType();
366
367
                        $TaxRule = new TaxRule();
368
                        $TaxRule->setProduct($Product);
369
                        $TaxRule->setProductClass($pc);
370
                        $TaxRule->setTaxRate($rate);
371
                        $TaxRule->setRoundingType($RoundingType);
372
                        $TaxRule->setTaxAdjust(0);
373
                        $TaxRule->setApplyDate(new \DateTime());
374
                        $this->entityManager->persist($TaxRule);
375
                    }
376
                } else {
377
                    if ($TaxRule) {
378
                        $this->taxRuleRepository->delete($TaxRule);
379
                        $pc->setTaxRule(null);
380
                    }
381
                }
382
            }
383
        }
384
385
        // デフォルト規格を非表示にする.
386
        $DefaultProductClass = $this->productClassRepository->findOneBy([
387
            'Product' => $Product,
388
            'ClassCategory1' => null,
389
            'ClassCategory2' => null,
390
        ]);
391
        $DefaultProductClass->setVisible(false);
392
393
        $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
    protected function createMatrixForm(
407
        $ProductClasses = [],
408
        ClassName $ClassName1 = null,
409
        ClassName $ClassName2 = null,
410
        array $options = []
411
    ) {
412
        $options = array_merge(['csrf_protection' => false], $options);
413
        $builder = $this->formFactory->createBuilder(ProductClassMatrixType::class, [
414
            'product_classes' => $ProductClasses,
415
            'class_name1' => $ClassName1,
416
            'class_name2' => $ClassName2,
417
        ], $options);
418
419
        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
    protected function findProduct($id)
433
    {
434
        $qb = $this->productRepository->createQueryBuilder('p')
435
            ->addSelect(['pc', 'cc1', 'cc2'])
436
            ->leftJoin('p.ProductClasses', 'pc')
437
            ->leftJoin('pc.ClassCategory1', 'cc1')
438
            ->leftJoin('pc.ClassCategory2', 'cc2')
439
            ->where('p.id = :id')
440
            ->andWhere('pc.visible = :pc_visible')
441
            ->setParameter('id', $id)
442
            ->setParameter('pc_visible', true)
443
            ->orderBy('cc1.sort_no', 'DESC')
444
            ->addOrderBy('cc2.sort_no', 'DESC');
445
446
        try {
447
            return $qb->getQuery()->getSingleResult();
448
        } catch (NoResultException $e) {
449
            return null;
450
        }
451
    }
452
}
453