schedule monitor

This commit is contained in:
2021-03-30 11:31:10 +00:00
parent 40ed5dd5a5
commit 39b0a8c372
66 changed files with 3443 additions and 36 deletions

View File

@@ -42,6 +42,8 @@ namespace Composer\Autoload;
*/
class ClassLoader
{
private $vendorDir;
// PSR-4
private $prefixLengthsPsr4 = array();
private $prefixDirsPsr4 = array();
@@ -57,6 +59,13 @@ class ClassLoader
private $missingClasses = array();
private $apcuPrefix;
private static $registeredLoaders = array();
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
}
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
@@ -300,6 +309,17 @@ class ClassLoader
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
@@ -308,6 +328,10 @@ class ClassLoader
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
@@ -367,6 +391,16 @@ class ClassLoader
return $file;
}
/**
* Returns the currently registered loaders indexed by their corresponding vendor directories.
*
* @return self[]
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup

View File

@@ -12,6 +12,7 @@
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
@@ -29,7 +30,7 @@ private static $installed = array (
'aliases' =>
array (
),
'reference' => '4b72a59e62d368371546246a68aef55ee21c2705',
'reference' => '40ed5dd5a576916f6561953482f68722d1ecce8f',
'name' => 'laravel/laravel',
),
'versions' =>
@@ -463,7 +464,7 @@ private static $installed = array (
'aliases' =>
array (
),
'reference' => '4b72a59e62d368371546246a68aef55ee21c2705',
'reference' => '40ed5dd5a576916f6561953482f68722d1ecce8f',
),
'laravel/socialite' =>
array (
@@ -528,6 +529,15 @@ private static $installed = array (
),
'reference' => '159c3d2bf27568f9af87d6c3f4bb616a251eb12b',
),
'lorisleiva/cron-translator' =>
array (
'pretty_version' => 'v0.1.1',
'version' => '0.1.1.0',
'aliases' =>
array (
),
'reference' => '784a6f6255a4b5f45da5d89dc6ec631a14d7b011',
),
'mockery/mockery' =>
array (
'pretty_version' => '1.4.2',
@@ -946,6 +956,15 @@ private static $installed = array (
),
'reference' => '7b72592e0d823e2948c413f5e661de0fd3431db5',
),
'spatie/laravel-schedule-monitor' =>
array (
'pretty_version' => '2.0.1',
'version' => '2.0.1.0',
'aliases' =>
array (
),
'reference' => '4ffdaea81b5c27a119b6cbac8f4894657b8fd6d9',
),
'spomky-labs/base64url' =>
array (
'pretty_version' => 'v2.0.4',
@@ -1356,6 +1375,8 @@ private static $installed = array (
),
),
);
private static $canGetVendors;
private static $installedByVendor = array();
@@ -1365,7 +1386,17 @@ private static $installed = array (
public static function getInstalledPackages()
{
return array_keys(self::$installed['versions']);
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
@@ -1378,7 +1409,13 @@ return array_keys(self::$installed['versions']);
public static function isInstalled($packageName)
{
return isset(self::$installed['versions'][$packageName]);
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return true;
}
}
return false;
}
@@ -1413,42 +1450,50 @@ return $provided->matches($constraint);
public static function getVersionRanges($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset(self::$installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = self::$installed['versions'][$packageName]['pretty_version'];
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', self::$installed['versions'][$packageName])) {
$ranges = array_merge($ranges, self::$installed['versions'][$packageName]['aliases']);
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', self::$installed['versions'][$packageName])) {
$ranges = array_merge($ranges, self::$installed['versions'][$packageName]['replaced']);
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', self::$installed['versions'][$packageName])) {
$ranges = array_merge($ranges, self::$installed['versions'][$packageName]['provided']);
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
public static function getVersion($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset(self::$installed['versions'][$packageName]['version'])) {
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return self::$installed['versions'][$packageName]['version'];
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
@@ -1457,15 +1502,19 @@ return self::$installed['versions'][$packageName]['version'];
public static function getPrettyVersion($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset(self::$installed['versions'][$packageName]['pretty_version'])) {
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return self::$installed['versions'][$packageName]['pretty_version'];
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
@@ -1474,15 +1523,19 @@ return self::$installed['versions'][$packageName]['pretty_version'];
public static function getReference($packageName)
{
if (!isset(self::$installed['versions'][$packageName])) {
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset(self::$installed['versions'][$packageName]['reference'])) {
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return self::$installed['versions'][$packageName]['reference'];
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
@@ -1491,7 +1544,9 @@ return self::$installed['versions'][$packageName]['reference'];
public static function getRootPackage()
{
return self::$installed['root'];
$installed = self::getInstalled();
return $installed[0]['root'];
}
@@ -1526,5 +1581,32 @@ return self::$installed;
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
}
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
if (self::$canGetVendors) {
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
$installed[] = self::$installedByVendor[$vendorDir] = require $vendorDir.'/composer/installed.php';
}
}
}
$installed[] = self::$installed;
return $installed;
}
}

View File

@@ -16,7 +16,9 @@ return array(
'App\\Console\\Commands\\Files\\MoonFormatter' => $baseDir . '/app/Console/Commands/Files/MoonFormatter.php',
'App\\Console\\Commands\\Files\\UpdateItemCompositionFromSDECommand' => $baseDir . '/app/Console/Commands/Files/UpdateItemCompositionFromSDECommand.php',
'App\\Console\\Commands\\Finances\\UpdateAllianceWalletJournal' => $baseDir . '/app/Console/Commands/Finances/UpdateAllianceWalletJournal.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesDataCleanup' => $baseDir . '/app/Console/Commands/MiningTaxes/MiningTaxesDataCleanup.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesInvoices' => $baseDir . '/app/Console/Commands/MiningTaxes/MiningTaxesInvoices.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesInvoicesNew' => $baseDir . '/app/Console/Commands/MiningTaxes/MiningTaxesInvoicesNew.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesLedgers' => $baseDir . '/app/Console/Commands/MiningTaxes/MiningTaxesLedgers.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesObservers' => $baseDir . '/app/Console/Commands/MiningTaxes/MiningTaxesObservers.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesPayments' => $baseDir . '/app/Console/Commands/MiningTaxes/MiningTaxesPayments.php',
@@ -31,7 +33,6 @@ return array(
'App\\Http\\Controllers\\Blacklist\\BlacklistController' => $baseDir . '/app/Http/Controllers/Blacklist/BlacklistController.php',
'App\\Http\\Controllers\\Contracts\\SupplyChainController' => $baseDir . '/app/Http/Controllers/Contracts/SupplyChainController.php',
'App\\Http\\Controllers\\Controller' => $baseDir . '/app/Http/Controllers/Controller.php',
'App\\Http\\Controllers\\Dashboard\\AdminController' => $baseDir . '/app/Http/Controllers/Dashboard/AdminController.php',
'App\\Http\\Controllers\\Dashboard\\AdminDashboardController' => $baseDir . '/app/Http/Controllers/Dashboard/AdminDashboardController.php',
'App\\Http\\Controllers\\Dashboard\\DashboardController' => $baseDir . '/app/Http/Controllers/Dashboard/DashboardController.php',
'App\\Http\\Controllers\\Logistics\\FuelController' => $baseDir . '/app/Http/Controllers/Logistics/FuelController.php',
@@ -50,10 +51,16 @@ return array(
'App\\Http\\Middleware\\TrimStrings' => $baseDir . '/app/Http/Middleware/TrimStrings.php',
'App\\Http\\Middleware\\TrustProxies' => $baseDir . '/app/Http/Middleware/TrustProxies.php',
'App\\Http\\Middleware\\VerifyCsrfToken' => $baseDir . '/app/Http/Middleware/VerifyCsrfToken.php',
'App\\Jobs\\Commands\\Data\\PurgeUsersJob' => $baseDir . '/app/Jobs/Commands/Data/PurgeUsersJob.php',
'App\\Jobs\\Commands\\Eve\\ItemPricesUpdateJob' => $baseDir . '/app/Jobs/Commands/Eve/ItemPricesUpdateJob.php',
'App\\Jobs\\Commands\\Eve\\ProcessSendEveMailJob' => $baseDir . '/app/Jobs/Commands/Eve/ProcessSendEveMailJob.php',
'App\\Jobs\\Commands\\Eve\\ProcessSendEveMailJobRL' => $baseDir . '/app/Jobs/Commands/Eve/ProcessSendEveMailJobRL.php',
'App\\Jobs\\Commands\\Finances\\UpdateItemPricesJob' => $baseDir . '/app/Jobs/Commands/Finances/UpdateItemPricesJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\FetchMiningTaxesLedgersJob' => $baseDir . '/app/Jobs/Commands/MiningTaxes/FetchMiningTaxesLedgersJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\FetchMiningTaxesObserversJob' => $baseDir . '/app/Jobs/Commands/MiningTaxes/FetchMiningTaxesObserversJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\ProcessMiningTaxesLedgersJob' => $baseDir . '/app/Jobs/Commands/MiningTaxes/ProcessMiningTaxesLedgersJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\ProcessMiningTaxesPaymentsJob' => $baseDir . '/app/Jobs/Commands/MiningTaxes/ProcessMiningTaxesPaymentsJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\SendMiningTaxesInvoicesJob' => $baseDir . '/app/Jobs/Commands/MiningTaxes/SendMiningTaxesInvoicesJob.php',
'App\\Jobs\\Commands\\SupplyChain\\EndSupplyChainContractJob' => $baseDir . '/app/Jobs/Commands/SupplyChain/EndSupplyChainContractJob.php',
'App\\Library\\Esi\\Esi' => $baseDir . '/app/Library/Esi/Esi.php',
'App\\Library\\Helpers\\AssetHelper' => $baseDir . '/app/Library/Helpers/AssetHelper.php',
@@ -65,6 +72,8 @@ return array(
'App\\Library\\Helpers\\TaxesHelper' => $baseDir . '/app/Library/Helpers/TaxesHelper.php',
'App\\Library\\Moons\\MoonCalc' => $baseDir . '/app/Library/Moons/MoonCalc.php',
'App\\Models\\Admin\\AllowedLogin' => $baseDir . '/app/Models/Admin/AllowedLogin.php',
'App\\Models\\AfterActionReports\\AfterActionReport' => $baseDir . '/app/Models/AfterActionReports/AfterActionReport.php',
'App\\Models\\AfterActionReports\\AfterActionReportComment' => $baseDir . '/app/Models/AfterActionReports/AfterActionReportComment.php',
'App\\Models\\Blacklist\\BlacklistEntity' => $baseDir . '/app/Models/Blacklist/BlacklistEntity.php',
'App\\Models\\Contracts\\SupplyChainBid' => $baseDir . '/app/Models/Contracts/SupplyChainBid.php',
'App\\Models\\Contracts\\SupplyChainContract' => $baseDir . '/app/Models/Contracts/SupplyChainContract.php',
@@ -2698,6 +2707,15 @@ return array(
'League\\OAuth1\\Client\\Signature\\RsaSha1Signature' => $vendorDir . '/league/oauth1-client/src/Signature/RsaSha1Signature.php',
'League\\OAuth1\\Client\\Signature\\Signature' => $vendorDir . '/league/oauth1-client/src/Signature/Signature.php',
'League\\OAuth1\\Client\\Signature\\SignatureInterface' => $vendorDir . '/league/oauth1-client/src/Signature/SignatureInterface.php',
'Lorisleiva\\CronTranslator\\CronParsingException' => $vendorDir . '/lorisleiva/cron-translator/src/CronParsingException.php',
'Lorisleiva\\CronTranslator\\CronTranslator' => $vendorDir . '/lorisleiva/cron-translator/src/CronTranslator.php',
'Lorisleiva\\CronTranslator\\CronType' => $vendorDir . '/lorisleiva/cron-translator/src/CronType.php',
'Lorisleiva\\CronTranslator\\DaysOfMonthField' => $vendorDir . '/lorisleiva/cron-translator/src/DaysOfMonthField.php',
'Lorisleiva\\CronTranslator\\DaysOfWeekField' => $vendorDir . '/lorisleiva/cron-translator/src/DaysOfWeekField.php',
'Lorisleiva\\CronTranslator\\Field' => $vendorDir . '/lorisleiva/cron-translator/src/Field.php',
'Lorisleiva\\CronTranslator\\HoursField' => $vendorDir . '/lorisleiva/cron-translator/src/HoursField.php',
'Lorisleiva\\CronTranslator\\MinutesField' => $vendorDir . '/lorisleiva/cron-translator/src/MinutesField.php',
'Lorisleiva\\CronTranslator\\MonthsField' => $vendorDir . '/lorisleiva/cron-translator/src/MonthsField.php',
'Mockery' => $vendorDir . '/mockery/mockery/library/Mockery.php',
'Mockery\\Adapter\\Phpunit\\MockeryPHPUnitIntegration' => $vendorDir . '/mockery/mockery/library/Mockery/Adapter/Phpunit/MockeryPHPUnitIntegration.php',
'Mockery\\Adapter\\Phpunit\\MockeryPHPUnitIntegrationAssertPostConditions' => $vendorDir . '/mockery/mockery/library/Mockery/Adapter/Phpunit/MockeryPHPUnitIntegrationAssertPostConditions.php',
@@ -4093,6 +4111,32 @@ return array(
'SocialiteProviders\\Manager\\SocialiteWasCalled' => $vendorDir . '/socialiteproviders/manager/src/SocialiteWasCalled.php',
'SolarSystemSeeder' => $baseDir . '/database/seeds/SolarSystemSeeder.php',
'Spatie\\RateLimitedMiddleware\\RateLimited' => $vendorDir . '/spatie/laravel-rate-limited-job-middleware/src/RateLimited.php',
'Spatie\\ScheduleMonitor\\Commands\\CleanLogCommand' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/CleanLogCommand.php',
'Spatie\\ScheduleMonitor\\Commands\\ListCommand' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/ListCommand.php',
'Spatie\\ScheduleMonitor\\Commands\\SyncCommand' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/SyncCommand.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\DuplicateTasksTable' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/Tables/DuplicateTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\MonitoredTasksTable' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/Tables/MonitoredTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\ReadyForMonitoringTasksTable' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/Tables/ReadyForMonitoringTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\ScheduledTasksTable' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/Tables/ScheduledTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\UnnamedTasksTable' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/Tables/UnnamedTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\VerifyCommand' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Commands/VerifyCommand.php',
'Spatie\\ScheduleMonitor\\EventHandlers\\BackgroundCommandListener' => $vendorDir . '/spatie/laravel-schedule-monitor/src/EventHandlers/BackgroundCommandListener.php',
'Spatie\\ScheduleMonitor\\EventHandlers\\ScheduledTaskEventSubscriber' => $vendorDir . '/spatie/laravel-schedule-monitor/src/EventHandlers/ScheduledTaskEventSubscriber.php',
'Spatie\\ScheduleMonitor\\Jobs\\PingOhDearJob' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Jobs/PingOhDearJob.php',
'Spatie\\ScheduleMonitor\\Models\\MonitoredScheduledTask' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Models/MonitoredScheduledTask.php',
'Spatie\\ScheduleMonitor\\Models\\MonitoredScheduledTaskLogItem' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Models/MonitoredScheduledTaskLogItem.php',
'Spatie\\ScheduleMonitor\\ScheduleMonitorServiceProvider' => $vendorDir . '/spatie/laravel-schedule-monitor/src/ScheduleMonitorServiceProvider.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\OhDearPayloadFactory' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/OhDearPayloadFactory.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\Payloads\\FailedPayload' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/Payloads/FailedPayload.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\Payloads\\FinishedPayload' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/Payloads/FinishedPayload.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\Payloads\\Payload' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/Payloads/Payload.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\ScheduledTaskFactory' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/ScheduledTaskFactory.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\ScheduledTasks' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/ScheduledTasks.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\ClosureTask' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/ClosureTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\CommandTask' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/CommandTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\JobTask' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/JobTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\ShellTask' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/ShellTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\Task' => $vendorDir . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/Task.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'Symfony\\Component\\Console\\Application' => $vendorDir . '/symfony/console/Application.php',
'Symfony\\Component\\Console\\Color' => $vendorDir . '/symfony/console/Color.php',

View File

@@ -38,6 +38,7 @@ return array(
'Symfony\\Component\\ErrorHandler\\' => array($vendorDir . '/symfony/error-handler'),
'Symfony\\Component\\CssSelector\\' => array($vendorDir . '/symfony/css-selector'),
'Symfony\\Component\\Console\\' => array($vendorDir . '/symfony/console'),
'Spatie\\ScheduleMonitor\\' => array($vendorDir . '/spatie/laravel-schedule-monitor/src'),
'Spatie\\RateLimitedMiddleware\\' => array($vendorDir . '/spatie/laravel-rate-limited-job-middleware/src'),
'SocialiteProviders\\Manager\\' => array($vendorDir . '/socialiteproviders/manager/src'),
'Seat\\Eseye\\' => array($vendorDir . '/eveseat/eseye/src'),
@@ -54,6 +55,7 @@ return array(
'Opis\\Closure\\' => array($vendorDir . '/opis/closure/src'),
'NunoMaduro\\Collision\\' => array($vendorDir . '/nunomaduro/collision/src'),
'Monolog\\' => array($vendorDir . '/monolog/monolog/src/Monolog'),
'Lorisleiva\\CronTranslator\\' => array($vendorDir . '/lorisleiva/cron-translator/src'),
'League\\OAuth1\\Client\\' => array($vendorDir . '/league/oauth1-client/src'),
'League\\MimeTypeDetection\\' => array($vendorDir . '/league/mime-type-detection/src'),
'League\\Flysystem\\' => array($vendorDir . '/league/flysystem/src'),

View File

@@ -25,7 +25,7 @@ class ComposerAutoloaderInitc3f953f8a7291d41a76e1664339777c9
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInitc3f953f8a7291d41a76e1664339777c9', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader();
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(\dirname(__FILE__)));
spl_autoload_unregister(array('ComposerAutoloaderInitc3f953f8a7291d41a76e1664339777c9', 'loadClassLoader'));
$useStaticLoader = PHP_VERSION_ID >= 50600 && !defined('HHVM_VERSION') && (!function_exists('zend_loader_file_encoded') || !zend_loader_file_encoded());

View File

@@ -84,6 +84,7 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'Symfony\\Component\\ErrorHandler\\' => 31,
'Symfony\\Component\\CssSelector\\' => 30,
'Symfony\\Component\\Console\\' => 26,
'Spatie\\ScheduleMonitor\\' => 23,
'Spatie\\RateLimitedMiddleware\\' => 29,
'SocialiteProviders\\Manager\\' => 27,
'Seat\\Eseye\\' => 11,
@@ -118,6 +119,7 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
),
'L' =>
array (
'Lorisleiva\\CronTranslator\\' => 26,
'League\\OAuth1\\Client\\' => 21,
'League\\MimeTypeDetection\\' => 25,
'League\\Flysystem\\' => 17,
@@ -319,6 +321,10 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
array (
0 => __DIR__ . '/..' . '/symfony/console',
),
'Spatie\\ScheduleMonitor\\' =>
array (
0 => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src',
),
'Spatie\\RateLimitedMiddleware\\' =>
array (
0 => __DIR__ . '/..' . '/spatie/laravel-rate-limited-job-middleware/src',
@@ -383,6 +389,10 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
array (
0 => __DIR__ . '/..' . '/monolog/monolog/src/Monolog',
),
'Lorisleiva\\CronTranslator\\' =>
array (
0 => __DIR__ . '/..' . '/lorisleiva/cron-translator/src',
),
'League\\OAuth1\\Client\\' =>
array (
0 => __DIR__ . '/..' . '/league/oauth1-client/src',
@@ -556,7 +566,9 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'App\\Console\\Commands\\Files\\MoonFormatter' => __DIR__ . '/../..' . '/app/Console/Commands/Files/MoonFormatter.php',
'App\\Console\\Commands\\Files\\UpdateItemCompositionFromSDECommand' => __DIR__ . '/../..' . '/app/Console/Commands/Files/UpdateItemCompositionFromSDECommand.php',
'App\\Console\\Commands\\Finances\\UpdateAllianceWalletJournal' => __DIR__ . '/../..' . '/app/Console/Commands/Finances/UpdateAllianceWalletJournal.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesDataCleanup' => __DIR__ . '/../..' . '/app/Console/Commands/MiningTaxes/MiningTaxesDataCleanup.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesInvoices' => __DIR__ . '/../..' . '/app/Console/Commands/MiningTaxes/MiningTaxesInvoices.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesInvoicesNew' => __DIR__ . '/../..' . '/app/Console/Commands/MiningTaxes/MiningTaxesInvoicesNew.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesLedgers' => __DIR__ . '/../..' . '/app/Console/Commands/MiningTaxes/MiningTaxesLedgers.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesObservers' => __DIR__ . '/../..' . '/app/Console/Commands/MiningTaxes/MiningTaxesObservers.php',
'App\\Console\\Commands\\MiningTaxes\\MiningTaxesPayments' => __DIR__ . '/../..' . '/app/Console/Commands/MiningTaxes/MiningTaxesPayments.php',
@@ -571,7 +583,6 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'App\\Http\\Controllers\\Blacklist\\BlacklistController' => __DIR__ . '/../..' . '/app/Http/Controllers/Blacklist/BlacklistController.php',
'App\\Http\\Controllers\\Contracts\\SupplyChainController' => __DIR__ . '/../..' . '/app/Http/Controllers/Contracts/SupplyChainController.php',
'App\\Http\\Controllers\\Controller' => __DIR__ . '/../..' . '/app/Http/Controllers/Controller.php',
'App\\Http\\Controllers\\Dashboard\\AdminController' => __DIR__ . '/../..' . '/app/Http/Controllers/Dashboard/AdminController.php',
'App\\Http\\Controllers\\Dashboard\\AdminDashboardController' => __DIR__ . '/../..' . '/app/Http/Controllers/Dashboard/AdminDashboardController.php',
'App\\Http\\Controllers\\Dashboard\\DashboardController' => __DIR__ . '/../..' . '/app/Http/Controllers/Dashboard/DashboardController.php',
'App\\Http\\Controllers\\Logistics\\FuelController' => __DIR__ . '/../..' . '/app/Http/Controllers/Logistics/FuelController.php',
@@ -590,10 +601,16 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'App\\Http\\Middleware\\TrimStrings' => __DIR__ . '/../..' . '/app/Http/Middleware/TrimStrings.php',
'App\\Http\\Middleware\\TrustProxies' => __DIR__ . '/../..' . '/app/Http/Middleware/TrustProxies.php',
'App\\Http\\Middleware\\VerifyCsrfToken' => __DIR__ . '/../..' . '/app/Http/Middleware/VerifyCsrfToken.php',
'App\\Jobs\\Commands\\Data\\PurgeUsersJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/Data/PurgeUsersJob.php',
'App\\Jobs\\Commands\\Eve\\ItemPricesUpdateJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/Eve/ItemPricesUpdateJob.php',
'App\\Jobs\\Commands\\Eve\\ProcessSendEveMailJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/Eve/ProcessSendEveMailJob.php',
'App\\Jobs\\Commands\\Eve\\ProcessSendEveMailJobRL' => __DIR__ . '/../..' . '/app/Jobs/Commands/Eve/ProcessSendEveMailJobRL.php',
'App\\Jobs\\Commands\\Finances\\UpdateItemPricesJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/Finances/UpdateItemPricesJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\FetchMiningTaxesLedgersJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/MiningTaxes/FetchMiningTaxesLedgersJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\FetchMiningTaxesObserversJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/MiningTaxes/FetchMiningTaxesObserversJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\ProcessMiningTaxesLedgersJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/MiningTaxes/ProcessMiningTaxesLedgersJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\ProcessMiningTaxesPaymentsJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/MiningTaxes/ProcessMiningTaxesPaymentsJob.php',
'App\\Jobs\\Commands\\MiningTaxes\\SendMiningTaxesInvoicesJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/MiningTaxes/SendMiningTaxesInvoicesJob.php',
'App\\Jobs\\Commands\\SupplyChain\\EndSupplyChainContractJob' => __DIR__ . '/../..' . '/app/Jobs/Commands/SupplyChain/EndSupplyChainContractJob.php',
'App\\Library\\Esi\\Esi' => __DIR__ . '/../..' . '/app/Library/Esi/Esi.php',
'App\\Library\\Helpers\\AssetHelper' => __DIR__ . '/../..' . '/app/Library/Helpers/AssetHelper.php',
@@ -605,6 +622,8 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'App\\Library\\Helpers\\TaxesHelper' => __DIR__ . '/../..' . '/app/Library/Helpers/TaxesHelper.php',
'App\\Library\\Moons\\MoonCalc' => __DIR__ . '/../..' . '/app/Library/Moons/MoonCalc.php',
'App\\Models\\Admin\\AllowedLogin' => __DIR__ . '/../..' . '/app/Models/Admin/AllowedLogin.php',
'App\\Models\\AfterActionReports\\AfterActionReport' => __DIR__ . '/../..' . '/app/Models/AfterActionReports/AfterActionReport.php',
'App\\Models\\AfterActionReports\\AfterActionReportComment' => __DIR__ . '/../..' . '/app/Models/AfterActionReports/AfterActionReportComment.php',
'App\\Models\\Blacklist\\BlacklistEntity' => __DIR__ . '/../..' . '/app/Models/Blacklist/BlacklistEntity.php',
'App\\Models\\Contracts\\SupplyChainBid' => __DIR__ . '/../..' . '/app/Models/Contracts/SupplyChainBid.php',
'App\\Models\\Contracts\\SupplyChainContract' => __DIR__ . '/../..' . '/app/Models/Contracts/SupplyChainContract.php',
@@ -3238,6 +3257,15 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'League\\OAuth1\\Client\\Signature\\RsaSha1Signature' => __DIR__ . '/..' . '/league/oauth1-client/src/Signature/RsaSha1Signature.php',
'League\\OAuth1\\Client\\Signature\\Signature' => __DIR__ . '/..' . '/league/oauth1-client/src/Signature/Signature.php',
'League\\OAuth1\\Client\\Signature\\SignatureInterface' => __DIR__ . '/..' . '/league/oauth1-client/src/Signature/SignatureInterface.php',
'Lorisleiva\\CronTranslator\\CronParsingException' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/CronParsingException.php',
'Lorisleiva\\CronTranslator\\CronTranslator' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/CronTranslator.php',
'Lorisleiva\\CronTranslator\\CronType' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/CronType.php',
'Lorisleiva\\CronTranslator\\DaysOfMonthField' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/DaysOfMonthField.php',
'Lorisleiva\\CronTranslator\\DaysOfWeekField' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/DaysOfWeekField.php',
'Lorisleiva\\CronTranslator\\Field' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/Field.php',
'Lorisleiva\\CronTranslator\\HoursField' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/HoursField.php',
'Lorisleiva\\CronTranslator\\MinutesField' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/MinutesField.php',
'Lorisleiva\\CronTranslator\\MonthsField' => __DIR__ . '/..' . '/lorisleiva/cron-translator/src/MonthsField.php',
'Mockery' => __DIR__ . '/..' . '/mockery/mockery/library/Mockery.php',
'Mockery\\Adapter\\Phpunit\\MockeryPHPUnitIntegration' => __DIR__ . '/..' . '/mockery/mockery/library/Mockery/Adapter/Phpunit/MockeryPHPUnitIntegration.php',
'Mockery\\Adapter\\Phpunit\\MockeryPHPUnitIntegrationAssertPostConditions' => __DIR__ . '/..' . '/mockery/mockery/library/Mockery/Adapter/Phpunit/MockeryPHPUnitIntegrationAssertPostConditions.php',
@@ -4633,6 +4661,32 @@ class ComposerStaticInitc3f953f8a7291d41a76e1664339777c9
'SocialiteProviders\\Manager\\SocialiteWasCalled' => __DIR__ . '/..' . '/socialiteproviders/manager/src/SocialiteWasCalled.php',
'SolarSystemSeeder' => __DIR__ . '/../..' . '/database/seeds/SolarSystemSeeder.php',
'Spatie\\RateLimitedMiddleware\\RateLimited' => __DIR__ . '/..' . '/spatie/laravel-rate-limited-job-middleware/src/RateLimited.php',
'Spatie\\ScheduleMonitor\\Commands\\CleanLogCommand' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/CleanLogCommand.php',
'Spatie\\ScheduleMonitor\\Commands\\ListCommand' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/ListCommand.php',
'Spatie\\ScheduleMonitor\\Commands\\SyncCommand' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/SyncCommand.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\DuplicateTasksTable' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/Tables/DuplicateTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\MonitoredTasksTable' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/Tables/MonitoredTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\ReadyForMonitoringTasksTable' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/Tables/ReadyForMonitoringTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\ScheduledTasksTable' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/Tables/ScheduledTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\Tables\\UnnamedTasksTable' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/Tables/UnnamedTasksTable.php',
'Spatie\\ScheduleMonitor\\Commands\\VerifyCommand' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Commands/VerifyCommand.php',
'Spatie\\ScheduleMonitor\\EventHandlers\\BackgroundCommandListener' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/EventHandlers/BackgroundCommandListener.php',
'Spatie\\ScheduleMonitor\\EventHandlers\\ScheduledTaskEventSubscriber' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/EventHandlers/ScheduledTaskEventSubscriber.php',
'Spatie\\ScheduleMonitor\\Jobs\\PingOhDearJob' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Jobs/PingOhDearJob.php',
'Spatie\\ScheduleMonitor\\Models\\MonitoredScheduledTask' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Models/MonitoredScheduledTask.php',
'Spatie\\ScheduleMonitor\\Models\\MonitoredScheduledTaskLogItem' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Models/MonitoredScheduledTaskLogItem.php',
'Spatie\\ScheduleMonitor\\ScheduleMonitorServiceProvider' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/ScheduleMonitorServiceProvider.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\OhDearPayloadFactory' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/OhDearPayloadFactory.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\Payloads\\FailedPayload' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/Payloads/FailedPayload.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\Payloads\\FinishedPayload' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/Payloads/FinishedPayload.php',
'Spatie\\ScheduleMonitor\\Support\\OhDearPayload\\Payloads\\Payload' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/OhDearPayload/Payloads/Payload.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\ScheduledTaskFactory' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/ScheduledTaskFactory.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\ScheduledTasks' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/ScheduledTasks.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\ClosureTask' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/ClosureTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\CommandTask' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/CommandTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\JobTask' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/JobTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\ShellTask' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/ShellTask.php',
'Spatie\\ScheduleMonitor\\Support\\ScheduledTasks\\Tasks\\Task' => __DIR__ . '/..' . '/spatie/laravel-schedule-monitor/src/Support/ScheduledTasks/Tasks/Task.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'Symfony\\Component\\Console\\Application' => __DIR__ . '/..' . '/symfony/console/Application.php',
'Symfony\\Component\\Console\\Color' => __DIR__ . '/..' . '/symfony/console/Color.php',

View File

@@ -2040,6 +2040,56 @@
},
"install-path": "../league/oauth1-client"
},
{
"name": "lorisleiva/cron-translator",
"version": "v0.1.1",
"version_normalized": "0.1.1.0",
"source": {
"type": "git",
"url": "https://github.com/lorisleiva/cron-translator.git",
"reference": "784a6f6255a4b5f45da5d89dc6ec631a14d7b011"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/lorisleiva/cron-translator/zipball/784a6f6255a4b5f45da5d89dc6ec631a14d7b011",
"reference": "784a6f6255a4b5f45da5d89dc6ec631a14d7b011",
"shasum": ""
},
"require-dev": {
"phpunit/phpunit": "^8.0"
},
"time": "2020-03-01T14:44:47+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Lorisleiva\\CronTranslator\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Loris LEIVA",
"email": "loris.leiva@gmail.com",
"homepage": "https://lorisleiva.com"
}
],
"description": "Makes CRON expressions human-readable",
"homepage": "https://github.com/lorisleiva/cron-translator",
"keywords": [
"cron",
"expression",
"human"
],
"support": {
"issues": "https://github.com/lorisleiva/cron-translator/issues",
"source": "https://github.com/lorisleiva/cron-translator/tree/v0.1.1"
},
"install-path": "../lorisleiva/cron-translator"
},
{
"name": "mockery/mockery",
"version": "1.4.2",
@@ -4645,6 +4695,84 @@
],
"install-path": "../spatie/laravel-rate-limited-job-middleware"
},
{
"name": "spatie/laravel-schedule-monitor",
"version": "2.0.1",
"version_normalized": "2.0.1.0",
"source": {
"type": "git",
"url": "https://github.com/spatie/laravel-schedule-monitor.git",
"reference": "4ffdaea81b5c27a119b6cbac8f4894657b8fd6d9"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/spatie/laravel-schedule-monitor/zipball/4ffdaea81b5c27a119b6cbac8f4894657b8fd6d9",
"reference": "4ffdaea81b5c27a119b6cbac8f4894657b8fd6d9",
"shasum": ""
},
"require": {
"illuminate/bus": "^7.19|^8.0",
"lorisleiva/cron-translator": "^0.1.1",
"php": "^7.4"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"laravel/legacy-factories": "^1.0.4",
"ohdearapp/ohdear-php-sdk": "^3.0",
"orchestra/testbench": "^5.0|^6.0",
"phpunit/phpunit": "^9.0",
"spatie/phpunit-snapshot-assertions": "^4.2",
"spatie/test-time": "^1.2",
"vimeo/psalm": "^3.11"
},
"suggest": {
"ohdearapp/ohdear-php-sdk": "Needed to sync your schedule with Oh Dear"
},
"time": "2020-10-06T10:38:45+00:00",
"type": "library",
"extra": {
"laravel": {
"providers": [
"Spatie\\ScheduleMonitor\\ScheduleMonitorServiceProvider"
]
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Spatie\\ScheduleMonitor\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"description": "Monitor scheduled tasks in a Laravel app",
"homepage": "https://github.com/spatie/laravel-schedule-monitor",
"keywords": [
"laravel-schedule-monitor",
"spatie"
],
"support": {
"issues": "https://github.com/spatie/laravel-schedule-monitor/issues",
"source": "https://github.com/spatie/laravel-schedule-monitor/tree/2.0.1"
},
"funding": [
{
"url": "https://github.com/spatie",
"type": "github"
}
],
"install-path": "../spatie/laravel-schedule-monitor"
},
{
"name": "spomky-labs/base64url",
"version": "v2.0.4",

View File

@@ -6,7 +6,7 @@
'aliases' =>
array (
),
'reference' => '4b72a59e62d368371546246a68aef55ee21c2705',
'reference' => '40ed5dd5a576916f6561953482f68722d1ecce8f',
'name' => 'laravel/laravel',
),
'versions' =>
@@ -440,7 +440,7 @@
'aliases' =>
array (
),
'reference' => '4b72a59e62d368371546246a68aef55ee21c2705',
'reference' => '40ed5dd5a576916f6561953482f68722d1ecce8f',
),
'laravel/socialite' =>
array (
@@ -505,6 +505,15 @@
),
'reference' => '159c3d2bf27568f9af87d6c3f4bb616a251eb12b',
),
'lorisleiva/cron-translator' =>
array (
'pretty_version' => 'v0.1.1',
'version' => '0.1.1.0',
'aliases' =>
array (
),
'reference' => '784a6f6255a4b5f45da5d89dc6ec631a14d7b011',
),
'mockery/mockery' =>
array (
'pretty_version' => '1.4.2',
@@ -923,6 +932,15 @@
),
'reference' => '7b72592e0d823e2948c413f5e661de0fd3431db5',
),
'spatie/laravel-schedule-monitor' =>
array (
'pretty_version' => '2.0.1',
'version' => '2.0.1.0',
'aliases' =>
array (
),
'reference' => '4ffdaea81b5c27a119b6cbac8f4894657b8fd6d9',
),
'spomky-labs/base64url' =>
array (
'pretty_version' => 'v2.0.4',

View File

@@ -4,8 +4,8 @@
$issues = array();
if (!(PHP_VERSION_ID >= 70300)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.';
if (!(PHP_VERSION_ID >= 70400)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 7.4.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {

View File

@@ -0,0 +1,42 @@
name: Tests
on: [push]
jobs:
phpunit73:
name: "Tests on PHP 7.3"
runs-on: ubuntu-latest
container:
image: lorisleiva/laravel-docker:7.3
steps:
- uses: actions/checkout@v2
- name: Validate composer.json and composer.lock
run: composer validate
- name: Cache dependencies
uses: actions/cache@v1
with:
path: /composer/cache/files
key: dependencies-composer-${{ hashFiles('composer.json') }}
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run tests
run: phpunit
phpunit74:
name: "Tests on PHP 7.4"
runs-on: ubuntu-latest
container:
image: lorisleiva/laravel-docker:7.4
steps:
- uses: actions/checkout@v2
- name: Validate composer.json and composer.lock
run: composer validate
- name: Cache dependencies
uses: actions/cache@v1
with:
path: /composer/cache/files
key: dependencies-composer-${{ hashFiles('composer.json') }}
- name: Install dependencies
run: composer install --prefer-dist --no-progress --no-suggest
- name: Run tests
run: phpunit

View File

@@ -0,0 +1,4 @@
vendor
composer.lock
.phpunit.result.cache
build

View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2020 Loris Leiva
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View File

@@ -0,0 +1,31 @@
# CRON Translator
⏰️ Makes CRON expressions human-readable
![intro-rounded](https://user-images.githubusercontent.com/3642397/60768671-7d6c7100-a0be-11e9-8cee-8a8d2780d76f.png)
## Installation
```sh
composer require lorisleiva/cron-translator
```
## Usage
```php
use Lorisleiva\CronTranslator\CronTranslator;
CronTranslator::translate('* * * * *'); // => Every minute
CronTranslator::translate('30 22 * * *'); // => Every day at 10:30pm
CronTranslator::translate('0 16 * * 1'); // => Every Monday at 4:00pm
CronTranslator::translate('0 0 1 1 *'); // => Every year on January the 1st at 12:00am
CronTranslator::translate('0 0 1 * *'); // => The 1st of every month at 12:00am
CronTranslator::translate('0 * * * 1'); // => Once an hour on Mondays
CronTranslator::translate('* 1-20 * * *'); // => Every minute 20 hours a day
CronTranslator::translate('0,30 * * * *'); // => Twice an hour
CronTranslator::translate('0 1-5 * * *'); // => 5 times a day
CronTranslator::translate('0 1 1-5 * *'); // => 5 days a month at 1:00am
CronTranslator::translate('*/2 * * * *'); // => Every 2 minutes
CronTranslator::translate('* 1/3 2 * *'); // => Every minute of every 3 hours on the 2nd of every month
CronTranslator::translate('1-3/5 * * * *'); // => 3 times every 5 minutes
CronTranslator::translate('1,2 0 */2 1,2 *'); // => Twice an hour every 2 days 2 months a year at 12am
```

View File

@@ -0,0 +1,33 @@
{
"name": "lorisleiva/cron-translator",
"description": "Makes CRON expressions human-readable",
"keywords": [
"cron",
"expression",
"human"
],
"homepage": "https://github.com/lorisleiva/cron-translator",
"license": "MIT",
"authors": [
{
"name": "Loris LEIVA",
"email": "loris.leiva@gmail.com",
"homepage": "https://lorisleiva.com"
}
],
"require-dev": {
"phpunit/phpunit" : "^8.0"
},
"autoload": {
"psr-4": {
"Lorisleiva\\CronTranslator\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Lorisleiva\\CronTranslator\\Tests\\": "tests"
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit backupGlobals="false"
backupStaticAttributes="false"
bootstrap="vendor/autoload.php"
colors="true"
convertErrorsToExceptions="true"
convertNoticesToExceptions="true"
convertWarningsToExceptions="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Test Suite">
<directory suffix="Test.php">./tests</directory>
</testsuite>
</testsuites>
<filter>
<whitelist processUncoveredFilesFromWhitelist="true">
<directory suffix=".php">./src</directory>
</whitelist>
</filter>
</phpunit>

View File

@@ -0,0 +1,13 @@
<?php
namespace Lorisleiva\CronTranslator;
use Exception;
class CronParsingException extends Exception
{
public function __construct($cron)
{
parent::__construct("Failed to parse the following CRON expression: {$cron}");
}
}

View File

@@ -0,0 +1,98 @@
<?php
namespace Lorisleiva\CronTranslator;
class CronTranslator
{
private static $extendedMap = [
'@yearly' => '0 0 1 1 *',
'@annually' => '0 0 1 1 *',
'@monthly' => '0 0 1 * *',
'@weekly' => '0 0 * * 0',
'@daily' => '0 0 * * *',
'@hourly' => '0 * * * *'
];
public static function translate($cron)
{
if (isset(self::$extendedMap[$cron])) {
$cron = self::$extendedMap[$cron];
}
try {
$fields = static::parseFields($cron);
$orderedFields = static::orderFields($fields);
$fieldsAsObject = static::getFieldsAsObject($fields);
$translations = array_map(function ($field) use ($fieldsAsObject) {
return $field->translate($fieldsAsObject);
}, $orderedFields);
return ucfirst(implode(' ', array_filter($translations)));
} catch (\Throwable $th) {
throw new CronParsingException($cron);
}
}
protected static function parseFields($cron)
{
$fields = explode(' ', $cron);
return [
new MinutesField($fields[0]),
new HoursField($fields[1]),
new DaysOfMonthField($fields[2]),
new MonthsField($fields[3]),
new DaysOfWeekField($fields[4]),
];
}
protected static function orderFields($fields)
{
// Group fields by CRON types.
$onces = static::filterType($fields, 'Once');
$everys = static::filterType($fields, 'Every');
$incrementsAndMultiples = static::filterType($fields, 'Increment', 'Multiple');
// Decide whether to keep one or zero CRON type "Every".
$firstEvery = reset($everys)->position ?? PHP_INT_MIN;
$firstIncrementOrMultiple = reset($incrementsAndMultiples)->position ?? PHP_INT_MAX;
$numberOfEverysKept = $firstIncrementOrMultiple < $firstEvery ? 0 : 1;
// Mark fields that will not be displayed as dropped.
// This allows other fields to check whether some
// information is missing and adapt their translation.
foreach (array_slice($everys, $numberOfEverysKept) as $field) {
$field->dropped = true;
}
return array_merge(
// Place one or zero "Every" field at the beginning.
array_slice($everys, 0, $numberOfEverysKept),
// Place all "Increment" and "Multiple" fields in the middle.
$incrementsAndMultiples,
// Finish with the "Once" fields reversed (i.e. from months to minutes).
array_reverse($onces)
);
}
protected static function filterType($fields, ...$types)
{
return array_filter($fields, function ($field) use ($types) {
return $field->hasType(...$types);
});
}
protected static function getFieldsAsObject($fields)
{
return (object) [
'minute' => $fields[0],
'hour' => $fields[1],
'day' => $fields[2],
'month' => $fields[3],
'weekday' => $fields[4],
];
}
}

View File

@@ -0,0 +1,88 @@
<?php
namespace Lorisleiva\CronTranslator;
class CronType
{
const TYPES = [
'Every', 'Increment', 'Multiple', 'Once',
];
public $type;
public $value;
public $count;
public $increment;
private function __construct($type, $value = null, $count = null, $increment = null)
{
$this->type = $type;
$this->value = $value;
$this->count = $count;
$this->increment = $increment;
}
public static function every()
{
return new static('Every');
}
public static function increment($increment, $count = 1)
{
return new static('Increment', null, $count, $increment);
}
public static function multiple($count)
{
return new static('Multiple', null, $count);
}
public static function once($value)
{
return new static('Once', $value);
}
public static function parse($expression)
{
// Parse "*".
if ($expression === '*') {
return static::every();
}
// Parse fixed values like "1".
if (preg_match("/^[0-9]+$/", $expression)) {
return static::once((int) $expression);
}
// Parse multiple selected values like "1,2,5".
if (preg_match("/^[0-9]+(,[0-9]+)+$/", $expression)) {
return static::multiple(count(explode(',', $expression)));
}
// Parse ranges of selected values like "1-5".
if (preg_match("/^([0-9]+)\-([0-9]+)$/", $expression, $matches)) {
$count = $matches[2] - $matches[1] + 1;
return $count > 1
? static::multiple($count)
: static::once((int) $matches[1]);
}
// Parse incremental expressions like "*/2", "1-4/10" or "1,3/4".
if (preg_match("/(.+)\/([0-9]+)$/", $expression, $matches)) {
$range = static::parse($matches[1]);
if ($range->hasType('Once', 'Every')) {
return static::Increment($matches[2]);
}
if ($range->hasType('Multiple')) {
return static::Increment($matches[2], $range->count);
}
}
// Unsupported expressions throw exceptions.
throw new CronParsingException($expression);
}
public function hasType()
{
return in_array($this->type, func_get_args());
}
}

View File

@@ -0,0 +1,65 @@
<?php
namespace Lorisleiva\CronTranslator;
class DaysOfMonthField extends Field
{
public $position = 2;
public function translateEvery($fields)
{
if ($fields->weekday->hasType('Once')) {
return "every {$fields->weekday->format()}";
}
return 'every day';
}
public function translateIncrement()
{
if ($this->count > 1) {
return "{$this->count} days out of {$this->increment}";
}
return "every {$this->increment} days";
}
public function translateMultiple()
{
return "{$this->count} days a month";
}
public function translateOnce($fields)
{
if ($fields->month->hasType('Once')) {
return; // MonthsField adapts to "On January the 1st".
}
if ($fields->month->hasType('Every') && ! $fields->month->dropped) {
return; // MonthsField adapts to "The 1st of every month".
}
if ($fields->month->hasType('Every') && $fields->month->dropped) {
return 'on the ' . $this->format() . ' of every month';
}
return 'on the ' . $this->format();
}
public function format()
{
if (in_array($this->value, [1, 21, 31])) {
return $this->value . 'st';
}
if (in_array($this->value, [2, 22])) {
return $this->value . 'nd';
}
if (in_array($this->value, [3, 23])) {
return $this->value . 'rd';
}
return $this->value . 'th';
}
}

View File

@@ -0,0 +1,54 @@
<?php
namespace Lorisleiva\CronTranslator;
class DaysOfWeekField extends Field
{
public $position = 4;
public function translateEvery()
{
return 'every year';
}
public function translateIncrement()
{
if ($this->count > 1) {
return "{$this->count} days of the week out of {$this->increment}";
}
return "every {$this->increment} days of the week";
}
public function translateMultiple()
{
return "{$this->count} days a week";
}
public function translateOnce($fields)
{
if ($fields->day->hasType('Every') && ! $fields->day->dropped) {
return; // DaysOfMonthField adapts to "Every Sunday".
}
return "on {$this->format()}s";
}
public function format()
{
if ($this->value < 0 || $this->value > 7) {
throw new \Exception();
}
return [
0 => 'Sunday',
1 => 'Monday',
2 => 'Tuesday',
3 => 'Wednesday',
4 => 'Thursday',
5 => 'Friday',
6 => 'Saturday',
7 => 'Sunday',
][$this->value];
}
}

View File

@@ -0,0 +1,47 @@
<?php
namespace Lorisleiva\CronTranslator;
abstract class Field
{
public $expression;
public $type;
public $value;
public $count;
public $increment;
public $dropped = false;
public $position;
public function __construct($expression)
{
$this->expression = $expression;
$cronType = CronType::parse($expression);
$this->type = $cronType->type;
$this->value = $cronType->value;
$this->count = $cronType->count;
$this->increment = $cronType->increment;
}
public function translate($fields)
{
foreach (CronType::TYPES as $type) {
if ($this->hasType($type) && method_exists($this, "translate{$type}")) {
return $this->{"translate{$type}"}($fields);
}
}
}
public function hasType()
{
return in_array($this->type, func_get_args());
}
public function times($count)
{
switch ($count) {
case 1: return 'once';
case 2: return 'twice';
default: return "{$count} times";
}
}
}

View File

@@ -0,0 +1,61 @@
<?php
namespace Lorisleiva\CronTranslator;
class HoursField extends Field
{
public $position = 1;
public function translateEvery($fields)
{
if ($fields->minute->hasType('Once')) {
return 'once an hour';
}
return 'every hour';
}
public function translateIncrement($fields)
{
if ($fields->minute->hasType('Once')) {
return $this->times($this->count) . " every {$this->increment} hours";
}
if ($this->count > 1) {
return "{$this->count} hours out of {$this->increment}";
}
if ($fields->minute->hasType('Every')) {
return "of every {$this->increment} hours";
}
return "every {$this->increment} hours";
}
public function translateMultiple($fields)
{
if ($fields->minute->hasType('Once')) {
return $this->times($this->count) . " a day";
}
return "{$this->count} hours a day";
}
public function translateOnce($fields)
{
return 'at ' . $this->format(
$fields->minute->hasType('Once') ? $fields->minute : null
);
}
public function format($minute = null)
{
$amOrPm = $this->value < 12 ? 'am' : 'pm';
$hour = $this->value === 0 ? 12 : $this->value;
$hour = $hour > 12 ? $hour - 12 : $hour;
return $minute
? "{$hour}:{$minute->format()}{$amOrPm}"
: "{$hour}{$amOrPm}";
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Lorisleiva\CronTranslator;
class MinutesField extends Field
{
public $position = 0;
public function translateEvery()
{
return 'every minute';
}
public function translateIncrement()
{
if ($this->count > 1) {
return $this->times($this->count) . " every {$this->increment} minutes";
}
return "every {$this->increment} minutes";
}
public function translateMultiple()
{
return $this->times($this->count) . " an hour";
}
public function format()
{
return ($this->value < 10 ? '0' : '') . $this->value;
}
}

View File

@@ -0,0 +1,62 @@
<?php
namespace Lorisleiva\CronTranslator;
class MonthsField extends Field
{
public $position = 3;
public function translateEvery($fields)
{
if ($fields->day->hasType('Once')) {
return 'the ' . $fields->day->format() . ' of every month';
}
return 'every month';
}
public function translateIncrement()
{
if ($this->count > 1) {
return "{$this->count} months out of {$this->increment}";
}
return "every {$this->increment} months";
}
public function translateMultiple()
{
return "{$this->count} months a year";
}
public function translateOnce($fields)
{
if ($fields->day->hasType('Once')) {
return "on {$this->format()} the {$fields->day->format()}";
}
return "on {$this->format()}";
}
public function format()
{
if ($this->value < 1 || $this->value > 12) {
throw new \Exception();
}
return [
1 => 'January',
2 => 'February',
3 => 'March',
4 => 'April',
5 => 'May',
6 => 'June',
7 => 'July',
8 => 'August',
9 => 'September',
10 => 'October',
11 => 'November',
12 => 'December',
][$this->value];
}
}

View File

@@ -0,0 +1,142 @@
<?php
namespace Lorisleiva\CronTranslator\Tests;
class CronTranslatorTest extends TestCase
{
/** @test */
public function it_translates_expressions_with_every_and_once()
{
// All 32 (2^5) combinations of Every/Once.
$this->assertCronTranslateTo('Every minute', '* * * * *');
$this->assertCronTranslateTo('Every minute on Sundays', '* * * * 0');
$this->assertCronTranslateTo('Every minute on January', '* * * 1 *');
$this->assertCronTranslateTo('Every minute on Sundays on January', '* * * 1 0');
$this->assertCronTranslateTo('Every minute on the 1st of every month', '* * 1 * *');
$this->assertCronTranslateTo('Every minute on Sundays on the 1st of every month', '* * 1 * 0');
$this->assertCronTranslateTo('Every minute on January the 1st', '* * 1 1 *');
$this->assertCronTranslateTo('Every minute on Sundays on January the 1st', '* * 1 1 0');
$this->assertCronTranslateTo('Every minute at 12am', '* 0 * * *');
$this->assertCronTranslateTo('Every minute on Sundays at 12am', '* 0 * * 0');
$this->assertCronTranslateTo('Every minute on January at 12am', '* 0 * 1 *');
$this->assertCronTranslateTo('Every minute on Sundays on January at 12am', '* 0 * 1 0');
$this->assertCronTranslateTo('Every minute on the 1st of every month at 12am', '* 0 1 * *');
$this->assertCronTranslateTo('Every minute on Sundays on the 1st of every month at 12am', '* 0 1 * 0');
$this->assertCronTranslateTo('Every minute on January the 1st at 12am', '* 0 1 1 *');
$this->assertCronTranslateTo('Every minute on Sundays on January the 1st at 12am', '* 0 1 1 0');
$this->assertCronTranslateTo('Once an hour', '0 * * * *');
$this->assertCronTranslateTo('Once an hour on Sundays', '0 * * * 0');
$this->assertCronTranslateTo('Once an hour on January', '0 * * 1 *');
$this->assertCronTranslateTo('Once an hour on Sundays on January', '0 * * 1 0');
$this->assertCronTranslateTo('Once an hour on the 1st of every month', '0 * 1 * *');
$this->assertCronTranslateTo('Once an hour on Sundays on the 1st of every month', '0 * 1 * 0');
$this->assertCronTranslateTo('Once an hour on January the 1st', '0 * 1 1 *');
$this->assertCronTranslateTo('Once an hour on Sundays on January the 1st', '0 * 1 1 0');
$this->assertCronTranslateTo('Every day at 12:00am', '0 0 * * *');
$this->assertCronTranslateTo('Every Sunday at 12:00am', '0 0 * * 0');
$this->assertCronTranslateTo('Every day on January at 12:00am', '0 0 * 1 *');
$this->assertCronTranslateTo('Every Sunday on January at 12:00am', '0 0 * 1 0');
$this->assertCronTranslateTo('The 1st of every month at 12:00am', '0 0 1 * *');
$this->assertCronTranslateTo('The 1st of every month on Sundays at 12:00am', '0 0 1 * 0');
$this->assertCronTranslateTo('Every year on January the 1st at 12:00am', '0 0 1 1 *');
$this->assertCronTranslateTo('On Sundays on January the 1st at 12:00am', '0 0 1 1 0');
// More realistic examples.
$this->assertCronTranslateTo('Every year on January the 1st at 12:00pm', '0 12 1 1 *');
$this->assertCronTranslateTo('Every minute on Mondays at 3pm', '* 15 * * 1');
$this->assertCronTranslateTo('Every minute on January the 3rd', '* * 3 1 *');
$this->assertCronTranslateTo('Every minute on Mondays on April', '* * * 4 1');
$this->assertCronTranslateTo('On Mondays on April the 22nd at 3:10pm', '10 15 22 4 1');
// Paparazzi examples.
$this->assertCronTranslateTo('Every day at 10:00pm', '0 22 * * *');
$this->assertCronTranslateTo('Every day at 9:00am', '0 9 * * *');
$this->assertCronTranslateTo('Every Monday at 4:00pm', '0 16 * * 1');
$this->assertCronTranslateTo('Every year on January the 1st at 12:00am', '0 0 1 1 *');
$this->assertCronTranslateTo('The 1st of every month at 12:00am', '0 0 1 * *');
}
/** @test */
public function it_translate_expressions_with_multiple()
{
$this->assertCronTranslateTo('Every minute 2 hours a day', '* 8,18 * * *');
$this->assertCronTranslateTo('Every minute 3 hours a day', '* 8,18,20 * * *');
$this->assertCronTranslateTo('Every minute 20 hours a day', '* 1-20 * * *');
$this->assertCronTranslateTo('Twice an hour', '0,30 * * * *');
$this->assertCronTranslateTo('Twice an hour 5 hours a day', '0,30 1-5 * * *');
$this->assertCronTranslateTo('5 times a day', '0 1-5 * * *');
$this->assertCronTranslateTo('Every minute 5 hours a day', '* 1-5 * * *');
$this->assertCronTranslateTo('5 days a month at 1:00am', '0 1 1-5 * *');
$this->assertCronTranslateTo('5 days a month 2 months a year at 1:00am', '0 1 1-5 5,6 *');
$this->assertCronTranslateTo('2 months a year on the 5th at 1:00am', '0 1 5 5,6 *');
$this->assertCronTranslateTo('The 5th of every month 4 days a week at 1:00am', '0 1 5 * 1-4');
}
/** @test */
public function it_translate_expressions_with_increment()
{
$this->assertCronTranslateTo('Every 2 minutes', '*/2 * * * *');
$this->assertCronTranslateTo('Every 2 minutes', '1/2 * * * *');
$this->assertCronTranslateTo('Twice every 4 minutes', '1,3/4 * * * *');
$this->assertCronTranslateTo('3 times every 5 minutes', '1-3/5 * * * *');
$this->assertCronTranslateTo('Every 2 minutes at 2pm', '*/2 14 * * *');
$this->assertCronTranslateTo('Once an hour every 2 days', '0 * */2 * *');
$this->assertCronTranslateTo('Every minute every 2 days', '* * */2 * *');
$this->assertCronTranslateTo('Once every 2 hours', '0 */2 * * *');
$this->assertCronTranslateTo('Twice every 5 hours', '0 1,2/5 * * *');
$this->assertCronTranslateTo('Every minute 2 hours out of 5', '* 1,2/5 * * *');
$this->assertCronTranslateTo('Every day every 4 months at 12:00am', '0 0 * */4 *');
}
/** @test */
public function it_adds_junctions_to_certain_combinations_of_cron_types()
{
$this->assertCronTranslateTo('Every minute of every 2 hours', '* */2 * * *');
$this->assertCronTranslateTo('Every minute of every 3 hours on the 2nd of every month', '* 1/3 2 * *');
}
/** @test */
public function it_converts_ranges_of_one_into_once_cron_types()
{
$this->assertCronTranslateTo('Every minute at 8am', '* 8-8 * * *');
$this->assertCronTranslateTo('Every minute on January', '* * * 1-1 *');
}
/** @test */
public function it_handles_extended_cron_syntax()
{
$this->assertCronTranslateTo('Once an hour', '@hourly');
$this->assertCronTranslateTo('Every day at 12:00am', '@daily');
$this->assertCronTranslateTo('Every Sunday at 12:00am', '@weekly');
$this->assertCronTranslateTo('The 1st of every month at 12:00am', '@monthly');
$this->assertCronTranslateTo('Every year on January the 1st at 12:00am', '@yearly');
$this->assertCronTranslateTo('Every year on January the 1st at 12:00am', '@annually');
}
/** @test */
public function it_returns_parsing_errors_when_something_goes_wrong()
{
$this->assertCronThrowsParsingError('I_AM_NOT_A_CRON_EXPRESSION');
$this->assertCronThrowsParsingError('A * * * *');
$this->assertCronThrowsParsingError('1,2-3 * * * *');
$this->assertCronThrowsParsingError('1/2/3 * * * *');
$this->assertCronThrowsParsingError('* * * 0 *');
$this->assertCronThrowsParsingError('* * * 13 *');
$this->assertCronThrowsParsingError('* * * * 8');
}
/**
* @skip
* @doesNotPerformAssertions
*/
public function result_generator()
{
$this->generateCombinationsFromMatrix([
['*', '0', '1,2', '*/2'],
['*', '0', '1,2', '*/2'],
['*', '1', '1,2', '*/2'],
['*', '1', '1,2', '*/2'],
['*', '0', '1,2', '*/2'],
]);
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace Lorisleiva\CronTranslator\Tests;
use Lorisleiva\CronTranslator\CronTranslator;
use PHPUnit\Framework\TestCase as BaseTestCase;
use Lorisleiva\CronTranslator\CronParsingException;
class TestCase extends BaseTestCase
{
public function assertCronTranslateTo($expected, $actual)
{
$this->assertEquals($expected, CronTranslator::translate($actual));
}
public function assertCronThrowsParsingError($cron)
{
try {
CronTranslator::translate($cron);
} catch (CronParsingException $expression) {
return $this->addToAssertionCount(1);
}
$this->fail("Expected CronParsingError exception for [$cron]");
}
public function generateCombinationsFromMatrix($matrix)
{
function combinations($matrix, $acc = []) {
if (empty($matrix)) {
return [implode(' ', $acc)];
}
$current = array_shift($matrix);
$results = [];
foreach ($current as $value) {
$results[] = combinations($matrix, array_merge($acc, [$value]));
}
return array_merge(...$results);
}
foreach (combinations($matrix) as $cron) {
echo "\n" . $cron . "\t=> " . CronTranslator::translate($cron);
}
}
}

View File

@@ -0,0 +1,37 @@
<?php
$finder = Symfony\Component\Finder\Finder::create()
->notPath('bootstrap/*')
->notPath('storage/*')
->notPath('resources/view/mail/*')
->in([
__DIR__ . '/src',
__DIR__ . '/tests',
])
->name('*.php')
->notName('*.blade.php')
->ignoreDotFiles(true)
->ignoreVCS(true);
return PhpCsFixer\Config::create()
->setRules([
'@PSR2' => true,
'array_syntax' => ['syntax' => 'short'],
'ordered_imports' => ['sortAlgorithm' => 'alpha'],
'no_unused_imports' => true,
'not_operator_with_successor_space' => true,
'trailing_comma_in_multiline_array' => true,
'phpdoc_scalar' => true,
'unary_operator_spaces' => true,
'binary_operator_spaces' => true,
'blank_line_before_statement' => [
'statements' => ['break', 'continue', 'declare', 'return', 'throw', 'try'],
],
'phpdoc_single_line_var_spacing' => true,
'phpdoc_var_without_name' => true,
'method_argument_space' => [
'on_multiline' => 'ensure_fully_multiline',
'keep_multiple_spaces_after_comma' => true,
]
])
->setFinder($finder);

View File

@@ -0,0 +1,31 @@
# Changelog
All notable changes to `laravel-schedule-monitor` will be documented in this file
## 2.0.1 - 2020-10-06
- report right exit code for scheduled tasks in background
## 2.0.0 - 2020-09-29
- add support for timezones
## 1.0.4 - 2020-09-08
- add support for Laravel 8
## 1.0.3 - 2020-07-14
- fix link config file
## 1.0.2 - 2020-07-14
- add `CarbonImmutable` support (#3)
## 1.0.1 - 2020-07-12
- improve output of commands
## 1.0.0 - 2020-07-09
- initial release

View File

@@ -0,0 +1,55 @@
# Contributing
Contributions are **welcome** and will be fully **credited**.
Please read and understand the contribution guide before creating an issue or pull request.
## Etiquette
This project is open source, and as such, the maintainers give their free time to build and maintain the source code
held within. They make the code freely available in the hope that it will be of use to other developers. It would be
extremely unfair for them to suffer abuse or anger for their hard work.
Please be considerate towards maintainers when raising issues or presenting pull requests. Let's show the
world that developers are civilized and selfless people.
It's the duty of the maintainer to ensure that all submissions to the project are of sufficient
quality to benefit the project. Many developers have different skillsets, strengths, and weaknesses. Respect the maintainer's decision, and do not be upset or abusive if your submission is not used.
## Viability
When requesting or submitting new features, first consider whether it might be useful to others. Open
source projects are used by many developers, who may have entirely different needs to your own. Think about
whether or not your feature is likely to be used by other users of the project.
## Procedure
Before filing an issue:
- Attempt to replicate the problem, to ensure that it wasn't a coincidental incident.
- Check to make sure your feature suggestion isn't already present within the project.
- Check the pull requests tab to ensure that the bug doesn't have a fix in progress.
- Check the pull requests tab to ensure that the feature isn't already in progress.
Before submitting a pull request:
- Check the codebase to ensure that your feature doesn't already exist.
- Check the pull requests to ensure that another person hasn't already submitted the feature or fix.
## Requirements
If the project maintainer has any additional requirements, you will find them listed here.
- **[PSR-2 Coding Standard](https://github.com/php-fig/fig-standards/blob/master/accepted/PSR-2-coding-style-guide.md)** - The easiest way to apply the conventions is to install [PHP Code Sniffer](https://pear.php.net/package/PHP_CodeSniffer).
- **Add tests!** - Your patch won't be accepted if it doesn't have tests.
- **Document any change in behaviour** - Make sure the `README.md` and any other relevant documentation are kept up-to-date.
- **Consider our release cycle** - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option.
- **One pull request per feature** - If you want to do more than one thing, send multiple pull requests.
- **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please [squash them](https://www.git-scm.com/book/en/v2/Git-Tools-Rewriting-History#Changing-Multiple-Commit-Messages) before submitting.
**Happy coding**!

View File

@@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) Immutable
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,282 @@
# Monitor scheduled tasks in a Laravel app
[![Latest Version on Packagist](https://img.shields.io/packagist/v/spatie/laravel-schedule-monitor.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-schedule-monitor)
![Tests](https://github.com/spatie/laravel-schedule-monitor/workflows/Tests/badge.svg)
[![Total Downloads](https://img.shields.io/packagist/dt/spatie/laravel-schedule-monitor.svg?style=flat-square)](https://packagist.org/packages/spatie/laravel-schedule-monitor)
This package will monitor your Laravel schedule. It will write an entry to a log table in the db each time a schedule tasks starts, end, fails or is skipped. Using the `list` command you can check when the scheduled tasks have been executed.
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/list-with-failure.png)
This package can also sync your schedule with [Oh Dear](https://ohdear.app). Oh Dear will send you a notification whenever a scheduled task doesn't run on time or fails.
## Support us
Learn how to create a package like this one, by watching our premium video course:
[![Laravel Package training](https://spatie.be/github/package-training.jpg)](https://laravelpackage.training)
We invest a lot of resources into creating [best in class open source packages](https://spatie.be/open-source). You can support us by [buying one of our paid products](https://spatie.be/open-source/support-us).
We highly appreciate you sending us a postcard from your hometown, mentioning which of our package(s) you are using. You'll find our address on [our contact page](https://spatie.be/about-us). We publish all received postcards on [our virtual postcard wall](https://spatie.be/open-source/postcards).
## Installation
You can install the package via composer:
```bash
composer require spatie/laravel-schedule-monitor
```
#### Preparing the database
You must publish and run migrations:
```bash
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="migrations"
php artisan migrate
```
#### Publishing the config file
You can publish the config file with:
```bash
php artisan vendor:publish --provider="Spatie\ScheduleMonitor\ScheduleMonitorServiceProvider" --tag="config"
```
This is the contents of the published config file:
```php
return [
/*
* The schedule monitor will log each start, finish and failure of all scheduled jobs.
* After a while the `monitored_scheduled_task_log_items` might become big.
* Here you can specify the amount of days log items should be kept.
*/
'delete_log_items_older_than_days' => 30,
/*
* The date format used for all dates displayed on the output of commands
* provided by this package.
*/
'date_format' => 'Y-m-d H:i:s',
/*
* Oh Dear can notify you via Mail, Slack, SMS, web hooks, ... when a
* scheduled task does not run on time.
*
* More info: https://ohdear.app/cron-checks
*/
'oh_dear' => [
/*
* You can generate an API token at the Oh Dear user settings screen
*
* https://ohdear.app/user-settings/api
*/
'api_token' => env('OH_DEAR_API_TOKEN', ''),
/*
* The id of the site you want to sync the schedule with.
*
* You'll find this id on the settings page of a site at Oh Dear.
*/
'site_id' => env('OH_DEAR_SITE_ID'),
/*
* To keep scheduled jobs as short as possible, Oh Dear will be pinged
* via a queued job. Here you can specify the name of the queue you wish to use.
*/
'queue' => env('OH_DEAR_QUEUE'),
],
];
```
#### Cleaning the database
You must register the `schedule-monitor:clean` tasks in your console kernel. This command will clean up old records from the schedule monitor log table.
```php
// app/Console/Kernel.php
class Kernel extends ConsoleKernel
{
protected function schedule(Schedule $schedule)
{
$schedule->command('schedule-monitor:sync')->dailyAt('04:56');
$schedule->command('schedule-monitor:clean')->daily();
}
}
```
#### Syncing the schedule
Every time you deploy your application, you should execute the `schedule-monitor:sync` command
```bash
schedule-monitor:sync
```
This command is responsible for syncing your schedule with the database, and optionally Oh Dear. We highly recommend adding this command to the script that deploys your production environment.
In a non-production environment you should manually run `schedule-monitor:sync`. You can verify if everything synced correctly using `schedule-monitor:list`.
## Usage
To monitor your schedule you should first run `schedule-monitor:sync`. This command will take a look at your schedule and create an entry for each task in the `monitored_scheduled_tasks` table.
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/sync.png)
To view all monitored scheduled tasks, you can run `schedule-monitor:list`. This command will list all monitored scheduled tasks. It will show you when a scheduled task has last started, finished, or failed.
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/list.png)
The package will write an entry to the `monitored_scheduled_task_log_items` table in the db each time a schedule tasks starts, end, fails or is skipped. Take a look at the contest of that table if you want to know when and how scheduled tasks did execute. The log items also hold other interesting metrics like memory usage, execution time, and more.
### Naming tasks
Schedule monitor will try to automatically determine a name for a scheduled task. For commands this is the command name, for anonymous jobs the class name of the first argument will be used. For some tasks, like scheduled closures, a name cannot be determined automatically.
To manually set a name of the scheduled task, you can tack on `monitorName()`.
Here's an example.
```php
// in app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('your-command')->daily()->monitorName('a-custom-name');
$schedule->call(fn () => 1 + 1)->hourly()->monitorName('addition-closure');
}
```
When you change the name of task, the schedule monitor will remove all log items of the monitor with the old name, and create a new monitor using the new name of the task.
### Setting a grace time
When the package detects that the last run of a scheduled task did not run in time, the `schedule-monitor` list will display that task using a red background color. In this screenshot the task named `your-command` ran too late.
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/list-with-failure.png)
The package will determine that a task ran too late if it was not finished at the time it was supposed to run + the grace time. You can think of the grace time as the number of minutes that a task under normal circumstances needs to finish. By default, the package grants a grace time of 5 minutes to each task.
You can customize the grace time by using the `graceTimeInMinutes` method on a task. In this example a grace time of 10 minutes is used for the `your-command` task.
```php
// in app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('your-command')->daily()->graceTimeInMinutes(10);
}
```
### Ignoring scheduled tasks
You can avoid a scheduled task being monitored by tacking on `doNotMonitor` when scheduling the task.
```php
// in app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('your-command')->daily()->doNotMonitor();
}
```
### Getting notified when a scheduled task doesn't finish in time
This package can sync your schedule with the [Oh Dear](https://ohdear.app) cron check. Oh Dear will send you a notification whenever a scheduled task does not finish on time.
To get started you will first need to install the Oh Dear SDK.
```bash
composer require ohdearapp/ohdear-php-sdk
```
Next you, need to make sure the `api_token` and `site_id` keys of the `schedule-monitor` are filled with an API token, and an Oh Dear site id. To verify that these values hold correct values you can run this command.
```bash
php artisan schedule-monitor:verify
```
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/verify.png)
To sync your schedule with Oh Dear run this command:
```bash
php artisan schedule-monitor:sync
```
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/sync-oh-dear.png)
After that, the `list` command should show that all the scheduled tasks in your app are registered on Oh Dear.
![screenshot](https://github.com/spatie/laravel-schedule-monitor/blob/master/docs/list-oh-dear.png)
To keep scheduled jobs as short as possible, Oh Dear will be pinged via queued jobs. To ensure speedy delivery to Oh Dear, and to avoid false positive notifications, we highly recommend creating a dedicated queue for these jobs. You can put the name of that queue in the `queue` key of the config file.
Oh Dear will wait for the completion of a schedule tasks for a given amount of minutes. This is called the grace time. By default, all scheduled tasks will have a grace time of 5 minutes. To customize this value, you can tack on `graceTimeInMinutes` to your scheduled tasks.
Here's an example where Oh Dear will send a notification if the task didn't finish by 00:10.
```php
// in app/Console/Kernel.php
protected function schedule(Schedule $schedule)
{
$schedule->command('your-command')->daily()->graceTimeInMinutes(10);
}
```
## Unsupported methods
Currently, this package does not work for tasks that use these methods:
- `between`
- `unlessBetween`
- `when`
- `skip`
## Third party scheduled task monitors
We assume that, when your scheduled tasks do not run properly, a scheduled task that sends out notifications would probably not run either. That's why this package doesn't send out notifications by itself.
These services can notify you when scheduled tasks do not run properly:
- [Oh Dear](https://ohdear.app)
- [thenping.me](https://thenping.me) (in closed beta)
- [Cronbox](https://cronbox.app)
- [Healthchecks.io](https://healthchecks.io)
- [Cronitor](https://cronitor.io)
- [Cronhub](https://cronhub.io/)
- [DeadMansSnitch](https://deadmanssnitch.com/)
- [CronAlarm](https://www.cronalarm.com/)
- [PushMon](https://www.pushmon.com/)
## Testing
``` bash
composer test
```
## Changelog
Please see [CHANGELOG](CHANGELOG.md) for more information on what has changed recently.
## Contributing
Please see [CONTRIBUTING](CONTRIBUTING.md) for details.
## Security
If you discover any security related issues, please email freek@spatie.be instead of using the issue tracker.
## Credits
- [Freek Van der Herten](https://github.com/freekmurze)
- [All Contributors](../../contributors)
## License
The MIT License (MIT). Please see [License File](LICENSE.md) for more information.

View File

@@ -0,0 +1,5 @@
## Upgrading
### From v1 to v2
Add a column `timezone` (string, nullable) to the `monitored_scheduled_tasks` table. In existing rows you should fill to column with the timezone in your app.

View File

@@ -0,0 +1,64 @@
{
"name": "spatie/laravel-schedule-monitor",
"description": "Monitor scheduled tasks in a Laravel app",
"keywords": [
"spatie",
"laravel-schedule-monitor"
],
"homepage": "https://github.com/spatie/laravel-schedule-monitor",
"license": "MIT",
"authors": [
{
"name": "Freek Van der Herten",
"email": "freek@spatie.be",
"homepage": "https://spatie.be",
"role": "Developer"
}
],
"require": {
"php": "^7.4",
"illuminate/bus": "^7.19|^8.0",
"lorisleiva/cron-translator": "^0.1.1"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "^2.16",
"laravel/legacy-factories": "^1.0.4",
"ohdearapp/ohdear-php-sdk": "^3.0",
"orchestra/testbench": "^5.0|^6.0",
"phpunit/phpunit": "^9.0",
"spatie/phpunit-snapshot-assertions": "^4.2",
"spatie/test-time": "^1.2",
"vimeo/psalm": "^3.11"
},
"suggest": {
"ohdearapp/ohdear-php-sdk": "Needed to sync your schedule with Oh Dear"
},
"autoload": {
"psr-4": {
"Spatie\\ScheduleMonitor\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"Spatie\\ScheduleMonitor\\Tests\\": "tests"
}
},
"scripts": {
"psalm": "vendor/bin/psalm",
"test": "vendor/bin/phpunit",
"test-coverage": "vendor/bin/phpunit --coverage-html coverage",
"format": "vendor/bin/php-cs-fixer fix --allow-risky=yes"
},
"config": {
"sort-packages": true
},
"extra": {
"laravel": {
"providers": [
"Spatie\\ScheduleMonitor\\ScheduleMonitorServiceProvider"
]
}
},
"minimum-stability": "dev",
"prefer-stable": true
}

View File

@@ -0,0 +1,44 @@
<?php
return [
/*
* The schedule monitor will log each start, finish and failure of all scheduled jobs.
* After a while the `monitored_scheduled_task_log_items` might become big.
* Here you can specify the amount of days log items should be kept.
*/
'delete_log_items_older_than_days' => 30,
/*
* The date format used for all dates displayed on the output of commands
* provided by this package.
*/
'date_format' => 'Y-m-d H:i:s',
/*
* Oh Dear can notify you via Mail, Slack, SMS, web hooks, ... when a
* scheduled task does not run on time.
*
* More info: https://ohdear.app/cron-checks
*/
'oh_dear' => [
/*
* You can generate an API token at the Oh Dear user settings screen
*
* https://ohdear.app/user-settings/api
*/
'api_token' => env('OH_DEAR_API_TOKEN', ''),
/*
* The id of the site you want to sync the schedule with.
*
* You'll find this id on the settings page of a site at Oh Dear.
*/
'site_id' => env('OH_DEAR_SITE_ID'),
/*
* To keep scheduled jobs as short as possible, Oh Dear will be pinged
* via a queued job. Here you can specify the name of the queue you wish to use.
*/
'queue' => env('OH_DEAR_QUEUE'),
],
];

View File

@@ -0,0 +1,50 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
class CreateScheduleMonitorTables extends Migration
{
public function up()
{
Schema::create('monitored_scheduled_tasks', function (Blueprint $table) {
$table->bigIncrements('id');
$table->string('name');
$table->string('type')->nullable();
$table->string('cron_expression');
$table->string('timezone')->nullable();
$table->string('ping_url')->nullable();
$table->dateTime('last_started_at')->nullable();
$table->dateTime('last_finished_at')->nullable();
$table->dateTime('last_failed_at')->nullable();
$table->dateTime('last_skipped_at')->nullable();
$table->dateTime('registered_on_oh_dear_at')->nullable();
$table->dateTime('last_pinged_at')->nullable();
$table->integer('grace_time_in_minutes');
$table->timestamps();
});
Schema::create('monitored_scheduled_task_log_items', function (Blueprint $table) {
$table->bigIncrements('id');
$table->unsignedBigInteger('monitored_scheduled_task_id');
$table
->foreign('monitored_scheduled_task_id', 'fk_scheduled_task_id')
->references('id')
->on('monitored_scheduled_tasks')
->cascadeOnDelete();
$table->string('type');
$table->json('meta')->nullable();
$table->timestamps();
});
}
}

View File

@@ -0,0 +1,25 @@
<?xml version="1.0"?>
<psalm
errorLevel="4"
findUnusedVariablesAndParams="true"
resolveFromConfigFile="true"
useDocblockPropertyTypes="true"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="https://getpsalm.org/schema/config"
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
>
<projectFiles>
<directory name="src"/>
<ignoreFiles>
<directory name="vendor" />
</ignoreFiles>
</projectFiles>
<issueHandlers>
<UndefinedThisPropertyAssignment>
<errorLevel type="suppress">
<file name="src/ScheduleMonitorServiceProvider.php"/>
</errorLevel>
</UndefinedThisPropertyAssignment>
</issueHandlers>
</psalm>

View File

@@ -0,0 +1,29 @@
<?php
namespace Spatie\ScheduleMonitor\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;
class CleanLogCommand extends Command
{
public $signature = 'schedule-monitor:clean';
public $description = 'Display monitored scheduled tasks';
public function handle()
{
$cutOffInDays = config('schedule-monitor.delete_log_items_older_than_days');
$this->comment('Deleting all log items older than ' . $cutOffInDays .' '. Str::plural('day', $cutOffInDays) . '...');
$cutOff = now()->subDays(config('schedule-monitor.delete_log_items_older_than_days'));
$numberOfRecordsDeleted = MonitoredScheduledTaskLogItem::query()
->where('created_at', '<', $cutOff->toDateTimeString())
->delete();
$this->info('Deleted ' . $numberOfRecordsDeleted . ' '. Str::plural('log item', $numberOfRecordsDeleted) . '!');
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Spatie\ScheduleMonitor\Commands;
use Illuminate\Console\Command;
use Spatie\ScheduleMonitor\Commands\Tables\DuplicateTasksTable;
use Spatie\ScheduleMonitor\Commands\Tables\MonitoredTasksTable;
use Spatie\ScheduleMonitor\Commands\Tables\ReadyForMonitoringTasksTable;
use Spatie\ScheduleMonitor\Commands\Tables\UnnamedTasksTable;
class ListCommand extends Command
{
public $signature = 'schedule-monitor:list';
public $description = 'Display monitored scheduled tasks';
public function handle()
{
(new MonitoredTasksTable($this))->render();
(new ReadyForMonitoringTasksTable($this))->render();
(new UnnamedTasksTable($this))->render();
(new DuplicateTasksTable($this))->render();
$this->line('');
}
}

View File

@@ -0,0 +1,107 @@
<?php
namespace Spatie\ScheduleMonitor\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Str;
use OhDear\PhpSdk\OhDear;
use OhDear\PhpSdk\Resources\CronCheck;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTask;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class SyncCommand extends Command
{
public $signature = 'schedule-monitor:sync';
public $description = 'Sync the schedule of the app with the schedule monitor';
public function handle()
{
$this->info('Start syncing schedule...' . PHP_EOL);
$this
->syncScheduledTasksWithDatabase()
->syncMonitoredScheduledTaskWithOhDear();
$monitoredScheduledTasksCount = MonitoredScheduledTask::count();
$this->info('');
$this->info('All done! Now monitoring ' . $monitoredScheduledTasksCount . ' ' . Str::plural('scheduled task', $monitoredScheduledTasksCount) . '.');
$this->info('');
$this->info('Run `php artisan schedule-monitor:list` to see which jobs are now monitored.');
}
protected function syncScheduledTasksWithDatabase(): self
{
$this->comment('Start syncing schedule with database...');
$monitoredScheduledTasks = ScheduledTasks::createForSchedule()
->uniqueTasks()
->map(function (Task $task) {
return MonitoredScheduledTask::updateOrCreate(
['name' => $task->name()],
[
'type' => $task->type(),
'cron_expression' => $task->cronExpression(),
'timezone' => $task->timezone(),
'grace_time_in_minutes' => $task->graceTimeInMinutes(),
]
);
});
MonitoredScheduledTask::query()
->whereNotIn('id', $monitoredScheduledTasks->pluck('id'))
->delete();
return $this;
}
protected function syncMonitoredScheduledTaskWithOhDear(): self
{
if (! class_exists(OhDear::class)) {
return $this;
}
$siteId = config('schedule-monitor.oh_dear.site_id');
if (! $siteId) {
$this->warn('Not syncing schedule with Oh Dear because not `site_id` is not set in the `oh-dear` config file. Learn how to set this up at https://ohdear.app/TODO-add-link.');
return $this;
}
$this->comment('Start syncing schedule with Oh Dear...');
$monitoredScheduledTasks = MonitoredScheduledTask::get();
$cronChecks = $monitoredScheduledTasks
->map(function (MonitoredScheduledTask $monitoredScheduledTask) {
return [
'name' => $monitoredScheduledTask->name,
'type' => 'cron',
'cron_expression' => $monitoredScheduledTask->cron_expression,
'grace_time_in_minutes' => $monitoredScheduledTask->grace_time_in_minutes,
'server_timezone' => $monitoredScheduledTask->timezone,
'description' => '',
];
})
->toArray();
$cronChecks = app(OhDear::class)->site($siteId)->syncCronChecks($cronChecks);
$this->comment('Successfully synced schedule with Oh Dear!');
collect($cronChecks)
->each(
function (CronCheck $cronCheck) {
if (! $monitoredScheduledTask = MonitoredScheduledTask::findForCronCheck($cronCheck)) {
return;
}
$monitoredScheduledTask->update(['ping_url' => $cronCheck->pingUrl]);
$monitoredScheduledTask->markAsRegisteredOnOhDear();
}
);
return $this;
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ScheduleMonitor\Commands\Tables;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class DuplicateTasksTable extends ScheduledTasksTable
{
public function render(): void
{
$duplicateTasks = ScheduledTasks::createForSchedule()->duplicateTasks();
if ($duplicateTasks->isEmpty()) {
return;
}
$this->command->line('');
$this->command->line('Duplicate tasks');
$this->command->line('---------------');
$this->command->line('These tasks could not be monitored because they have a duplicate name.');
$this->command->line('');
$headers = ['Type', 'Frequency'];
$rows = $duplicateTasks->map(function (Task $task) {
return [
'name' => $task->name(),
'type' => ucfirst($task->type()),
'cron_expression' => $task->humanReadableCron(),
];
});
$this->command->table($headers, $rows);
$this->command->line('');
$this->command->line('To monitor these tasks you should add `->monitorName()` in the schedule to manually specify a unique name.');
}
}

View File

@@ -0,0 +1,125 @@
<?php
namespace Spatie\ScheduleMonitor\Commands\Tables;
use OhDear\PhpSdk\OhDear;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class MonitoredTasksTable extends ScheduledTasksTable
{
public function render(): void
{
$this->command->line('');
$this->command->line('Monitored tasks');
$this->command->line('---------------');
$tasks = ScheduledTasks::createForSchedule()
->uniqueTasks()
->filter(fn (Task $task) => $task->isBeingMonitored());
if ($tasks->isEmpty()) {
$this->command->line('');
$this->command->warn('There currently are no tasks being monitored!');
return;
}
$headers = [
'Name',
'Type',
'Frequency',
'Last started at',
'Last finished at',
'Last failed at',
'Next run date',
'Grace time',
];
if ($this->usingOhDear()) {
$headers = array_merge($headers, [
'Registered at Oh Dear',
]);
}
$dateFormat = config('schedule-monitor.date_format');
$rows = $tasks->map(function (Task $task) use ($dateFormat) {
$row = [
'name' => $task->name(),
'type' => ucfirst($task->type()),
'cron_expression' => $task->humanReadableCron(),
'started_at' => optional($task->lastRunStartedAt())->format($dateFormat) ?? 'Did not start yet',
'finished_at' => $this->getLastRunFinishedAt($task),
'failed_at' => $this->getLastRunFailedAt($task),
'next_run' => $task->nextRunAt()->format($dateFormat),
'grace_time' => $task->graceTimeInMinutes(),
];
if ($this->usingOhDear()) {
$row = array_merge($row, [
'registered_at_oh_dear' => $task->isBeingMonitoredAtOhDear() ? '✅' : '❌',
]);
}
return $row;
});
$this->command->table($headers, $rows);
if ($this->usingOhDear()) {
if ($tasks->contains(fn (Task $task) => ! $task->isBeingMonitoredAtOhDear())) {
$this->command->line('');
$this->command->line('Some tasks are not registered on Oh Dear. You will not be notified when they do not run on time.');
$this->command->line('Run `php artisan schedule-monitor:sync` to register them and receive notifications.');
}
}
}
public function getLastRunFinishedAt(Task $task)
{
$dateFormat = config('schedule-monitor.date_format');
$formattedLastRunFinishedAt = optional($task->lastRunFinishedAt())->format($dateFormat) ?? '';
if ($task->lastRunFinishedTooLate()) {
$formattedLastRunFinishedAt = "<bg=red>{$formattedLastRunFinishedAt}</>";
}
return $formattedLastRunFinishedAt;
}
public function getLastRunFailedAt(Task $task): string
{
if (! $lastRunFailedAt = $task->lastRunFailedAt()) {
return '';
}
$dateFormat = config('schedule-monitor.date_format');
$formattedLastFailedAt = $lastRunFailedAt->format($dateFormat);
if ($task->lastRunFailed()) {
$formattedLastFailedAt = "<bg=red>{$formattedLastFailedAt}</>";
}
return $formattedLastFailedAt;
}
protected function usingOhDear(): bool
{
if (! class_exists(OhDear::class)) {
return false;
}
if (empty(config('schedule-monitor.oh_dear.api_token'))) {
return false;
}
if (empty(config('schedule-monitor.oh_dear.site_id'))) {
return false;
}
return true;
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Spatie\ScheduleMonitor\Commands\Tables;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class ReadyForMonitoringTasksTable extends ScheduledTasksTable
{
public function render(): void
{
$tasks = ScheduledTasks::createForSchedule()
->uniqueTasks()
->reject(fn (Task $task) => $task->isBeingMonitored());
if ($tasks->isEmpty()) {
return;
}
$this->command->line('');
$this->command->line('Run sync to start monitoring');
$this->command->line('----------------------------');
$this->command->line('');
$this->command->line('These tasks will be monitored after running `php artisan schedule-monitor:sync`');
$this->command->line('');
$tasks = ScheduledTasks::createForSchedule()
->uniqueTasks()
->reject(fn (Task $task) => $task->isBeingMonitored());
$headers = ['Name', 'Type', 'Frequency'];
$rows = $tasks->map(function (Task $task) {
return [
'name' => $task->name(),
'type' => ucfirst($task->type()),
'cron_expression' => $task->humanReadableCron(),
];
});
$this->command->table($headers, $rows);
}
}

View File

@@ -0,0 +1,17 @@
<?php
namespace Spatie\ScheduleMonitor\Commands\Tables;
use Illuminate\Console\Command;
abstract class ScheduledTasksTable
{
protected Command $command;
public function __construct(Command $command)
{
$this->command = $command;
}
abstract public function render(): void;
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ScheduleMonitor\Commands\Tables;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTasks;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class UnnamedTasksTable extends ScheduledTasksTable
{
public function render(): void
{
$unnamedTasks = ScheduledTasks::createForSchedule()->unnamedTasks();
if ($unnamedTasks->isEmpty()) {
return;
}
$this->command->line('');
$this->command->line('Unnamed tasks');
$this->command->line('-------------');
$this->command->line('These tasks cannot be monitored because no name could be determined for them.');
$this->command->line('');
$headers = ['Type', 'Frequency'];
$rows = $unnamedTasks->map(function (Task $task) {
return [
'type' => ucfirst($task->type()),
'cron_expression' => $task->humanReadableCron(),
];
});
$this->command->table($headers, $rows);
$this->command->line('');
$this->command->line('To monitor these tasks you should add `->monitorName()` in the schedule to manually specify a name.');
}
}

View File

@@ -0,0 +1,76 @@
<?php
namespace Spatie\ScheduleMonitor\Commands;
use Exception;
use Illuminate\Console\Command;
use OhDear\PhpSdk\OhDear;
class VerifyCommand extends Command
{
public $signature = 'schedule-monitor:verify';
public $description = 'Verify that the Oh Dear connection is configured correctly';
public function handle()
{
$ohDearConfig = config('schedule-monitor.oh_dear');
$this->info('Verifying if Oh Dear is configured correctly...');
$this->line('');
$this
->verifySdkInstalled()
->verifyApiToken($ohDearConfig)
->verifySiteId($ohDearConfig)
->verifyConnection($ohDearConfig);
$this->line('');
$this->info('All ok!');
$this->info('Run `php artisan schedule-monitor:sync` to sync your scheduled tasks with Oh Dear.');
}
public function verifySdkInstalled(): self
{
if (! class_exists(OhDear::class)) {
throw new Exception("You must install the Oh Dear SDK in order to sync your schedule with Oh Dear. Run `composer require ohdearapp/ohdear-php-sdk`.");
}
$this->comment('The Oh Dear SDK is installed.');
return $this;
}
protected function verifyApiToken(array $ohDearConfig): self
{
if (empty($ohDearConfig['api_token'])) {
throw new Exception('No API token found. Make sure you added an API token to the `api_token` key of the `server-monitor` config file. You can generate a new token here: https://ohdear.app/user-settings/api');
}
$this->comment('Oh Dear API token found.');
return $this;
}
protected function verifySiteId(array $ohDearConfig): self
{
if (empty($ohDearConfig['site_id'])) {
throw new Exception('No site id found. Make sure you added an site id to the `site_id` key of the `server-monitor` config file. You can found your site id on the settings page of a site on Oh Dear.');
}
$this->comment('Oh Dear site id found.');
return $this;
}
protected function verifyConnection(array $ohDearConfig)
{
$this->comment('Trying to reach Oh Dear...');
$site = app(OhDear::class)->site($ohDearConfig['site_id']);
$this->comment("Successfully connected to Oh Dear. The configured site URL is: {$site->sortUrl}");
return $this;
}
}

View File

@@ -0,0 +1,39 @@
<?php
namespace Spatie\ScheduleMonitor\EventHandlers;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\Schedule;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTask;
class BackgroundCommandListener
{
public function handle(CommandStarting $event)
{
if ($event->command !== 'schedule:finish') {
return;
}
collect(app(Schedule::class)->events())
->filter(fn (Event $task) => $task->runInBackground)
->each(function (Event $task) {
$task
->then(
function () use ($task) {
if (! $monitoredTask = MonitoredScheduledTask::findForTask($task)) {
return;
}
$event = new ScheduledTaskFinished(
$task,
0
);
$monitoredTask->markAsFinished($event);
}
);
});
}
}

View File

@@ -0,0 +1,36 @@
<?php
namespace Spatie\ScheduleMonitor\EventHandlers;
use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Events\ScheduledTaskSkipped;
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Contracts\Events\Dispatcher;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTask;
class ScheduledTaskEventSubscriber
{
public function subscribe(Dispatcher $events): void
{
$events->listen(
ScheduledTaskStarting::class,
fn (ScheduledTaskStarting $event) => optional(MonitoredScheduledTask::findForTask($event->task))->markAsStarting($event)
);
$events->listen(
ScheduledTaskFinished::class,
fn (ScheduledTaskFinished $event) => optional(MonitoredScheduledTask::findForTask($event->task))->markAsFinished($event)
);
$events->listen(
ScheduledTaskFailed::class,
fn (ScheduledTaskFailed $event) => optional(MonitoredScheduledTask::findForTask($event->task))->markAsFailed($event)
);
$events->listen(
ScheduledTaskSkipped::class,
fn (ScheduledTaskSkipped $event) => optional(MonitoredScheduledTask::findForTask($event->task))->markAsSkipped($event)
);
}
}

View File

@@ -0,0 +1,41 @@
<?php
namespace Spatie\ScheduleMonitor\Jobs;
use Illuminate\Bus\Queueable;
use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Http;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;
use Spatie\ScheduleMonitor\Support\OhDearPayload\OhDearPayloadFactory;
class PingOhDearJob implements ShouldQueue
{
public $deleteWhenMissingModels = true;
use Dispatchable, SerializesModels, InteractsWithQueue, Queueable;
public MonitoredScheduledTaskLogItem $logItem;
public function __construct(MonitoredScheduledTaskLogItem $logItem)
{
$this->logItem = $logItem;
if ($queue = config('schedule-monitor.oh_dear.queue')) {
$this->onQueue($queue);
}
}
public function handle()
{
if (! $payload = OhDearPayloadFactory::createForLogItem($this->logItem)) {
return;
}
Http::post($payload->url(), $payload->data());
$this->logItem->monitoredScheduledTask->update(['last_pinged_at' => now()]);
}
}

View File

@@ -0,0 +1,178 @@
<?php
namespace Spatie\ScheduleMonitor\Models;
use Illuminate\Console\Events\ScheduledTaskFailed;
use Illuminate\Console\Events\ScheduledTaskFinished;
use Illuminate\Console\Events\ScheduledTaskSkipped;
use Illuminate\Console\Events\ScheduledTaskStarting;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Str;
use OhDear\PhpSdk\Resources\CronCheck;
use Spatie\ScheduleMonitor\Jobs\PingOhDearJob;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\ScheduledTaskFactory;
class MonitoredScheduledTask extends Model
{
public $guarded = [];
protected $casts = [
'registered_on_oh_dear_at' => 'datetime',
'last_pinged_at' => 'datetime',
'last_started_at' => 'datetime',
'last_finished_at' => 'datetime',
'last_skipped_at' => 'datetime',
'last_failed_at' => 'datetime',
'grace_time_in_minutes' => 'integer',
];
public function logItems(): HasMany
{
return $this->hasMany(MonitoredScheduledTaskLogItem::class)->orderByDesc('id');
}
public static function findByName(string $name): ?self
{
return MonitoredScheduledTask::where('name', $name)->first();
}
public static function findForTask(Event $event): ?self
{
$task = ScheduledTaskFactory::createForEvent($event);
if (empty($task->name())) {
return null;
}
return MonitoredScheduledTask::findByName($task->name());
}
public static function findForCronCheck(CronCheck $cronCheck): ?self
{
return MonitoredScheduledTask::findByName($cronCheck->name);
}
public function markAsRegisteredOnOhDear(): self
{
if (is_null($this->registered_on_oh_dear_at)) {
$this->update(['registered_on_oh_dear_at' => now()]);
}
return $this;
}
public function markAsStarting(ScheduledTaskStarting $event): self
{
$logItem = $this->createLogItem(MonitoredScheduledTaskLogItem::TYPE_STARTING);
$logItem->updateMeta([
'memory' => memory_get_usage(true),
]);
$this->update([
'last_started_at' => now(),
]);
return $this;
}
public function markAsFinished(ScheduledTaskFinished $event): self
{
if ($this->eventConcernsBackgroundTaskThatCompletedInForeground($event)) {
return $this;
}
if ($event->task->exitCode !== 0 && ! is_null($event->task->exitCode)) {
return $this->markAsFailed($event);
}
$logItem = $this->createLogItem(MonitoredScheduledTaskLogItem::TYPE_FINISHED);
$logItem->updateMeta([
'runtime' => $event->task->runInBackground ? null : $event->runtime,
'exit_code' => $event->task->exitCode,
'memory' => $event->task->runInBackground ? null : memory_get_usage(true),
]);
$this->update(['last_finished_at' => now()]);
$this->pingOhDear($logItem);
return $this;
}
public function eventConcernsBackgroundTaskThatCompletedInForeground(ScheduledTaskFinished $event): bool
{
if (! $event->task->runInBackground) {
return false;
}
return $event->task->exitCode === null;
}
/**
* @param ScheduledTaskFailed|ScheduledTaskFinished $event
*
* @return $this
*/
public function markAsFailed($event): self
{
$logItem = $this->createLogItem(MonitoredScheduledTaskLogItem::TYPE_FAILED);
if ($event instanceof ScheduledTaskFailed) {
$logItem->updateMeta([
'failure_message' => Str::limit(optional($event->exception)->getMessage(), 255),
]);
}
if ($event instanceof ScheduledTaskFinished) {
$logItem->updateMeta([
'runtime' => $event->runtime,
'exit_code' => $event->task->exitCode,
'memory' => memory_get_usage(true),
]);
}
$this->update(['last_failed_at' => now()]);
$this->pingOhDear($logItem);
return $this;
}
public function markAsSkipped(ScheduledTaskSkipped $event): self
{
$this->createLogItem(MonitoredScheduledTaskLogItem::TYPE_SKIPPED);
$this->update(['last_skipped_at' => now()]);
return $this;
}
protected function pingOhDear(MonitoredScheduledTaskLogItem $logItem): self
{
if (empty($this->ping_url)) {
return $this;
}
if (! in_array($logItem->type, [
MonitoredScheduledTaskLogItem::TYPE_FAILED,
MonitoredScheduledTaskLogItem::TYPE_FINISHED,
])) {
return $this;
}
dispatch(new PingOhDearJob($logItem));
return $this;
}
protected function createLogItem(string $type): MonitoredScheduledTaskLogItem
{
return $this->logItems()->create([
'type' => $type,
]);
}
}

View File

@@ -0,0 +1,32 @@
<?php
namespace Spatie\ScheduleMonitor\Models;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
class MonitoredScheduledTaskLogItem extends Model
{
public $guarded = [];
public const TYPE_STARTING = 'starting';
public const TYPE_FINISHED = 'finished';
public const TYPE_FAILED = 'failed';
public const TYPE_SKIPPED = 'skipped';
public $casts = [
'meta' => 'array',
];
public function monitoredScheduledTask(): BelongsTo
{
return $this->belongsTo(MonitoredScheduledTask::class);
}
public function updateMeta(array $values): self
{
$this->update(['meta' => $values]);
return $this;
}
}

View File

@@ -0,0 +1,108 @@
<?php
namespace Spatie\ScheduleMonitor;
use Illuminate\Console\Events\CommandStarting;
use Illuminate\Console\Scheduling\Event as SchedulerEvent;
use Illuminate\Support\Facades\Event;
use Illuminate\Support\ServiceProvider;
use OhDear\PhpSdk\OhDear;
use Spatie\ScheduleMonitor\Commands\CleanLogCommand;
use Spatie\ScheduleMonitor\Commands\ListCommand;
use Spatie\ScheduleMonitor\Commands\SyncCommand;
use Spatie\ScheduleMonitor\Commands\VerifyCommand;
use Spatie\ScheduleMonitor\EventHandlers\BackgroundCommandListener;
use Spatie\ScheduleMonitor\EventHandlers\ScheduledTaskEventSubscriber;
class ScheduleMonitorServiceProvider extends ServiceProvider
{
public function boot()
{
$this
->registerPublishables()
->registerCommands()
->configureOhDearApi()
->registerEventHandlers()
->registerSchedulerEventMacros();
}
public function register()
{
$this->mergeConfigFrom(__DIR__.'/../config/schedule-monitor.php', 'schedule-monitor');
}
protected function registerPublishables(): self
{
if ($this->app->runningInConsole()) {
$this->publishes([
__DIR__ . '/../config/schedule-monitor.php' => config_path('schedule-monitor.php'),
], 'config');
if (! class_exists('CreateScheduleMonitorTables')) {
$this->publishes([
__DIR__ . '/../database/migrations/create_schedule_monitor_tables.php.stub' => database_path('migrations/' . date('Y_m_d_His', time()) . '_create_schedule_monitor_tables.php'),
], 'migrations');
}
}
return $this;
}
protected function registerCommands(): self
{
$this->commands([
CleanLogCommand::class,
ListCommand::class,
SyncCommand::class,
VerifyCommand::class,
]);
return $this;
}
protected function configureOhDearApi(): self
{
if (! class_exists(OhDear::class)) {
return $this;
}
$this->app->bind(OhDear::class, function () {
$apiToken = config('schedule-monitor.oh_dear.api_token');
return new OhDear($apiToken, 'https://ohdear.app/api/');
});
return $this;
}
protected function registerEventHandlers(): self
{
Event::subscribe(ScheduledTaskEventSubscriber::class);
Event::listen(CommandStarting::class, BackgroundCommandListener::class);
return $this;
}
protected function registerSchedulerEventMacros(): self
{
SchedulerEvent::macro('monitorName', function (string $monitorName) {
$this->monitorName = $monitorName;
return $this;
});
SchedulerEvent::macro('graceTimeInMinutes', function (int $graceTimeInMinutes) {
$this->graceTimeInMinutes = $graceTimeInMinutes;
return $this;
});
SchedulerEvent::macro('doNotMonitor', function () {
$this->doNotMonitor = true;
return $this;
});
return $this;
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Spatie\ScheduleMonitor\Support\OhDearPayload;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;
use Spatie\ScheduleMonitor\Support\OhDearPayload\Payloads\FailedPayload;
use Spatie\ScheduleMonitor\Support\OhDearPayload\Payloads\FinishedPayload;
use Spatie\ScheduleMonitor\Support\OhDearPayload\Payloads\Payload;
class OhDearPayloadFactory
{
public static function createForLogItem(MonitoredScheduledTaskLogItem $logItem): ?Payload
{
$payloadClasses = [
FailedPayload::class,
FinishedPayload::class,
];
$payloadClass = collect($payloadClasses)
->first(fn (string $payloadClass) => $payloadClass::canHandle($logItem));
if (! $payloadClass) {
return null;
}
return new $payloadClass($logItem);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Spatie\ScheduleMonitor\Support\OhDearPayload\Payloads;
use Illuminate\Support\Arr;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;
class FailedPayload extends Payload
{
public static function canHandle(MonitoredScheduledTaskLogItem $logItem): bool
{
return $logItem->type === MonitoredScheduledTaskLogItem::TYPE_FAILED;
}
public function url()
{
return "{$this->baseUrl()}/failed";
}
public function data(): array
{
return Arr::only($this->logItem->meta ?? [], [
'failure_message',
]);
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Spatie\ScheduleMonitor\Support\OhDearPayload\Payloads;
use Illuminate\Support\Arr;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;
class FinishedPayload extends Payload
{
public static function canHandle(MonitoredScheduledTaskLogItem $logItem): bool
{
return $logItem->type === MonitoredScheduledTaskLogItem::TYPE_FINISHED;
}
public function url()
{
return "{$this->baseUrl()}/finished";
}
public function data(): array
{
return Arr::only($this->logItem->meta ?? [], [
'runtime',
'exit_code',
'memory',
]);
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Spatie\ScheduleMonitor\Support\OhDearPayload\Payloads;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTaskLogItem;
abstract class Payload
{
protected MonitoredScheduledTaskLogItem $logItem;
abstract public static function canHandle(MonitoredScheduledTaskLogItem $logItem): bool;
public function __construct(MonitoredScheduledTaskLogItem $logItem)
{
$this->logItem = $logItem;
}
abstract public function url();
abstract public function data();
protected function baseUrl(): string
{
return $this->logItem->monitoredScheduledTask->ping_url;
}
}

View File

@@ -0,0 +1,26 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks;
use Illuminate\Console\Scheduling\Event;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\ClosureTask;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\CommandTask;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\JobTask;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\ShellTask;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class ScheduledTaskFactory
{
public static function createForEvent(Event $event): Task
{
$taskClass = collect([
ClosureTask::class,
JobTask::class,
CommandTask::class,
ShellTask::class,
])
->first(fn (string $taskClass) => $taskClass::canHandleEvent($event));
return new $taskClass($event);
}
}

View File

@@ -0,0 +1,66 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Console\Scheduling\Schedule;
use Illuminate\Support\Collection;
use Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks\Task;
class ScheduledTasks
{
protected Schedule $schedule;
protected Collection $tasks;
public static function createForSchedule()
{
$schedule = app(Schedule::class);
return new static($schedule);
}
public function __construct(Schedule $schedule)
{
$this->schedule = $schedule;
$this->tasks = collect($this->schedule->events())
->map(
fn (Event $event): Task => ScheduledTaskFactory::createForEvent($event)
);
}
public function uniqueTasks(): Collection
{
return $this->tasks
->filter(fn (Task $task) => $task->shouldMonitor())
->reject(fn (Task $task) => empty($task->name()))
->unique(fn (Task $task) => $task->name())
->values();
}
public function duplicateTasks(): Collection
{
$uniqueTasksIds = $this->uniqueTasks()
->map(fn (Task $task) => $task->uniqueId())
->toArray();
return $this->tasks
->filter(fn (Task $task) => $task->shouldMonitor())
->reject(fn (Task $task) => empty($task->name()))
->reject(fn (Task $task) => in_array($task->uniqueId(), $uniqueTasksIds))
->values();
}
public function unmonitoredTasks(): Collection
{
return $this->tasks->reject(fn (Task $task) => $task->shouldMonitor());
}
public function unnamedTasks(): Collection
{
return $this->tasks
->filter(fn (Task $task) => empty($task->name()))
->values();
}
}

View File

@@ -0,0 +1,28 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks;
use Illuminate\Console\Scheduling\CallbackEvent;
use Illuminate\Console\Scheduling\Event;
class ClosureTask extends Task
{
public static function canHandleEvent(Event $event): bool
{
if (! $event instanceof CallbackEvent) {
return false;
}
return $event->getSummaryForDisplay() === 'Closure';
}
public function type(): string
{
return 'closure';
}
public function defaultName(): ?string
{
return null;
}
}

View File

@@ -0,0 +1,45 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks;
use Illuminate\Console\Scheduling\CallbackEvent;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Str;
class CommandTask extends Task
{
public static function canHandleEvent(Event $event): bool
{
if ($event instanceof CallbackEvent) {
return false;
}
return Str::contains($event->command, self::artisanString());
}
public function defaultName(): ?string
{
return Str::after($this->event->command, self::artisanString() . ' ');
}
public function type(): string
{
return 'command';
}
public static function artisanString(): string
{
$baseString = 'artisan';
$quote = self::isRunningWindows()
? '"'
: "'";
return "{$quote}{$baseString}{$quote}";
}
protected static function isRunningWindows()
{
return strtoupper(substr(PHP_OS, 0, 3)) === 'WIN';
}
}

View File

@@ -0,0 +1,38 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks;
use Illuminate\Console\Scheduling\CallbackEvent;
use Illuminate\Console\Scheduling\Event;
class JobTask extends Task
{
protected Event $task;
public static function canHandleEvent(Event $event): bool
{
if (! $event instanceof CallbackEvent) {
return false;
}
if (! is_null($event->command)) {
return false;
}
if (empty($event->description)) {
return false;
}
return class_exists($event->description);
}
public function defaultName(): ?string
{
return $this->event->description;
}
public function type(): string
{
return 'job';
}
}

View File

@@ -0,0 +1,29 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks;
use Illuminate\Console\Scheduling\CallbackEvent;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Str;
class ShellTask extends Task
{
public static function canHandleEvent(Event $event): bool
{
if ($event instanceof CallbackEvent) {
return true;
}
return true;
}
public function defaultName(): ?string
{
return Str::limit($this->event->command, 255);
}
public function type(): string
{
return 'shell';
}
}

View File

@@ -0,0 +1,172 @@
<?php
namespace Spatie\ScheduleMonitor\Support\ScheduledTasks\Tasks;
use Carbon\CarbonInterface;
use Cron\CronExpression;
use Illuminate\Console\Scheduling\Event;
use Illuminate\Support\Facades\Date;
use Illuminate\Support\Str;
use Lorisleiva\CronTranslator\CronParsingException;
use Lorisleiva\CronTranslator\CronTranslator;
use Spatie\ScheduleMonitor\Models\MonitoredScheduledTask;
abstract class Task
{
protected Event $event;
protected string $uniqueId;
protected ?MonitoredScheduledTask $monitoredScheduledTask = null;
abstract public static function canHandleEvent(Event $event): bool;
abstract public function defaultName(): ?string;
abstract public function type(): string;
public function __construct(Event $event)
{
$this->event = $event;
$this->uniqueId = (string)Str::uuid();
if (! empty($this->name())) {
$this->monitoredScheduledTask = MonitoredScheduledTask::findByName($this->name());
}
}
public function uniqueId(): string
{
return $this->uniqueId;
}
public function name(): ?string
{
return $this->event->monitorName ?? $this->defaultName();
}
public function shouldMonitor(): bool
{
if (! isset($this->event->doNotMonitor)) {
return true;
}
return ! $this->event->doNotMonitor;
}
public function isBeingMonitored(): bool
{
return ! is_null($this->monitoredScheduledTask);
}
public function isBeingMonitoredAtOhDear(): bool
{
if (! $this->isBeingMonitored()) {
return false;
}
return ! empty($this->monitoredScheduledTask->ping_url);
}
public function previousRunAt(): CarbonInterface
{
$dateTime = CronExpression::factory($this->cronExpression())->getPreviousRunDate(now());
return Date::instance($dateTime);
}
public function nextRunAt(CarbonInterface $now = null): CarbonInterface
{
$dateTime = CronExpression::factory($this->cronExpression())->getNextRunDate(
$now ?? now(),
0,
false,
$this->timezone()
);
$date = Date::instance($dateTime);
$date->setTimezone(config('app.timezone'));
return $date;
}
public function lastRunStartedAt(): ?CarbonInterface
{
return optional($this->monitoredScheduledTask)->last_started_at;
}
public function lastRunFinishedAt(): ?CarbonInterface
{
return optional($this->monitoredScheduledTask)->last_finished_at;
}
public function lastRunFailedAt(): ?CarbonInterface
{
return optional($this->monitoredScheduledTask)->last_failed_at;
}
public function lastRunSkippedAt(): ?CarbonInterface
{
return optional($this->monitoredScheduledTask)->last_skipped_at;
}
public function lastRunFinishedTooLate(): bool
{
if (! $this->isBeingMonitored()) {
return false;
}
$lastFinishedAt = $this->lastRunFinishedAt()
? $this->lastRunFinishedAt()
: $this->monitoredScheduledTask->created_at;
$expectedNextRunStart = $this->nextRunAt($lastFinishedAt->subSecond());
$shouldHaveFinishedAt = $expectedNextRunStart->addMinutes($this->graceTimeInMinutes());
return $shouldHaveFinishedAt->isPast();
}
public function lastRunFailed(): bool
{
if (! $this->isBeingMonitored()) {
return false;
}
if (! $lastRunFailedAt = $this->lastRunFailedAt()) {
return false;
}
if (! $lastRunStartedAt = $this->lastRunStartedAt()) {
return true;
}
return $lastRunFailedAt->isAfter($lastRunStartedAt->subSecond());
}
public function graceTimeInMinutes()
{
return $this->event->graceTimeInMinutes ?? 5;
}
public function cronExpression(): string
{
return $this->event->getExpression();
}
public function timezone(): string
{
return (string)$this->event->timezone;
}
public function humanReadableCron(): string
{
try {
return CronTranslator::translate($this->cronExpression());
} catch (CronParsingException $exception) {
return $this->cronExpression();
}
}
}