Completed
Pull Request — master (#268)
by
unknown
02:03 queued 49s
created

Xhgui_Storage_File   F

Complexity

Total Complexity 84

Size/Duplication

Total Lines 492
Duplicated Lines 6.5 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

Changes 0
Metric Value
wmc 84
lcom 2
cbo 3
dl 32
loc 492
rs 2
c 0
b 0
f 0

17 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
F find() 0 104 28
A count() 0 8 2
A findOne() 0 7 1
A remove() 0 12 3
A drop() 0 5 1
A aggregate() 0 29 3
D sortByColumn() 12 55 19
A getMetafileNameFromProfileName() 0 5 1
A insert() 0 9 2
A update() 0 4 1
C importFile() 0 37 12
A getWatchedFunctions() 0 9 2
A addWatchedFunction() 10 13 2
A updateWatchedFunction() 10 13 2
A removeWatchedFunction() 0 6 2
A getRequestTimeFromFilename() 0 12 2

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 Xhgui_Storage_File 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 Xhgui_Storage_File, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
class Xhgui_Storage_File extends Xhgui_Storage_Abstract implements
4
    Xhgui_StorageInterface,
5
    Xhgui_WatchedFunctionsStorageInterface
6
{
7
8
    /**
9
     * @var string
10
     */
11
    protected $path     = '../data/';
12
13
    /**
14
     * @var string
15
     */
16
    protected $prefix   = 'xhgui.data';
17
18
    /**
19
     * @var bool|mixed
20
     */
21
    protected $separateMeta = true;
22
23
    /**
24
     * @var mixed
25
     */
26
    protected $dataSerializer;
27
28
    /**
29
     * @var mixed
30
     */
31
    protected $metaSerializer;
32
33
    /**
34
     * @var string
35
     */
36
    protected $watchedFunctionsPathPrefix = '../watched_functions/';
37
38
    /**
39
     * @var int[]
40
     */
41
    protected $countCache;
42
43
    /**
44
     * @var Xhgui_Storage_Filter
45
     */
46
    private $filter;
47
48
    /**
49
     * Xhgui_Storage_File constructor.
50
     * @param $config
51
     */
52
    public function __construct($config)
53
    {
54
        $this->prefix           = 'xhgui.data';
55
56
        $this->path             = $config['save.handler.path'];
57
        $this->separateMeta     = $config['save.handler.separate_meta'];
58
        $this->dataSerializer   = $config['save.handler.serializer'];
59
        $this->metaSerializer   = $config['save.handler.meta_serializer'];
60
    }
61
62
    /**
63
     * @inheritDoc
64
     * @param Xhgui_Storage_Filter $filter
65
     * @param bool $projections
66
     * @return Xhgui_Storage_ResultSet
67
     */
68
    public function find(Xhgui_Storage_Filter $filter, $projections = false)
69
    {
70
71
        if ($filter->getId()) {
72
            $result = glob($this->path. DIRECTORY_SEPARATOR.$filter->getId());
73
        } else {
74
            $result = glob($this->path. $this->prefix . '*');
75
            sort($result);
76
        }
77
78
        $ret = [];
79
        foreach ($result as $i => $file) {
80
            // skip meta files.
81
            if (strpos($file, '.meta') !== false) {
82
                continue;
83
            }
84
85
            // try to detect timestamp in filename, to optimize searching.
86
            // If that fails we need to get it after file import from meta.
87
            $reqTimeFromFilename = $this->getRequestTimeFromFilename($file);
88
            if (!empty($reqTimeFromFilename)) {
89
                if (null !== $filter->getStartDate() &&
90
                    $this->getDateTimeFromString($filter->getStartDate(), 'start') >= $reqTimeFromFilename) {
0 ignored issues
show
Documentation introduced by
$filter->getStartDate() is of type object<DateTime>, but the function expects a string|integer.

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...
91
                    continue;
92
                }
93
94
                if (null !== $filter->getEndDate() &&
95
                    $this->getDateTimeFromString($filter->getEndDate(), 'end') <= $reqTimeFromFilename ) {
0 ignored issues
show
Documentation introduced by
$filter->getEndDate() is of type object<DateTime>, but the function expects a string|integer.

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...
96
                    continue;
97
                }
98
            }
99
100
            $metaFile   = $this->getMetafileNameFromProfileName($file);
101
102
            $meta       = $this->importFile($metaFile, true);
103
            if ($meta === false) {
104
                continue;
105
            }
106
107
            $profile    = $this->importFile($file, false);
108
            if ($profile === false) {
109
                continue;
110
            }
111
112
            if (!empty($profile['meta'])) {
113
                $meta = array_merge($meta, $profile['meta']);
114
            }
115
116
            if (empty($reqTimeFromFilename) && (null !== $filter->getStartDate() || null !== $filter->getEndDate())){
117
                if (null !== $filter->getStartDate() &&
118
                    $this->getDateTimeFromString($filter->getStartDate(), 'start') >= $filter->getStartDate()) {
0 ignored issues
show
Documentation introduced by
$filter->getStartDate() is of type object<DateTime>, but the function expects a string|integer.

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...
119
                    continue;
120
                }
121
                if (null !== $filter->getEndDate() &&
122
                    $this->getDateTimeFromString($filter->getEndDate(), 'end') <= $filter->getEndDate()) {
0 ignored issues
show
Documentation introduced by
$filter->getEndDate() is of type object<DateTime>, but the function expects a string|integer.

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...
123
                    continue;
124
                }
125
            }
126
127
            if ($filter->getUrl() &&
128
                strpos($meta['simple_url'], $filter->getUrl()) === false &&
129
                strpos($meta['SERVER']['SERVER_NAME'].$meta['simple_url'], $filter->getUrl()) === false
130
            ) {
131
                continue;
132
            }
133
134
            if (!empty($profile['profile'])) {
135
                $profile = $profile['profile'];
136
            }
137
138
            if (!empty($profile['_id'])) {
139
                $id = $profile['_id'];
140
            } else {
141
                $id = basename($file);
142
            }
143
            if (!empty($profile)) {
144
                $ret[$id] = [
145
                    'profile'   => $profile,
146
                    '_id'       => $id,
147
                    'meta'      => $meta,
148
                ];
149
            } else {
150
                $ret[$id] = $profile;
151
            }
152
        }
153
154
        try {
155
            if (!empty($filter->getSort()) && !empty($ret)) {
156
                $this->filter = $filter;
157
                usort($ret, array($this, 'sortByColumn'));
158
                unset($this->filter);
159
            }
160
        } catch (InvalidArgumentException $e) {
161
            // ignore for now.
162
        }
163
        
164
        $cacheId = md5(serialize($filter->toArray()));
165
166
        $this->countCache[$cacheId] = count($ret);
167
        $ret = array_slice($ret, $filter->getPerPage()*($filter->getPage()-1), $filter->getPerPage());
168
        $ret = array_column($ret, null, '_id');
169
170
        return new Xhgui_Storage_ResultSet($ret, $this->countCache[$cacheId]);
171
    }
172
173
    /**
174
     * @inheritDoc
175
     * @param Xhgui_Storage_Filter $filter
176
     * @return int
177
     */
178
    public function count(Xhgui_Storage_Filter $filter)
179
    {
180
        $cacheId = md5(serialize($filter->toArray()));
181
        if (empty($this->countCache[$cacheId])) {
182
            $this->find($filter);
183
        }
184
        return $this->countCache[$cacheId];
185
    }
186
187
    /**
188
     * @inheritDoc
189
     * @param $id
190
     * @return mixed
191
     */
192
    public function findOne($id)
193
    {
194
        $filter = new Xhgui_Storage_Filter();
195
        $filter->setId($id);
196
        $resultSet = $this->find($filter);
197
        return $resultSet->current();
198
    }
199
200
    /**
201
     * @inheritDoc
202
     * @param $id
203
     * @return bool
204
     */
205
    public function remove($id)
206
    {
207
        if (file_exists($this->path.$id)) {
208
            $metaFileName = $this->getMetafileNameFromProfileName($id);
209
            if (file_exists($this->path.$metaFileName)) {
210
                unlink($this->path.$metaFileName);
211
            }
212
            unlink($this->path.$id);
213
            return true;
214
        }
215
        return false;
216
    }
217
218
    /**
219
     * @inheritDoc
220
     */
221
    public function drop()
222
    {
223
        array_map('unlink', glob($this->path.'*.xhprof'));
224
        array_map('unlink', glob($this->path.'*.meta'));
225
    }
226
227
    /**
228
     * @inheritDoc
229
     * @param $match
230
     * @param $col
231
     * @param int $percentile
232
     * @return array
233
     */
234
    public function aggregate(Xhgui_Storage_Filter $filter, $col, $percentile = 1)
235
    {
236
        $ret = $this->find($filter);
237
238
        $result = [
239
            'ok'        => 1,
240
            'result'    => [],
241
        ];
242
243
        foreach ($ret as $row) {
244
            $request_date = $row['meta']['request_date'];
245
            $result['result'][$request_date]['wall_times'][]    = $row['profile']['main()']['wt'];
246
            $result['result'][$request_date]['cpu_times'][]     = $row['profile']['main()']['cpu'];
247
            $result['result'][$request_date]['mu_times'][]      = $row['profile']['main()']['mu'];
248
            $result['result'][$request_date]['pmu_times'][]     = $row['profile']['main()']['pmu'];
249
250
            if (empty($result['result'][$request_date]['row_count'])) {
251
                $result['result'][$request_date]['row_count'] = 0;
252
            }
253
            $result['result'][$request_date]['row_count']++;
254
255
            $result['result'][$request_date]['raw_index'] =
256
                $result['result'][$request_date]['row_count']*($percentile/100);
257
258
            $result['result'][$request_date]['_id']= $request_date;
259
        }
260
261
        return $result;
262
    }
263
264
265
    /**
266
     * Column sorter
267
     *
268
     * @param $a
269
     * @param $b
270
     * @return int
271
     */
272
    public function sortByColumn($a, $b)
273
    {
274
        $sort = $this->filter->getSort();
275
        switch ($sort) {
276
            case 'ct':
277
            case 'wt':
278
            case 'cpu':
279
            case 'mu':
280 View Code Duplication
            case 'pmu':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
281
                $aValue = $a['profile']['main()'][$sort];
282
                $bValue = $b['profile']['main()'][$sort];
283
                break;
284
285 View Code Duplication
            case 'time':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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...
286
                $aValue = $a['meta']['request_ts']['sec'];
287
                $bValue = $b['meta']['request_ts']['sec'];
288
                break;
289
290
            case 'controller':
291
            case 'action':
292
            case 'application':
293
            case 'branch':
294 View Code Duplication
            case 'version':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across 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
                $aValue = $a['meta'][$sort];
296
                $bValue = $b['meta'][$sort];
297
                break;
298
299
            default:
300
                throw new InvalidArgumentException('Invalid sort mode');
301
                break;
0 ignored issues
show
Unused Code introduced by
break; does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
302
        }
303
304
        if ($aValue == $bValue) {
305
            return 0;
306
        }
307
308
        if (is_numeric($aValue) || is_numeric($bValue)) {
309
            if ($this->filter->getDirection() === 'desc') {
310
                if ($aValue < $bValue) {
311
                    return 1;
312
                }
313
                return -1;
314
            }
315
316
            if ($aValue > $bValue) {
317
                return 1;
318
            }
319
            return -1;
320
        }
321
322
        if ($this->filter->getDirection() === 'desc') {
323
            return strnatcmp($aValue, $bValue);
324
        }
325
        return strnatcmp($bValue, $aValue);
326
    }
327
328
    /**
329
     * Generate meta profile name from profile file name.
330
     *
331
     * In most cases just add .meta extension
332
     *
333
     * @param $file
334
     * @return mixed
335
     */
336
    protected function getMetafileNameFromProfileName($file)
337
    {
338
        $metaFile = $file.'.meta';
339
        return $metaFile;
340
    }
341
342
343
    /**
344
     * @inheritDoc
345
     * @param $data
346
     */
347
    public function insert(array $data)
348
    {
349
350
        if (empty($data['_id'])) {
351
            $data['_id'] = md5($data['name']);
352
        }
353
354
        file_put_contents($this->path.''.$this->prefix.$data['_id'].'.json', json_encode($data));
355
    }
356
357
    /**
358
     * @inheritDoc
359
     * @param $id
360
     * @param $data
361
     */
362
    public function update($id, array $data)
363
    {
364
        file_put_contents($this->path.''.$this->prefix.$id.'.json', json_encode($data));
365
    }
366
367
    /**
368
     * Load profile file from disk, prepare it and return parsed array
369
     *
370
     * @param $path
371
     * @param bool $meta
372
     * @return mixed
373
     */
374
    protected function importFile($path, $meta = false)
375
    {
376
        if ($meta) {
377
            $serializer = $this->metaSerializer;
378
        } else {
379
            $serializer = $this->dataSerializer;
380
        }
381
382
        if (!file_exists($path) || !is_readable($path)) {
383
            return false;
384
        }
385
        
386
        switch ($serializer) {
387
            default:
388
            case 'json':
389
                return json_decode(file_get_contents($path), true);
390
391
            case 'serialize':
392
                if (PHP_MAJOR_VERSION > 7) {
393
                    return unserialize(file_get_contents($path), false);
394
                }
395
                /** @noinspection UnserializeExploitsInspection */
396
                return unserialize(file_get_contents($path));
397
398
            case 'igbinary_serialize':
399
            case 'igbinary_unserialize':
400
            case 'igbinary':
401
                /** @noinspection PhpComposerExtensionStubsInspection */
402
                return igbinary_unserialize(file_get_contents($path));
403
404
            // this is a path to a file on disk
405
            case 'php':
406
            case 'var_export':
407
                /** @noinspection PhpIncludeInspection */
408
                return include $path;
409
        }
410
    }
411
412
    /**
413
     * @inheritDoc
414
     * @return array
415
     */
416
    public function getWatchedFunctions()
417
    {
418
        $ret = [];
419
        $files = glob($this->watchedFunctionsPathPrefix.'*.json');
420
        foreach ($files as $file) {
421
            $ret[] = json_decode(file_get_contents($file), true);
422
        }
423
        return $ret;
424
    }
425
426
    /**
427
     * @inheritDoc
428
     * @param $name
429
     * @return bool
430
     */
431 View Code Duplication
    public function addWatchedFunction($name)
0 ignored issues
show
Duplication introduced by
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
        $name = trim($name);
434
        if (empty($name)) {
435
            return false;
436
        }
437
        $id = md5($name);
438
        $i = file_put_contents(
439
            $this->watchedFunctionsPathPrefix.$id.'.json',
440
            json_encode(['id'=>$id, 'name'=>$name])
441
        );
442
        return $i > 0;
443
    }
444
445
    /**
446
     * @inheritDoc
447
     * @param $id
448
     * @param $name
449
     * @return bool
450
     */
451 View Code Duplication
    public function updateWatchedFunction($id, $name)
0 ignored issues
show
Duplication introduced by
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...
452
    {
453
        $name = trim($name);
454
        if (empty($name)) {
455
            return false;
456
        }
457
458
        $i = file_put_contents(
459
            $this->watchedFunctionsPathPrefix.$id.'.json',
460
            json_encode(['id'=>$id, 'name'=>trim($name)])
461
        );
462
        return $i > 0;
463
    }
464
465
    /**
466
     * @inheritDoc
467
     * @param $id
468
     */
469
    public function removeWatchedFunction($id)
470
    {
471
        if (file_exists($this->watchedFunctionsPathPrefix.$id.'.json')) {
472
            unlink($this->watchedFunctionsPathPrefix.$id.'.json');
473
        }
474
    }
475
476
    /**
477
     * Parse filename and try to get request time from filename
478
     *
479
     * @param $fileName
480
     * @return bool|DateTime
481
     */
482
    public function getRequestTimeFromFilename($fileName)
483
    {
484
        $matches = [];
485
        // default pattern is: xhgui.data.<timestamp>.<microseconds>_a68888
486
        //  xhgui.data.15 55 31 04 66 .6606_a68888
487
        preg_match('/(?<t>[\d]{10})(\.(?<m>[\d]{1,6}))?.+/i', $fileName, $matches);
488
        try {
489
            return DateTime::createFromFormat('U u', $matches['t'].' '. $matches['m']);
490
        } catch (Exception $e) {
491
            return null;
492
        }
493
    }
494
}
495