Passed
Push — master ( cd6b77...0457a6 )
by Dmitry
32:58 queued 22:11
created

FormulaEngine::getLeasing()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 3

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 6

Importance

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