Completed
Pull Request — master (#268)
by
unknown
04:24
created

Xhgui_Storage_File   F

Complexity

Total Complexity 85

Size/Duplication

Total Lines 488
Duplicated Lines 9.02 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

Changes 0
Metric Value
wmc 85
lcom 2
cbo 3
dl 44
loc 488
rs 2
c 0
b 0
f 0

15 Methods

Rating   Name   Duplication   Size   Complexity  
A __construct() 0 9 1
F find() 3 114 32
A count() 0 8 2
A findOne() 0 7 1
A remove() 0 12 3
A drop() 0 5 1
A aggregate() 9 40 3
D sortByColumn() 12 55 19
A getMetafileNameFromProfileName() 0 5 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 View Code Duplication
            if (null !== $filter->getCookie() &&
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...
135
                strpos($meta['SERVER']['HTTP_COOKIE'], $filter->getCookie()) === false
136
            ) {
137
                continue;
138
            }
139
140
            if (null !== $filter->getIp() && $meta['SERVER']['REMOTE_ADDR'] !== $filter->getIp()) {
141
                continue;
142
            }
143
144
            if (!empty($profile['profile'])) {
145
                $profile = $profile['profile'];
146
            }
147
148
            if (!empty($profile['_id'])) {
149
                $id = $profile['_id'];
150
            } else {
151
                $id = basename($file);
152
            }
153
            if (!empty($profile)) {
154
                $ret[$id] = [
155
                    'profile' => $profile,
156
                    '_id'     => $id,
157
                    'meta'    => $meta,
158
                ];
159
            } else {
160
                $ret[$id] = $profile;
161
            }
162
        }
163
164
        try {
165
            if (!empty($filter->getSort()) && !empty($ret)) {
166
                $this->filter = $filter;
167
                usort($ret, array($this, 'sortByColumn'));
168
                unset($this->filter);
169
            }
170
        } catch (InvalidArgumentException $e) {
171
            // ignore for now.
172
        }
173
174
        $cacheId = md5(serialize($filter->toArray()));
175
176
        $this->countCache[$cacheId] = count($ret);
177
        $ret = array_slice($ret, $filter->getPerPage() * ($filter->getPage() - 1), $filter->getPerPage());
178
        $ret = array_column($ret, null, '_id');
179
180
        return new Xhgui_Storage_ResultSet($ret, $this->countCache[$cacheId]);
181
    }
182
183
    /**
184
     * @inheritDoc
185
     * @param Xhgui_Storage_Filter $filter
186
     * @return int
187
     */
188
    public function count(Xhgui_Storage_Filter $filter)
189
    {
190
        $cacheId = md5(serialize($filter->toArray()));
191
        if (empty($this->countCache[$cacheId])) {
192
            $this->find($filter);
193
        }
194
        return $this->countCache[$cacheId];
195
    }
196
197
    /**
198
     * @inheritDoc
199
     * @param $id
200
     * @return mixed
201
     */
202
    public function findOne($id)
203
    {
204
        $filter = new Xhgui_Storage_Filter();
205
        $filter->setId($id);
206
        $resultSet = $this->find($filter);
207
        return $resultSet->current();
208
    }
209
210
    /**
211
     * @inheritDoc
212
     * @param $id
213
     * @return bool
214
     */
215
    public function remove($id)
216
    {
217
        if (file_exists($this->path . $id)) {
218
            $metaFileName = $this->getMetafileNameFromProfileName($id);
219
            if (file_exists($this->path . $metaFileName)) {
220
                unlink($this->path . $metaFileName);
221
            }
222
            unlink($this->path . $id);
223
            return true;
224
        }
225
        return false;
226
    }
227
228
    /**
229
     * @inheritDoc
230
     */
231
    public function drop()
232
    {
233
        array_map('unlink', glob($this->path . '*.xhprof'));
234
        array_map('unlink', glob($this->path . '*.meta'));
235
    }
