Completed
Push — master ( c79300...9eb332 )
by Arne
02:48
created

StandardIndexMerger::merge()   B

Complexity

Conditions 10
Paths 20

Size

Total Lines 49

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 49
rs 7.246
c 0
b 0
f 0
cc 10
nc 20
nop 5

How to fix   Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
namespace Storeman\IndexMerger;
4
5
use Psr\Log\LoggerAwareInterface;
6
use Psr\Log\LoggerInterface;
7
use Psr\Log\NullLogger;
8
use Storeman\Config\Configuration;
9
use Storeman\ConflictHandler\ConflictHandlerInterface;
10
use Storeman\Hash\HashProvider;
11
use Storeman\Index\Comparison\IndexObjectComparison;
12
use Storeman\Index\Index;
13
use Storeman\Index\IndexObject;
14
15
class StandardIndexMerger implements IndexMergerInterface, LoggerAwareInterface
16
{
17
    public const VERIFY_CONTENT = 1;
18
19
20
    /**
21
     * @var Configuration
22
     */
23
    protected $configuration;
24
25
    /**
26
     * @var HashProvider
27
     */
28
    protected $hashProvider;
29
30
    /**
31
     * @var LoggerInterface
32
     */
33
    protected $logger;
34
35
    public function __construct(Configuration $configuration, HashProvider $hashProvider)
36
    {
37
        $this->configuration = $configuration;
38
        $this->hashProvider = $hashProvider;
39
        $this->logger = new NullLogger();
40
    }
41
42
    /**
43
     * {@inheritdoc}
44
     */
45
    public function setLogger(LoggerInterface $logger)
46
    {
47
        $this->logger = $logger;
48
    }
49
50
    /**
51
     * {@inheritdoc}
52
     */
53
    public function merge(ConflictHandlerInterface $conflictHandler, Index $remoteIndex, Index $localIndex, ?Index $lastLocalIndex, int $options = 0): Index
54
    {
55
        $this->logger->info(sprintf("Merging indices using %s (Options: %s)", static::class, static::getOptionsDebugString($options)));
56
57
        $mergedIndex = new Index();
58
        $lastLocalIndex = $lastLocalIndex ?: new Index();
59
60
        $diff = $localIndex->getDifference($remoteIndex, IndexObject::CMP_IGNORE_BLOBID | IndexObject::CMP_IGNORE_INODE);
61
62
        $this->logger->debug(sprintf("Found %d differences between local and remote index", count($diff)));
63
64
        foreach ($diff as $cmp)
65
        {
66
            /** @var IndexObjectComparison $cmp */
67
68
            $localObject = $localIndex->getObjectByPath($cmp->getRelativePath());
69
            $lastLocalObject = $lastLocalIndex->getObjectByPath($cmp->getRelativePath());
70
            $remoteObject = $remoteIndex->getObjectByPath($cmp->getRelativePath());
71
72
            $localObjectModified = $this->isLocalObjectModified($localObject, $lastLocalObject, $options);
73
            $remoteObjectModified = $this->isRemoteObjectModified($remoteObject, $lastLocalObject);
74
75
            if ($localObjectModified && $remoteObjectModified)
76
            {
77
                $mergedIndex->addObject($this->resolveConflict($conflictHandler, $remoteObject, $localObject, $lastLocalObject));
0 ignored issues
show
Bug introduced by
It seems like $remoteObject defined by $remoteIndex->getObjectB...cmp->getRelativePath()) on line 70 can be null; however, Storeman\IndexMerger\Sta...rger::resolveConflict() does not accept null, maybe add an additional type check?

Unless you are absolutely sure that the expression can never be null because of other conditions, we strongly recommend to add an additional type check to your code:

/** @return stdClass|null */
function mayReturnNull() { }

function doesNotAcceptNull(stdClass $x) { }

// With potential error.
function withoutCheck() {
    $x = mayReturnNull();
    doesNotAcceptNull($x); // Potential error here.
}

// Safe - Alternative 1
function withCheck1() {
    $x = mayReturnNull();
    if ( ! $x instanceof stdClass) {
        throw new \LogicException('$x must be defined.');
    }
    doesNotAcceptNull($x);
}

// Safe - Alternative 2
function withCheck2() {
    $x = mayReturnNull();
    if ($x instanceof stdClass) {
        doesNotAcceptNull($x);
    }
}
Loading history...
78
            }
79
            elseif ($localObjectModified && $localObject !== null)
80
            {
81
                $mergedIndex->addObject($localObject);
82
            }
83
            elseif ($remoteObjectModified && $remoteObject !== null)
84
            {
85
                $mergedIndex->addObject($remoteObject);
86
            }
87
        }
88
89
        $intersection = $localIndex->getIntersection($remoteIndex, IndexObject::CMP_IGNORE_BLOBID | IndexObject::CMP_IGNORE_INODE);
90
91
        $this->logger->debug(sprintf("Found %d similarities between local and remote index", count($intersection)));
92
93
        foreach ($intersection as $cmp)
94
        {
95
            /** @var IndexObjectComparison $cmp */
96
97
            $mergedIndex->addObject($cmp->getIndexObjectA());
98
        }
99
100
        return $mergedIndex;
101
    }
