Passed
Push — master ( 10a888...e30075 )
by Matthew
06:51
created

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