Transaction   F
last analyzed

Complexity

Total Complexity 60

Size/Duplication

Total Lines 499
Duplicated Lines 32.46 %

Coupling/Cohesion

Components 1
Dependencies 4

Importance

Changes 0
Metric Value
wmc 60
lcom 1
cbo 4
dl 162
loc 499
rs 3.6
c 0
b 0
f 0

18 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 19 4
B get() 0 44 6
B getMulti() 0 34 6
A set() 13 13 2
A setMulti() 0 14 2
A delete() 22 22 2
A deleteMulti() 0 23 3
A add() 19 19 3
A replace() 19 19 3
A cas() 0 21 4
B increment() 30 30 7
B decrement() 30 30 7
A touch() 18 18 3
A flush() 0 21 3
A getCollection() 11 11 2
A commit() 0 6 1
A rollback() 0 7 1
A clear() 0 5 1

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complex Class

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like Transaction 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 Transaction, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace MatthiasMullie\Scrapbook\Buffered\Utils;
4
5
use MatthiasMullie\Scrapbook\KeyValueStore;
6
use MatthiasMullie\Scrapbook\Adapters\Collections\MemoryStore as BufferCollection;
0 ignored issues
show
Bug introduced by Matthias Mullie
This use statement conflicts with another class in this namespace, MatthiasMullie\Scrapbook...\Utils\BufferCollection.

Let’s assume that you have a directory layout like this:

