Passed
Push — master ( 364662...9798b7 )
by Andrii
02:56
created

FormulaEngine::validate()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 5
CRAP Score 2

Importance

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