236
237
    /**
238
     * @inheritDoc
239
     * @param $match
240
     * @param $col
241
     * @param int $percentile
242
     * @return array
243
     */
244
    public function aggregate(Xhgui_Storage_Filter $filter, $col, $percentile = 1)
245
    {
246
        $ret = $this->find($filter);
247
248
        $result = [
249
            'ok'     => 1,
250
            'result' => [],
251
        ];
252
253
        foreach ($ret as $row) {
254
            $date = \DateTime::createFromFormat(
255
                'U u',
256
                $row['meta']['request_ts_micro']['sec'] . ' ' . $row['meta']['request_ts_micro']['usec']
257
            );
258
            $formattedDate = $date->format('Y-m-d H:i');
259
260 View Code Duplication
            if (empty($result['result'][$formattedDate])) {
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...
261
                $result['result'][$formattedDate] = [
262
                    'wall_times' => [],
263
                    'cpu_times'  => [],
264
                    'mu_times'   => [],
265
                    'pmu_times'  => [],
266
                    'row_count'  => 0
267
                ];
268
            }
269
270
            $result['result'][$formattedDate]['wall_times'][] = $row['profile']['main()']['wt'];
271
            $result['result'][$formattedDate]['cpu_times'][] = $row['profile']['main()']['cpu'];
272
            $result['result'][$formattedDate]['mu_times'][] = $row['profile']['main()']['mu'];
273
            $result['result'][$formattedDate]['pmu_times'][] = $row['profile']['main()']['pmu'];
274
            $result['result'][$formattedDate]['row_count']++;
275
276
            $result['result'][$formattedDate]['raw_index'] =
277
                $result['result'][$formattedDate]['row_count'] * ($percentile / 100);
278
279
            $result['result'][$formattedDate]['_id'] = $date->format('Y-m-d H:i:s');
280
        }
281
282
        return $result;
283
    }
284
285
286
    /**
287
     * Column sorter
288
     *
289
     * @param $a
290
     * @param $b
291
     * @return int
292
     */
293
    public function sortByColumn($a, $b)
294
    {
295
        $sort = $this->filter->getSort();
296
        switch ($sort) {
297
            case 'ct':
298
            case 'wt':
299
            case 'cpu':
300
            case 'mu':
301 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...
302
                $aValue = $a['profile']['main()'][$sort];
303
                $bValue = $b['profile']['main()'][$sort];
304
                break;
305
306 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...
307
                $aValue = $a['meta']['request_ts']['sec'];
308
                $bValue = $b['meta']['request_ts']['sec'];
309
                break;
310
311
            case 'controller':
312
            case 'action':
313
            case 'application':
314
            case 'branch':
315 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...
316
                $aValue = $a['meta'][$sort];
317
                $bValue = $b['meta'][$sort];
318
                break;
319
320
            default:
321
                throw new InvalidArgumentException('Invalid sort mode');
322
                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...
323
        }
324
325
        if ($aValue == $bValue) {
326
            return 0;
327
        }
328
329
        if (is_numeric($aValue) || is_numeric($bValue)) {
330
            if ($this->filter->getDirection() === 'desc') {
331
                if ($aValue < $bValue) {
332
                    return 1;
333
                }
334
                return -1;
335
            }
336
337
            if ($aValue > $bValue) {
338
                return 1;
339
            }
340
            return -1;
341
        }
342
343
        if ($this->filter->getDirection() === 'desc') {
344
            return strnatcmp($aValue, $bValue);
345
        }
346
        return strnatcmp($bValue, $aValue);
347
    }
348
349
    /**
350
     * Generate meta profile name from profile file name.
351
     *
352
     * In most cases just add .meta extension
353
     *
354
     * @param $file
355
     * @return mixed
356
     */
357
    protected function getMetafileNameFromProfileName($file)
