Completed
Push — master ( 1abf17...425208 )
by Mathieu
06:14
created

AbstractTimeGraphWidget   B

Complexity

Total Complexity 38

Size/Duplication

Total Lines 375
Duplicated Lines 18.13 %

Coupling/Cohesion

Components 1
Dependencies 2

Importance

Changes 2
Bugs 0 Features 0
Metric Value
wmc 38
c 2
b 0
f 0
lcom 1
cbo 2
dl 68
loc 375
rs 8.3999

22 Methods

Rating   Name   Duplication   Size   Complexity  
A setGroupingType() 0 17 4
A groupingType() 0 4 1
A setDateFormat() 0 10 2
A dateFormat() 0 4 1
A setSqlDateFormat() 0 10 2
A sqlDateFormat() 0 4 1
A setStartDate() 19 19 4
A startDate() 0 4 1
A setEndDate() 19 19 4
A endDate() 0 4 1
A setDateInterval() 0 13 3
A dateInterval() 0 4 1
A setGroupingTypeByHour() 10 10 1
A setGroupingTypeByDay() 10 10 1
A setGroupingTypeByMonth() 10 10 1
B dbRows() 0 44 3
A fillRows() 0 20 4
A categories() 0 6 1
A series() 0 17 2
objType() 0 1 ?
seriesOptions() 0 1 ?
categoryFunction() 0 1 ?

How to fix   Duplicated Code   

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:

1
<?php
2
namespace Charcoal\Admin\Widget\Graph;
3
4
// Dependencies from `PHP`
5
use \DateInterval;
6
use \DateTime;
7
use \DateTimeInterface;
8
use \InvalidArgumentException;
9
use \PDO;
10
11
// Dependencies from `charcoal-core`
12
use \Charcoal\Model\ModelFactory;
13
14
// From `charcoal-admin`
15
use \Charcoal\Admin\Widget\Graph\AbstractGraphWidget;
16
use \Charcoal\Amin\Widget\Graph\GraphWidgetInterface;
0 ignored issues
show
Bug introduced by
This use statement conflicts with another class in this namespace, Charcoal\Admin\Widget\Graph\GraphWidgetInterface.

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...
17
18
/**
19
 * Base Time Graph widget.
20
 *
21
 * This widget implements the core feature to create a specialized
22
 * graph widget that is meant to display object data "over" time.
23
 */