102
103
    protected function isLocalObjectModified(?IndexObject $localObject, ?IndexObject $lastLocalObject, int $options): bool
104
    {
105
        if (!$lastLocalObject)
106
        {
107
            return $localObject !== null;
108
        }
109
110
        $localObjectModified = !$lastLocalObject->equals($localObject, IndexObject::CMP_IGNORE_BLOBID | IndexObject::CMP_IGNORE_INODE);
111
112
        // eventually verify file content
113
        if (!$localObjectModified && $localObject->isFile() && $options & static::VERIFY_CONTENT)
114
        {
115
            $existingHashes = iterator_to_array($lastLocalObject->getHashes());
116
            $configuredAlgorithms = $this->configuration->getFileChecksums();
117
118
            if (!empty($comparableAlgorithms = array_intersect($configuredAlgorithms, array_keys($existingHashes))))
119
            {
120
                $this->hashProvider->loadHashes($localObject, $comparableAlgorithms);
121
122
                foreach ($comparableAlgorithms as $algorithm)
123
                {
124
                    if ($this->hashProvider->getHash($localObject, $algorithm) !== $existingHashes[$algorithm])
125
                    {
126
                        $localObjectModified = true;
127
                    }
128
                }
129
            }
130
        }
131
132
        return $localObjectModified;
133
    }
134
135
    protected function isRemoteObjectModified(?IndexObject $remoteObject, ?IndexObject $lastLocalObject): bool
136
    {
137
        if ($lastLocalObject)
138
        {
139
            return !$lastLocalObject->equals($remoteObject, IndexObject::CMP_IGNORE_BLOBID | IndexObject::CMP_IGNORE_INODE);
140
        }
141
        else
142
        {
143
            return $remoteObject !== null;
144
        }
145
    }
146
147
    protected function resolveConflict(ConflictHandlerInterface $conflictHandler, IndexObject $remoteObject, ?IndexObject $localObject, ?IndexObject $lastLocalObject): IndexObject
148
    {
149
        $this->logger->notice("Resolving conflict at {$remoteObject->getRelativePath()}...");
150
151
        assert(($localObject === null) || ($localObject->getRelativePath() === $remoteObject->getRelativePath()));
152
        assert(($lastLocalObject === null) || ($lastLocalObject->getRelativePath() === $remoteObject->getRelativePath()));
153
154
        $solution = $conflictHandler->handleConflict($remoteObject, $localObject, $lastLocalObject);
155
156
        $return = null;
0 ignored issues
show
Unused Code introduced by
$return is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
157
        switch ($solution)
158
        {
159
            case ConflictHandlerInterface::USE_LOCAL:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
160
161
                $this->logger->info("Using local version for conflict at {$remoteObject->getRelativePath()}");
162
163
                $return = $localObject;
164
165
                break;
166
167
            case ConflictHandlerInterface::USE_REMOTE:
0 ignored issues
show
Coding Style introduced by
case statements should be defined using a colon.

As per the PSR-2 coding standard, case statements should not be wrapped in curly braces. There is no need for braces, since each case is terminated by the next break.

There is also the option to use a semicolon instead of a colon, this is discouraged because many programmers do not even know it works and the colon is universal between programming languages.

switch ($expr) {
    case "A": { //wrong
        doSomething();
        break;
    }
    case "B"; //wrong
        doSomething();
        break;
    case "C": //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
168
169
                $this->logger->info("Using remote version for conflict at {$remoteObject->getRelativePath()}");
170
171
                $return = $remoteObject;
172
173
                break;
174
175
            default:
0 ignored issues
show
Coding Style introduced by
DEFAULT statements must be defined using a colon

As per the PSR-2 coding standard, default statements should not be wrapped in curly braces.

switch ($expr) {
    default: { //wrong
        doSomething();
        break;
    }
}

switch ($expr) {
    default: //right
        doSomething();
        break;
}

To learn more about the PSR-2 coding standard, please refer to the PHP-Fig.

Loading history...
176
177
                throw new \LogicException();
178
        }
179
180
        return $return;
181
    }
182
183
    public static function getOptionsDebugString(int $options): string
184
    {
185
        $strings = [];
186
187
        if ($options & static::VERIFY_CONTENT)
188
        {
189
            $strings[] = 'VERIFY_CONTENT';
190
        }
191
192
        return empty($strings) ? '-' : implode(',', $strings);
193
    }
194
}
195