Completed
Push — master ( 46949b...2ae60e )
by Andrii
03:08
created

FeatureContext::isExpectedError()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
1
<?php
2
/**
3
 * PHP Billing Library
4
 *
5
 * @link      https://github.com/hiqdev/php-billing
6
 * @package   php-billing
7
 * @license   BSD-3-Clause
8
 * @copyright Copyright (c) 2017-2018, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\php\billing\tests\behat\bootstrap;
12
13
use Behat\Behat\Context\Context;
14
use Closure;
15
use DateTimeImmutable;
16
use hiqdev\php\billing\action\Action;
17
use hiqdev\php\billing\charge\Charge;
18
use hiqdev\php\billing\charge\ChargeInterface;
19
use hiqdev\php\billing\customer\Customer;
20
use hiqdev\php\billing\formula\FormulaEngine;
21
use hiqdev\php\billing\price\SinglePrice;
22
use hiqdev\php\billing\target\Target;
23
use hiqdev\php\billing\type\Type;
24
use hiqdev\php\units\Quantity;
25
use Money\Currencies\ISOCurrencies;
26
use Money\Parser\DecimalMoneyParser;
27
use NumberFormatter;
28
use PHPUnit\Framework\Assert;
29
30
/**
31
 * Defines application features from the specific context.
32
 */
