FormulaEngine   A
last analyzed

Complexity

Total Complexity 38

Size/Duplication

Total Lines 227
Duplicated Lines 0 %

Test Coverage

Coverage 80.77%

Importance

Changes 5
Bugs 0 Features 0
Metric Value
eloc 95
c 5
b 0
f 0
dl 0
loc 227
rs 9.36
ccs 63
cts 78
cp 0.8077
wmc 38

15 Methods

Rating   Name   Duplication   Size   Complexity  
A setAsserter() 0 8 2
A getContext() 0 7 2
A getOnce() 0 7 2
A getDiscount() 0 7 2
A getCap() 0 7 2
A __construct() 0 7 2
A interpret() 0 23 6
A validate() 0 8 2
A buildContext() 0 10 1
A getInstallment() 0 7 2
A normalize() 0 14 3
A build() 0 20 6
A getRuler() 0 8 2
A getIncrease() 0 7 2
A getAsserter() 0 7 2
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-2020, HiQDev (http://hiqdev.com/)
9
 */
10
11
namespace hiqdev\php\billing\formula;
12
13
use Exception;
14
use hiqdev\php\billing\charge\ChargeModifier;
15
use hiqdev\php\billing\charge\modifiers\Cap;
16
use hiqdev\php\billing\charge\modifiers\Discount;
17
use hiqdev\php\billing\charge\modifiers\Increase;
18
use hiqdev\php\billing\charge\modifiers\Installment;
19
use hiqdev\php\billing\charge\modifiers\Once;
20
use Hoa\Ruler\Context;
21
use Hoa\Ruler\Model\Model;
22
use Hoa\Ruler\Ruler;
23
use Hoa\Visitor\Visit;
24
use Psr\SimpleCache\CacheInterface;
25
26
/**
27
 * @author Andrii Vasyliev <[email protected]>
28
 */
29
class FormulaEngine implements FormulaEngineInterface
30
{
31
    public const FORMULAS_SEPARATOR = "\n";
32
33
    /**
34
     * @var Ruler
35
     */
36
    protected $ruler;
37
38
    /**
39
     * @var Visit|Asserter
40
     */
41
    protected $asserter;
42
43
    /**
44
     * @var Context
45
     */
46
    protected $context;
47
48
    /**
49
     * @var ChargeModifier
50
     */
51
    protected $discount;
52
53
    /**
54
     * @var ChargeModifier
55
     */
56 14
    protected $installment;
57
58 14
    /**
59
     * @var ChargeModifier
60
     */
61
    protected $increase;
62 14
63 14
    protected ?Cap $cap = null;
64
65 6
    protected ?Once $once = null;
66
    /**
67
     * @var CacheInterface
68 6
     */
69 5
    private $cache;
70 1
71
    public function __construct(CacheInterface $cache)
72 1
    {
73 1
        if (!class_exists(Context::class)) {
74
            throw new Exception('to use formula engine install `hoa/ruler`');
75
        }
76
77
        $this->cache = $cache;
78
    }
79
80 5
    public function build(string $formula): ChargeModifier
81 1
    {
82
        try {
83
            $model = $this->interpret($formula);
84 4
            $result = $this->getRuler()->assert($model, $this->getContext());
85
        } catch (FormulaSemanticsError $e) {
86
            throw FormulaSemanticsError::fromException($e, $formula);
87
        } catch (FormulaEngineException $e) {
88
            throw $e;
89
        } catch (\Hoa\Ruler\Exception\Asserter $e) {
90 6
            throw FormulaRuntimeError::fromException($e, $formula);
91
        } catch (\Exception $e) {
92
            throw FormulaRuntimeError::fromException($e, $formula, 'Formula run failed');
93 6
        }
94
95 6
        if (!$result instanceof ChargeModifier) {
0 ignored issues
show
introduced by
$result is never a sub-type of hiqdev\php\billing\charge\ChargeModifier.
Loading history...
96 6
            throw FormulaRuntimeError::create($formula, 'Formula run returned unexpected result');
97 6
        }
98 6
99 5
        return $result;
100
    }
101
102 5
    /**
103 1
     * @throws FormulaEngineException
104 1
     */
105
    public function interpret(string $formula): Model
106
    {
107
        try {
108
            $normalize = $this->normalize($formula);
109
            if ($normalize === null) {
110
                $normalize = '';
111
            }
112 14
            $rule = str_replace(self::FORMULAS_SEPARATOR, ' AND ', $normalize);
113
114 14
            $key = md5(__METHOD__ . $rule);
115
            $model = $this->cache->get($key);
116 14
            if ($model === null) {
117 14
                $model = $this->getRuler()->interpret($rule);
118 5
                $this->cache->set($key, $model);
119
            }
120
121 10
            return $model;
122 14
        } catch (\Hoa\Compiler\Exception\Exception $exception) {
123 14
            throw FormulaSyntaxError::fromException($exception, $formula);
124
        } catch (\Hoa\Ruler\Exception\Interpreter $exception) {
125 14
            throw FormulaSyntaxError::fromException($exception, $formula);
126
        } catch (\Throwable $exception) {
127
            throw FormulaSyntaxError::create($formula, 'Failed to interpret formula: ' . $exception->getMessage());
128
        }
129
    }
130
131
    public function normalize(string $formula): ?string
132
    {
133 4
        $lines = explode(self::FORMULAS_SEPARATOR, $formula);
134
        $normalized = array_map(function ($value) {
135
            $value = trim($value);
136 4
            if ('' === $value) {
137
                return null;
138 2
            }
139 2
140 2
            return $value;
141
        }, $lines);
142
        $cleared = array_filter($normalized);
143
144 6
        return empty($cleared) ? null : implode(self::FORMULAS_SEPARATOR, $cleared);
145
    }
146 6
147 6
    /**
148 6
     * Validates $formula.
149
     *
150
     * @return string|null `null` when formula has no errors or string error message
151 6
     */
152
    public function validate(string $formula): ?string
153
    {
154
        try {
155
            $this->build($formula);
156
157
            return null;
158
        } catch (FormulaEngineException $e) {
159
            return $e->getMessage();
160
        }
161
    }
162
163
    public function getRuler(): Ruler
164 6
    {
165
        if ($this->ruler === null) {
166 6
            $this->ruler = new Ruler();
167 6
            $this->ruler->setAsserter($this->getAsserter());
168
        }
169
170 6
        return $this->ruler;
171
    }
172
173 5
    public function setAsserter(Visit $asserter): self
174
    {
175 5
        $this->asserter = $asserter;
176 5
        if ($this->ruler !== null) {
177
            $this->ruler->setAsserter($asserter);
178
        }
179 5
180
        return $this;
181
    }
182 5
183
    public function getAsserter(): Visit
184 5
    {
185 5
        if ($this->asserter === null) {
186 5
            $this->asserter = new Asserter();
187
        }
188 5
189
        return $this->asserter;
190
    }
191 5
192
    public function getContext(): Context
193 5
    {
194 5
        if ($this->context === null) {
195
            $this->context = $this->buildContext();
196
        }
197 5
198
        return $this->context;
199
    }
200 5
201
    protected function buildContext(): Context
202 5
    {
203 5
        $context = new Context();
204
        $context['discount'] = $this->getDiscount();
205
        $context['installment'] = $this->getInstallment();
206 5
        $context['increase'] = $this->getIncrease();
207
        $context['cap'] = $this->getCap();
208
        $context['once'] = $this->getOnce();
209
210
        return $context;
211
    }
212
213
    public function getDiscount(): ChargeModifier
214
    {
215
        if ($this->discount === null) {
216
            $this->discount = new Discount();
217
        }
218
219
        return $this->discount;
220
    }
221
222
    public function getInstallment(): ChargeModifier
223
    {
224
        if ($this->installment === null) {
225
            $this->installment = new Installment();
226
        }
227
228
        return $this->installment;
229
    }
230
231
    public function getIncrease(): ChargeModifier
232
    {
233
        if ($this->increase === null) {
234
            $this->increase = new Increase();
235
        }
236
237
        return $this->increase;
238
    }
239
240
    private function getCap(): ChargeModifier
241
    {
242
        if ($this->cap === null) {
243
            $this->cap = new Cap();
244
        }
245
246
        return $this->cap;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->cap could return the type null which is incompatible with the type-hinted return hiqdev\php\billing\charge\ChargeModifier. Consider adding an additional type-check to rule them out.
Loading history...
247
    }
248
249
    private function getOnce(): ChargeModifier
250
    {
251
        if ($this->once === null) {
252
            $this->once = new Once();
253
        }
254
255
        return $this->once;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->once could return the type null which is incompatible with the type-hinted return hiqdev\php\billing\charge\ChargeModifier. Consider adding an additional type-check to rule them out.
Loading history...
256
    }
257
}
258