Issues (13)

src/Contract/Command.php (10 issues)

1
<?php
2
3
/**
4
 * This file is part of the bugloos/fault-tolerance-bundle project.
5
 * (c) Bugloos <https://bugloos.com/>
6
 * For the full copyright and license information, please view
7
 * the LICENSE file that was distributed with this source code.
8
 */
9
10
namespace Bugloos\FaultToleranceBundle\Contract;
11
12
use Bugloos\FaultToleranceBundle\Config\Config;
13
use Bugloos\FaultToleranceBundle\Enum\EventEnum;
14
use Bugloos\FaultToleranceBundle\Exception\FallbackNotAvailableException;
15
use Bugloos\FaultToleranceBundle\Exception\RuntimeException;
16
use Bugloos\FaultToleranceBundle\Factory\CircuitBreakerFactory;
17
use Bugloos\FaultToleranceBundle\Factory\RequestCacheFactory;
18
use Bugloos\FaultToleranceBundle\RequestCache\RequestCache;
19
use Bugloos\FaultToleranceBundle\RequestLog\RequestLog;
20
use LogicException;
21
use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface;
0 ignored issues
show
The type Symfony\Contracts\HttpCl...lientExceptionInterface was not found. Maybe you did not declare it correctly or list all dependencies?

The issue could also be caused by a filter entry in the build configuration. If the path has been excluded in your configuration, e.g. excluded_paths: ["lib/*"], you can move it to the dependency path list as follows:

filter:
    dependency_paths: ["lib/*"]

For further information see https://scrutinizer-ci.com/docs/tools/php/php-scrutinizer/#list-dependency-paths

Loading history...
22
use Psr\Cache\InvalidArgumentException;
23
use Exception;
24
25
/**
26
 * @author Mojtaba Gheytasi <[email protected]>
27
 */
