Completed
Pull Request — master (#268)
by
unknown
01:30
created

Xhgui_Storage_File   F

Complexity

Total Complexity 74

Size/Duplication

Total Lines 473
Duplicated Lines 6.77 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

Changes 0
Metric Value
wmc 74
lcom 2
cbo 3
dl 32
loc 473
rs 2.48
c 0
b 0
f 0

17 Methods

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