Passed
Push — master ( ce9894...a2c9fd )
by Sebastiaan
02:10
created

Event::getCommand()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
1
<?php
2
3
namespace Basebuilder\Scheduling;
4
5
use Carbon\Carbon;
6
use Closure;
7
use Cron\CronExpression;
8
use Symfony\Component\Process\Process;
9
use Symfony\Component\Process\ProcessUtils;
10
use Webmozart\Assert\Assert;
11
12
class Event
13
{
14
    /**
15
     * The command to run.
16
     * @var string
17
     */
18
    protected $command;
19
20
    /**
21
     * The working directory.
22
     * @var string
23
     */
24
    protected $cwd;
25
26
    /**
27
     * The user the command should run as.
28
     * @var string
29
     */
30
    protected $user;
31
32
    /**
33
     * The cron expression representing the event's frequency.
34
     * @var string
35
     */
36
    protected $expression = '* * * * * *';
37
38
    /**
39
     * The timezone the date should be evaluated on.
40
     * @var \DateTimeZone|string
41
     */
42
    protected $timezone;
43
44
    /**
45
     * Indicates if the command should not overlap itself.
46
     * @var bool
47
     */
48
    protected $mutuallyExclusive = false;
49
50
    /**
51
     * Indicates if the command should run in background.
52
     * @var bool
53
     */
54
    protected $runInBackground = true;
55
56
    /**
57
     * The array of filter callbacks. These must return true
58
     * @var array
59
     */
60
    protected $filters = [];
61
62
    /**
63
     * The array of reject callbacks.
64
     * @var array
65
     */
66
    protected $rejects = [];
67
68
    /**
69
     * The location that output should be sent to.
70
     * @var string
71
     */
72
    protected $output = '/dev/null';
73
74
    /**
75
     * The location of error output
76
     * @var string
77
     */
78
    protected $errorOutput = '/dev/null';
79
80
    /**
81
     * Indicates whether output should be appended or added (> vs >>)
82
     * @var bool
83
     */
84
    protected $shouldAppendOutput = true;
85
86
    /**
87
     * The array of callbacks to be run before the event is started.
88
     * @var array
89
     */
90
    protected $beforeCallbacks = [];
91
92
    /**
93
     * The array of callbacks to be run after the event is finished.
94
     * @var array
95
     */
96
    protected $afterCallbacks = [];
97
98
    /**
99
     * The human readable description of the event.
100
     * @var string
101
     */
102
    public $description;
103
104
    public function __construct(/* string */ $command)
105
    {
106
        Assert::stringNotEmpty($command);
107
108
        $this->command = $command;
109
    }
110
111
    /**
112
     * @return string
113
     */
114
    public function getCommand()
115
    {
116
        return $this->command;
117
    }
118
119
    /**
120
     * @return string
121
     */
122
    public function __toString()
123
    {
124
        return $this->getCommand();
125
    }
126
127
    /**
128
     * Build the command string.
129
     *
130
     * @return string
131
     */
132
    public function compileCommand()
133
    {
134
        $redirect    = $this->shouldAppendOutput ? '>>' : '>';
135
        $output      = ProcessUtils::escapeArgument($this->output);
136
        $errorOutput = ProcessUtils::escapeArgument($this->errorOutput);
137
138
        // e.g. 1>> /dev/null 2>> /dev/null
139
        $outputRedirect = ' 1' . $redirect . ' ' . $output . ' 2' . $redirect . ' ' . $errorOutput;
140
141
        $parts = [];
142
143
        if ($this->cwd) {
144
            $parts[] =  'cd ' . $this->cwd . ';';
145
        }
146
147
        if ($this->user) {
148
            $parts[] = 'sudo -u ' . $this->user . ' --';
149
        }
150
151
        $wrapped = $this->mutuallyExclusive
152
            ? '(touch ' . $this->getMutexPath() . '; ' . $this->command . '; rm ' . $this->getMutexPath() . ')' . $outputRedirect
0 ignored issues
show
Coding Style introduced by
This line exceeds maximum limit of 120 characters; contains 129 characters

Overly long lines are hard to read on any screen. Most code styles therefor impose a maximum limit on the number of characters in a line.

Loading history...
153
            : $this->command . $outputRedirect;
154
155
        $parts[] = "sh -c '{$wrapped}'";
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $wrapped instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
156
157
        $command = implode(' ', $parts);
158
159
        return $command;
160
    }
161
162
    /**
163
     * Run the given event.
164
     *
165
     * @return Process
166
     */
167
    public function run()
168
    {
169
        foreach ($this->beforeCallbacks as $callback) {
170
            call_user_func($callback);
171
        }
172
173
        if (!$this->runInBackground) {
174
            $process = $this->runCommandInForeground();
175
        } else {
176
            $process = $this->runCommandInBackground();
177
        }
178
179
        foreach ($this->afterCallbacks as $callback) {
180
            call_user_func($callback);
181
        }
182
183
        return $process;
184
    }
185
186
    /**
187
     * Run the command in the foreground.
188
     *
189
     * @return Process
190
     */
191 View Code Duplication
    protected function runCommandInForeground()
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...
192
    {
193
        $process = new Process($this->compileCommand(), $this->cwd, null, null, null);
194
        $process->run();
195
196
        return $process;
197
    }
198
199
    /**
200
     * Run the command in the background.
201
     *
202
     * @return Process
203
     */
204 View Code Duplication
    protected function runCommandInBackground()
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...
205
    {
206
        $process = new Process($this->compileCommand() . ' &', $this->cwd, null, null, null);
207
        $process->run();
208
209
        return $process;
210
    }
211
212
    /**
213
     * Get the mutex path for managing concurrency
214
     *
215
     * @return string
216
     */
217
    protected function getMutexPath()
218
    {
219
        return rtrim(sys_get_temp_dir(), '/') . '/scheduled-event-' . md5($this->cwd . $this->command);
220
    }
221
222
    /**
223
     * Do not allow the event to overlap each other.
224
     *
225
     * @return $this
226
     */
227
    public function preventOverlapping()
228
    {
229
        $this->mutuallyExclusive = true;
230
231
        // Skip the event if it's locked (processing)
232
        $this->skip(function() {
233
            return $this->isLocked();
234
        });
235
236
        return $this;
237
    }
238
239
    /**
240
     * Tells you whether this event has been denied from mutual exclusiveness
241
     *
242
     * @return bool
243
     */
244
    protected function isLocked()
245
    {
246
        return file_exists($this->getMutexPath());
247
    }
248
249
    /**
250
     * Schedule the event to run between start and end time.
251
     *
252
     * @param  string  $startTime
253
     * @param  string  $endTime
254
     * @return $this
255
     */
256
    public function between($startTime, $endTime)
257
    {
258
        return $this->when($this->inTimeInterval($startTime, $endTime));
259
    }
260
261
    /**
262
     * Schedule the event to not run between start and end time.
263
     *
264
     * @param  string  $startTime
265
     * @param  string  $endTime
266
     * @return $this
267
     */
268
    public function notBetween($startTime, $endTime)
269
    {
270
        return $this->skip($this->inTimeInterval($startTime, $endTime));
271
    }
272
273
    /**
274
     * Schedule the event to run between start and end time.
275
     *
276
     * @param  string  $startTime
277
     * @param  string  $endTime
278
     * @return \Closure
279
     */
280
    private function inTimeInterval($startTime, $endTime)
281
    {
282
        return function () use ($startTime, $endTime) {
283
            $now = Carbon::now()->getTimestamp();
284
            return $now >= strtotime($startTime) && $now <= strtotime($endTime);
285
        };
286
    }
287
288
    /**
289
     * State that the command should run in the foreground
290
     *
291
     * @return $this
292
     */
293
    public function runInForeground()
294
    {
295
        $this->runInBackground = false;
296
297
        return $this;
298
    }
299
300
    /**
301
     * State that the command should run in the background.
302
     *
303
     * @return $this
304
     */
305
    public function runInBackground()
306
    {
307
        $this->runInBackground = true;
308
309
        return $this;
310
    }
311
312
    /**
313
     * Set the timezone the date should be evaluated on.
314
     *
315
     * @return $this
316
     */
317
    public function timezone(\DateTimeZone $timezone)
318
    {
319
        $this->timezone = $timezone;
320
321
        return $this;
322
    }
323
324
    /**
325
     * Set which user the command should run as.
326
     *
327
     * @param  string?  $user
0 ignored issues
show
Documentation introduced by
The doc-type string? could not be parsed: Unknown type name "string?" at position 0. (view supported doc-types)

This check marks PHPDoc comments that could not be parsed by our parser. To see which comment annotations we can parse, please refer to our documentation on supported doc-types.

Loading history...
328
     * @return $this
329
     */
330
    public function asUser(/* string? */ $user)
331
    {
332
        Assert::nullOrString($user);
333
334
        $this->user = $user;
335
336
        return $this;
337
    }
338
339
    /**
340
     * Determine if the given event should run based on the Cron expression.
341
     *
342
     * @return bool
343
     */
344
    public function isDue()
345
    {
346
        return $this->expressionPasses() && $this->filtersPass();
347
    }
348
349
    /**
350
     * Determine if the Cron expression passes.
351
     *
352
     * @return boolean
353
     */
354
    protected function expressionPasses()
355
    {
356
        $date = Carbon::now();
357
358
        if ($this->timezone) {
359
            $date->setTimezone($this->timezone);
360
        }
361
362
        return $this->getCronExpression()->isDue($date->toDateTimeString());
363
    }
364
365
    /**
366
     * Determine if the filters pass for the event.
367
     *
368
     * @return boolean
369
     */
370
    protected function filtersPass()
371
    {
372
        foreach ($this->filters as $callback) {
373
            if (!call_user_func($callback)) {
374
                return false;
375
            }
376
        }
377
378
        foreach ($this->rejects as $callback) {
379
            if (call_user_func($callback)) {
380
                return false;
381
            }
382
        }
383
384
        return true;
385
    }
386
387
    /**
388
     * Change the current working directory.
389
     *
390
     * @param  string $directory
391
     * @return $this
392
     */
393
    public function in(/* string */ $directory)
0 ignored issues
show
Coding Style introduced by
This method's name is shorter than the configured minimum length of 3 characters.

Even though PHP does not care about the name of your methods, it is generally a good practice to choose method names which can be easily understood by other human readers.

Loading history...
394
    {
395
        Assert::stringNotEmpty($directory);
396
397
        $this->cwd = $directory;
398
399
        return $this;
400
    }
401
402
    /**
403
     * Whether we append or redirect output
404
     *
405
     * @param bool $switch
406
     * @return $this
407
     */
408
    public function appendOutput(/* boolean */ $switch = true)
409
    {
410
        Assert::boolean($switch);
411
412
        $this->shouldAppendOutput = $switch;
413
414
        return $this;
415
    }
416
417
    /**
418
     * Set the file or location where to send file descriptor 1 to
419
     *
420
     * @param string $output
421
     * @return $this
422
     */
423
    public function outputTo(/* string */ $output)
424
    {
425
        Assert::stringNotEmpty($output);
426
427
        $this->output = $output;
428
429
        return $this;
430
    }
431
432
    /**
433
     * Set the file or location where to send file descriptor 2 to
434
     *
435
     * @param string $output
436
     * @return $this
437
     */
438
    public function errorOutputTo(/* string */ $output)
439
    {
440
        Assert::stringNotEmpty($output);
441
442
        $this->errorOutput = $output;
443
444
        return $this;
445
    }
446
447
    /**
448
     * The Cron expression representing the event's frequency.
449
     *
450
     * @param  string  $expression
451
     * @return $this
452
     */
453
    public function cron(/* string */ $expression)
454
    {
455
        Assert::stringNotEmpty($expression);
456
457
        $this->expression = $expression;
458
459
        return $this;
460
    }
461
462
    /**
463
     * @return CronExpression
464
     */
465
    public function getCronExpression()
466
    {
467
        return CronExpression::factory($this->expression);
468
    }
469
470
    /**
471
     * Change the minute when the job should run (0-59, *, *\/2 etc)
472
     *
473
     * @param  string|int $minute
474
     * @return $this
475
     */
476
    public function minute($minute)
477
    {
478
        return $this->spliceIntoPosition(1, $minute);
479
    }
480
481
    /**
482
     * Schedule the event to run every minute.
483
     *
484
     * @return $this
485
     */
486
    public function everyMinute()
487
    {
488
        return $this->minute('*');
489
    }
490
491
    /**
492
     * Schedule this event to run every 5 minutes
493
     *
494
     * @return Event
495
     */
496
    public function everyFiveMinutes()
497
    {
498
        return $this->everyNMinutes(5);
499
    }
500
501
    /**
502
     * Schedule the event to run every N minutes
503
     *
504
     * @param  int $n
505
     * @return $this
506
     */
507
    public function everyNMinutes(/* int */ $n)
0 ignored issues
show
Comprehensibility introduced by
Avoid variables with short names like $n. Configured minimum length is 2.

Short variable names may make your code harder to understand. Variable names should be self-descriptive. This check looks for variable names who are shorter than a configured minimum.

Loading history...
508
    {
509
        Assert::integer($n);
510
511
        return $this->minute("*/{$n}");
0 ignored issues
show
Coding Style Best Practice introduced by
As per coding-style, please use concatenation or sprintf for the variable $n instead of interpolation.

It is generally a best practice as it is often more readable to use concatenation instead of interpolation for variables inside strings.

// Instead of
$x = "foo $bar $baz";

// Better use either
$x = "foo " . $bar . " " . $baz;
$x = sprintf("foo %s %s", $bar, $baz);
Loading history...
512
    }
513
514
    /**
515
     * Set the hour when the job should run (0-23, *, *\/2, etc)
516
     *
517
     * @param  string|int $hour
518
     * @return Event
519
     */
520
    public function hour($hour)
521
    {
522
        return $this->spliceIntoPosition(2, $hour);
523
    }
524
525
    /**
526
     * Schedule the event to run hourly.
527
     *
528
     * @return $this
529
     */
530
    public function hourly()
531
    {
532
        return $this
533
            ->spliceIntoPosition(1, 0)
534
            ->spliceIntoPosition(2, '*');
535
    }
536
537
    /**
538
     * Schedule the event to run daily.
539
     *
540
     * @return $this
541
     */
542
    public function daily()
543
    {
544
        return $this
545
            ->spliceIntoPosition(1, 0)
546
            ->spliceIntoPosition(2, 0);
547
    }
548
549
    /**
550
     * Schedule the event to run daily at a given time (10:00, 19:30, etc).
551
     *
552
     * @param  string  $time
553
     * @return $this
554
     */
555
    public function dailyAt(/* string */ $time)
556
    {
557
        Assert::stringNotEmpty($time);
558
559
        $segments = explode(':', $time);
560
561
        return $this->spliceIntoPosition(2, (int) $segments[0])
562
            ->spliceIntoPosition(1, count($segments) == 2 ? (int) $segments[1] : '0');
563
    }
564
565
    /**
566
     * Set the days of the week the command should run on.
567
     *
568
     * @param  array|mixed  $days
569
     * @return $this
570
     */
571
    public function days($days)
572
    {
573
        $days = is_array($days) ? $days : func_get_args();
0 ignored issues
show
Coding Style introduced by
Consider using a different name than the parameter $days. This often makes code more readable.
Loading history...
574
575
        return $this->spliceIntoPosition(5, implode(',', $days));
576
    }
577
578
    /**
579
     * Schedule the event to run only on weekdays.
580
     *
581
     * @return $this
582
     */
583
    public function weekdays()
584
    {
585
        return $this->spliceIntoPosition(5, '1-5');
586
    }
587
588
    /**
589
     * Schedule the event to run only on Mondays.
590
     *
591
     * @return $this
592
     */
593
    public function mondays()
594
    {
595
        return $this->days(1);
596
    }
597
598
    /**
599
     * Schedule the event to run only on Tuesdays.
600
     *
601
     * @return $this
602
     */
603
    public function tuesdays()
604
    {
605
        return $this->days(2);
606
    }
607
608
    /**
609
     * Schedule the event to run only on Wednesdays.
610
     *
611
     * @return $this
612
     */
613
    public function wednesdays()
614
    {
615
        return $this->days(3);
616
    }
617
618
    /**
619
     * Schedule the event to run only on Thursdays.
620
     *
621
     * @return $this
622
     */
623
    public function thursdays()
624
    {
625
        return $this->days(4);
626
    }
627
628
    /**
629
     * Schedule the event to run only on Fridays.
630
     *
631
     * @return $this
632
     */
633
    public function fridays()
634
    {
635
        return $this->days(5);
636
    }
637
638
    /**
639
     * Schedule the event to run only on Saturdays.
640
     *
641
     * @return $this
642
     */
643
    public function saturdays()
644
    {
645
        return $this->days(6);
646
    }
647
648
    /**
649
     * Schedule the event to run only on Sundays.
650
     *
651
     * @return $this
652
     */
653
    public function sundays()
654
    {
655
        return $this->days(0);
656
    }
657
658
    /**
659
     * Schedule the event to run weekly.
660
     *
661
     * @return $this
662
     */
663
    public function weekly()
664
    {
665
        return $this->spliceIntoPosition(1, 0)
666
            ->spliceIntoPosition(2, 0)
667
            ->spliceIntoPosition(5, 0);
668
    }
669
670
    /**
671
     * Schedule the event to run weekly on a given day and time.
672
     *
673
     * @param  int  $day
674
     * @param  string  $time
675
     * @return $this
676
     */
677
    public function weeklyOn($day, $time = '0:0')
678
    {
679
        $this->dailyAt($time);
680
        return $this->spliceIntoPosition(5, $day);
681
    }
682
683
    /**
684
     * Schedule the event to run monthly.
685
     *
686
     * @return $this
687
     */
688
    public function monthly()
689
    {
690
        return $this->spliceIntoPosition(1, 0)
691
            ->spliceIntoPosition(2, 0)
692
            ->spliceIntoPosition(3, 1);
693
    }
694
695
    /**
696
     * Schedule the event to run monthly on a given day and time.
697
     *
698
     * @param int  $day
699
     * @param string  $time
700
     * @return $this
701
     */
702
    public function monthlyOn($day = 1, $time = '0:0')
703
    {
704
        $this->dailyAt($time);
705
        return $this->spliceIntoPosition(3, $day);
706
    }
707
708
    /**
709
     * Schedule the event to run quarterly.
710
     *
711
     * @return $this
712
     */
713
    public function quarterly()
714
    {
715
        return $this->spliceIntoPosition(1, 0)
716
            ->spliceIntoPosition(2, 0)
717
            ->spliceIntoPosition(3, 1)
718
            ->spliceIntoPosition(4, '*/3');
719
    }
720
721
    /**
722
     * Schedule the event to run yearly.
723
     *
724
     * @return $this
725
     */
726
    public function yearly()
727
    {
728
        return $this->spliceIntoPosition(1, 0)
729
            ->spliceIntoPosition(2, 0)
730
            ->spliceIntoPosition(3, 1)
731
            ->spliceIntoPosition(4, 1);
732
    }
733
734
    /**
735
     * Splice the given value into the given position of the expression.
736
     *
737
     * @param  int  $position
738
     * @param  string  $value
739
     * @return $this
740
     */
741
    protected function spliceIntoPosition($position, $value)
742
    {
743
        $segments = explode(' ', $this->expression);
0 ignored issues
show
Coding Style introduced by
Equals sign not aligned with surrounding assignments; expected 16 spaces but found 1 space

This check looks for multiple assignments in successive lines of code. It will report an issue if the operators are not in a straight line.

To visualize

$a = "a";
$ab = "ab";
$abc = "abc";

will produce issues in the first and second line, while this second example

$a   = "a";
$ab  = "ab";
$abc = "abc";

will produce no issues.

Loading history...
744
        $segments[$position - 1] = $value;
745
        return $this->cron(implode(' ', $segments));
746
    }
747
748
    /**
749
     * Register a callback to further filter the schedule.
750
     *
751
     * @param  \Closure  $callback
752
     * @return $this
753
     */
754
    public function when(Closure $callback)
755
    {
756
        $this->filters[] = $callback;
757
758
        return $this;
759
    }
760
761
    /**
762
     * Register a callback to further filter the schedule.
763
     *
764
     * @param  \Closure  $callback
765
     * @return $this
766
     */
767
    public function skip(Closure $callback)
768
    {
769
        $this->rejects[] = $callback;
770
771
        return $this;
772
    }
773
774
    /**
775
     * Register a callback to be called before the operation.
776
     *
777
     * @param \Closure $callback
778
     * @return $this
779
     */
780
    public function before(Closure $callback)
781
    {
782
        $this->beforeCallbacks[] = $callback;
783
784
        return $this;
785
    }
786
787
    /**
788
     * Register a callback to be called after the operation.
789
     *
790
     * @param  \Closure  $callback
791
     * @return $this
792
     */
793
    public function after(Closure $callback)
794
    {
795
        $this->afterCallbacks[] = $callback;
796
797
        return $this;
798
    }
799
}
800