28
abstract class Command
29
{
30
    private CircuitBreakerFactory $circuitBreakerFactory;
31
32
    private RequestCacheFactory $requestCacheFactory;
33
34
    private RequestLog $requestLog;
35
36
    private array $config;
37
38
    /**
39
     * Command Key, used for grouping Circuit Breakers
40
     */
41
    protected string $commandKey = '';
42
43
    /**
44
     * Events logged during execution
45
     */
46
    private array $executionEvents = [];
47
48
    /**
49
     * Execution time in milliseconds
50
     */
51
    private int $executionTime;
52
53
    /**
54
     * Timestamp in milliseconds
55
     */
56
    private int $invocationStartTime;
57
58
    /**
59
     * Exception thrown if there was one
60
     */
61
    private \Exception $executionException;
62
63
    public function setCircuitBreakerFactory(CircuitBreakerFactory $circuitBreakerFactory)
64
    {
65
        $this->circuitBreakerFactory = $circuitBreakerFactory;
66
    }
67
68
    public function setRequestCacheFactory(RequestCacheFactory $requestCacheFactory)
69
    {
70
        $this->requestCacheFactory = $requestCacheFactory;
71
    }
72
73
    public function setRequestLog(RequestLog $requestLog)
74
    {
75
        $this->requestLog = $requestLog;
76
    }
77
78
    /**
79
     * Determines and returns command key, used for circuit breaker grouping
80
     */
81
    public function getCommandKey(): string
82
    {
83
        /* If the command key hasn't been defined in the class we use the current class name */
84
        if ($this->commandKey === '') {
85
            $this->commandKey = str_replace('\\', '.', get_class($this));
86
        }
87
88
        return $this->commandKey;
89
    }
90
91
    public function initializeConfig()
92
    {
93
        $this->config = $this->config() !== null ?
0 ignored issues
show
The condition $this->config() !== null is always false.
Loading history...
Are you sure the usage of $this->config() targeting Bugloos\FaultToleranceBu...tract\Command::config() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
94
            $this->config()->toArray() :
95
            (new Config())->toArray();
96
    }
97
98
    public function getConfig(): array
99
    {
100
        return $this->config;
101
    }
102
103
    protected function config(): ?Config
104
    {
105
        return null;
106
    }
107
108
    /**
109
     * Determines whether request caching is enabled for this command
110
     */
111
    private function isRequestCacheEnabled(): bool
112
    {
113
        return $this->config['requestCache']['enabled'] && $this->getCacheKey() !== null;
0 ignored issues
show
Are you sure the usage of $this->getCacheKey() targeting Bugloos\FaultToleranceBu...\Command::getCacheKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
114
    }
115
116
    /**
117
     * @throws Exception|InvalidArgumentException|LogicException
118
     */
119
    public function execute()
120
    {
121
        $circuitBreaker = $this->getCircuitBreaker();
122
123
        $cacheEnabled = $this->isRequestCacheEnabled();
124
125
        $this->recordExecutedCommand();
126
127
        if ($cacheEnabled) {
128
            $requestCache = $this->getCacheRequest();
129
            $cacheExists = $requestCache->exists($this->getCommandKey(), $this->getCacheKey());
0 ignored issues
show
Are you sure the usage of $this->getCacheKey() targeting Bugloos\FaultToleranceBu...\Command::getCacheKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
130
            if ($cacheExists) {
131
                $this->recordExecutionEvent(EventEnum::RESPONSE_FROM_CACHE);
132
                return $requestCache->get($this->getCommandKey(), $this->getCacheKey());
0 ignored issues
show
Are you sure the usage of $this->getCacheKey() targeting Bugloos\FaultToleranceBu...\Command::getCacheKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
133
            }
134
        }
135
        if (! $circuitBreaker->allowRequest()) {
136
            $this->recordExecutionEvent(EventEnum::SHORT_CIRCUITED);
137
            return $this->getFallbackOrThrowException();
138
        }
139
140
        $this->invocationStartTime = $this->getTimeInMilliseconds();
141
142
        try {
143
            $result = $this->run();
144
            $this->recordExecutionTime();
145
            $circuitBreaker->markAsSuccess();
146
            $this->recordExecutionEvent(EventEnum::SUCCESS);
147
        } catch (ClientExceptionInterface $exception) {
148
            /* without any tracking or fallback logic */
149
            $this->recordExecutionTime();
150
            throw new LogicException('Logic exception on proxy command : ' . static::class);
151
        } catch (Exception $exception) {
152
            $this->recordExecutionTime();
153
            $circuitBreaker->markAsFailure();
154
            $this->executionException = $exception;
155
            $this->recordExecutionEvent(EventEnum::FAILURE);
156
            return $this->getFallbackOrThrowException($exception);
157
        }
158
159
        if ($cacheEnabled) {
160
            $requestCache->put(
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $requestCache does not seem to be defined for all execution paths leading up to this point.
Loading history...
161
                $this->getCommandKey(),
162
                $this->getCacheKey(),
0 ignored issues
show
Are you sure the usage of $this->getCacheKey() targeting Bugloos\FaultToleranceBu...\Command::getCacheKey() seems to always return null.

This check looks for function or method calls that always return null and whose return value is used.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
if ($a->getObject()) {

The method getObject() can return nothing but null, so it makes no sense to use the return value.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
163
                $result,
164
                $this->config['requestCache']['expiresCacheAfter']
165
            );
166
        }
167
168
        return $result;
169
    }
170
171
    /**
172
     * The code to be executed
173
     */
174
    abstract protected function run();
175
176
    /**
177
     * Custom logic proceeding event generation
178
     */
179
    protected function processExecutionEvent(string $eventName)
0 ignored issues
show
The parameter $eventName is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

179
    protected function processExecutionEvent(/** @scrutinizer ignore-unused */ string $eventName)

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
180
    {
181
    }
182
183
    /**
184
     * Logic to record events and exceptions as they take place
185
     */
186
    private function recordExecutionEvent(string $eventName): void
187
    {
188
        $this->executionEvents[] = $eventName;
189
190
        $this->processExecutionEvent($eventName);
191
    }
192
193
    /**
194
     * Attempts to retrieve fallback by calling getFallback
195
     *
196
     * @param Exception|null $originalException (Optional) If null, the request was short-circuited
197
     * @return array
198
     * @throws Exception
199
     */
200
    private function getFallbackOrThrowException(Exception $originalException = null)
201
    {
202
        $message = $originalException === null ? 'Short-circuited' : $originalException->getMessage();
203
        try {
204
            if (! $this->config['fallback']['enabled']) {
205
                throw new RuntimeException(
206
                    $message . ' and fallback disabled',
207
                    get_class($this),
0 ignored issues
show
get_class($this) of type string is incompatible with the type integer expected by parameter $commandClass of Bugloos\FaultToleranceBu...xception::__construct(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

207
                    /** @scrutinizer ignore-type */ get_class($this),
Loading history...
208
                    $originalException
209
                );
210
            }
211
            try {
212
                $executionResult = $this->getFallback();
213
                $this->recordExecutionEvent(EventEnum::FALLBACK_SUCCESS);
214
                return $executionResult;
215
            } catch (FallbackNotAvailableException $fallbackException) {
216
                throw new RuntimeException(
217
                    $message . ' and no fallback available',
218
                    get_class($this),
219
                    $originalException
220
                );
221
            } catch (Exception $fallbackException) {
222
                $this->recordExecutionEvent(EventEnum::FALLBACK_FAILURE);
223
                throw new RuntimeException(
224
                    $message . ' and failed retrieving fallback',
225
                    get_class($this),
226
                    $originalException,
227
                    $fallbackException
228
                );
229
            }
230
        } catch (Exception $exception) {
231
            $this->recordExecutionEvent(EventEnum::EXCEPTION_THROWN);
232
            throw $exception;
233
        }
234
    }
235
236
    /**
237
     * Code for when execution fails for whatever reason
238
     *
239
     * @throws FallbackNotAvailableException When no custom fallback provided
240
     */
241
    protected function getFallback()
242
    {
243
        throw new FallbackNotAvailableException('No fallback available');
244
    }
245
246
    /**
247
     * Key to be used for request caching.
248
     *
249
     * By default this return null, which means "do not cache". To enable caching,
250
     * override this method and return a string key uniquely representing the state of a command instance.
251
     *
252
     * If multiple command instances are executed within current HTTP request, only the first one will be
253
     * executed and all others returned from cache.
254
     *
255
     * @return string|null
256
     */
257
    protected function getCacheKey(): ?string
258
    {
259
        return null;
260
    }
261
262
    /**
263
     * Returns events collected
264
     *
265
     * @return array
266
     */
267
    public function getExecutionEvents(): array
268
    {
269
        return $this->executionEvents;
270
    }
271
272
    /**
273
     * Returns execution time in milliseconds, null if not executed
274
     *
275
     * @return null|integer
276
     */
277
    public function getExecutionTimeInMilliseconds(): ?int
278
    {
279
        return $this->executionTime;
280
    }
281
282
    /**
283
     * Returns exception thrown while executing the command, if there was any
284
     *
285
     * @return Exception|null
286
     */
287
    public function getExecutionException(): ?Exception
288
    {
289
        return $this->executionException;
290
    }
291
292
    /**
293
     * Records command execution time if the command was executed, not short-circuited and not returned from cache
294
     */
295
    private function recordExecutionTime(): void
296
    {
297
        $this->executionTime = $this->getTimeInMilliseconds() - $this->invocationStartTime;
298
    }
299
300
    /**
301
     * Returns current time on the server in milliseconds
302
     *
303
     * @return float
304
     */
305
    private function getTimeInMilliseconds(): float
306
    {
307
        return floor(microtime(true) * 1000);
308
    }
309
310
    /**
311
     * Adds reference to the command to the current request log
312
     */
313
    private function recordExecutedCommand(): void
314
    {
315
        if ($this->isRequestLogEnabled()) {
316
            $this->requestLog->addExecutedCommand($this);
317
        }
318
    }
319
320
    private function isRequestLogEnabled(): bool
321
    {
322
        return $this->config['requestLog']['enabled'];
323
    }
324
325
    private function getCircuitBreaker()
326
    {
327
        return $this->circuitBreakerFactory->create(
328
            $this->getCommandKey(),
329
            $this->config['circuitBreaker']
330
        );
331
    }
332
333
    /**
334
     * @throws Exception
335
     */
336
    private function getCacheRequest(): RequestCache
337
    {
338
        return $this->requestCacheFactory->create(
339
            $this->config['requestCache']['storage']
340
        );
341
    }
342
}
343