StandardIndexMerger   C
last analyzed

Complexity

Total Complexity 56

Size/Duplication

Total Lines 327
Duplicated Lines 0 %

Coupling/Cohesion

Components 1
Dependencies 8

Importance

Changes 0
Metric Value
wmc 56
lcom 1
cbo 8
dl 0
loc 327
rs 5.5199
c 0
b 0
f 0

9 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 6 1
B merge() 0 46 8
B mergeObject1() 0 46 11
D mergeObject2() 0 81 16
B isLocalFileContentModified() 0 36 7
A isRemoteFileContentModified() 0 4 1
A resolveConflict() 0 34 5
A injectBlobIds() 0 15 4
A getOptionsDebugString() 0 11 3

How to fix   Complexity   

Complex Class

Complex classes like StandardIndexMerger often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes. You can also have a look at the cohesion graph to spot any un-connected, or weakly-connected components.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

While breaking up the class, it is a good idea to analyze how other classes use StandardIndexMerger, and based on these observations, apply Extract Interface, too.

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\Index;
12
use Storeman\Index\IndexObject;
13
14
class StandardIndexMerger implements IndexMergerInterface, LoggerAwareInterface
15
{
16
    use LoggerAwareTrait;
17
18
    public const VERIFY_CONTENT = 2;
19
20
    /**
21
     * @var Configuration
22
     */
23
    protected $configuration;
24
25
    /**
26
     * @var HashProvider
27
     */
28
    protected $hashProvider;
29
30
    public function __construct(Configuration $configuration, HashProvider $hashProvider)
31
    {
32
        $this->configuration = $configuration;
33
        $this->hashProvider = $hashProvider;
34
        $this->logger = new NullLogger();
35
    }
36
37
    /**
38
     * {@inheritdoc}
39
     */
40
    public function merge(ConflictHandlerInterface $conflictHandler, Index $remoteIndex, Index $localIndex, ?Index $lastLocalIndex, int $options = 0): Index
41
    {
42
        $this->logger->info(sprintf("Merging indices using %s (Options: %s)", static::class, static::getOptionsDebugString($options)));
43
44
        $mergedIndex = new Index();
45
        $lastLocalIndex = $lastLocalIndex ?: new Index();
46
47
        foreach ($localIndex as $localObject)
48
        {
49
            /** @var IndexObject $localObject */
50
51
            $remoteObject = $remoteIndex->getObjectByPath($localObject->getRelativePath());
52
            $lastLocalObject = $lastLocalIndex->getObjectByPath($localObject->getRelativePath());
53
54
            if ($mergedObject = $this->mergeObject1($conflictHandler, $remoteObject, $localObject, $lastLocalObject, $options))
0 ignored issues
show
Bug introduced by
It seems like $remoteObject defined by $remoteIndex->getObjectB...ect->getRelativePath()) on line 51 can be null; however, Storeman\IndexMerger\Sta...xMerger::mergeObject1() 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...
Bug introduced by
It seems like $lastLocalObject defined by $lastLocalIndex->getObje...ect->getRelativePath()) on line 52 can be null; however, Storeman\IndexMerger\Sta...xMerger::mergeObject1() 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...
Bug introduced by
Are you sure the assignment to $mergedObject is correct as $this->mergeObject1($con...tLocalObject, $options) (which targets Storeman\IndexMerger\Sta...xMerger::mergeObject1()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
55
            {
56
                $mergedIndex->addObject($mergedObject);
57
            }
58
        }
59
60
        foreach ($remoteIndex as $remoteObject)
61
        {
62
            /** @var IndexObject $remoteObject */
63
64
            $localObject = $localIndex->getObjectByPath($remoteObject->getRelativePath());
65
            $lastLocalObject = $lastLocalIndex->getObjectByPath($remoteObject->getRelativePath());
66
67
            if ($localObject !== null)
68
            {
69
                // already taken care of in local index iteration
70
                continue;
71
            }
72
73
            if ($mergedObject = $this->mergeObject1($conflictHandler, $remoteObject, $localObject, $lastLocalObject, $options))
0 ignored issues
show
Documentation introduced by
$localObject is of type null, but the function expects a object<Storeman\Index\IndexObject>.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
Bug introduced by
It seems like $lastLocalObject defined by $lastLocalIndex->getObje...ect->getRelativePath()) on line 65 can be null; however, Storeman\IndexMerger\Sta...xMerger::mergeObject1() 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...
Bug introduced by
Are you sure the assignment to $mergedObject is correct as $this->mergeObject1($con...tLocalObject, $options) (which targets Storeman\IndexMerger\Sta...xMerger::mergeObject1()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
74
            {
75
                $mergedIndex->addObject($mergedObject);
76
            }
77
        }
78
79
        if ($options & static::INJECT_BLOBID)
80
        {
81
            $this->injectBlobIds($mergedIndex, $localIndex);
82
        }
83
84
        return $mergedIndex;
85
    }