24
abstract class AbstractTimeGraphWidget extends AbstractGraphWidget implements TimeGraphWidgetInterface
25
{
26
    /**
27
     * @var array $dbRows
28
     */
29
    private $dbRows;
30
31
    /**
32
     * The date grouping type can be "hour", "day" or "month".
33
     *
34
     * @var string $groupingType
35
     */
36
    private $groupingType;
37
38
    /**
39
     * @var string $dateFirnat
40
     */
41
    private $dateFormat;
42
43
    /**
44
     * @var string $sqlDateFormat
45
     */
46
    private $sqlDateFormat;
47
48
    /**
49
     * @var DateTimeInterface $startDate
50
     */
51
    private $startDate;
52
53
    /**
54
     * @var DateTimeInterface $endDate
55
     */
56
    private $endDate;
57
58
    /**
59
     * @var DateInterval $dateInterval
60
     */
61
    private $dateInterval;
62
63
    /**
64
     * @param string $type The group type.
65
     * @throws InvalidArgumentException If the group type is not a valid type.
66
     * @return TimeGraphWidgetInterface Chainable
67
     */
68
    public function setGroupingType($type)
69
    {
70
        if ($type == 'hour') {
71
            $this->groupingType = 'hour';
72
            return $this->setGroupingTypeByHour();
73
        } elseif ($type == 'day') {
74
            $this->groupingType = 'day';
75
            return $this->setGroupingTypeByDay();
76
        } elseif ($type == 'month') {
77
            $this->groupingType = 'month';
78
            return $this->setGroupingTypeByMonth();
79
        } else {
80
            throw new InvalidArgumentException(
81
                'Invalid group type: can be "hour", "day" or "month".'
82
            );
83
        }
84
    }
85
86
    /**
87
     * @return string
88
     */
89
    public function groupingType()
90
    {
91
        return $this->groupingType;
92
    }
93
94
    /**
95
     * @param string $format The date format.
96
     * @throws InvalidArgumentException If the format argument is not a string.
97
     * @return TimeGraphWidgetInterface Chainable
98
     */
99
    public function setDateFormat($format)
100
    {
101
        if (!is_string($format)) {
102
            throw new InvalidArgumentException(
103
                'Date format must be a string'
104
            );
105
        }
106
        $this->dateFormat = $format;
107
        return $this;
108
    }
109
110
    /**
111
     * @return string
112
     */
113
    public function dateFormat()
114
    {
115
        return $this->dateFormat;
116
    }
117
118
    /**
119
     * @param string $format The date format.
120
     * @throws InvalidArgumentException If the format argument is not a string.
121
     * @return TimeGraphWidgetInterface Chainable
122
     */
123
    public function setSqlDateFormat($format)
124
    {
125
        if (!is_string($format)) {
126
            throw new InvalidArgumentException(
127
                'SQL date format must be a string'
128
            );
129
        }
130
        $this->sqlDateFormat = $format;
131
        return $this;
132
    }
133
134
    /**
135
     * @return string
136
     */
137
    public function sqlDateFormat()
138
    {
139
        return $this->sqlDateFormat;
140
    }
141
142
    /**
143
     * @param string|DateTimeInterface $ts The start date.
144
     * @throws InvalidArgumentException If the date is not a valid datetime format.
145
     * @return TimeGraphWidgetInterface Chainable
146
     */
147 View Code Duplication
    public function setStartDate($ts)
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...
148
    {
149
        if (is_string($ts)) {
150
            try {
151
                $ts = new DateTime($ts);
152
            } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The class Charcoal\Admin\Widget\Graph\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
153
                throw new InvalidArgumentException(
154
                    'Invalid start date: '.$e->getMessage()
155
                );
156
            }
157
        }
158
        if (!($ts instanceof DateTimeInterface)) {
159
            throw new InvalidArgumentException(
160
                'Invalid "Start Date" value. Must be a date/time string or a DateTime object.'
161
            );
162
        }
163
        $this->startDate = $ts;
164
        return $this;
165
    }
166
167
    /**
168
     * @return DateTimeInterface
169
     */
170
    public function startDate()
171
    {
172
        return $this->startDate;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->startDate; (DateTimeInterface) is incompatible with the return type declared by the interface Charcoal\Admin\Widget\Gr...getInterface::startDate of type Charcoal\Admin\Widget\Graph\DateTimeInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
173
    }
174
175
    /**
176
     * @param string|DateTimeInterface $ts The end date.
177
     * @throws InvalidArgumentException If the date is not a valid datetime format.
178
     * @return TimeGraphWidgetInterface Chainable
179
     */
180 View Code Duplication
    public function setEndDate($ts)
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...
181
    {
182
        if (is_string($ts)) {
183
            try {
184
                $ts = new DateTime($ts);
185
            } catch (Exception $e) {
0 ignored issues
show
Bug introduced by
The class Charcoal\Admin\Widget\Graph\Exception does not exist. Did you forget a USE statement, or did you not list all dependencies?

Scrutinizer analyzes your composer.json/composer.lock file if available to determine the classes, and functions that are defined by your dependencies.

It seems like the listed class was neither found in your dependencies, nor was it found in the analyzed files in your repository. If you are using some other form of dependency management, you might want to disable this analysis.

Loading history...
186
                throw new InvalidArgumentException(
187
                    'Invalid end date: '.$e->getMessage()
188
                );
189
            }
190
        }
191
        if (!($ts instanceof DateTimeInterface)) {
192
            throw new InvalidArgumentException(
193
                'Invalid "End Date" value. Must be a date/time string or a DateTime object.'
194
            );
195
        }
196
        $this->endDate = $ts;
197
        return $this;
198
    }
