Completed
Pull Request — master (#127)
by Matthew
16:03
created

TrendsController::timings()   A

Complexity

Conditions 6
Paths 8

Size

Total Lines 21
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 13
CRAP Score 6.0131

Importance

Changes 0
Metric Value
cc 6
eloc 14
nc 8
nop 1
dl 0
loc 21
ccs 13
cts 14
cp 0.9286
crap 6.0131
rs 9.2222
c 0
b 0
f 0
1
<?php
2
3
namespace Dtc\QueueBundle\Controller;
4
5
use Doctrine\ODM\MongoDB\DocumentManager;
6
use Doctrine\ORM\EntityManager;
7
use Dtc\QueueBundle\Doctrine\DoctrineJobTimingManager;
8
use Dtc\QueueBundle\Model\JobTiming;
9
use Dtc\QueueBundle\ODM\JobManager;
10
use Dtc\QueueBundle\ODM\JobTimingManager;
11
use Symfony\Component\DependencyInjection\ContainerInterface;
12
use Symfony\Component\HttpFoundation\JsonResponse;
13
use Symfony\Component\HttpFoundation\Request;
14
15
class TrendsController
16
{
17
    use ControllerTrait;
18
19
    private $container;
20
21
    public function __construct(ContainerInterface $container) {
22
        $this->container = $container;
23
    }
24
25
    /**
26
     * Show a graph of job trends.
27 1
     */
28
    public function trends()
29 1
    {
30 1
        $recordTimings = $this->container->getParameter('dtc_queue.timings.record');
31 1
        $foundYearFunction = class_exists('Oro\ORM\Query\AST\Platform\Functions\Mysql\Year') || class_exists('DoctrineExtensions\Query\Mysql\Year');
32 1
        $params = ['record_timings' => $recordTimings, 'states' => JobTiming::getStates(), 'found_year_function' => $foundYearFunction];
33
        $this->addCssJs($params);
34 1
35
        return $this->render('@DtcQueue/Queue/trends.html.twig', $params);
36
    }
37
38
    public function timings(Request $request)
39
    {
40 1
        $begin = $request->query->get('begin');
41
        $end = $request->query->get('end');
42 1
        $type = $request->query->get('type', 'HOUR');
43 1
        $beginDate = \DateTime::createFromFormat('Y-m-d\TH:i:s.uO', $begin) ?: null;
44 1
        if ($beginDate instanceof \DateTime) {
45 1
            $beginDate->setTimezone(new \DateTimeZone(date_default_timezone_get()));
46 1
        }
47
        $endDate = \DateTime::createFromFormat('Y-m-d\TH:i:s.uO', $end) ?: \Dtc\QueueBundle\Util\Util::getMicrotimeDateTime();
48
        if ($endDate instanceof \DateTime) {
0 ignored issues
show
introduced by
$endDate is always a sub-type of DateTime.
Loading history...
49 1
            $endDate->setTimezone(new \DateTimeZone(date_default_timezone_get()));
50 1
        }
51 1
52
        $recordTimings = $this->container->getParameter('dtc_queue.timings.record');
53
        $params = [];
54 1
        if ($recordTimings) {
55 1
            $params = $this->calculateTimings($type, $beginDate, $endDate);
56 1
        }
57 1
58
        return new JsonResponse($params);
59
    }
60 1
61
    /**
62
     * @param \DateTime|null $beginDate
63
     * @param \DateTime      $endDate
64
     */
65
    protected function calculateTimings($type, $beginDate, $endDate)
66
    {
67 1
        $params = [];
68
        $this->validateJobTimingManager();
69 1
70 1
        /** @var DoctrineJobTimingManager $jobTimingManager */
71
        $jobTimingManager = $this->get('dtc_queue.manager.job_timing');
0 ignored issues
show
Bug introduced by
The method get() does not exist on Dtc\QueueBundle\Controller\TrendsController. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-call  annotation

71
        /** @scrutinizer ignore-call */ 
72
        $jobTimingManager = $this->get('dtc_queue.manager.job_timing');

This check looks for calls to methods that do not seem to exist on a given type. It looks for the method on the type itself as well as in inherited classes or implemented interfaces.

This is most likely a typographical error or the method has been renamed.

Loading history...
72
        if ($jobTimingManager instanceof JobTimingManager) {
73 1
            $timings = $this->getJobTimingsOdm($type, $endDate, $beginDate);
74 1
        } else {
75 1
            $timings = $this->getJobTimingsOrm($type, $endDate, $beginDate);
76
        }
77 1
78
        $timingStates = JobTiming::getStates();
79
        $timingsDates = [];
80 1
        foreach (array_keys($timingStates) as $state) {
81 1
            if (!isset($timings[$state])) {
82 1
                continue;
83 1
            }
84 1
            $timingsData = $timings[$state];
85
            $timingsDates = array_unique(array_merge(array_keys($timingsData), $timingsDates));
86 1
        }
87 1
88
        $format = $this->getDateFormat($type);
89
        usort($timingsDates, function ($date1str, $date2str) use ($format) {
90 1
            $date1 = \DateTime::createFromFormat($format, $date1str);
91 1
            $date2 = \DateTime::createFromFormat($format, $date2str);
92
            if (!$date2) {
93
                return false;
94
            }
95
            if (!$date1) {
96
                return false;
97
            }
98
            $date1->setTimezone(new \DateTimeZone(date_default_timezone_get()));
99
            $date2->setTimezone(new \DateTimeZone(date_default_timezone_get()));
100
101
            return $date1 > $date2;
102
        });
103
104 1
        $timingsDatesAdjusted = $this->getTimingsDatesAdjusted($timingsDates, $format);
105
        $this->setTimingsData($timingStates, $timings, $timingsDates, $params);
106 1
        $params['timings_dates'] = $timingsDates;
107 1
        $params['timings_dates_rfc3339'] = $timingsDatesAdjusted;
108 1
109 1
        return $params;
110
    }
111 1
112
    /**
113
     * Timings offset by timezone if necessary.
114
     *
115
     * @param string $format
116
     *
117
     * @return array
118
     */
119
    protected function getTimingsDatesAdjusted(array $timingsDates, $format)
120
    {
121 1
        $timezoneOffset = $this->container->getParameter('dtc_queue.timings.timezone_offset');
122
        $timingsDatesAdjusted = [];
123 1
        foreach ($timingsDates as $dateStr) {
124 1
            $date = \DateTime::createFromFormat($format, $dateStr);
125 1
            if (false === $date) {
126 1
                throw new \InvalidArgumentException("'$dateStr' is not in the right format: ".DATE_RFC3339);
127 1
            }
128
            $date->setTimezone(new \DateTimeZone(date_default_timezone_get()));
129
            if (0 !== $timezoneOffset) {
130 1
                // This may too simplistic in areas that observe DST - does the database or PHP code observe DST?
131 1
                $date->setTimestamp($date->getTimestamp() + ($timezoneOffset * 3600));
132
            }
133
            $timingsDatesAdjusted[] = $date->format(DATE_RFC3339);
134
        }
135 1
136
        return $timingsDatesAdjusted;
137
    }
138 1
139
    protected function setTimingsData(array $timingStates, array $timings, array $timingsDates, array &$params)
140
    {
141 1
        foreach (array_keys($timingStates) as $state) {
142
            if (!isset($timings[$state])) {
143 1
                continue;
144 1
            }
145 1
146
            $timingsData = $timings[$state];
147
            foreach ($timingsDates as $date) {
148 1
                $params['timings_data_'.$state][] = isset($timingsData[$date]) ? $timingsData[$date] : 0;
149 1
            }
150 1
        }
151
    }
152
153 1
    /**
154
     * @param string                                 $type
155
     * @param \Doctrine\ODM\MongoDB\Aggregation\Expr $expr
156
     *
157
     * @return mixed
158
     */
159
    protected function addJobTimingsDateInfo($type, $expr)
160
    {
161 1
        switch ($type) {
162
            case 'YEAR':
163 1
                return $expr->field('year')
164 1
                    ->year('$finishedAt');
165 1
            case 'MONTH':
166 1
                return $expr->field('year')
167 1
                    ->year('$finishedAt')
168 1
                    ->field('month')
169 1
                    ->month('$finishedAt');
170 1
            case 'DAY':
171 1
                return $expr->field('year')
172 1
                    ->year('$finishedAt')
173 1
                    ->field('month')
174 1
                    ->month('$finishedAt')
175 1
                    ->field('day')
176 1
                    ->dayOfMonth('$finishedAt');
177 1
            case 'HOUR':
178 1
                return $expr->field('year')
179 1
                    ->year('$finishedAt')
180 1
                    ->field('month')
181 1
                    ->month('$finishedAt')
182 1
                    ->field('day')
183 1
                    ->dayOfMonth('$finishedAt')
184 1
                    ->field('hour')
185 1
                    ->hour('$finishedAt');
186 1
            case 'MINUTE':
187 1
                return $expr->field('year')
188 1
                    ->year('$finishedAt')
189 1
                    ->field('month')
190 1
                    ->month('$finishedAt')
191 1
                    ->field('day')
192 1
                    ->dayOfMonth('$finishedAt')
193 1
                    ->field('hour')
194 1
                    ->hour('$finishedAt')
195 1
                    ->field('minute')
196 1
                    ->minute('$finishedAt');
197 1
            default:
198 1
                throw new \InvalidArgumentException("Unknown type $type");
199
        }
200
    }
201
202
    protected function getJobTImingsOdmMapReduce($builder, $type, \DateTime $end, \DateTime $begin = null)
203
    {
204
        $regexInfo = $this->getRegexDate($type);
205
        if (!$begin) {
206
            $begin = clone $end;
207
            $begin->sub($regexInfo['interval']);
208
        }
209
210
        $mapFunc = 'function() {
211
            var dateStr = this.finishedAt.toISOString();
212
            dateStr = dateStr.replace(/'.$regexInfo['replace']['regex']."/,'".$regexInfo['replace']['replacement']."');
213
            var dateBegin = new Date('".$begin->format('c')."');
214
            var dateEnd = new Date('".$end->format('c')."');
215
            if (this.finishedAt >= dateBegin && this.finishedAt <= dateEnd) {
216
                var result = {};
217
                result[dateStr] = 1;
218
                emit(this.status, result);
219
            }
220
        }";
221
222
        // Run a map reduce function get worker and status break down
223
        $reduceFunc = JobManager::REDUCE_FUNCTION;
224
        $builder->map($mapFunc)
225
            ->reduce($reduceFunc);
226
        $query = $builder->getQuery();
227
        $results = $query->execute();
228
        $resultHash = [];
229
        foreach ($results as $info) {
230
            $resultHash[$info['_id']] = $info['value'];
231
        }
232
233
        return $resultHash;
234
    }
235
236
    protected function getJobTimingsOdm($type, \DateTime $end, \DateTime $begin = null)
237
    {
238 1
        /** @var JobTimingManager $runManager */
239
        $jobTimingManager = $this->get('dtc_queue.manager.job_timing');
240
        $jobTimingClass = $jobTimingManager->getJobTimingClass();
241 1
242 1
        /** @var DocumentManager $documentManager */
243
        $documentManager = $jobTimingManager->getObjectManager();
244
245 1
        $builder = $documentManager->createQueryBuilder($jobTimingClass);
246
        if (method_exists($builder, 'map')) {
247 1
            return $this->getJobTimingsOdmMapReduce($builder, $type, $end, $begin);
248 1
        }
249
250
        $regexInfo = $this->getRegexDate($type);
251
        if (!$begin) {
252 1
            $begin = clone $end;
253 1
            $begin->sub($regexInfo['interval']);
254 1
        }
255 1
256
        $aggregationBuilder = $documentManager->createAggregationBuilder($jobTimingClass);
257
        $expr = $this->addJobTimingsDateInfo($type, $aggregationBuilder->expr());
258 1
        $expr = $expr->field('status')
259 1
                ->expression('$status');
260 1
261 1
        $aggregationBuilder
262
            ->match()
263
                ->field('finishedAt')
264 1
                ->gte($begin)
265 1
                ->lte($end)
266 1
            ->group()
267 1
                    ->field('id')
268 1
                    ->expression($expr)
269 1
                    ->field('value')
270 1
                    ->sum(1);
271 1
272 1
        $results = $aggregationBuilder->execute();
273
        $resultHash = [];
274 1
        foreach ($results as $result) {
275 1
            $key = $result['_id']['status'];
276 1
            $dateStr = $this->getAggregationResultDateStr($type, $result['_id']);
277 1
            $resultHash[$key][$dateStr] = $result['value'];
278 1
        }
279 1
280
        return $resultHash;
281
    }
282 1
283
    /**
284
     * Formats the aggregation result into the desired date string format.
285
     *
286
     * @param string $type
287
     *
288
     * @return string
289
     */
290
    protected function getAggregationResultDateStr($type, array $result)
291
    {
292 1
        switch ($type) {
293
            case 'YEAR':
294 1
                return $result['year'];
295 1
            case 'MONTH':
296 1
                return "{$result['year']}-".str_pad($result['month'], 2, '0', STR_PAD_LEFT);
297 1
            case 'DAY':
298 1
                $str = "{$result['year']}-".str_pad($result['month'], 2, '0', STR_PAD_LEFT);
299 1
                $str .= '-'.str_pad($result['day'], 2, '0', STR_PAD_LEFT);
300 1
301 1
                return $str;
302
            case 'HOUR':
303 1
                $str = "{$result['year']}-".str_pad($result['month'], 2, '0', STR_PAD_LEFT);
304 1
                $str .= '-'.str_pad($result['day'], 2, '0', STR_PAD_LEFT);
305 1
                $str .= ' '.str_pad($result['hour'], 2, '0', STR_PAD_LEFT);
306 1
307 1
                return $str;
308
            case 'MINUTE':
309 1
                $str = "{$result['year']}-".str_pad($result['month'], 2, '0', STR_PAD_LEFT);
310 1
                $str .= '-'.str_pad($result['day'], 2, '0', STR_PAD_LEFT);
311 1
                $str .= ' '.str_pad($result['hour'], 2, '0', STR_PAD_LEFT);
312 1
                $str .= ':'.str_pad($result['minute'], 2, '0', STR_PAD_LEFT);
313 1
314 1
                return $str;
315
            default:
316 1
                throw new \InvalidArgumentException("Invalid date format type '$type''");
317
        }
318
    }
319
320
    protected function getDateFormat($type)
321
    {
322 1
        switch ($type) {
323
            case 'YEAR':
324 1
                return 'Y';
325 1
            case 'MONTH':
326 1
                return 'Y-m';
327 1
            case 'DAY':
328 1
                return 'Y-m-d';
329 1
            case 'HOUR':
330 1
                return 'Y-m-d H';
331 1
            case 'MINUTE':
332 1
                return 'Y-m-d H:i';
333 1
            default:
334 1
                throw new \InvalidArgumentException("Invalid date format type '$type''");
335
        }
336
    }
337
338
    protected function getRegexDate($type)
339
    {
340 1
        switch ($type) {
341
            case 'YEAR':
342 1
                return ['replace' => ['regex' => '(\d+)\-(\d+)\-(\d+)T(\d+):(\d+):(\d+).+$', 'replacement' => '$1'],
343 1
                    'interval' => new \DateInterval('P10Y'), ];
344 1
            case 'MONTH':
345 1
                return ['replace' => ['regex' => '(\d+)\-(\d+)\-(\d+)T(\d+):(\d+):(\d+).+$', 'replacement' => '$1-$2'],
346 1
                    'interval' => new \DateInterval('P12M'), ];
347 1
            case 'DAY':
348 1
                return ['replace' => ['regex' => '(\d+)\-(\d+)\-(\d+)T(\d+):(\d+):(\d+).+$', 'replacement' => '$1-$2-$3'],
349 1
                    'interval' => new \DateInterval('P31D'), ];
350 1
            case 'HOUR':
351 1
                return ['replace' => ['regex' => '(\d+)\-(\d+)\-(\d+)T(\d+):(\d+):(\d+).+$', 'replacement' => '$1-$2-$3 $4'],
352 1
                    'interval' => new \DateInterval('PT24H'), ];
353 1
            case 'MINUTE':
354 1
                return ['replace' => ['regex' => '(\d+)\-(\d+)\-(\d+)T(\d+):(\d+):(\d+).+$', 'replacement' => '$1-$2-$3 $4:$5'],
355 1
                    'interval' => new \DateInterval('PT3600S'), ];
356 1
        }
357 1
        throw new \InvalidArgumentException("Invalid type $type");
358
    }
359
360
    protected function getOrmGroupBy($type)
361
    {
362 1
        switch ($type) {
363
            case 'YEAR':
364 1
                return ['groupby' => 'YEAR(j.finishedAt)',
365 1
                    'interval' => new \DateInterval('P10Y'), ];
366 1
            case 'MONTH':
367 1
                return ['groupby' => 'CONCAT(YEAR(j.finishedAt),\'-\',MONTH(j.finishedAt))',
368 1
                    'interval' => new \DateInterval('P12M'), ];
369 1
            case 'DAY':
370 1
                return ['groupby' => 'CONCAT(YEAR(j.finishedAt),\'-\',MONTH(j.finishedAt),\'-\',DAY(j.finishedAt))',
371 1
                    'interval' => new \DateInterval('P31D'), ];
372 1
            case 'HOUR':
373 1
                return ['groupby' => 'CONCAT(YEAR(j.finishedAt),\'-\',MONTH(j.finishedAt),\'-\',DAY(j.finishedAt),\' \',HOUR(j.finishedAt))',
374 1
                    'interval' => new \DateInterval('PT24H'), ];
375 1
            case 'MINUTE':
376 1
                return ['groupby' => 'CONCAT(YEAR(j.finishedAt),\'-\',MONTH(j.finishedAt),\'-\',DAY(j.finishedAt),\' \',HOUR(j.finishedAt),\':\',MINUTE(j.finishedAt))',
377 1
                    'interval' => new \DateInterval('PT3600S'), ];
378 1
        }
379 1
        throw new \InvalidArgumentException("Invalid type $type");
380
    }
381
382
    protected function getJobTimingsOrm($type, \DateTime $end, \DateTime $begin = null)
383
    {
384 1
        /** @var JobTimingManager $jobTimingManager */
385
        $jobTimingManager = $this->get('dtc_queue.manager.job_timing');
386
        $jobTimingClass = $jobTimingManager->getJobTimingClass();
387 1
        /** @var EntityManager $entityManager */
388 1
        $entityManager = $jobTimingManager->getObjectManager();
389
390 1
        $groupByInfo = $this->getOrmGroupBy($type);
391
392 1
        if (!$begin) {
393
            $begin = clone $end;
394 1
            $begin->sub($groupByInfo['interval']);
395 1
        }
396 1
397
        $queryBuilder = $entityManager->createQueryBuilder()->select("j.status as status, count(j.finishedAt) as thecount, {$groupByInfo['groupby']} as thedate")
398
            ->from($jobTimingClass, 'j')
399 1
            ->where('j.finishedAt <= :end')
400 1
            ->andWhere('j.finishedAt >= :begin')
401 1
            ->setParameter(':end', $end)
402 1
            ->setParameter(':begin', $begin)
403 1
            ->groupBy('status')
404 1
            ->addGroupBy('thedate');
405 1
406 1
        $result = $queryBuilder
407
            ->getQuery()->getArrayResult();
408
409 1
        $resultHash = [];
410
        foreach ($result as $row) {
411 1
            $date = $this->formatOrmDateTime($type, $row['thedate']);
412 1
            $resultHash[$row['status']][$date] = intval($row['thecount']);
413 1
        }
414 1
415
        return $resultHash;
416
    }
417 1
418
    protected function strPadLeft($str)
419
    {
420 1
        return str_pad($str, 2, '0', STR_PAD_LEFT);
421
    }
422 1
423
    protected function formatOrmDateTime($type, $str)
424
    {
425 1
        switch ($type) {
426
            case 'MONTH':
427 1
                $parts = explode('-', $str);
428 1
429 1
                return $parts[0].'-'.$this->strPadLeft($parts[1]);
430
            case 'DAY':
431 1
                $parts = explode('-', $str);
432 1
433 1
                return $parts[0].'-'.$this->strPadLeft($parts[1]).'-'.$this->strPadLeft($parts[2]);
434
            case 'HOUR':
435 1
                $parts = explode(' ', $str);
436 1
                $dateParts = explode('-', $parts[0]);
437 1
438 1
                return $dateParts[0].'-'.$this->strPadLeft($dateParts[1]).'-'.$this->strPadLeft($dateParts[2]).' '.$this->strPadLeft($parts[1]);
439
            case 'MINUTE':
440 1
                $parts = explode(' ', $str);
441 1
                $dateParts = explode('-', $parts[0]);
442 1
                $timeParts = explode(':', $parts[1]);
443 1
444 1
                return $dateParts[0].'-'.$this->strPadLeft($dateParts[1]).'-'.$this->strPadLeft($dateParts[2]).' '.$this->strPadLeft($timeParts[0]).':'.$this->strPadLeft($timeParts[1]);
445
        }
446 1
447
        return $str;
448
    }
449
}
450