86
87
    /**
88
     * First stage object merging looking at primarily at pure existence.
89
     *
90
     * @param ConflictHandlerInterface $conflictHandler
91
     * @param IndexObject $remoteObject
92
     * @param IndexObject $localObject
93
     * @param IndexObject $lastLocalObject
94
     * @param int $options
95
     * @return IndexObject
96
     */
97
    protected function mergeObject1(ConflictHandlerInterface $conflictHandler, ?IndexObject $remoteObject, ?IndexObject $localObject, ?IndexObject $lastLocalObject, int $options = 0): ?IndexObject
98
    {
99
        if ($remoteObject === null && $localObject === null)
100
        {
101
            // locally and remotely deleted
102
            return null;
103
        }
104
        elseif ($lastLocalObject === null)
105
        {
106
            if ($remoteObject === null)
107
            {
108
                // locally created
109
                return clone $localObject;
110
            }
111
            elseif ($localObject === null)
112
            {
113
                // remotely created
114
                return clone $remoteObject;
115
            }
116
            elseif ($remoteObject !== null && $localObject !== null)
117
            {
118
                // remotely and locally created
119
                return $this->resolveConflict($conflictHandler, $remoteObject, $localObject, $lastLocalObject);
120
            }
121
        }
122
        elseif ($remoteObject === null)
123
        {
124
            // remotely deleted and locally changed
125
            return $this->resolveConflict($conflictHandler, $remoteObject, $localObject, $lastLocalObject);
126
        }
127
        elseif ($localObject === null)
128
        {
129
            if ($remoteObject->equals($lastLocalObject))
130
            {
131
                // locally deleted
132
                return null;
133
            }
134
            else
135
            {
136
                // remotely changed and locally deleted
137
                return $this->resolveConflict($conflictHandler, $remoteObject, $localObject, $lastLocalObject);
138
            }
139
        }
140
141
        return $this->mergeObject2($conflictHandler, $remoteObject, $localObject, $lastLocalObject, $options);
142
    }
143
144
    /**
145
     * Second stage object merging.
146
     *
147
     * @param ConflictHandlerInterface $conflictHandler
148
     * @param IndexObject $remoteObject
149
     * @param IndexObject $localObject
150
     * @param IndexObject $lastLocalObject
151
     * @param int $options
152
     * @return IndexObject
153
     */
154
    protected function mergeObject2(ConflictHandlerInterface $conflictHandler, IndexObject $remoteObject, IndexObject $localObject, IndexObject $lastLocalObject, int $options = 0): IndexObject
155
    {
156
        if ($remoteObject->getType() !== $localObject->getType())
157
        {
158
            return $this->resolveConflict($conflictHandler, $remoteObject, $localObject, $lastLocalObject);
159
        }
160
161
        $attributes = [
162
            'size' => null,
163
            'inode' => null,
164
            'blobId' => null,
165
            'hashes' => null,
166
        ];
167
168
        foreach (['permissions', 'mtime', 'linkTarget'] as $attribute)
169
        {
170
            $modifiedRemote = $lastLocalObject[$attribute] !== $remoteObject[$attribute];
171
            $modifiedLocal = $lastLocalObject[$attribute] !== $localObject[$attribute];
172
173
            if ($modifiedRemote && $modifiedLocal)
174
            {
175
                switch ($conflictHandler->handleConflict($remoteObject, $localObject, $lastLocalObject))
176
                {
177
                    case ConflictHandlerInterface::USE_LOCAL: $modifiedRemote = false; break;
178
                    case ConflictHandlerInterface::USE_REMOTE: $modifiedLocal = false; break;
179
                    default: throw new \LogicException();
180
                }
181
            }
182
183
            if ($modifiedRemote || !$modifiedLocal)
184
            {
185
                $attributes[$attribute] = $remoteObject[$attribute];
186
            }
187
            else
188
            {
189
                $attributes[$attribute] = $localObject[$attribute];
190
            }
191
        }
192
193
        if ($localObject->isFile())
194
        {
195
            $remoteFileContentModified = $this->isRemoteFileContentModified($remoteObject, $lastLocalObject);
196
            $localFileContentModified = $this->isLocalFileContentModified($localObject, $lastLocalObject, $options);
197
198
            if ($remoteFileContentModified && $localFileContentModified)
199
            {
200
                switch ($conflictHandler->handleConflict($remoteObject, $localObject, $lastLocalObject))
201
                {
202
                    case ConflictHandlerInterface::USE_LOCAL: $remoteFileContentModified = false; break;
203
                    case ConflictHandlerInterface::USE_REMOTE: $localFileContentModified = false; break;
204
                    default: throw new \LogicException();
205
                }
206
            }
207
208
            if ($remoteFileContentModified || !$localFileContentModified)
209
            {
210
                $attributes['size'] = $remoteObject->getSize();
211
                $attributes['blobId'] = $remoteObject->getBlobId();
212
                $attributes['hashes'] = clone $remoteObject->getHashes();
213
            }
214
            else
215
            {
216
                $attributes['size'] = $localObject->getSize();
217
                $attributes['inode'] = $localObject->getInode();
218
                $attributes['hashes'] = clone $localObject->getHashes();
219
            }
220
        }
221
222
        return new IndexObject(
223
            $localObject->getRelativePath(),
224
            $localObject->getType(),
225
            $attributes['mtime'],
226
            null,
227
            $attributes['permissions'],
228
            $attributes['size'],
229
            $attributes['inode'],
230
            $attributes['linkTarget'],
231
            $attributes['blobId'],
232
            $attributes['hashes']
233
        );
234
    }