33
class FeatureContext implements Context
34
{
35
    protected $engine;
36
37
    /** @var Customer */
38
    protected $customer;
39
    /**
40
     * @var \hiqdev\php\billing\price\PriceInterface|\hiqdev\php\billing\charge\FormulaChargeModifierTrait
41
     *
42
     * TODO: FormulaChargeModifierTrait::setFormula() must be moved to interface
43
     */
44
    protected $price;
45
46
    /** @var string */
47
    protected $formula;
48
49
    /**
50
     * @var \hiqdev\php\billing\action\ActionInterface|\hiqdev\php\billing\action\AbstractAction
51
     */
52
    protected $action;
53
    /**
54
     * @var ChargeInterface[]
55
     */
56
    protected $charges;
57
58
    /** @var \Money\MoneyParser */
59
    protected $moneyParser;
60
61
    /** @var string */
62
    protected $expectedError;
63
64
    /**
65
     * Initializes context.
66
     */
67
    public function __construct()
68
    {
69
        $this->customer = new Customer(null, 'somebody');
70
        $this->moneyParser = new DecimalMoneyParser(new ISOCurrencies());
71
    }
72
73
    /**
74
     * @Given /(\S+) (\S+) price is ([0-9.]+) (\w+) per (\w+)/
75
     */
76
    public function priceIs($target, $type, $sum, $currency, $unit)
77
    {
78
        $type = new Type(Type::ANY, $type);
79
        $target = new Target(Target::ANY, $target);
80
        $quantity = Quantity::create($unit, 0);
81
        $sum = $this->moneyParser->parse($sum, $currency);
82
        $this->price = new SinglePrice(null, $type, $target, null, $quantity, $sum);
83
    }
84
85
    /**
86
     * @Given /action is (\S+) (\w+) ([0-9.]+) (\S+)/
87
     */
88
    public function actionIs($target, $type, $amount, $unit)
89
    {
90
        $type = new Type(Type::ANY, $type);
91
        $target = new Target(Target::ANY, $target);
92
        $quantity = Quantity::create($unit, $amount);
93
        $time = new DateTimeImmutable();
94
        $this->action = new Action(null, $type, $target, $quantity, $this->customer, $time);
95
    }
96
97
    /**
98
     * @Given /formula is (.+)/
99
     * @param string $formula
100
     */
101
    public function formulaIs(string $formula): void
102
    {
103
        $this->formula = $formula;
104
    }
105
106
    protected function getFormulaEngine()
107
    {
108
        if ($this->engine === null) {
109
            $this->engine = new FormulaEngine();
110
        }
111
112
        return $this->engine;
113
    }
114
115
    /**
116
     * @When /action date is ([0-9.-]+)/
117
     * @param string $date
118
     * @throws \Exception
119
     */
120
    public function actionDateIs(string $date): void
121
    {
122
        $this->action->setTime(new DateTimeImmutable($date));
0 ignored issues
show
Bug introduced by
The method setTime does only exist in hiqdev\php\billing\action\AbstractAction, but not in hiqdev\php\billing\action\ActionInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
123
    }
124
125
    /**
126
     * @Then /^error is (.+)$/
127
     * @param string $error
128
     */
129
    public function errorIs(string $error): void
130
    {
131
        $this->expectedError = $error;
132
    }
133
134
    /**
135
     * @Then /^(\w+) charge is $/
136
     * @param string $numeral
137
     */
138
    public function emptyCharge(string $numeral): void
139
    {
140
        $this->chargeIs($numeral);
141
    }
142
143
    /**
144
     * @Then /^(\w+) charge is (\S+) +([0-9.]+) ([A-Z]{3})$/
145
     */
146
    public function chargeWithSum($numeral, $type = null, $sum = null, $currency = null): void
147
    {
148
        $this->chargeIs($numeral, $type, $sum, $currency);
149
    }
150
151
    /**
152
     * @Then /^(\w+) charge is (\S+) +([0-9.]+) ([A-Z]{3}) reason (.+)/
153
     */
154
    public function chargeWithReason($numeral, $type = null, $sum = null, $currency = null, $reason = null): void
155
    {
156
        $this->chargeIs($numeral, $type, $sum, $currency, $reason);
157
    }
158
159
    public function chargeIs($numeral, $type = null, $sum = null, $currency = null, $reason = null): void
160
    {
161
        $no = $this->ensureNo($numeral);
162
        if ($no === 0) {
163
            $this->calculateCharges();
164
        }
165
        $this->assertCharge($this->charges[$no] ?? null, $type, $sum, $currency, $reason);
166
    }
167
168
    /**
169
     * @When /^calculating charges$/
170
     */
171
    public function calculateCharges(): void
172
    {
173
        $this->expectError(function () {
174
            $this->price->setFormula($this->getFormulaEngine()->build($this->formula));
0 ignored issues
show
Bug introduced by
The method setFormula does only exist in hiqdev\php\billing\charg...mulaChargeModifierTrait, but not in hiqdev\php\billing\price\PriceInterface.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
175
            $this->charges = $this->price->calculateCharges($this->action);
0 ignored issues
show
Bug introduced by
The method calculateCharges does only exist in hiqdev\php\billing\price\PriceInterface, but not in hiqdev\php\billing\charg...mulaChargeModifierTrait.

It seems like the method you are trying to call exists only in some of the possible types.

Let’s take a look at an example:

class A
{
    public function foo() { }
}

class B extends A
{
    public function bar() { }
}

/**
 * @param A|B $x
 */
function someFunction($x)
{
    $x->foo(); // This call is fine as the method exists in A and B.
    $x->bar(); // This method only exists in B and might cause an error.
}

Available Fixes

  1. Add an additional type-check:

    /**
     * @param A|B $x
     */
    function someFunction($x)
    {
        $x->foo();
    
        if ($x instanceof B) {
            $x->bar();
        }
    }
    
  2. Only allow a single type to be passed if the variable comes from a parameter:

    function someFunction(B $x) { /** ... */ }
    
Loading history...
176
        });
177
    }
178
179
    public function expectError(Closure $closure): void
180
    {
181
        try {
182
            call_user_func($closure);
183
        } catch (\Exception $e) {
184
            if ($this->isExpectedError($e)) {
185
                $this->expectedError = null;
186
            } else {
187
                throw $e;
188
            }
189
        }
190
    }
191
192
    protected function isExpectedError(\Exception $e): bool
193
    {
194
        return $this->startsWith($e->getMessage(), $this->expectedError);
195
    }
196
197
    protected function startsWith(string $string, string $prefix = null): bool
198
    {
199
        return strncmp($string, $prefix, strlen($prefix)) === 0;
200
    }
201
202
    /**
203
     * @param ChargeInterface|null $charge
204
     * @param string|null $type
205
     * @param string|null $sum
206
     * @param string|null $currency
207
     * @param string|null $reason
208
     */
209
    public function assertCharge($charge, $type, $sum, $currency, $reason): void
210
    {
211
        if (empty($type) && empty($sum) && empty($currency)) {
212
            Assert::assertNull($charge);
213
214
            return;
215
        }
216
        Assert::assertInstanceOf(Charge::class, $charge);
217
        Assert::assertSame($type, $charge->getPrice()->getType()->getName());
0 ignored issues
show
Bug introduced by
It seems like $charge is not always an object, but can also be of type null. Maybe add an additional type check?

If a variable is not always an object, we recommend to add an additional type check to ensure your method call is safe:

function someFunction(A $objectMaybe = null)
{
    if ($objectMaybe instanceof A) {
        $objectMaybe->doSomething();
    }
}
Loading history...
218
        $money = $this->moneyParser->parse($sum, $currency);
219
        Assert::assertEquals($money, $charge->getSum()); // TODO: Should we add `getSum()` to ChargeInterface?
220
        if ($reason !== null) {
221
            Assert::assertSame($reason, $charge->getComment()); // TODO: Should we add `getComment()` to ChargeInterface?
222
        }
223
    }
224
225
    private function ensureNo(string $numeral): int
226
    {
227
        $formatter = new NumberFormatter('en_EN', NumberFormatter::SPELLOUT);
228
        $result = $formatter->parse($numeral);
229
        if ($result === false) {
230
            throw new \Exception("Wrong numeral '$numeral'");
231
        }
232
233
        return --$result;
234
    }
235
}
236