.
|-- OtherDir
|   |-- Bar.php
|   `-- Foo.php
`-- SomeDir
    `-- Foo.php

and let’s assume the following content of Bar.php:

// Bar.php
namespace OtherDir;

use SomeDir\Foo; // This now conflicts the class OtherDir\Foo

If both files OtherDir/Foo.php and SomeDir/Foo.php are loaded in the same runtime, you will see a PHP error such as the following:

PHP Fatal error:  Cannot use SomeDir\Foo as Foo because the name is already in use in OtherDir/Foo.php

However, as OtherDir/Foo.php does not necessarily have to be loaded and the error is only triggered if it is loaded before OtherDir/Bar.php, this problem might go unnoticed for a while. In order to prevent this error from surfacing, you must import the namespace with a different alias:

// Bar.php
namespace OtherDir;

use SomeDir\Foo as SomeDirFoo; // There is no conflict anymore.
Loading history...
7
8
/**
9
 * This is a helper class for BufferedStore & TransactionalStore, which buffer
10
 * real cache requests in memory.
11
 *
12
 * This class accepts 2 caches: a KeyValueStore object (the real cache) and a
13
 * Buffer instance (to read data from as long as it hasn't been committed)
14
 *
15
 * Every write action will first store the data in the Buffer instance, and
16
 * then pas update along to $defer.
17
 * Once commit() is called, $defer will execute all these updates against the
18
 * real cache. All deferred writes that fail to apply will cause that cache key
19
 * to be deleted, to ensure cache consistency.
20
 * Until commit() is called, all data is read from the temporary Buffer instance.
21
 *
22
 * @author Matthias Mullie <[email protected]>
23
 * @copyright Copyright (c) 2014, Matthias Mullie. All rights reserved
24
 * @license LICENSE MIT
25
 */
26
class Transaction implements KeyValueStore
27
{
28
    /**
29
     * @var KeyValueStore
30
     */
31
    protected $cache;
32
33
    /**
34
     * @var Buffer
35
     */
36
    protected $local;
37
38
    /**
39
     * We'll return stub CAS tokens in order to reliably replay the CAS actions
40
     * to the real cache. This will hold a map of stub token => value, used to
41
     * verify when we do the actual CAS.
42
     *
43
     * @see cas()
44
     *
45
     * @var mixed[]
46
     */
47
    protected $tokens = array();
48
49
    /**
50
     * Deferred updates to be committed to real cache.
51
     *
52
     * @var Defer
53
     */
54
    protected $defer;
55
56
    /**
57
     * Suspend reads from real cache. This is used when a flush is issued but it
58
     * has not yet been committed. In that case, we don't want to fall back to
59
     * real cache values, because they're about to be flushed.
60
     *
61
     * @var bool
62
     */
63
    protected $suspend = false;
64
65
    /**
66
     * @var Transaction[]
67
     */
68
    protected $collections = array();
69
70
    /**
71
     * @param Buffer|BufferCollection $local
72
     * @param KeyValueStore           $cache
73
     */
74
    public function __construct(/* Buffer|BufferCollection */ $local, KeyValueStore $cache)
75
    {
76
        // can't do double typehint, so let's manually check the type
77
        if (!$local instanceof Buffer && !$local instanceof BufferCollection) {
78
            $error = 'Invalid class for $local: '.get_class($local);
79
            if (class_exists('\TypeError')) {
80
                throw new \TypeError($error);
81
            }
82
            trigger_error($error, E_USER_ERROR);
83
        }
84
85
        $this->cache = $cache;
86
87
        // (uncommitted) writes must never be evicted (even if that means
88
        // crashing because we run out of memory)
89
        $this->local = $local;
0 ignored issues
show
Documentation Bug introduced by Matthias Mullie
It seems like $local can also be of type object<MatthiasMullie\Sc...ollections\MemoryStore>. However, the property $local is declared as type object<MatthiasMullie\Sc...\Buffered\Utils\Buffer>. Maybe add an additional type check?

Our type inference engine has found a suspicous assignment of a value to a property. This check raises an issue when a value that can be of a mixed type is assigned to a property that is type hinted more strictly.

For example, imagine you have a variable $accountId that can either hold an Id object or false (if there is no account id yet). Your code now assigns that value to the id property of an instance of the Account class. This class holds a proper account, so the id value must no longer be false.

Either this assignment is in error or a type check should be added for that assignment.

class Id
{
    public $id;

    public function __construct($id)
    {
        $this->id = $id;
    }

}

class Account
{
    /** @var  Id $id */
    public $id;
}

$account_id = false;

if (starsAreRight()) {
    $account_id = new Id(42);
}

$account = new Account();
if ($account instanceof Id)
{
    $account->id = $account_id;
}
Loading history...
90
91
        $this->defer = new Defer($this->cache);
92
    }
93
94
    /**
95
     * {@inheritdoc}
96
     */
97
    public function get($key, &$token = null)
98
    {
99
        $value = $this->local->get($key, $token);
100
101
        // short-circuit reading from real cache if we have an uncommitted flush
102
        if ($this->suspend && $token === null) {
103
            // flush hasn't been committed yet, don't read from real cache!
104
            return false;
105
        }
106
107
        if ($value === false) {
108
            if ($this->local->expired($key)) {
109
                /*
110
                 * Item used to exist in local cache, but is now expired. This
111
                 * is used when values are to be deleted: we don't want to reach
112
                 * out to real storage because that would respond with the not-
113
                 * yet-deleted value.
114
                 */
115
116
                return false;
117
            }
118
119
            // unknown in local cache = fetch from source cache
120
            $value = $this->cache->get($key, $token);
121
        }
122
123
        // no value = quit early, don't generate a useless token
124
        if ($value === false) {
125
            return false;
126
        }
127
128
        /*
129
         * $token will be unreliable to the deferred updates so generate
130
         * a custom one and keep the associated value around.
131
         * Read more details in PHPDoc for function cas().
132
         * uniqid is ok here. Doesn't really have to be unique across
133
         * servers, just has to be unique every time it's called in this
134
         * one particular request - which it is.
135
         */
136
        $token = uniqid();
137
        $this->tokens[$token] = serialize($value);
138
139
        return $value;
140
    }
141
142
    /**
143
     * {@inheritdoc}
144
     */
145
    public function getMulti(array $keys, array &$tokens = null)
146
    {
147
        // retrieve all that we can from local cache
148
        $values = $this->local->getMulti($keys);
149
        $tokens = array();
150
151
        // short-circuit reading from real cache if we have an uncommitted flush
152
        if (!$this->suspend) {
153
            // figure out which missing key we need to get from real cache
154
            $keys = array_diff($keys, array_keys($values));
155
            foreach ($keys as $i => $key) {
156
                // don't reach out to real cache for keys that are about to be gone
157
                if ($this->local->expired($key)) {
158
                    unset($keys[$i]);
159
                }
160
            }
161
162
            // fetch missing values from real cache
163
            if ($keys) {
0 ignored issues
show
Bug Best Practice introduced by Matthias Mullie
The expression $keys of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
164
                $missing = $this->cache->getMulti($keys);
165
                $values += $missing;
166
            }
167
        }
168
169
        // any tokens we get will be unreliable, so generate some replacements
170
        // (more elaborate explanation in get())
171
        foreach ($values as $key => $value) {
172
            $token = uniqid();
173
            $tokens[$key] = $token;
174
            $this->tokens[$token] = serialize($value);
175
        }
176
177
        return $values;
178
    }
179
180
    /**
181
     * {@inheritdoc}
182
     */
183 View Code Duplication
    public function set($key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
184
    {
185
        // store the value in memory, so that when we ask for it again later in
186
        // this same request, we get the value we just set
187
        $success = $this->local->set($key, $value, $expire);
188
        if ($success === false) {
189
            return false;
190
        }
191
192
        $this->defer->set($key, $value, $expire);
193
194
        return true;
195
    }
196
197
    /**
198
     * {@inheritdoc}
199
     */
200
    public function setMulti(array $items, $expire = 0)
201
    {
202
        // store the values in memory, so that when we ask for it again later in
203
        // this same request, we get the value we just set
204
        $success = $this->local->setMulti($items, $expire);
205
206
        // only attempt to store those that we've set successfully to local
207
        $successful = array_intersect_key($items, $success);
208
        if (!empty($successful)) {
209
            $this->defer->setMulti($successful, $expire);
210
        }
211
212
        return $success;
213
    }
214
215
    /**
216
     * {@inheritdoc}
217
     */
218 View Code Duplication
    public function delete($key)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
219
    {
220
        // check the current value to see if it currently exists, so we can
221
        // properly return true/false as would be expected from KeyValueStore
222
        $value = $this->get($key);
223
        if ($value === false) {
224
            return false;
225
        }
226
227
        /*
228
         * To make sure that subsequent get() calls for this key don't return
229
         * a value (it's supposed to be deleted), we'll make it expired in our
230
         * temporary bag (as opposed to deleting it from out bag, in which case
231
         * we'd fall back to fetching it from real store, where the transaction
232
         * might not yet be committed)
233
         */
234
        $this->local->set($key, $value, -1);
235
236
        $this->defer->delete($key);
237
238
        return true;
239
    }
240
241
    /**
242
     * {@inheritdoc}
243
     */
244
    public function deleteMulti(array $keys)
245
    {
246
        // check the current values to see if they currently exists, so we can
247
        // properly return true/false as would be expected from KeyValueStore
248
        $items = $this->getMulti($keys);
249
        $success = array();
250
        foreach ($keys as $key) {
251
            $success[$key] = array_key_exists($key, $items);
252
        }
253
254
        // only attempt to store those that we've deleted successfully to local
255
        $values = array_intersect_key($success, array_flip($keys));
256
        if (empty($values)) {
257
            return array();
258
        }
259
260
        // mark all as expired in local cache (see comment in delete())
261
        $this->local->setMulti($values, -1);
262
263
        $this->defer->deleteMulti(array_keys($values));
264
265
        return $success;
266
    }
267
268
    /**
269
     * {@inheritdoc}
270
     */
271 View Code Duplication
    public function add($key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
272
    {
273
        // before adding, make sure the value doesn't yet exist (in real cache,
274
        // nor in memory)
275
        if ($this->get($key) !== false) {
276
            return false;
277
        }
278
279
        // store the value in memory, so that when we ask for it again later
280
        // in this same request, we get the value we just set
281
        $success = $this->local->set($key, $value, $expire);
282
        if ($success === false) {
283
            return false;
284
        }
285
286
        $this->defer->add($key, $value, $expire);
287
288
        return true;
289
    }
290
291
    /**
292
     * {@inheritdoc}
293
     */
294 View Code Duplication
    public function replace($key, $value, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
295
    {
296
        // before replacing, make sure the value actually exists (in real cache,
297
        // or already created in memory)
298
        if ($this->get($key) === false) {
299
            return false;
300
        }
301
302
        // store the value in memory, so that when we ask for it again later
303
        // in this same request, we get the value we just set
304
        $success = $this->local->set($key, $value, $expire);
305
        if ($success === false) {
306
            return false;
307
        }
308
309
        $this->defer->replace($key, $value, $expire);
310
311
        return true;
312
    }
313
314
    /**
315
     * Since our CAS is deferred, the CAS token we got from our original
316
     * get() will likely not be valid by the time we want to store it to
317
     * the real cache. Imagine this scenario:
318
     * * a value is fetched from (real) cache
319
     * * an new value key is CAS'ed (into temp cache - real CAS is deferred)
320
     * * this key's value is fetched again (this time from temp cache)
321
     * * and a new value is CAS'ed again (into temp cache...).
322
     *
323
     * In this scenario, when we finally want to replay the write actions
324
     * onto the real cache, the first 3 actions would likely work fine.
325
     * The last (second CAS) however would not, since it never got a real
326
     * updated $token from the real cache.
327
     *
328
     * To work around this problem, all get() calls will return a unique
329
     * CAS token and store the value-at-that-time associated with that
330
     * token. All we have to do when we want to write the data to real cache
331
     * is, right before was CAS for real, get the value & (real) cas token
332
     * from storage & compare that value to the one we had stored. If that
333
     * checks out, we can safely resume the CAS with the real token we just
334
     * received.
335
     *
336
     * {@inheritdoc}
337
     */
338
    public function cas($token, $key, $value, $expire = 0)
339
    {
340
        $originalValue = isset($this->tokens[$token]) ? $this->tokens[$token] : null;
341
342
        // value is no longer the same as what we used for token
343
        if (serialize($this->get($key)) !== $originalValue) {
344
            return false;
345
        }
346
347
        // "CAS" value to local cache/memory
348
        $success = $this->local->set($key, $value, $expire);
349
        if ($success === false) {
350
            return false;
351
        }
352
353
        // only schedule the CAS to be performed on real cache if it was OK on
354
        // local cache
355
        $this->defer->cas($originalValue, $key, $value, $expire);
356
357
        return true;
358
    }
359
360
    /**
361
     * {@inheritdoc}
362
     */
363 View Code Duplication
    public function increment($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
364
    {
365
        if ($offset <= 0 || $initial < 0) {
366
            return false;
367
        }
368
369
        // get existing value (from real cache or memory) so we know what to
370
        // increment in memory (where we may not have anything yet, so we should
371
        // adjust our initial value to what's already in real cache)
372
        $value = $this->get($key);
373
        if ($value === false) {
374
            $value = $initial - $offset;
375
        }
376
377
        if (!is_numeric($value) || !is_numeric($offset)) {
378
            return false;
379
        }
380
381
        // store the value in memory, so that when we ask for it again later
382
        // in this same request, we get the value we just set
383
        $value = max(0, $value + $offset);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression max(0, $value + $offset); of type integer|double adds the type double to the return on line 391 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::increment of type integer|boolean.
Loading history...
384
        $success = $this->local->set($key, $value, $expire);
385
        if ($success === false) {
386
            return false;
387
        }
388
389
        $this->defer->increment($key, $offset, $initial, $expire);
390
391
        return $value;
392
    }
393
394
    /**
395
     * {@inheritdoc}
396
     */
397 View Code Duplication
    public function decrement($key, $offset = 1, $initial = 0, $expire = 0)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
398
    {
399
        if ($offset <= 0 || $initial < 0) {
400
            return false;
401
        }
402
403
        // get existing value (from real cache or memory) so we know what to
404
        // increment in memory (where we may not have anything yet, so we should
405
        // adjust our initial value to what's already in real cache)
406
        $value = $this->get($key);
407
        if ($value === false) {
408
            $value = $initial + $offset;
409
        }
410
411
        if (!is_numeric($value) || !is_numeric($offset)) {
412
            return false;
413
        }
414
415
        // store the value in memory, so that when we ask for it again later
416
        // in this same request, we get the value we just set
417
        $value = max(0, $value - $offset);
0 ignored issues
show
Bug Compatibility introduced by Matthias Mullie
The expression max(0, $value - $offset); of type integer|double adds the type double to the return on line 425 which is incompatible with the return type declared by the interface MatthiasMullie\Scrapbook\KeyValueStore::decrement of type integer|boolean.
Loading history...
418
        $success = $this->local->set($key, $value, $expire);
419
        if ($success === false) {
420
            return false;
421
        }
422
423
        $this->defer->decrement($key, $offset, $initial, $expire);
424
425
        return $value;
426
    }
427
428
    /**
429
     * {@inheritdoc}
430
     */
431 View Code Duplication
    public function touch($key, $expire)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
432
    {
433
        // grab existing value (from real cache or memory) and re-save (to
434
        // memory) with updated expiration time
435
        $value = $this->get($key);
436
        if ($value === false) {
437
            return false;
438
        }
439
440
        $success = $this->local->set($key, $value, $expire);
441
        if ($success === false) {
442
            return false;
443
        }
444
445
        $this->defer->touch($key, $expire);
446
447
        return true;
448
    }
449
450
    /**
451
     * {@inheritdoc}
452
     */
453
    public function flush()
454
    {
455
        foreach ($this->collections as $collection) {
456
            $collection->flush();
457
        }
458
459
        $success = $this->local->flush();
460
        if ($success === false) {
461
            return false;
462
        }
463
464
        // clear all buffered writes, flush wipes them out anyway
465
        $this->clear();
466
467
        // make sure that reads, from now on until commit, don't read from cache
468
        $this->suspend = true;
469
470
        $this->defer->flush();
471
472
        return true;
473
    }
474
475
    /**
476
     * {@inheritdoc}
477
     */
478 View Code Duplication
    public function getCollection($name)
0 ignored issues
show
Duplication introduced by Matthias Mullie
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
479
    {
480
        if (!isset($this->collections[$name])) {
481
            $this->collections[$name] = new static(
482
                $this->local->getCollection($name),
0 ignored issues
show
Documentation introduced by Matthias Mullie
$this->local->getCollection($name) is of type object<MatthiasMullie\Scrapbook\KeyValueStore>, but the function expects a object<MatthiasMullie\Sc...ollections\MemoryStore>.

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...
483
                $this->cache->getCollection($name)
484
            );
485
        }
486
487
        return $this->collections[$name];
488
    }
489
490
    /**
491
     * Commits all deferred updates to real cache.
492
     * that had already been written to will be deleted.
493
     *
494
     * @return bool
495
     */
496
    public function commit()
497
    {
498
        $this->clear();
499
500
        return $this->defer->commit();
501
    }
502
503
    /**
504
     * Roll back all scheduled changes.
505
     *
506
     * @return bool
507
     */
508
    public function rollback()
509
    {
510
        $this->clear();
511
        $this->defer->clear();
512
513
        return true;
514
    }
515
516
    /**
517
     * Clears all transaction-related data stored in memory.
518
     */
519
    protected function clear()
520
    {
521
        $this->tokens = array();
522
        $this->suspend = false;
523
    }
524
}
525