358
    {
359
        $metaFile = $file . '.meta';
360
        return $metaFile;
361
    }
362
363
    /**
364
     * Load profile file from disk, prepare it and return parsed array
365
     *
366
     * @param $path
367
     * @param bool $meta
368
     * @return mixed
369
     */
370
    protected function importFile($path, $meta = false)
371
    {
372
        if ($meta) {
373
            $serializer = $this->metaSerializer;
374
        } else {
375
            $serializer = $this->dataSerializer;
376
        }
377
378
        if (!file_exists($path) || !is_readable($path)) {
379
            return false;
380
        }
381
382
        switch ($serializer) {
383
            default:
384
            case 'json':
385
                return json_decode(file_get_contents($path), true);
386
387
            case 'serialize':
388
                if (PHP_MAJOR_VERSION > 7) {
389
                    return unserialize(file_get_contents($path), false);
390
                }
391
                /** @noinspection UnserializeExploitsInspection */
392
                return unserialize(file_get_contents($path));
393
394
            case 'igbinary_serialize':
395
            case 'igbinary_unserialize':
396
            case 'igbinary':
397
                /** @noinspection PhpComposerExtensionStubsInspection */
398
                return igbinary_unserialize(file_get_contents($path));
399
400
            // this is a path to a file on disk
401
            case 'php':
402
            case 'var_export':
403
                /** @noinspection PhpIncludeInspection */
404
                return include $path;
405
        }
406
    }
407
408
    /**
409
     * @inheritDoc
410
     * @return array
411
     */
412
    public function getWatchedFunctions()
413
    {
414
        $ret = [];
415
        $files = glob($this->watchedFunctionsPathPrefix . '*.json');
416
        foreach ($files as $file) {
417
            $ret[] = json_decode(file_get_contents($file), true);
418
        }
419
        return $ret;
420
    }
421
422
    /**
423
     * @inheritDoc
424
     * @param $name
425
     * @return bool
426
     */
427 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...
428
    {
429
        $name = trim($name);
430
        if (empty($name)) {
431
            return false;
432
        }
433
        $id = md5($name);
434
        $i = file_put_contents(
435
            $this->watchedFunctionsPathPrefix . $id . '.json',
436
            json_encode(['id' => $id, 'name' => $name])
437
        );
438
        return $i > 0;
439
    }
440
441
    /**
442
     * @inheritDoc
443
     * @param $id
444
     * @param $name
445
     * @return bool
446
     */
447 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...
448
    {
449
        $name = trim($name);
450
        if (empty($name)) {
451
            return false;
452
        }
453
454
        $i = file_put_contents(
455
            $this->watchedFunctionsPathPrefix . $id . '.json',
456
            json_encode(['id' => $id, 'name' => trim($name)])
457
        );
458
        return $i > 0;
459
    }
460
461
    /**
462
     * @inheritDoc
463
     * @param $id
464
     */
465
    public function removeWatchedFunction($id)
466
    {
467
        if (file_exists($this->watchedFunctionsPathPrefix . $id . '.json')) {
468
            unlink($this->watchedFunctionsPathPrefix . $id . '.json');
469
        }
470
    }
471
472
    /**
473
     * Parse filename and try to get request time from filename
474
     *
475
     * @param $fileName
476
     * @return bool|DateTime
477
     */
478
    public function getRequestTimeFromFilename($fileName)
479
    {
480
        $matches = [];
481
        // default pattern is: xhgui.data.<timestamp>.<microseconds>_a68888
482
        //  xhgui.data.15 55 31 04 66 .6606_a68888
483
        preg_match('/(?<t>[\d]{10})(\.(?<m>[\d]{1,6}))?.+/i', $fileName, $matches);
484
        try {
485
            return DateTime::createFromFormat('U u', $matches['t'] . ' ' . $matches['m']);
486
        } catch (Exception $e) {
487
            return null;
488
        }
489
    }
490
}
491