Event   F
last analyzed

Complexity

Total Complexity 68

Size/Duplication

Total Lines 703
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
dl 0
loc 703
rs 2.3529
c 0
b 0
f 0
wmc 68

51 Methods

Rating   Name   Duplication   Size   Complexity  
A spliceIntoPosition() 0 6 1
A buildCommand() 0 5 2
A isDue() 0 3 2
A monthly() 0 3 1
A mondays() 0 3 1
A thursdays() 0 3 1
B filtersPass() 0 11 5
A expressionPasses() 0 8 2
A everyMinute() 0 3 1
A run() 0 11 2
A callAfterCallbacks() 0 4 2
A twiceDaily() 0 3 1
A runCommandInBackground() 0 4 1
A weekly() 0 3 1
A tuesdays() 0 3 1
A timezone() 0 5 1
A wednesdays() 0 3 1
A daily() 0 3 1
A dailyAt() 0 6 2
A hourly() 0 3 1
A everyTenMinutes() 0 3 1
A runCommandInForeground() 0 6 1
A everyFiveMinutes() 0 3 1
A sundays() 0 3 1
A cron() 0 5 1
A __construct() 0 6 1
A days() 0 5 2
A everyThirtyMinutes() 0 3 1
A fridays() 0 3 1
A everyNMinutes() 0 3 1
A weekdays() 0 3 1
A yearly() 0 3 1
A saturdays() 0 3 1
A at() 0 3 1
A weeklyOn() 0 5 1
A sendOutputTo() 0 5 1
A getSummaryForDisplay() 0 7 2
A getDescription() 0 3 1
A skip() 0 5 1
A getDefaultOutput() 0 6 2
A thenPing() 0 4 1
A emailOutput() 0 7 1
A getEmailSubject() 0 7 2
A when() 0 5 1
A description() 0 5 1
A hasTimezone() 0 3 1
A getExpression() 0 3 1
A emailOutputTo() 0 9 4
A then() 0 5 1
A user() 0 5 1
A getTimezone() 0 3 1

How to fix   Complexity   

Complex Class

Complex classes like Event 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.

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 Event, and based on these observations, apply Extract Interface, too.

1
<?php
2
3
namespace yii2mod\scheduling;
4
5
use Cron\CronExpression;
6
use GuzzleHttp\Client as HttpClient;
7
use Symfony\Component\Process\Process;
8
use yii\base\Application;
9
use yii\base\Component;
10
use yii\base\InvalidCallException;
11
use yii\mail\MailerInterface;
12
13
/**
14
 * Class Event
15
 *
16
 * @package yii2mod\scheduling
17
 */
