Completed
Push — master ( 44a00f...9727c6 )
by Arne
02:20
created

StandardIndexMerger::merge()   B

Complexity

Conditions 8
Paths 48

Size

Total Lines 46

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 46
rs 7.9337
c 0
b 0
f 0
cc 8
nc 48
nop 5
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;
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...
178
                    case ConflictHandlerInterface::USE_REMOTE: $modifiedLocal = false; break;
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...
179
                    default: throw new \LogicException();
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

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


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

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

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

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

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

Loading history...
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;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

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

    doSomethingElse(); //wrong
    break;

}

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

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

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

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

Loading history...
203
                    case ConflictHandlerInterface::USE_REMOTE: $localFileContentModified = false; break;
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

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

    doSomethingElse(); //wrong
    break;

}

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

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

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

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

Loading history...
204
                    default: throw new \LogicException();
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

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


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

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

Loading history...
Coding Style introduced by
Terminating statement must be on a line by itself

As per the PSR-2 coding standard, the break (or other terminating) statement must be on a line of its own.

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

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

Loading history...
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
        $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...
288
        switch ($solution)
289
        {
290
            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...
291
292
                $this->logger->info("Using local version for conflict at {$remoteObject->getRelativePath()}");
293
294
                $return = clone $localObject;
295
296
                break;
297
298
            case ConflictHandlerInterface::USE_REMOTE:
0 ignored issues
show
Coding Style introduced by
The case body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a case statement must start on the line immediately following the case statement.

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

    doSomethingElse(); //wrong
    break;

}

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

Loading history...
299
300
                $this->logger->info("Using remote version for conflict at {$remoteObject->getRelativePath()}");
301
302
                $return = clone $remoteObject;
303
304
                break;
305
306
            default:
0 ignored issues
show
Coding Style introduced by
The default body in a switch statement must start on the line following the statement.

According to the PSR-2, the body of a default statement must start on the line immediately following the statement.

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


switch ($expr) {
    default:

        doSomething(); //wrong
        break;
}

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

Loading history...
307
308
                throw new \LogicException();
309
        }
310
311
        return $return;
312
    }
313
314
    protected function injectBlobIds(Index $mergedIndex, Index $localIndex): void
315
    {
316
        foreach ($mergedIndex as $object)
317
        {
318
            /** @var IndexObject $object*/
319
320
            if ($object->getBlobId() !== null)
321
            {
322
                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...
323
                {
324
                    $localObject->setBlobId($object->getBlobId());
325
                }
326
            }
327
        }
328
    }
329
330
    public static function getOptionsDebugString(int $options): string
331
    {
332
        $strings = [];
333
334
        if ($options & static::VERIFY_CONTENT)
335
        {
336
            $strings[] = 'VERIFY_CONTENT';
337
        }
338
339
        return empty($strings) ? '-' : implode(',', $strings);
340
    }
341
}
342