Completed
Push — master ( 7d5317...877556 )
by Arne
02:32
created

StandardIndexMerger::merge()   C

Complexity

Conditions 17
Paths 54

Size

Total Lines 64

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 64
rs 5.2166
c 0
b 0
f 0
cc 17
nc 54
nop 5

How to fix   Long Method    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\LoggerAwareTrait;
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
    use LoggerAwareTrait;
18
19
    public const VERIFY_CONTENT = 2;
20
21
    protected const CMP_OPTIONS =
22
        IndexObject::CMP_IGNORE_BLOBID |
23
        IndexObject::CMP_IGNORE_INODE |
24
        IndexObject::CMP_IGNORE_CTIME
25
    ;
26
27
    /**
28
     * @var Configuration
29
     */
30
    protected $configuration;
31
32
    /**
33
     * @var HashProvider
34
     */
35
    protected $hashProvider;
36
37
    public function __construct(Configuration $configuration, HashProvider $hashProvider)
38
    {
39
        $this->configuration = $configuration;
40
        $this->hashProvider = $hashProvider;
41
        $this->logger = new NullLogger();
42
    }
43
44
    /**
45
     * {@inheritdoc}
46
     */
47
    public function merge(ConflictHandlerInterface $conflictHandler, Index $remoteIndex, Index $localIndex, ?Index $lastLocalIndex, int $options = 0): Index
48
    {
49
        $this->logger->info(sprintf("Merging indices using %s (Options: %s)", static::class, static::getOptionsDebugString($options)));
50
51
        $mergedIndex = new Index();
52
        $lastLocalIndex = $lastLocalIndex ?: new Index();
53
54
        $diff = $localIndex->getDifference($remoteIndex, static::CMP_OPTIONS);
55
56
        $this->logger->debug(sprintf("Found %d differences between local and remote index", count($diff)));
57
58
        foreach ($diff as $cmp)
59
        {
60
            /** @var IndexObjectComparison $cmp */
61
62
            $localObject = $localIndex->getObjectByPath($cmp->getRelativePath());
63
            $lastLocalObject = $lastLocalIndex->getObjectByPath($cmp->getRelativePath());
64
            $remoteObject = $remoteIndex->getObjectByPath($cmp->getRelativePath());
65
66
            $localObjectModified = $this->isLocalObjectModified($localObject, $lastLocalObject, $options);
67
            $remoteObjectModified = $this->isRemoteObjectModified($remoteObject, $lastLocalObject);
68
69
            if ($options & static::INJECT_BLOBID && $localObject !== null && !$localObjectModified)
70
            {
71
                assert($lastLocalObject !== null || $remoteObject !== null);
72
73
                $localObject->setBlobId($lastLocalObject ? $lastLocalObject->getBlobId() : $remoteObject->getBlobId());
74
            }
75
76
            if ($localObjectModified && $remoteObjectModified)
77
            {
78
                $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 64 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...
79
            }
80
            elseif ($localObjectModified && $localObject !== null)
81
            {
82
                $mergedIndex->addObject($localObject);
83
            }
84
            elseif ($remoteObjectModified && $remoteObject !== null)
85
            {
86
                $mergedIndex->addObject($remoteObject);
87
            }
88
        }
89
90
        $intersection = $localIndex->getIntersection($remoteIndex, static::CMP_OPTIONS);
91
92
        $this->logger->debug(sprintf("Found %d similarities between local and remote index", count($intersection)));
93
94
        foreach ($intersection as $cmp)
95
        {
96
            /** @var IndexObjectComparison $cmp */
97
98
            // indexObjectB refers to remote object which we want to use to re-use the already existing blobId
99
            $remoteObject = $cmp->getIndexObjectB();
100
101
            $mergedIndex->addObject($remoteObject);
102
103
            if ($options & static::INJECT_BLOBID && $remoteObject->isFile())
104
            {
105
                $cmp->getIndexObjectA()->setBlobId($remoteObject->getBlobId());
106
            }
107
        }
108
109
        return $mergedIndex;
110
    }
111
112
    protected function isLocalObjectModified(?IndexObject $localObject, ?IndexObject $lastLocalObject, int $options): bool
113
    {
114
        if (!$lastLocalObject)
115
        {
116
            return $localObject !== null;
117
        }
118
119
        $localObjectModified = !$lastLocalObject->equals($localObject, static::CMP_OPTIONS);
120
121
        // eventually verify file content
122
        if (!$localObjectModified && $localObject->isFile() && $options & static::VERIFY_CONTENT)
123
        {
124
            $existingHashes = iterator_to_array($lastLocalObject->getHashes());
125
            $configuredAlgorithms = $this->configuration->getFileChecksums();
126
127
            if (!empty($comparableAlgorithms = array_intersect($configuredAlgorithms, array_keys($existingHashes))))
128
            {
129
                $this->hashProvider->loadHashes($localObject, $comparableAlgorithms);
130
131
                foreach ($comparableAlgorithms as $algorithm)
132
                {
133
                    if ($this->hashProvider->getHash($localObject, $algorithm) !== $existingHashes[$algorithm])
134
                    {
135
                        $localObjectModified = true;
136
                    }
137
                }
138
            }
139
        }
140
141
        return $localObjectModified;
142
    }
143
144
    protected function isRemoteObjectModified(?IndexObject $remoteObject, ?IndexObject $lastLocalObject): bool
145
    {
146
        if ($lastLocalObject)
147
        {
148
            return !$lastLocalObject->equals($remoteObject, static::CMP_OPTIONS);
149
        }
150
        else
151
        {
152
            return $remoteObject !== null;
153
        }
154
    }
155
156
    protected function resolveConflict(ConflictHandlerInterface $conflictHandler, IndexObject $remoteObject, ?IndexObject $localObject, ?IndexObject $lastLocalObject): IndexObject
157
    {
158
        $this->logger->notice("Resolving conflict at {$remoteObject->getRelativePath()}...");
159
160
        assert(($localObject === null) || ($localObject->getRelativePath() === $remoteObject->getRelativePath()));
161
        assert(($lastLocalObject === null) || ($lastLocalObject->getRelativePath() === $remoteObject->getRelativePath()));
162
163
        $solution = $conflictHandler->handleConflict($remoteObject, $localObject, $lastLocalObject);
164
165
        $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...
166
        switch ($solution)
167
        {
168
            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...
169
170
                $this->logger->info("Using local version for conflict at {$remoteObject->getRelativePath()}");
171
172
                $return = $localObject;
173
174
                break;
175
176
            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...
177
178
                $this->logger->info("Using remote version for conflict at {$remoteObject->getRelativePath()}");
179
180
                $return = $remoteObject;
181
182
                break;
183
184
            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...
185
186
                throw new \LogicException();
187
        }
188
189
        return $return;
190
    }
191
192
    public static function getOptionsDebugString(int $options): string
193
    {
194
        $strings = [];
195
196
        if ($options & static::VERIFY_CONTENT)
197
        {
198
            $strings[] = 'VERIFY_CONTENT';
199
        }
200
201
        return empty($strings) ? '-' : implode(',', $strings);
202
    }
203
}
204