Completed
Push — master ( 31d53b...10a888 )
by Matthew
75:43 queued 71:47
created

TrendsController::strPadLeft()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 3
Code Lines 1

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 3
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 1
nc 1
nop 1
crap 1
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 Sensio\Bundle\FrameworkExtraBundle\Configuration\Route;
12
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Template;
13
use Symfony\Bundle\FrameworkBundle\Controller\Controller;
14
use Symfony\Component\HttpFoundation\JsonResponse;
15
use Symfony\Component\HttpFoundation\Request;
16
17
class TrendsController extends Controller
0 ignored issues
show
Deprecated Code introduced by
The class Symfony\Bundle\Framework...e\Controller\Controller has been deprecated: since Symfony 4.2, use "Symfony\Bundle\FrameworkBundle\Controller\AbstractController" instead. ( Ignorable by Annotation )

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

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