Completed
Pull Request — master (#268)
by
unknown
01:25 queued 14s
created

Xhgui_Storage_File   F

Complexity

Total Complexity 83

Size/Duplication

Total Lines 489
Duplicated Lines 6.54 %

Coupling/Cohesion

Components 2
Dependencies 3

Importance

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

17 Methods

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