Failed Conditions
Pull Request — master (#32)
by Mathieu
02:28
created

RetryStrategy::changeServer()   B

Complexity

Conditions 5
Paths 4

Size

Total Lines 15
Code Lines 10

Duplication

Lines 0
Ratio 0 %

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 15
rs 8.8571
cc 5
eloc 10
nc 4
nop 2
1
<?php
2
3
namespace Ez\DbLinker\RetryStrategy;
4
5
use Exception;
6
use stdClass;
7
use Doctrine\DBAL\Driver\Connection;
8
use Ez\DbLinker\Driver\Connection\MasterSlavesConnection;
9
use Ez\DbLinker\Driver\Connection\RetryConnection;
10
11
trait RetryStrategy
12
{
13
    private $retryLimit;
14
    private $nestingLimit;
15
16
    public function __construct($retryLimit = INF, $nestingLimit = 1)
17
    {
18
        $this->retryLimit = $retryLimit;
19
        $this->nestingLimit = $nestingLimit;
20
    }
21
22
    public function shouldRetry(Exception $exception, Callable $callable, $context) {
0 ignored issues
show
Unused Code introduced by
The parameter $callable is not used and could be removed.

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

Loading history...
23
        if (!$this->canRetry($context)) {
24
            return false;
25
        }
26
        $this->nestingLimit--;
27
        $strategy = $this->errorCodeStrategy($this->errorCode($exception));
28
        $res = $this->applyStrategy($strategy, $context);
29
        $this->nestingLimit++;
30
        return $res;
31
    }
32
33
    public function retryLimit()
34
    {
35
        return $this->retryLimit > 0 ? (int) $this->retryLimit : 0;
36
    }
37
38
    private function canRetry($context)
39
    {
40
        return $this->retryLimit > 0 &&
41
            $this->nestingLimit > 0 &&
42
            (!$context instanceof RetryConnection || $context->transactionLevel() === 0);
43
    }
44
45
    private function errorCodeStrategy($errorCode)
46
    {
47
        $strategy = (object) [
48
            'retry' => true,
49
            'wait' => 0,
50
            'changeServer' => false,
51
            'reconnect' => false,
52
        ];
53
        $errorCodeStrategies = $this->errorCodeStrategies();
54
        if (array_key_exists($errorCode, $errorCodeStrategies)) {
55
            foreach ($errorCodeStrategies[$errorCode] as $behavior => $value) {
56
                $strategy->$behavior = $value;
57
            }
58
            return $strategy;
59
        }
60
        return (object) ['retry' => false];
61
    }
62
63
    private function applyStrategy(stdClass $strategy, $context) {
64
        if ($strategy->retry === false || !$this->changeServer($strategy, $context)) {
65
            return false;
66
        }
67
        sleep($strategy->wait);
68
        $this->reconnect($strategy, $context);
69
        $this->retryLimit--;
70
        return true;
71
    }
72
73
    private function changeServer(stdClass $strategy, $context)
74
    {
75
        if (!$strategy->changeServer) {
76
            return true;
77
        }
78
        if (!$context instanceof RetryConnection) {
79
            return false;
80
        }
81
        $wrappedConnection = $context->wrappedConnection();
82
        if ($wrappedConnection instanceof MasterSlavesConnection && !$wrappedConnection->isConnectedToMaster()) {
83
            $wrappedConnection->disableCurrentSlave();
84
            return true;
85
        }
86
        return false;
87
    }
88
89
    private function reconnect(stdClass $strategy, $context)
90
    {
91
        if ($strategy->reconnect && $context instanceof Connection) {
92
            $context->close();
0 ignored issues
show
Bug introduced by
It seems like you code against a concrete implementation and not the interface Doctrine\DBAL\Driver\Connection as the method close() does only exist in the following implementations of said interface: Doctrine\DBAL\Connection, Doctrine\DBAL\Connections\MasterSlaveConnection, Doctrine\DBAL\Portability\Connection, Doctrine\DBAL\Sharding\PoolingShardConnection, Ez\DbLinker\Driver\Connection\RetryConnection.

Let’s take a look at an example:

interface User
{
    /** @return string */
    public function getPassword();
}

class MyUser implements User
{
    public function getPassword()
    {
        // return something
    }

    public function getDisplayName()
    {
        // return some name.
    }
}

class AuthSystem
{
    public function authenticate(User $user)
    {
        $this->logger->info(sprintf('Authenticating %s.', $user->getDisplayName()));
        // do something.
    }
}

In the above example, the authenticate() method works fine as long as you just pass instances of MyUser. However, if you now also want to pass a different implementation of User which does not have a getDisplayName() method, the code will break.

Available Fixes

  1. Change the type-hint for the parameter:

    class AuthSystem
    {
        public function authenticate(MyUser $user) { /* ... */ }
    }
    
  2. Add an additional type-check:

    class AuthSystem
    {
        public function authenticate(User $user)
        {
            if ($user instanceof MyUser) {
                $this->logger->info(/** ... */);
            }
    
            // or alternatively
            if ( ! $user instanceof MyUser) {
                throw new \LogicException(
                    '$user must be an instance of MyUser, '
                   .'other instances are not supported.'
                );
            }
    
        }
    }
    
Note: PHP Analyzer uses reverse abstract interpretation to narrow down the types inside the if block in such a case.
  1. Add the method to the interface:

    interface User
    {
        /** @return string */
        public function getPassword();
    
        /** @return string */
        public function getDisplayName();
    }
    
Loading history...
93
        }
94
    }
95
96
    protected abstract function errorCodeStrategies();
97
    protected abstract function errorCode(Exception $exception);
98
}
99