18
class Event extends Component
19
{
20
    /**
21
     * Event is triggered before running the command.
22
     */
23
    const EVENT_BEFORE_RUN = 'beforeRun';
24
25
    /**
26
     * Event is triggered after running the command.
27
     */
28
    const EVENT_AFTER_RUN = 'afterRun';
29
30
    /**
31
     * Command string
32
     *
33
     * @var string
34
     */
35
    public $command;
36
37
    /**
38
     * The cron expression representing the event's frequency.
39
     *
40
     * @var string
41
     */
42
    protected $_expression = '* * * * * *';
43
44
    /**
45
     * The timezone the date should be evaluated on.
46
     *
47
     * @var \DateTimeZone|string
48
     */
49
    protected $_timezone;
50
51
    /**
52
     * The user the command should run as.
53
     *
54
     * @var string
55
     */
56
    protected $_user;
57
58
    /**
59
     * The filter callback.
60
     *
61
     * @var \Closure
62
     */
63
    protected $_filter;
64
65
    /**
66
     * The reject callback.
67
     *
68
     * @var \Closure
69
     */
70
    protected $_reject;
71
72
    /**
73
     * The location that output should be sent to.
74
     *
75
     * @var string
76
     */
77
    protected $_output = null;
78
79
    /**
80
     * The array of callbacks to be run after the event is finished.
81
     *
82
     * @var array
83
     */
84
    protected $_afterCallbacks = [];
85
86
    /**
87
     * The human readable description of the event.
88
     *
89
     * @var string
90
     */
91
    protected $_description;
92
93
    /**
94
     * Create a new event instance.
95
     *
96
     * @param string $command
97
     * @param array $config
98
     */
99
    public function __construct($command, $config = [])
100
    {
101
        $this->command = $command;
102
        $this->_output = $this->getDefaultOutput();
103
104
        parent::__construct($config);
105
    }
106
107
    /**
108
     * Run the given event.
109
     *
110
     * @param Application $app
111
     */
112
    public function run(Application $app)
113
    {
114
        $this->trigger(self::EVENT_BEFORE_RUN);
115
116
        if (count($this->_afterCallbacks) > 0) {
117
            $this->runCommandInForeground($app);
118
        } else {
119
            $this->runCommandInBackground($app);
120
        }
121
122
        $this->trigger(self::EVENT_AFTER_RUN);
123
    }
124
125
    /**
126
     * Run the command in the foreground.
127
     *
128
     * @param Application $app
129
     */
130
    protected function runCommandInForeground(Application $app)
131
    {
132
        (new Process(
133
            trim($this->buildCommand(), '& '), dirname($app->request->getScriptFile()), null, null, null
134
        ))->run();
135
        $this->callAfterCallbacks($app);
136
    }
137
138
    /**
139
     * Build the command string.
140
     *
141
     * @return string
142
     */
143
    public function buildCommand()
144
    {
145
        $command = $this->command . ' > ' . $this->_output . ' 2>&1 &';
146
147
        return $this->_user ? 'sudo -u ' . $this->_user . ' ' . $command : $command;
148
    }
149
150
    /**
151
     * Call all of the "after" callbacks for the event.
152
     *
153
     * @param Application $app
154
     */
155
    protected function callAfterCallbacks(Application $app)
156
    {
157
        foreach ($this->_afterCallbacks as $callback) {
158
            call_user_func($callback, $app);
159
        }
160
    }
161
162
    /**
163
     * Run the command in the background using exec.
164
     *
165
     * @param Application $app
166
     */
167
    protected function runCommandInBackground(Application $app)
168
    {
169
        chdir(dirname($app->request->getScriptFile()));
170
        exec($this->buildCommand());
171
    }
172
173
    /**
174
     * Determine if the given event should run based on the Cron expression.
175
     *
176
     * @param Application $app
177
     *
178
     * @return bool
179
     */
180
    public function isDue(Application $app)
181
    {
182
        return $this->expressionPasses() && $this->filtersPass($app);
183
    }
184
185
    /**
186
     * Determine if the Cron expression passes.
187
     *
188
     * @return bool
189
     */
190
    protected function expressionPasses()
191
    {
192
        $date = new \DateTime('now');
193
        if ($this->_timezone) {
194
            $date->setTimezone($this->_timezone);
195
        }
196
197
        return CronExpression::factory($this->_expression)->isDue($date);
198
    }
199
200
    /**
201
     * Determine if the filters pass for the event.
202
     *
203
     * @param Application $app
204
     *
205
     * @return bool
206
     */
207
    protected function filtersPass(Application $app)
208
    {
209
        if (is_callable($this->_filter) && call_user_func($this->_filter, $app) === false) {
210
            return false;
211
        }
212
213
        if (is_callable($this->_reject) && call_user_func($this->_reject, $app)) {
214
            return false;
215
        }
216
217
        return true;
218
    }
219
220
    /**
221
     * Schedule the event to run hourly.
222
     *
223
     * @return $this
224
     */
225
    public function hourly()
226
    {
227
        return $this->cron('0 * * * * *');
228
    }
229
230
    /**
231
     * The Cron expression representing the event's frequency.
232
     *
233
     * @param string $expression
234
     *
235
     * @return $this
236
     */
237
    public function cron($expression)
238
    {
239
        $this->_expression = $expression;
240
241
        return $this;
242
    }
243
244
    /**
245
     * Schedule the event to run daily.
246
     *
247
     * @return $this
248
     */
249
    public function daily()
250
    {
251
        return $this->cron('0 0 * * * *');
252
    }
253
254
    /**
255
     * Schedule the command at a given time.
256
     *
257
     * @param string $time
258
     *
259
     * @return $this
260
     */
261
    public function at($time)
262
    {
263
        return $this->dailyAt($time);
264
    }
265
266
    /**
267
     * Schedule the event to run daily at a given time (10:00, 19:30, etc).
268
     *
269
     * @param string $time
270
     *
271
     * @return $this
272
     */
273
    public function dailyAt($time)
274
    {
275
        $segments = explode(':', $time);
276
277
        return $this->spliceIntoPosition(2, (int) $segments[0])
278
            ->spliceIntoPosition(1, count($segments) == 2 ? (int) $segments[1] : '0');
279
    }
280
281
    /**
282
     * Splice the given value into the given position of the expression.
283
     *
284
     * @param int $position
285
     * @param string $value
286
     *
287
     * @return Event
288
     */
289
    protected function spliceIntoPosition($position, $value)
290
    {
291
        $segments = explode(' ', $this->_expression);
292
        $segments[$position - 1] = $value;
293
294
        return $this->cron(implode(' ', $segments));
295
    }
296
297
    /**
298
     * Schedule the event to run twice daily.
299
     *
300
     * @return $this
301
     */
302
    public function twiceDaily()
303
    {
304
        return $this->cron('0 1,13 * * * *');
305
    }
306
307
    /**
308
     * Schedule the event to run only on weekdays.
309
     *
310
     * @return $this
311
     */
312
    public function weekdays()
313
    {
314
        return $this->spliceIntoPosition(5, '1-5');
315
    }
316
317
    /**
318
     * Schedule the event to run only on Mondays.
319
     *
320
     * @return $this
321
     */
322
    public function mondays()
323
    {
324
        return $this->days(1);
325
    }
326
327
    /**
328
     * Set the days of the week the command should run on.
329
     *
330
     * @param array|int $days
331
     *
332
     * @return $this
333
     */
334
    public function days($days)
335
    {
336
        $days = is_array($days) ? $days : func_get_args();
337
338
        return $this->spliceIntoPosition(5, implode(',', $days));
339
    }
340
341
    /**
342
     * Schedule the event to run only on Tuesdays.
343
     *
344
     * @return $this
345
     */
346
    public function tuesdays()
347
    {
348
        return $this->days(2);
349
    }
350
351
    /**
352
     * Schedule the event to run only on Wednesdays.
353
     *
354
     * @return $this
355
     */
356
    public function wednesdays()
357
    {
358
        return $this->days(3);
359
    }
360
361
    /**
362
     * Schedule the event to run only on Thursdays.
363
     *
364
     * @return $this
365
     */
366
    public function thursdays()
367
    {
368
        return $this->days(4);
369
    }
370
371
    /**
372
     * Schedule the event to run only on Fridays.
373
     *
374
     * @return $this
375
     */
376
    public function fridays()
377
    {
378
        return $this->days(5);
379
    }
380
381
    /**
382
     * Schedule the event to run only on Saturdays.
383
     *
384
     * @return $this
385
     */
386
    public function saturdays()
387
    {
388
        return $this->days(6);
389
    }
390
391
    /**
392
     * Schedule the event to run only on Sundays.
393
     *
394
     * @return $this
395
     */
396
    public function sundays()
397
    {
398
        return $this->days(0);
399
    }
400
401
    /**
402
     * Schedule the event to run weekly.
403
     *
404
     * @return $this
405
     */
406
    public function weekly()
407
    {
408
        return $this->cron('0 0 * * 0 *');
409
    }
410
411
    /**
412
     * Schedule the event to run weekly on a given day and time.
413
     *
414
     * @param int $day
415
     * @param string $time
416
     *
417
     * @return $this
418
     */
419
    public function weeklyOn($day, $time = '0:0')
420
    {
421
        $this->dailyAt($time);
422
423
        return $this->spliceIntoPosition(5, $day);
424
    }
425
426
    /**
427
     * Schedule the event to run monthly.
428
     *
429
     * @return $this
430
     */
431
    public function monthly()
432
    {
433
        return $this->cron('0 0 1 * * *');
434
    }
435
436
    /**
437
     * Schedule the event to run yearly.
438
     *
439
     * @return $this
440
     */
441
    public function yearly()
442
    {
443
        return $this->cron('0 0 1 1 * *');
444
    }
445
446
    /**
447
     * Schedule the event to run every minute.
448
     *
449
     * @return $this
450
     */
451
    public function everyMinute()
452
    {
453
        return $this->cron('* * * * * *');
454
    }
455
456
    /**
457
     * Schedule the event to run every N minutes.
458
     *
459
     * @param int|string $minutes
460
     *
461
     * @return $this
462
     */
463
    public function everyNMinutes($minutes)
464
    {
465
        return $this->cron('*/' . $minutes . ' * * * * *');
466
    }
467
468
    /**
469
     * Schedule the event to run every five minutes.
470
     *
471
     * @return $this
472
     */
473
    public function everyFiveMinutes()
474
    {
475
        return $this->everyNMinutes(5);
476
    }
477
478
    /**
479
     * Schedule the event to run every ten minutes.
480
     *
481
     * @return $this
482
     */
483
    public function everyTenMinutes()
484
    {
485
        return $this->everyNMinutes(10);
486
    }
487
488
    /**
489
     * Schedule the event to run every thirty minutes.
490
     *
491
     * @return $this
492
     */
493
    public function everyThirtyMinutes()
494
    {
495
        return $this->cron('0,30 * * * * *');
496
    }
497
498
    /**
499
     * Set the timezone the date should be evaluated on.
500
     *
501
     * @param \DateTimeZone|string $timezone
502
     *
503
     * @return $this
504
     */
505
    public function timezone($timezone)
506
    {
507
        $this->_timezone = $timezone;
508
509
        return $this;
510
    }
511
512
    /**
513
     * @return bool
514
     */
515
    public function hasTimezone()
516
    {
517
        return $this->_timezone !== null;
518
    }
519
520
    /**
521
     * @return \DateTimeZone|string
522
     */
523
    public function getTimezone()
524
    {
525
        return $this->_timezone;
526
    }
527
528
    /**
529
     * Set which user the command should run as.
530
     *
531
     * @param string $user
532
     *
533
     * @return $this
534
     */
535
    public function user($user)
536
    {
537
        $this->_user = $user;
538
539
        return $this;
540
    }
541
542
    /**
543
     * Register a callback to further filter the schedule.
544
     *
545
     * @param \Closure $callback
546
     *
547
     * @return $this
548
     */
549
    public function when(\Closure $callback)
550
    {
551
        $this->_filter = $callback;
552
553
        return $this;
554
    }
555
556
    /**
557
     * Register a callback to further filter the schedule.
558
     *
559
     * @param \Closure $callback
560
     *
561
     * @return $this
562
     */
563
    public function skip(\Closure $callback)
564
    {
565
        $this->_reject = $callback;
566
567
        return $this;
568
    }
569
570
    /**
571
     * Send the output of the command to a given location.
572
     *
573
     * @param string $location
574
     *
575
     * @return $this
576
     */
577
    public function sendOutputTo($location)
578
    {
579
        $this->_output = $location;
580
581
        return $this;
582
    }
583
584
    /**
585
     * E-mail the results of the scheduled operation.
586
     *
587
     * @param array $addresses
588
     *
589
     * @return $this
590
     *
591
     * @throws \LogicException
592
     */
593
    public function emailOutputTo($addresses)
594
    {
595
        if (is_null($this->_output) || $this->_output == $this->getDefaultOutput()) {
596
            throw new InvalidCallException('Must direct output to a file in order to e-mail results.');
597
        }
598
        $addresses = is_array($addresses) ? $addresses : func_get_args();
599
600
        return $this->then(function (Application $app) use ($addresses) {
601
            $this->emailOutput($app->mailer, $addresses);
602
        });
603
    }
604
605
    /**
606
     * Register a callback to be called after the operation.
607
     *
608
     * @param \Closure $callback
609
     *
610
     * @return $this
611
     */
612
    public function then(\Closure $callback)
613
    {
614
        $this->_afterCallbacks[] = $callback;
615
616
        return $this;
617
    }
618
619
    /**
620
     * E-mail the output of the event to the recipients.
621
     *
622
     * @param MailerInterface $mailer
623
     * @param array $addresses
624
     */
625
    protected function emailOutput(MailerInterface $mailer, $addresses)
626
    {
627
        $mailer->compose()
628
            ->setTextBody(file_get_contents($this->_output))
629
            ->setSubject($this->getEmailSubject())
630
            ->setTo($addresses)
631
            ->send();
632
    }
633
634
    /**
635
     * Get the e-mail subject line for output results.
636
     *
637
     * @return string
638
     */
639
    protected function getEmailSubject()
640
    {
641
        if ($this->_description) {
642
            return 'Scheduled Job Output (' . $this->_description . ')';
643
        }
644
645
        return 'Scheduled Job Output';
646
    }
647
648
    /**
649
     * Register a callback to the ping a given URL after the job runs.
650
     *
651
     * @param string $url
652
     *
653
     * @return $this
654
     */
655
    public function thenPing($url)
656
    {
657
        return $this->then(function () use ($url) {
658
            (new HttpClient())->get($url);
659
        });
660
    }
661
662
    /**
663
     * Set the human-friendly description of the event.
664
     *
665
     * @param string $description
666
     *
667
     * @return $this
668
     */
669
    public function description($description)
670
    {
671
        $this->_description = $description;
672
673
        return $this;
674
    }
675
676
    /**
677
     * Get the Cron description for the event.
678
     *
679
     * @return string
680
     */
681
    public function getDescription()
682
    {
683
        return $this->_description;
684
    }
685
686
    /**
687
     * Get the summary of the event for display.
688
     *
689
     * @return string
690
     */
691
    public function getSummaryForDisplay()
692
    {
693
        if (is_string($this->_description)) {
694
            return $this->_description;
695
        }
696
697
        return $this->buildCommand();
698
    }
699
700
    /**
701
     * Get the Cron expression for the event.
702
     *
703
     * @return string
704
     */
705
    public function getExpression()
706
    {
707
        return $this->_expression;
708
    }
709
710
    /**
711
     * Get default output
712
     *
713
     * @return string
714
     */
715
    public function getDefaultOutput()
716
    {
717
        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {
718
            return 'NUL';
719
        } else {
720
            return '/dev/null';
721
        }
722
    }
723
}
724