235
236
    protected function isLocalFileContentModified(IndexObject $localObject, IndexObject $lastLocalObject, int $options): bool
237
    {
238
        assert($localObject->isFile());
239
        assert($lastLocalObject->isFile());
240
241
        $modified = false;
242
        $modified |= !$localObject->getHashes()->equals($lastLocalObject->getHashes());
243
        $modified |= $localObject->getSize() !== $lastLocalObject->getSize();
244
        $modified |= $localObject->getInode() !== $lastLocalObject->getInode();
245
        $modified |= $localObject->getMtime() !== $lastLocalObject->getMtime();
246
247
        $verifyContent = false;
248
        $verifyContent |= !$modified && $localObject->getCtime() !== $lastLocalObject->getCtime();
249
        $verifyContent |= !$modified && $options & static::VERIFY_CONTENT;
250
251
        if ($verifyContent)
252
        {
253
            $existingHashes = iterator_to_array($lastLocalObject->getHashes());
254
            $configuredAlgorithms = $this->configuration->getFileChecksums();
255
256
            if (!empty($comparableAlgorithms = array_intersect($configuredAlgorithms, array_keys($existingHashes))))
257
            {
258
                $this->hashProvider->loadHashes($localObject, $comparableAlgorithms);
259
260
                foreach ($comparableAlgorithms as $algorithm)
261
                {
262
                    if ($this->hashProvider->getHash($localObject, $algorithm) !== $existingHashes[$algorithm])
263
                    {
264
                        $modified = true;
265
                    }
266
                }
267
            }
268
        }
269
270
        return $modified;
271
    }
272
273
    protected function isRemoteFileContentModified(IndexObject $remoteObject, IndexObject $lastLocalObject): bool
274
    {
275
        return $remoteObject->getBlobId() !== $lastLocalObject->getBlobId();
276
    }
277
278
    protected function resolveConflict(ConflictHandlerInterface $conflictHandler, IndexObject $remoteObject, ?IndexObject $localObject, ?IndexObject $lastLocalObject): IndexObject
279
    {
280
        $this->logger->notice("Resolving conflict at {$remoteObject->getRelativePath()}...");
281
282
        assert(($localObject === null) || ($localObject->getRelativePath() === $remoteObject->getRelativePath()));
283
        assert(($lastLocalObject === null) || ($lastLocalObject->getRelativePath() === $remoteObject->getRelativePath()));
284
285
        $solution = $conflictHandler->handleConflict($remoteObject, $localObject, $lastLocalObject);
286
287
        switch ($solution)
288
        {
289
            case ConflictHandlerInterface::USE_LOCAL:
290
291
                $this->logger->info("Using local version for conflict at {$remoteObject->getRelativePath()}");
292
293
                $return = clone $localObject;
294
295
                break;
296
297
            case ConflictHandlerInterface::USE_REMOTE:
298
299
                $this->logger->info("Using remote version for conflict at {$remoteObject->getRelativePath()}");
300
301
                $return = clone $remoteObject;
302
303
                break;
304
305
            default:
306
307
                throw new \LogicException();
308
        }
309
310
        return $return;
311
    }
312
313
    protected function injectBlobIds(Index $mergedIndex, Index $localIndex): void
314
    {
315
        foreach ($mergedIndex as $object)
316
        {
317
            /** @var IndexObject $object*/
318
319
            if ($object->getBlobId() !== null)
320
            {
321
                if ($localObject = $localIndex->getObjectByPath($object->getRelativePath()))
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $localObject is correct as $localIndex->getObjectBy...ect->getRelativePath()) (which targets Storeman\Index\Index::getObjectByPath()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

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

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

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

Loading history...
322
                {
323
                    $localObject->setBlobId($object->getBlobId());
324
                }
325
            }
326
        }
327
    }
328
329
    public static function getOptionsDebugString(int $options): string
330
    {
331
        $strings = [];
332
333
        if ($options & static::VERIFY_CONTENT)
334
        {
335
            $strings[] = 'VERIFY_CONTENT';
336
        }
337
338
        return empty($strings) ? '-' : implode(',', $strings);
339
    }
340
}
341