diff --git a/README.md b/README.md index d56c95d..a64a97c 100644 --- a/README.md +++ b/README.md @@ -70,6 +70,23 @@ Scheduled jobs module for Infuse Framework * * * * * php /var/www/example.com/infuse cron:run ``` +### Events + +You can subscribe to events with [event subscribers](https://symfony.com/doc/current/components/event_dispatcher.html#using-event-subscribers) from the symfony/event-dispatcher component. Your subscribers an listen to these events: + +- `schedule_run.begin` +- `schedule_run.finished` +- `cron_job.begin` +- `cron_job.finished` + +When you have created an [event subscriber](https://symfony.com/doc/current/components/event_dispatcher.html#using-event-subscribers) you can add it to your config like this: + +```php +'cronSubscribers' => [ + 'App\EventSubscribers\MySubscriber' +] +``` + ### Webhooks You can optionally specify a URL that will be called upon a successful run. The output from the run will be available using the `m` query parameter. This was designed to be compatible with [Dead Man's Snitch](https://deadmanssnitch.com/). \ No newline at end of file diff --git a/composer.json b/composer.json index 34b884d..08d8f53 100644 --- a/composer.json +++ b/composer.json @@ -23,7 +23,8 @@ }, "require": { "php": ">=5.6.0", - "symfony/lock": "~3.0|~4.0" + "symfony/lock": "~3.0|~4.0", + "symfony/event-dispatcher": "~3.0|~4.0" }, "require-dev": { "infuse/infuse": "~1.6", diff --git a/src/Console/RunScheduledCommand.php b/src/Console/RunScheduledCommand.php index cf281fa..f035f87 100644 --- a/src/Console/RunScheduledCommand.php +++ b/src/Console/RunScheduledCommand.php @@ -42,8 +42,25 @@ private function getSchedule() $lockFactory = $this->app['lock_factory']; $namespace = $this->app['config']->get('app.hostname'); $schedule = new JobSchedule($jobs, $lockFactory, $namespace); + $this->addSubscribers($schedule); $schedule->setLogger($this->app['logger']); return $schedule; } + + /** + * @param JobSchedule $schedule + */ + private function addSubscribers(JobSchedule $schedule) + { + $subscribers = $this->app['config']->get('cronSubscribers', []); + foreach ($subscribers as $class) { + $subscriber = new $class(); + if (method_exists($subscriber, 'setApp')) { + $subscriber->setApp($this->app); + } + + $schedule->subscribe($subscriber); + } + } } diff --git a/src/Events/CronJobBeginEvent.php b/src/Events/CronJobBeginEvent.php new file mode 100644 index 0000000..2dd9836 --- /dev/null +++ b/src/Events/CronJobBeginEvent.php @@ -0,0 +1,31 @@ +jobId = $jobId; + } + + /** + * @return string + */ + public function getJobId() + { + return $this->jobId; + } +} diff --git a/src/Events/CronJobFinishedEvent.php b/src/Events/CronJobFinishedEvent.php new file mode 100644 index 0000000..dd18baa --- /dev/null +++ b/src/Events/CronJobFinishedEvent.php @@ -0,0 +1,46 @@ +jobId = $jobId; + $this->result = $result; + } + + /** + * @return string + */ + public function getJobId() + { + return $this->jobId; + } + + /** + * @return string + */ + public function getResult() + { + return $this->result; + } +} diff --git a/src/Events/ScheduleRunBeginEvent.php b/src/Events/ScheduleRunBeginEvent.php new file mode 100644 index 0000000..1722a89 --- /dev/null +++ b/src/Events/ScheduleRunBeginEvent.php @@ -0,0 +1,10 @@ +jobs = $jobs; $this->lockFactory = $lockFactory; $this->namespace = $namespace; + $this->dispatcher = new EventDispatcher(); } /** @@ -57,6 +67,46 @@ public function getAllJobs() return $this->jobs; } + /** + * Gets the event dispatcher. + * + * @return EventDispatcher + */ + public function getEventDispatcher() + { + return $this->dispatcher; + } + + /** + * Registers a listener for an event. + * + * @param string $eventName + * @param callable $listener + * @param int $priority + * + * @return $this + */ + public function listen($eventName, callable $listener, $priority = 0) + { + $this->dispatcher->addListener($eventName, $listener, $priority); + + return $this; + } + + /** + * Registers an event subscriber. + * + * @param EventSubscriberInterface $subscriber + * + * @return $this + */ + public function subscribe(EventSubscriberInterface $subscriber) + { + $this->dispatcher->addSubscriber($subscriber); + + return $this; + } + /** * Gets all of the jobs scheduled to run, now. * @@ -106,6 +156,9 @@ public function runScheduled(OutputInterface $output) { $success = true; + $event = new ScheduleRunBeginEvent(); + $this->dispatcher->dispatch($event::NAME, $event); + foreach ($this->getScheduledJobs() as $jobInfo) { $job = $jobInfo['model']; $run = $this->runJob($job, $jobInfo, $output); @@ -113,6 +166,9 @@ public function runScheduled(OutputInterface $output) $success = $run->succeeded() && $success; } + $event = new ScheduleRunFinishedEvent(); + $this->dispatcher->dispatch($event::NAME, $event); + return $success; } @@ -131,7 +187,7 @@ private function runJob(CronJob $job, array $jobInfo, OutputInterface $output) // set up the runner $class = array_value($jobInfo, 'class'); - $runner = new Runner($job, $class, $this->lockFactory, $this->namespace); + $runner = new Runner($job, $class, $this->dispatcher, $this->lockFactory, $this->namespace); if ($this->logger) { $runner->setLogger($this->logger); } diff --git a/src/Libs/Run.php b/src/Libs/Run.php index 126d469..df23a89 100644 --- a/src/Libs/Run.php +++ b/src/Libs/Run.php @@ -25,7 +25,7 @@ class Run private $output = []; /** - * @var int + * @var string */ private $result; @@ -69,7 +69,7 @@ public function getOutput() /** * Sets the result of the run. * - * @param int $result + * @param string $result * * @return self */ @@ -83,7 +83,7 @@ public function setResult($result) /** * Gets the result of the run. * - * @return int + * @return string */ public function getResult() { diff --git a/src/Libs/Runner.php b/src/Libs/Runner.php index 3c0cba0..f759502 100644 --- a/src/Libs/Runner.php +++ b/src/Libs/Runner.php @@ -3,8 +3,11 @@ namespace Infuse\Cron\Libs; use Exception; +use Infuse\Cron\Events\CronJobBeginEvent; +use Infuse\Cron\Events\CronJobFinishedEvent; use Infuse\Cron\Models\CronJob; use Psr\Log\LoggerAwareTrait; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Lock\Factory; class Runner @@ -16,6 +19,11 @@ class Runner */ private $jobModel; + /** + * @var EventDispatcher + */ + private $dispatcher; + /** * @var Lock */ @@ -27,15 +35,17 @@ class Runner private $class; /** - * @param CronJob $job - * @param string $class callable job class - * @param Factory $lockFactory - * @param string $namespace + * @param CronJob $job + * @param string $class callable job class + * @param EventDispatcher $dispatcher + * @param Factory $lockFactory + * @param string $namespace */ - public function __construct(CronJob $job, $class, Factory $lockFactory, $namespace = '') + public function __construct(CronJob $job, $class, EventDispatcher $dispatcher, Factory $lockFactory, $namespace = '') { $this->jobModel = $job; $this->class = $class; + $this->dispatcher = $dispatcher; $this->lock = new Lock($this->jobModel->id, $lockFactory, $namespace); } @@ -79,15 +89,27 @@ public function go($expires = 0, $successUrl = false, Run $run = null) return $run->setResult(Run::RESULT_LOCKED); } + // call the `cron_job.begin` event + $event = new CronJobBeginEvent($this->jobModel->id); + $this->dispatcher->dispatch($event::NAME, $event); + if ($event->isPropagationStopped()) { + $run->writeOutput('Rejected by cron_job.begin event listener') + ->setResult(Run::RESULT_FAILED); + } + // set up the callable $job = $this->setUp($this->class, $run); // this is where the job actually gets called - if ($job) { + if ($job && !$event->isPropagationStopped()) { $this->invoke($job, $run); } // perform post-run tasks: + // call the `cron_job.finished` event + $event = new CronJobFinishedEvent($this->jobModel->id, $run->getResult()); + $this->dispatcher->dispatch($event::NAME, $event); + // persist the result $this->saveRun($run); @@ -181,7 +203,9 @@ private function saveRun(Run $run) } /** - * Pings a URL about a successful run. + * @deprecated should be moved to an event listener + * + * Pings a URL about a successful run * * @param string $url * @param Run $run diff --git a/src/migrations/20140823163217_cron_job.php b/src/migrations/20140823163217_cron_job.php index 2db7fb3..2b0a8eb 100644 --- a/src/migrations/20140823163217_cron_job.php +++ b/src/migrations/20140823163217_cron_job.php @@ -3,7 +3,7 @@ /** * @author Jared King * - * @link http://jaredtking.com + * @see http://jaredtking.com * * @copyright 2015 Jared King * @license MIT diff --git a/src/migrations/20141019092521_cron_job_remove_locked.php b/src/migrations/20141019092521_cron_job_remove_locked.php index 4482afa..58b5a58 100644 --- a/src/migrations/20141019092521_cron_job_remove_locked.php +++ b/src/migrations/20141019092521_cron_job_remove_locked.php @@ -3,7 +3,7 @@ /** * @author Jared King * - * @link http://jaredtking.com + * @see http://jaredtking.com * * @copyright 2015 Jared King * @license MIT diff --git a/src/migrations/20141019092840_cron_job_null_fields.php b/src/migrations/20141019092840_cron_job_null_fields.php index d7ec692..a7656bd 100644 --- a/src/migrations/20141019092840_cron_job_null_fields.php +++ b/src/migrations/20141019092840_cron_job_null_fields.php @@ -3,7 +3,7 @@ /** * @author Jared King * - * @link http://jaredtking.com + * @see http://jaredtking.com * * @copyright 2015 Jared King * @license MIT diff --git a/tests/JobScheduleTest.php b/tests/JobScheduleTest.php index f4af503..df748be 100644 --- a/tests/JobScheduleTest.php +++ b/tests/JobScheduleTest.php @@ -11,6 +11,8 @@ namespace Infuse\Cron\Tests; +use Infuse\Cron\Events\ScheduleRunBeginEvent; +use Infuse\Cron\Events\ScheduleRunFinishedEvent; use Infuse\Cron\Libs\JobSchedule; use Infuse\Cron\Models\CronJob; use Infuse\Cron\Tests\Jobs\FailJob; @@ -46,6 +48,8 @@ class JobScheduleTest extends MockeryTestCase ], ]; public static $lockFactory; + public static $beginEvent; + public static $finishedEvent; public static function setUpBeforeClass() { @@ -61,16 +65,19 @@ public static function setUpBeforeClass() $lock->acquire(); } + private function getSchedule() + { + return new JobSchedule(self::$jobs, self::$lockFactory); + } + public function testGetAllJobs() { - $schedule = new JobSchedule(self::$jobs, self::$lockFactory); - $this->assertEquals(self::$jobs, $schedule->getAllJobs()); + $this->assertEquals(self::$jobs, $this->getSchedule()->getAllJobs()); } public function testGetScheduledJobs() { - $schedule = new JobSchedule(self::$jobs, self::$lockFactory); - $jobs = $schedule->getScheduledJobs(); + $jobs = $this->getSchedule()->getScheduledJobs(); $this->assertCount(4, $jobs); @@ -93,7 +100,16 @@ public function testRunScheduled() $output->shouldReceive('writeln') ->atLeast(1); - $schedule = new JobSchedule(self::$jobs, self::$lockFactory); + $schedule = $this->getSchedule(); + $schedule->listen(ScheduleRunBeginEvent::NAME, function (ScheduleRunBeginEvent $event) { + JobScheduleTest::$beginEvent = $event; + }); + $schedule->listen(ScheduleRunFinishedEvent::NAME, function (ScheduleRunFinishedEvent $event) { + JobScheduleTest::$finishedEvent = $event; + }); + + $subscriber = new TestEventSubscriber(); + $schedule->subscribe($subscriber); $this->assertFalse($schedule->runScheduled($output)); @@ -104,5 +120,8 @@ public function testRunScheduled() $this->assertEquals('test.success', $jobs[0]['model']->id); $this->assertEquals('test.locked', $jobs[1]['model']->id); $this->assertEquals('test.failed', $jobs[2]['model']->id); + + $this->assertInstanceOf(ScheduleRunBeginEvent::class, self::$beginEvent); + $this->assertInstanceOf(ScheduleRunFinishedEvent::class, self::$finishedEvent); } } diff --git a/tests/RunnerTest.php b/tests/RunnerTest.php index a7ff7c8..04dccec 100644 --- a/tests/RunnerTest.php +++ b/tests/RunnerTest.php @@ -11,6 +11,8 @@ namespace Infuse\Cron\Tests; +use Infuse\Cron\Events\CronJobBeginEvent; +use Infuse\Cron\Events\CronJobFinishedEvent; use Infuse\Cron\Libs\FileGetContentsMock; use Infuse\Cron\Libs\Run; use Infuse\Cron\Libs\Runner; @@ -23,12 +25,17 @@ use Infuse\Test; use Mockery; use Mockery\Adapter\Phpunit\MockeryTestCase; +use Stripe\Event; +use Symfony\Component\EventDispatcher\EventDispatcher; use Symfony\Component\Lock\Factory; use Symfony\Component\Lock\Store\FlockStore; class RunnerTest extends MockeryTestCase { + public static $dispatcher; public static $lockFactory; + public static $beginEvent; + public static $finishedEvent; public static function setUpBeforeClass() { @@ -39,6 +46,14 @@ public static function setUpBeforeClass() ->where('id', 'test%', 'like') ->execute(); + self::$dispatcher = new EventDispatcher(); + self::$dispatcher->addListener(CronJobBeginEvent::NAME, function (CronJobBeginEvent $event) { + self::$beginEvent = $event; + }); + self::$dispatcher->addListener(CronJobFinishedEvent::NAME, function (CronJobFinishedEvent $event) { + self::$finishedEvent = $event; + }); + $store = new FlockStore(sys_get_temp_dir()); self::$lockFactory = new Factory($store); } @@ -51,14 +66,14 @@ public function setUp() public function testGetJobModel() { $job = new CronJob(); - $runner = new Runner($job, TestJob::class, self::$lockFactory); + $runner = new Runner($job, TestJob::class, self::$dispatcher, self::$lockFactory); $this->assertEquals($job, $runner->getJobModel()); } public function testGetJobClass() { $job = new CronJob(); - $runner = new Runner($job, TestJob::class, self::$lockFactory); + $runner = new Runner($job, TestJob::class, self::$dispatcher, self::$lockFactory); $this->assertEquals(TestJob::class, $runner->getJobClass()); } @@ -70,7 +85,7 @@ public function testGoLocked() $job = new CronJob(); $job->id = 'test.locked'; $job->setApp(Test::$app); - $runner = new Runner($job, TestJob::class, self::$lockFactory); + $runner = new Runner($job, TestJob::class, self::$dispatcher, self::$lockFactory); $run = $runner->go(100); $this->assertInstanceOf(Run::class, $run); @@ -83,7 +98,7 @@ public function testGoClassMissing() { $job = new CronJob(); $job->id = 'test.class_missing'; - $runner = new Runner($job, '', self::$lockFactory); + $runner = new Runner($job, '', self::$dispatcher, self::$lockFactory); $run = $runner->go(); $this->assertInstanceOf(Run::class, $run); @@ -99,7 +114,7 @@ public function testGoClassDoesNotExist() { $job = new CronJob(); $job->id = 'test.does_not_exist'; - $runner = new Runner($job, 'DoesNotExist\MyJob', self::$lockFactory); + $runner = new Runner($job, 'DoesNotExist\MyJob', self::$dispatcher, self::$lockFactory); $run = $runner->go(); $this->assertInstanceOf(Run::class, $run); @@ -115,7 +130,7 @@ public function testGoException() { $job = new CronJob(); $job->id = 'test.exception'; - $runner = new Runner($job, ExceptionJob::class, self::$lockFactory); + $runner = new Runner($job, ExceptionJob::class, self::$dispatcher, self::$lockFactory); $run = $runner->go(); $this->assertInstanceOf(Run::class, $run); @@ -131,7 +146,27 @@ public function testGoFailed() { $job = new CronJob(); $job->id = 'test.fail'; - $runner = new Runner($job, FailJob::class, self::$lockFactory); + $runner = new Runner($job, FailJob::class, self::$dispatcher, self::$lockFactory); + + $run = $runner->go(); + $this->assertInstanceOf(Run::class, $run); + $this->assertEquals(Run::RESULT_FAILED, $run->getResult()); + + $this->assertTrue($job->persisted()); + $this->assertGreaterThan(0, $job->last_ran); + $this->assertFalse($job->last_run_succeeded); + } + + public function testGoRejectedBeginEvent() + { + $dispatcher = new EventDispatcher(); + $dispatcher->addListener(CronJobBeginEvent::NAME, function (CronJobBeginEvent $event) { + $event->stopPropagation(); + }); + + $job = new CronJob(); + $job->id = 'test.reject'; + $runner = new Runner($job, SuccessJob::class, $dispatcher, self::$lockFactory); $run = $runner->go(); $this->assertInstanceOf(Run::class, $run); @@ -140,13 +175,14 @@ public function testGoFailed() $this->assertTrue($job->persisted()); $this->assertGreaterThan(0, $job->last_ran); $this->assertFalse($job->last_run_succeeded); + $this->assertEquals('Rejected by cron_job.begin event listener', $job->last_run_output); } public function testGoSuccess() { $job = new CronJob(); $job->id = 'test.success'; - $runner = new Runner($job, SuccessJob::class, self::$lockFactory); + $runner = new Runner($job, SuccessJob::class, self::$dispatcher, self::$lockFactory); $run = $runner->go(); $this->assertInstanceOf(Run::class, $run); @@ -162,7 +198,7 @@ public function testGoSuccessNoReturnValue() { $job = new CronJob(); $job->id = 'test.invoke'; - $runner = new Runner($job, TestJob::class, self::$lockFactory); + $runner = new Runner($job, TestJob::class, self::$dispatcher, self::$lockFactory); $run = $runner->go(); $this->assertInstanceOf(Run::class, $run); @@ -178,7 +214,7 @@ public function testGoSuccessWithUrl() { $job = new CronJob(); $job->id = 'test.success_with_url'; - $runner = new Runner($job, SuccessWithUrlJob::class, self::$lockFactory); + $runner = new Runner($job, SuccessWithUrlJob::class, self::$dispatcher, self::$lockFactory); FileGetContentsMock::$functions->shouldReceive('file_get_contents') ->with('http://webhook.example.com/?m=yay') diff --git a/tests/TestEventSubscriber.php b/tests/TestEventSubscriber.php new file mode 100644 index 0000000..d9d43bc --- /dev/null +++ b/tests/TestEventSubscriber.php @@ -0,0 +1,30 @@ + 'runBegin', + ScheduleRunFinishedEvent::NAME => 'runFinished', + ]; + } + + public function runBegin(ScheduleRunBeginEvent $event) + { + self::$lastEvent = $event; + } + + public function runFinished(ScheduleRunFinishedEvent $event) + { + self::$lastEvent = $event; + } +}