199
200
    /**
201
     * @return DateTimeInterface
202
     */
203
    public function endDate()
204
    {
205
        return $this->endDate;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->endDate; (DateTimeInterface) is incompatible with the return type declared by the interface Charcoal\Admin\Widget\Gr...idgetInterface::endDate of type Charcoal\Admin\Widget\Graph\DateTimeInterface.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
206
    }
207
208
    /**
209
     * @param string|DateInterval $interval The date interval, between "categories".
210
     * @throws InvalidArgumentException If the argument is not a string or an interval object.
211
     * @return TimeGraphWidgetInterface Chainable
212
     */
213
    public function setDateInterval($interval)
214
    {
215
        if (is_string($interval)) {
216
            $this->dateInterval = DateInterval::createfromdatestring($interval);
217
        } elseif ($interval instanceof DateInterval) {
218
            $this->dateInterval = $interval;
219
        } else {
220
            throw new InvalidArgumentException(
221
                'Can not set date interval.'
222
            );
223
        }
224
        return $this;
225
    }
226
227
    /**
228
     * @return DateInterval
229
     */
230
    public function dateInterval()
231
    {
232
        return $this->dateInterval;
0 ignored issues
show
Bug Best Practice introduced by
The return type of return $this->dateInterval; (DateInterval) is incompatible with the return type declared by the interface Charcoal\Admin\Widget\Gr...Interface::dateInterval of type Charcoal\Admin\Widget\Graph\DateInterval.

If you return a value from a function or method, it should be a sub-type of the type that is given by the parent type f.e. an interface, or abstract method. This is more formally defined by the Lizkov substitution principle, and guarantees that classes that depend on the parent type can use any instance of a child type interchangably. This principle also belongs to the SOLID principles for object oriented design.

Let’s take a look at an example:

class Author {
    private $name;

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

    public function getName() {
        return $this->name;
    }
}

abstract class Post {
    public function getAuthor() {
        return 'Johannes';
    }
}

class BlogPost extends Post {
    public function getAuthor() {
        return new Author('Johannes');
    }
}

class ForumPost extends Post { /* ... */ }

function my_function(Post $post) {
    echo strtoupper($post->getAuthor());
}

Our function my_function expects a Post object, and outputs the author of the post. The base class Post returns a simple string and outputting a simple string will work just fine. However, the child class BlogPost which is a sub-type of Post instead decided to return an object, and is therefore violating the SOLID principles. If a BlogPost were passed to my_function, PHP would not complain, but ultimately fail when executing the strtoupper call in its body.

Loading history...
233
    }
234
235
     /**
236
      * @return TimeGraphWidgetInterface Chainable
237
      */
238 View Code Duplication
    protected function setGroupingTypeByHour()
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...
239
    {
240
        $this->setDateFormat('Y-m-d H:i');
241
        $this->setSqlDateFormat('%Y-%m-%d %H:%i');
242
        $this->setStartDate('-24 hours');
243
        $this->setEndDate('now');
244
        $this->setDateInterval('+1 hour');
245
246
        return $this;
247
    }
248
249
    /**
250
     * @return TimeGraphWidgetInterface Chainable
251
     */
252 View Code Duplication
    protected function setGroupingTypeByDay()
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...
253
    {
254
        $this->setDateFormat('Y-m-d');
255
        $this->setSqlDateFormat('%Y-%m-%d');
256
        $this->setStartDate('-30 days');
257
        $this->setEndDate('now');
258
        $this->setDateInterval('+1 day');
259
260
        return $this;
261
    }
262
263
    /**
264
     * @return TimeGraphWidgetInterface Chainable
265
     */
