Failed Conditions
Pull Request — experimental/sf (#3236)
by Kentaro
144:19 queued 116:23
created

PointProcessor   A

Complexity

Total Complexity 26

Size/Duplication

Total Lines 241
Duplicated Lines 8.3 %

Coupling/Cohesion

Components 1
Dependencies 7

Test Coverage

Coverage 94.81%

Importance

Changes 0
Metric Value
dl 20
loc 241
rs 10
c 0
b 0
f 0
ccs 73
cts 77
cp 0.9481
wmc 26
lcom 1
cbo 7

11 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 5 1
A process() 0 18 3
A validate() 0 22 4
A prepare() 10 10 2
A commit() 0 4 1
A rollback() 10 10 2
A supports() 0 16 4
A calculateAddPoint() 0 24 4
A addPointDiscountItem() 0 21 1
A removePointDiscountItem() 0 9 3
A pointToPrice() 0 4 1

How to fix   Duplicated Code   

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:

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\Service\PurchaseFlow\Processor;
15
16
use Doctrine\ORM\EntityManagerInterface;
17
use Eccube\Entity\BaseInfo;
18
use Eccube\Entity\ItemHolderInterface;
19
use Eccube\Entity\ItemInterface;
20
use Eccube\Entity\Master\OrderItemType;
21
use Eccube\Entity\Master\TaxDisplayType;
22
use Eccube\Entity\Master\TaxType;
23
use Eccube\Entity\Order;
24
use Eccube\Entity\OrderItem;
25
use Eccube\Service\PurchaseFlow\ItemHolderPreprocessor;
26
use Eccube\Service\PurchaseFlow\ItemHolderValidator;
27
use Eccube\Service\PurchaseFlow\PurchaseContext;
28
use Eccube\Service\PurchaseFlow\PurchaseProcessor;
29
30
/**
31
 * 購入フローにおけるポイント処理.
32
 */
33
class PointProcessor extends ItemHolderValidator implements ItemHolderPreprocessor, PurchaseProcessor
34
{
35
    /**
36
     * @var EntityManagerInterface
37
     */
38
    protected $entityManager;
39
40
    /**
41
     * @var BaseInfo
42
     */
43
    protected $BaseInfo;
44
45
    /**
46
     * AddPointProcessor constructor.
47
     *
48
     * @param EntityManagerInterface $entityManager
49
     * @param BaseInfo $BaseInfo
50
     */
51 787
    public function __construct(EntityManagerInterface $entityManager, BaseInfo $BaseInfo)
52
    {
53 787
        $this->entityManager = $entityManager;
54 787
        $this->BaseInfo = $BaseInfo;
55
    }
56
57
    /*
58
     * ItemHolderPreprocessor
59
     */
60
61
    /**
62
     * {@inheritdoc}
63
     */
64 230
    public function process(ItemHolderInterface $itemHolder, PurchaseContext $context)
65
    {
66 230
        if (!$this->supports($itemHolder)) {
67 17
            return;
68
        }
69
70 213
        $this->removePointDiscountItem($itemHolder);
71
72
        // 利用ポイントがある場合は割引明細を追加
73 213
        if ($itemHolder->getUsePoint() > 0) {
74 17
            $discount = $this->pointToPrice($itemHolder->getUsePoint());
75 17
            $this->addPointDiscountItem($itemHolder, $discount);
76
        }
77
78
        // 付与ポイントを計算
79 213
        $addPoint = $this->calculateAddPoint($itemHolder);
80 213
        $itemHolder->setAddPoint($addPoint);
81
    }
82
83
    /*
84
     * ItemHolderValidator
85
     */
86
87
    /**
88
     * {@inheritdoc}
89
     */
90 61
    protected function validate(ItemHolderInterface $itemHolder, PurchaseContext $context)
91
    {
92 61
        if (!$this->supports($itemHolder)) {
93 17
            return;
94
        }
95
96
        // 所有ポイント < 利用ポイント
97 44
        $Customer = $itemHolder->getCustomer();
98 44
        if ($Customer->getPoint() < $itemHolder->getUsePoint()) {
99
            // 利用ポイントが所有ポイントを上回っていた場合は所有ポイントで上書き
100 2
            $itemHolder->setUsePoint($Customer->getPoint());
101 2
            $this->throwInvalidItemException('利用ポイントが所有ポイントを上回っています.');
102
        }
103
104
        // 支払い金額 < 利用ポイント
105 42
        if ($itemHolder->getTotal() < 0) {
106
            // 利用ポイントが支払い金額を上回っていた場合は支払い金額が0円以上となるようにポイントを調整
107 1
            $overPoint = floor($itemHolder->getTotal() / $this->BaseInfo->getPointConversionRate());
108 1
            $itemHolder->setUsePoint($itemHolder->getUsePoint() + $overPoint);
109 1
            $this->throwInvalidItemException('利用ポイントがお支払い金額を上回っています.');
110
        }
111
    }
112
113
    /*
114
     * PurchaseProcessor
115
     */
116
117
    /**
118
     * {@inheritdoc}
119
     */
120 12 View Code Duplication
    public function prepare(ItemHolderInterface $itemHolder, PurchaseContext $context)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
121
    {
122 12
        if (!$this->supports($itemHolder)) {
123 3
            return;
124
        }
125
126
        // ユーザの保有ポイントを減算
127 9
        $Customer = $itemHolder->getCustomer();
128 9
        $Customer->setPoint($Customer->getPoint() - $itemHolder->getUsePoint());
129
    }
130
131
    /**
132
     * {@inheritdoc
133
     */
134
    public function commit(ItemHolderInterface $target, PurchaseContext $context)
135
    {
136
        // 何もしない
137
    }
138
139
    /**
140
     * {@inheritdoc
141
     */
142 2 View Code Duplication
    public function rollback(ItemHolderInterface $itemHolder, PurchaseContext $context)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in 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...
143
    {
144
        // 利用したポイントをユーザに戻す.
145 2
        if (!$this->supports($itemHolder)) {
146
            return;
147
        }
148
149 2
        $Customer = $itemHolder->getCustomer();
150 2
        $Customer->setPoint($Customer->getPoint() + $itemHolder->getUsePoint());
151
    }
152
153
    /*
154
     * Helper methods
155
     */
156
157
    /**
158
     * Processorが実行出来るかどうかを返す.
159
     *
160
     * 以下を満たす場合に実行できる.
161
     *
162
     * - ポイント設定が有効であること.
163
     * - $itemHolderがOrderエンティティであること.
164
     * - 会員のOrderであること.
165
     *
166
     * @param ItemHolderInterface $itemHolder
167
     *
168
     * @return bool
169
     */
170 237
    private function supports(ItemHolderInterface $itemHolder)
171
    {
172 237
        if (!$this->BaseInfo->isOptionPoint()) {
173
            return false;
174
        }
175
176 237
        if (!$itemHolder instanceof Order) {
177
            return false;
178
        }
179
180 237
        if (!$itemHolder->getCustomer()) {
181 17
            return false;
182
        }
183
184 220
        return true;
185
    }
186
187
    /**
188
     * 付与ポイントを計算.
189
     *
190
     * @param ItemHolderInterface $itemHolder
191
     *
192
     * @return int
193
     */
194 213
    private function calculateAddPoint(ItemHolderInterface $itemHolder)
195
    {
196 213
        $basicPointRate = $this->BaseInfo->getBasicPointRate();
197
198
        // 明細ごとのポイントを集計
199 213
        $totalPoint = array_reduce($itemHolder->getItems()->toArray(), function ($carry, ItemInterface $item) use ($basicPointRate) {
0 ignored issues
show
Bug introduced by
The method toArray cannot be called on $itemHolder->getItems() (of type array<integer,object<Ecc...\Entity\ItemInterface>>).

Methods can only be called on objects. This check looks for methods being called on variables that have been inferred to never be objects.

Loading history...
200 213
            $pointRate = $item->getPointRate();
201 213
            if ($pointRate === null) {
202 213
                $pointRate = $basicPointRate;
203
            }
204
205
            // TODO: ポイントは税抜き分しか割引されない、ポイント明細は税抜きのままでいいのか?
206 213
            if ($item->isPoint()) {
207 17
                $point = round($item->getPrice() * ($pointRate / 100)) * $item->getQuantity();
208
            } else {
209
                // ポイント = 単価 * ポイント付与率 * 数量
210 212
                $point = round($item->getPriceIncTax() * ($pointRate / 100)) * $item->getQuantity();
0 ignored issues
show
Bug introduced by
The method getPriceIncTax() does not exist on Eccube\Entity\ItemInterface. Did you maybe mean getPrice()?

This check marks calls to methods that do not seem to exist on an object.

This is most likely the result of a method being renamed without all references to it being renamed likewise.

Loading history...
211
            }
212
213 213
            return $carry + $point;
214 213
        }, 0);
215
216 213
        return $totalPoint < 0 ? 0 : $totalPoint;
217
    }
218
219
    /**
220
     * 明細追加処理.
221
     *
222
     * @param ItemHolderInterface $itemHolder
223
     * @param integer $discount
224
     */
225 17
    private function addPointDiscountItem(ItemHolderInterface $itemHolder, $discount)
226
    {
227 17
        $DiscountType = $this->entityManager->find(OrderItemType::class, OrderItemType::POINT);
228 17
        $TaxInclude = $this->entityManager->find(TaxDisplayType::class, TaxDisplayType::INCLUDED);
229 17
        $Taxation = $this->entityManager->find(TaxType::class, TaxType::NON_TAXABLE);
230
231
        // TODO TaxProcessorが先行して実行されるため, 税額等の値は個別にセットする.
232 17
        $OrderItem = new OrderItem();
233 17
        $OrderItem->setProductName($DiscountType->getName())
234 17
            ->setPrice($discount)
235 17
            ->setQuantity(1)
236 17
            ->setTax(0)
237 17
            ->setTaxRate(0)
238 17
            ->setTaxRuleId(null)
239 17
            ->setRoundingType(null)
240 17
            ->setOrderItemType($DiscountType)
241 17
            ->setTaxDisplayType($TaxInclude)
242 17
            ->setTaxType($Taxation)
243 17
            ->setOrder($itemHolder);
0 ignored issues
show
Documentation introduced by
$itemHolder is of type object<Eccube\Entity\ItemHolderInterface>, but the function expects a null|object<Eccube\Entity\Order>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
244 17
        $itemHolder->addItem($OrderItem);
245
    }
246
247
    /**
248
     * 既存のポイント明細を削除する.
249
     *
250
     * @param ItemHolderInterface $itemHolder
251
     */
252 213
    private function removePointDiscountItem(ItemHolderInterface $itemHolder)
253
    {
254 213
        foreach ($itemHolder->getItems() as $item) {
255 212
            if ($item->isPoint()) {
256
                $itemHolder->removeOrderItem($item);
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Eccube\Entity\ItemHolderInterface as the method removeOrderItem() does only exist in the following implementations of said interface: Eccube\Entity\Order.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
257 212
                $this->entityManager->remove($item);
258
            }
259
        }
260
    }
261
262
    /**
263
     * ポイントを金額に変換する.
264
     *
265
     * @param integer $point int ポイント
266
     *
267
     * @return int 金額
268
     */
269 17
    private function pointToPrice($point)
270
    {
271 17
        return intval($point * $this->BaseInfo->getPointConversionRate()) * -1;
272
    }
273
}
274