266 View Code Duplication
    protected function setGroupingTypeByMonth()
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...
267
    {
268
        $this->setDateFormat('Y-m');
269
        $this->setSqlDateFormat('%Y-%m');
270
        $this->setStartDate('-12 months');
271
        $this->setEndDate('now');
272
        $this->setDateInterval('+1 month');
273
274
        return $this;
275
    }
276
277
    /**
278
     * @return array
279
     */
280
    protected function dbRows()
281
    {
282
        if ($this->dbRows === null) {
283
            $factory = new ModelFactory([
0 ignored issues
show
Unused Code introduced by
The call to ModelFactory::__construct() has too many arguments starting with array('logger' => $this->logger).

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
284
                'logger' => $this->logger
285
            ]);
286
287
            $model = $factory->create($this->objType(), [
288
                'logger' => $this->logger
289
            ]);
290
291
            $sql = [];
292
            $seriesOptions = $this->seriesOptions();
293
            foreach ($seriesOptions as $serieId => $serieOpts) {
294
                $sql[] = $serieOpts['function'].' as '.$serieId;
295
            }
296
297
            $q = sprintf(
298
                '
299
                SELECT
300
                    %1$s as x,
301
                    '.implode(
302
                    ', ',
303
                    $sql
304
                ).'
305
                FROM
306
                    %2$s
307
                WHERE
308
                    %1$s BETWEEN  :start_date AND :end_date
309
                GROUP BY
310
                    %1$s
311
                ORDER BY
312
                    %1$s ASC',
313
                $this->categoryFunction(),
314
                $model->source()->table()
315
            );
316
            $res = $model->source()->dbQuery($q, [
317
                'start_date'    => $this->startDate()->format($this->dateFormat()),
318
                'end_date'      => $this->endDate()->format($this->dateFormat())
319
            ]);
320
            $this->dbRows = $res->fetchAll((PDO::FETCH_GROUP|PDO::FETCH_UNIQUE|PDO::FETCH_ASSOC));
321
        }
322
        return $this->dbRows;
323
    }
324
325
    /**
326
     * Fill all rows with 0 value for all series, when it is unset.
327
     *
328
     * @param array $rows The row structure to fill.
329
     * @return array
330
     */
331
    protected function fillRows(array $rows)
332
    {
333
        $row = [];
334
        $seriesOptions = $this->seriesOptions();
335
        foreach ($seriesOptions as $serieId => $serieOpts) {
336
            $row[$serieId] = '0';
337
        }
338
339
        $starts = clone($this->startDate());
340
        $ends = $this->endDate();
341
        while ($starts < $ends) {
342
            $x = $starts->format($this->dateFormat());
343
            if (!isset($rows[$x])) {
344
                $rows[$x] = $row;
345
            }
346
            $starts->add($this->dateInterval());
347
        }
348
        ksort($rows);
349
        return $rows;
350
    }
351
352
    /**
353
     * @return array Categories structure.
354
     */
355
    public function categories()
356
    {
357
        $rows = $this->dbRows();
358
        $rows = $this->fillRows($rows);
359
        return array_keys($rows);
360
    }
361
362
363
    /**
364
     * @return array Series structure.
365
     */
366
    public function series()
367
    {
368
        $rows = $this->dbRows();
369
        $rows = $this->fillRows($rows);
370
371
        $series = [];
372
        $options = $this->seriesOptions();
373
        foreach ($options as $serieId => $serieOptions) {
374
            $series[] = [
375
                'name' => (string)$serieOptions['name'],
376
                'type' => (string)$serieOptions['type'],
377
                'data' => array_column($rows, $serieId)
378
            ];
379
        }
380
381
        return $series;
382
    }
383
384
    /**
385
     * @return string
386
     */
387
    abstract protected function objType();
388
389
    /**
390
     * @return array
391
     */
392
    abstract protected function seriesOptions();
393
394
        /**
395
         * @return string
396
         */
397
    abstract protected function categoryFunction();
398
}
399