diff --git a/db/app_uoj233.sql b/db/app_uoj233.sql index 1d2c42f..70b46ed 100644 --- a/db/app_uoj233.sql +++ b/db/app_uoj233.sql @@ -429,6 +429,34 @@ LOCK TABLES `custom_test_submissions` WRITE; /*!40000 ALTER TABLE `custom_test_submissions` ENABLE KEYS */; UNLOCK TABLES; +-- +-- Table structure for table `emails` +-- + +/*!40101 SET @saved_cs_client = @@character_set_client */; +/*!40101 SET character_set_client = utf8mb4 */; +CREATE TABLE `emails` ( + `id` int UNSIGNED NOT NULL AUTO_INCREMENT, + `receiver` varchar(20) NOT NULL, + `subject` varchar(100) NOT NULL, + `content` text NOT NULL, + `created_at` datetime NOT NULL, + `send_time` datetime DEFAULT NULL, + `priority` int NOT NULL DEFAULT 0, + PRIMARY KEY (`id`), + KEY `send_time` (`send_time`) +) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4; +/*!40101 SET character_set_client = @saved_cs_client */; + +-- +-- Dumping data for table `emails` +-- + +LOCK TABLES `emails` WRITE; +/*!40000 ALTER TABLE `emails` DISABLE KEYS */; +/*!40000 ALTER TABLE `emails` ENABLE KEYS */; +UNLOCK TABLES; + -- -- Table structure for table `friend_links` -- diff --git a/web/app/cli.php b/web/app/cli.php index 280a7b6..c836c5b 100644 --- a/web/app/cli.php +++ b/web/app/cli.php @@ -10,65 +10,79 @@ require $_SERVER['DOCUMENT_ROOT'] . '/app/libs/uoj-lib.php'; $handlers = [ 'upgrade:up' => function ($name) { if (func_num_args() != 1) { - die("php7.4 cli.php upgrade:up \n"); + print("php cli.php upgrade:up \n"); + exit(1); } - Upgrader::transaction(function() use ($name) { + Upgrader::transaction(function () use ($name) { Upgrader::up($name); }); - die("finished!\n"); + print("finished!\n"); }, 'upgrade:down' => function ($name) { if (func_num_args() != 1) { - die("php7.4 cli.php upgrade:down \n"); + print("php cli.php upgrade:down \n"); + exit(1); } - Upgrader::transaction(function() use ($name) { + Upgrader::transaction(function () use ($name) { Upgrader::down($name); }); - die("finished!\n"); + print("finished!\n"); }, 'upgrade:refresh' => function ($name) { if (func_num_args() != 1) { - die("php7.4 cli.php upgrade:refresh \n"); + print("php cli.php upgrade:refresh \n"); + exit(1); } - Upgrader::transaction(function() use ($name) { + Upgrader::transaction(function () use ($name) { Upgrader::refresh($name); }); - die("finished!\n"); + print("finished!\n"); }, 'upgrade:remove' => function ($name) { if (func_num_args() != 1) { - die("php7.4 cli.php upgrade:remove \n"); + print("php cli.php upgrade:remove \n"); + exit(1); } - Upgrader::transaction(function() use ($name) { + Upgrader::transaction(function () use ($name) { Upgrader::remove($name); }); - die("finished!\n"); + print("finished!\n"); }, 'upgrade:latest' => function () { if (func_num_args() != 0) { - die("php7.4 cli.php upgrade:latest\n"); + print("php cli.php upgrade:latest\n"); + exit(1); } - Upgrader::transaction(function() { + Upgrader::transaction(function () { Upgrader::upgradeToLatest(); }); - die("finished!\n"); + print("finished!\n"); }, 'upgrade:remove-all' => function () { if (func_num_args() != 0) { - die("php7.4 cli.php upgrade:remove-all\n"); + print("php cli.php upgrade:remove-all\n"); + exit(1); } - Upgrader::transaction(function() { + Upgrader::transaction(function () { Upgrader::removeAll(); }); - die("finished!\n"); + print("finished!\n"); }, - 'help' => 'showHelp' + 'email:send-all' => function () { + if (func_num_args() != 0) { + print("php cli.php email:send-all\n"); + exit(1); + } + UOJMail::cronSendEmail(); + print("finished!\n"); + }, + 'help' => 'showHelp', ]; function showHelp() { global $handlers; echo "UOJ Command-Line Interface\n"; - echo "php7.4 cli.php params1 params2 ...\n"; + echo "php cli.php params1 params2 ...\n"; echo "\n"; echo "The following tasks are available:\n"; foreach ($handlers as $cmd => $handler) { diff --git a/web/app/composer.json b/web/app/composer.json index c857c4d..c248713 100644 --- a/web/app/composer.json +++ b/web/app/composer.json @@ -6,7 +6,8 @@ "erusev/parsedown": "^1.7", "php-curl-class/php-curl-class": "^9.13", "ext-dom": "20031129", - "ivopetkov/html5-dom-document-php": "2.*" + "ivopetkov/html5-dom-document-php": "2.*", + "peppeocchi/php-cron-scheduler": "^4.0" }, "autoload": { "classmap": [ diff --git a/web/app/controllers/forgot_pw.php b/web/app/controllers/forgot_pw.php index 838f591..5488b48 100644 --- a/web/app/controllers/forgot_pw.php +++ b/web/app/controllers/forgot_pw.php @@ -46,7 +46,7 @@ $forgot_form->handle = function (&$vdata) { unset($_SESSION['phrase']); if (!$user['email']) { - becomeMsgPage('用户未填写邮件地址,请联系管理员重置!'); + becomeMsgPage('用户未填写邮件地址,请联系管理员重置密码!'); } $oj_name = UOJConfig::$data['profile']['oj-name']; @@ -54,7 +54,6 @@ $forgot_form->handle = function (&$vdata) { $check_code = md5($user['username'] . "+" . $password . '+' . UOJTime::$time_now_str); $sufs = base64url_encode($user['username'] . "." . $check_code); $url = HTML::url("/reset_password", ['params' => ['p' => $sufs]]); - $oj_url = HTML::url('/'); $name = $user['username']; $remote_addr = UOJContext::remoteAddr(); $http_x_forwarded_for = UOJContext::httpXForwardedFor(); @@ -64,53 +63,28 @@ $forgot_form->handle = function (&$vdata) { $name .= ' (' . $user['realname'] . ')'; } - $html = << + sendEmail($user['username'], $oj_name_short . ' 密码找回', <<您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:{$url} (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)

+

如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。

-

{$name} 您好,

+
    +
  • 请求 IP: {$remote_addr}
  • +
  • 转发源 IP:{$http_x_forwarded_for}
  • +
  • 用户代理: {$user_agent}
  • +
+ EOD); -

您最近告知我们需要重置您在 {$oj_name_short} 上账号的密码。请访问以下链接:{$url} (如果无法点击链接,请试着复制链接并粘贴至浏览器中打开。)

-

如果您没有请求重置密码,则忽略此信息。该链接将在 72 小时后自动过期失效。

+ DB::update([ + "update user_info", + "set", [ + 'extra' => DB::json_set('extra', '$.reset_password_check_code', $check_code, '$.reset_password_time', UOJTime::$time_now_str), + ], + "where", [ + "username" => $user['username'], + ], + ]); -
    -
  • 请求 IP: {$remote_addr} (转发来源: {$http_x_forwarded_for})
  • -
  • 用户代理: {$user_agent}
  • -
- -

{$oj_name}

-

{$oj_url}

-EOD; - - $mailer = UOJMail::noreply(); - $mailer->addAddress($user['email'], $user['username']); - $mailer->Subject = $oj_name_short . " 密码找回"; - $mailer->msgHTML($html); - - $res = retry_loop(function () use (&$mailer) { - $res = $mailer->send(); - - if ($res) return true; - - UOJLog::error($mailer->ErrorInfo); - - return false; - }); - - if (!$res) { - becomeMsgPage('

邮件发送失败,请重试!

返回
'); - } else { - DB::update([ - "update user_info", - "set", [ - 'extra' => DB::json_set('extra', '$.reset_password_check_code', $check_code, '$.reset_password_time', UOJTime::$time_now_str), - ], - "where", [ - "username" => $user['username'], - ], - ]); - - becomeMsgPage('

邮件发送成功,请检查收件箱!

如果邮件未出现在收件箱中,请检查垃圾箱。
'); - } + becomeMsgPage('

邮件已发送,请检查收件箱!

如果邮件未出现在收件箱中,请检查垃圾箱。
'); }; $forgot_form->runAtServer(); ?> diff --git a/web/app/controllers/login.php b/web/app/controllers/login.php index ff97b1f..b8d8c38 100644 --- a/web/app/controllers/login.php +++ b/web/app/controllers/login.php @@ -42,6 +42,22 @@ function handleLoginPost() { } Auth::login($user['username']); + + $remote_addr = UOJContext::remoteAddr(); + $http_x_forwarded_for = UOJContext::httpXForwardedFor(); + $user_agent = UOJContext::httpUserAgent(); + sendEmail($user['username'], '新登录', <<您收到这封邮件是因为有人通过以下方式登录了您的帐户:

+ +
    +
  • 请求 IP: {$remote_addr}
  • +
  • 转发源 IP:{$http_x_forwarded_for}
  • +
  • 用户代理: {$user_agent}
  • +
+ +

如果这是您进行的登录操作,请忽略此邮件。如果您没有进行过登录操作,请立即重置您账号的密码。

+ EOD); + return "ok"; } diff --git a/web/app/controllers/subdomain/blog/blog.php b/web/app/controllers/subdomain/blog/blog.php index 89259d4..d0b84aa 100644 --- a/web/app/controllers/subdomain/blog/blog.php +++ b/web/app/controllers/subdomain/blog/blog.php @@ -153,7 +153,7 @@ $reply_form->handle = function (&$vdata) { } if ($blog['poster'] !== Auth::id() && !in_array($blog['poster'], $notified)) { $notified[] = $blog['poster']; - $content = $user_link . '回复了您的博客 ' . $blog['title'] . ' :点击此处查看'; + $content = $user_link . ' 回复了您的博客 ' . $blog['title'] . ' :点击此处查看'; sendSystemMsg($blog['poster'], '博客新回复通知', $content); } @@ -195,8 +195,7 @@ if (UOJUserBlog::userHasManagePermission(Auth::user())) { sendSystemMsg( $comment->info['poster'], '评论隐藏通知', - "

" . UOJUser::getLink($comment->info['poster'], ['color' => false]) . " 您好:

" . - "

您为博客 " . UOJBlog::cur()->getLink() . " 回复的评论 “" . substr($comment->info['content'], 0, 30) . "……” 已被管理员隐藏,隐藏原因为 “{$reason}”。

" + "您为博客 " . UOJBlog::cur()->getLink() . " 回复的评论 “" . substr($comment->info['content'], 0, 30) . "……” 已被管理员隐藏,隐藏原因为 “{$reason}”。" ); } }; diff --git a/web/app/libs/uoj-html-lib.php b/web/app/libs/uoj-html-lib.php index 37d07c7..308294c 100644 --- a/web/app/libs/uoj-html-lib.php +++ b/web/app/libs/uoj-html-lib.php @@ -106,12 +106,9 @@ function getLongTablePageRawUri($page) { unset($param['page']); } - if ($param) { - return $path . '?' . http_build_query($param); - } else { - return $path; - } + return HTML::url($path, ['params' => $param]); } + function getLongTablePageUri($page) { return HTML::escape(getLongTablePageRawUri($page)); } diff --git a/web/app/libs/uoj-utility-lib.php b/web/app/libs/uoj-utility-lib.php index 5a9f027..a4e69e7 100644 --- a/web/app/libs/uoj-utility-lib.php +++ b/web/app/libs/uoj-utility-lib.php @@ -206,6 +206,16 @@ function sendSystemMsg($username, $title, $content) { "(receiver, title, content, send_time)", "values", DB::tuple([$username, $title, $content, DB::now()]) ]); + + sendEmail($username, $title, $content); +} + +function sendEmail($username, $title, $content) { + DB::insert([ + "insert into emails", + "(receiver, subject, content, created_at)", + "values", DB::tuple([$username, $title, $content, DB::now()]) + ]); } function retry_loop(callable $f, $retry = 5, $ms = 10) { diff --git a/web/app/models/UOJContest.php b/web/app/models/UOJContest.php index 4605ce2..3b74b85 100644 --- a/web/app/models/UOJContest.php +++ b/web/app/models/UOJContest.php @@ -66,12 +66,9 @@ class UOJContest { calcStandings($contest, $data, $score, $standings, ['update_contests_submissions' => true]); for ($i = 0; $i < count($standings); $i++) { - $user_link = UOJUser::getLink($standings[$i][2][0], ['color' => false]); $tail = $standings[$i][0] == $total_score ? ',请继续保持。' : ',请继续努力!'; - $content = '

' . $user_link . ' 您好:

'; - $content .= '

' . '您参与的比赛 ' . $contest['name'] . ' 现已公布成绩,您的成绩为 ' . $standings[$i][0] . '' . $tail . '

'; - sendSystemMsg($standings[$i][2][0], '比赛成绩公布通知', $content); + sendSystemMsg($standings[$i][2][0], '比赛成绩公布通知', '您参与的比赛 ' . $contest['name'] . ' 现已公布成绩,您的成绩为 ' . $standings[$i][0] . '' . $tail); DB::update([ "update contests_registrants", "set", ["final_rank" => $standings[$i][3]], diff --git a/web/app/models/UOJMail.php b/web/app/models/UOJMail.php index d9978a6..4ed1f68 100644 --- a/web/app/models/UOJMail.php +++ b/web/app/models/UOJMail.php @@ -4,7 +4,7 @@ use PHPMailer\PHPMailer\PHPMailer; class UOJMail { public static function noreply() { - $mailer = new PHPMailer(); + $mailer = new PHPMailer(); $mailer->isSMTP(); $mailer->Host = UOJConfig::$data['mail']['noreply']['host']; $mailer->Port = UOJConfig::$data['mail']['noreply']['port']; @@ -12,9 +12,91 @@ class UOJMail { $mailer->SMTPSecure = UOJConfig::$data['mail']['noreply']['secure']; $mailer->Username = UOJConfig::$data['mail']['noreply']['username']; $mailer->Password = UOJConfig::$data['mail']['noreply']['password']; - $mailer->setFrom(UOJConfig::$data['mail']['noreply']['username'], "UOJ noreply"); + $mailer->setFrom(UOJConfig::$data['mail']['noreply']['username'], UOJConfig::$data['profile']['oj-name-short']); $mailer->CharSet = "utf-8"; $mailer->Encoding = "base64"; return $mailer; } + + public static function cronSendEmail() { + $emails = DB::selectAll([ + "select * from emails", + "where", DB::land([ + ["created_at", ">=", DB::raw("addtime(now(), '-24:00:00')")], + "send_time" => null, + ]), + "order by priority desc", + ]); + + $oj_name = UOJConfig::$data['profile']['oj-name']; + $oj_name_short = UOJConfig::$data['profile']['oj-name-short']; + $oj_url = HTML::url('/'); + $oj_logo_url = HTML::url('/images/logo_small.png'); + + foreach ($emails as $email) { + $user = UOJUser::query($email['receiver']); + $name = $user['username']; + + if ($user['realname']) { + $name .= ' (' . $user['realname'] . ')'; + } + + if ($user['email']) { + $mailer = UOJMail::noreply(); + $mailer->addAddress($user['email'], $user['username']); + $mailer->Subject = $email['subject']; + $mailer->msgHTML(<< + +
+
+
{$oj_name_short}
+ +
+
+
+ +

{$email['subject']}

+
{$name} 您好,
+
+ +
+ {$email['content']} +
+ +
+
+ + + +
+
+ 您之所以收到本邮件,是因为您是 {$oj_name} 的用户。 +
+ 本邮件由系统自动发送,请勿回复。 +
+
+ EOD); + + $res = retry_loop(function () use (&$mailer) { + $res = $mailer->send(); + + if ($res) return true; + + UOJLog::error($mailer->ErrorInfo); + + return false; + }); + + if ($res) { + DB::update("update emails set send_time = now() where id = {$email['id']}"); + echo '[UOJMail::cronSendEmail] ID: ' . $email['id'] . ' sent.' . "\n"; + } + } + } + + echo '[UOJMail::cronSendEmail] Done.' . "\n"; + } } diff --git a/web/app/scheduler.php b/web/app/scheduler.php new file mode 100644 index 0000000..ea7a43b --- /dev/null +++ b/web/app/scheduler.php @@ -0,0 +1,31 @@ + '/tmp' +]); + +echo '[UOJScheduler] Init', "\n"; + +// =========== JOBS =========== + +// Email +$scheduler->call('UOJMail::cronSendEmail', [], 'cronSendEmail') + ->at('* * * * *') + ->onlyOne() + ->before(function () { + echo "[cronSendEmail] started at " . time() . "\n"; + }) + ->then(function () { + echo "[cronSendEmail] ended at " . time() . "\n"; + }); + +// ============================ + +// Let the scheduler execute jobs which are due. +$scheduler->run(); diff --git a/web/app/vendor/composer/autoload_psr4.php b/web/app/vendor/composer/autoload_psr4.php index 6f487a5..8047cb1 100644 --- a/web/app/vendor/composer/autoload_psr4.php +++ b/web/app/vendor/composer/autoload_psr4.php @@ -6,9 +6,12 @@ $vendorDir = dirname(dirname(__FILE__)); $baseDir = dirname($vendorDir); return array( + 'Webmozart\\Assert\\' => array($vendorDir . '/webmozart/assert/src'), 'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'), 'Symfony\\Component\\Finder\\' => array($vendorDir . '/symfony/finder'), 'PHPMailer\\PHPMailer\\' => array($vendorDir . '/phpmailer/phpmailer/src'), 'Gregwar\\' => array($vendorDir . '/gregwar/captcha/src/Gregwar'), + 'GO\\' => array($vendorDir . '/peppeocchi/php-cron-scheduler/src/GO'), 'Curl\\' => array($vendorDir . '/php-curl-class/php-curl-class/src/Curl'), + 'Cron\\' => array($vendorDir . '/dragonmantank/cron-expression/src/Cron'), ); diff --git a/web/app/vendor/composer/autoload_static.php b/web/app/vendor/composer/autoload_static.php index 87966e2..5cafcee 100644 --- a/web/app/vendor/composer/autoload_static.php +++ b/web/app/vendor/composer/autoload_static.php @@ -14,6 +14,10 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900 ); public static $prefixLengthsPsr4 = array ( + 'W' => + array ( + 'Webmozart\\Assert\\' => 17, + ), 'S' => array ( 'Symfony\\Polyfill\\Php80\\' => 23, @@ -26,14 +30,20 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900 'G' => array ( 'Gregwar\\' => 8, + 'GO\\' => 3, ), 'C' => array ( 'Curl\\' => 5, + 'Cron\\' => 5, ), ); public static $prefixDirsPsr4 = array ( + 'Webmozart\\Assert\\' => + array ( + 0 => __DIR__ . '/..' . '/webmozart/assert/src', + ), 'Symfony\\Polyfill\\Php80\\' => array ( 0 => __DIR__ . '/..' . '/symfony/polyfill-php80', @@ -50,10 +60,18 @@ class ComposerStaticInit0d7c2cd5c2dbf2120e4372996869e900 array ( 0 => __DIR__ . '/..' . '/gregwar/captcha/src/Gregwar', ), + 'GO\\' => + array ( + 0 => __DIR__ . '/..' . '/peppeocchi/php-cron-scheduler/src/GO', + ), 'Curl\\' => array ( 0 => __DIR__ . '/..' . '/php-curl-class/php-curl-class/src/Curl', ), + 'Cron\\' => + array ( + 0 => __DIR__ . '/..' . '/dragonmantank/cron-expression/src/Cron', + ), ); public static $prefixesPsr0 = array ( diff --git a/web/app/vendor/composer/installed.json b/web/app/vendor/composer/installed.json index 483f472..fdc2dca 100644 --- a/web/app/vendor/composer/installed.json +++ b/web/app/vendor/composer/installed.json @@ -1,5 +1,69 @@ { "packages": [ + { + "name": "dragonmantank/cron-expression", + "version": "v3.3.2", + "version_normalized": "3.3.2.0", + "source": { + "type": "git", + "url": "https://github.com/dragonmantank/cron-expression.git", + "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dragonmantank/cron-expression/zipball/782ca5968ab8b954773518e9e49a6f892a34b2a8", + "reference": "782ca5968ab8b954773518e9e49a6f892a34b2a8", + "shasum": "" + }, + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^1.0", + "phpstan/phpstan-webmozart-assert": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0" + }, + "time": "2022-09-10T18:51:20+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": [ + "cron", + "schedule" + ], + "support": { + "issues": "https://github.com/dragonmantank/cron-expression/issues", + "source": "https://github.com/dragonmantank/cron-expression/tree/v3.3.2" + }, + "funding": [ + { + "url": "https://github.com/dragonmantank", + "type": "github" + } + ], + "install-path": "../dragonmantank/cron-expression" + }, { "name": "erusev/parsedown", "version": "1.7.4", @@ -213,6 +277,68 @@ }, "install-path": "../ivopetkov/html5-dom-document-php" }, + { + "name": "peppeocchi/php-cron-scheduler", + "version": "v4.0", + "version_normalized": "4.0.0.0", + "source": { + "type": "git", + "url": "https://github.com/peppeocchi/php-cron-scheduler.git", + "reference": "0acfa032e60f0ea22a27b96a6b15a673a31d3448" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/peppeocchi/php-cron-scheduler/zipball/0acfa032e60f0ea22a27b96a6b15a673a31d3448", + "reference": "0acfa032e60f0ea22a27b96a6b15a673a31d3448", + "shasum": "" + }, + "require": { + "dragonmantank/cron-expression": "^3.0", + "php": "^7.3 || ^8.0" + }, + "require-dev": { + "php-coveralls/php-coveralls": "^2.4", + "phpunit/phpunit": "~9.5", + "swiftmailer/swiftmailer": "~5.4 || ^6.0" + }, + "suggest": { + "swiftmailer/swiftmailer": "Required to send the output of a job to email address/es (~5.4 || ^6.0)." + }, + "time": "2021-04-22T21:32:03+00:00", + "type": "library", + "installation-source": "dist", + "autoload": { + "psr-4": { + "GO\\": "src/GO/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Giuseppe Occhipinti", + "email": "peppeocchi@gmail.com" + }, + { + "name": "Carsten Windler", + "email": "carsten@carstenwindler.de", + "homepage": "http://carstenwindler.de", + "role": "Contributor" + } + ], + "description": "PHP Cron Job Scheduler", + "keywords": [ + "cron job", + "scheduler" + ], + "support": { + "issues": "https://github.com/peppeocchi/php-cron-scheduler/issues", + "source": "https://github.com/peppeocchi/php-cron-scheduler/tree/v4.0" + }, + "install-path": "../peppeocchi/php-cron-scheduler" + }, { "name": "php-curl-class/php-curl-class", "version": "9.13.1", @@ -580,6 +706,67 @@ } ], "install-path": "../symfony/polyfill-php80" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "version_normalized": "1.11.0.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "time": "2022-06-03T18:03:27+00:00", + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "installation-source": "dist", + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "install-path": "../webmozart/assert" } ], "dev": true, diff --git a/web/app/vendor/composer/installed.php b/web/app/vendor/composer/installed.php index e4ba758..8f3b441 100644 --- a/web/app/vendor/composer/installed.php +++ b/web/app/vendor/composer/installed.php @@ -5,7 +5,7 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83', + 'reference' => '8314eb67602af0416e50f8514f64e1d9d1a3acd8', 'name' => '__root__', 'dev' => true, ), @@ -16,7 +16,16 @@ 'type' => 'library', 'install_path' => __DIR__ . '/../../', 'aliases' => array(), - 'reference' => 'aa6bf6a363501f9836d2bb26a513c2ac3985de83', + 'reference' => '8314eb67602af0416e50f8514f64e1d9d1a3acd8', + 'dev_requirement' => false, + ), + 'dragonmantank/cron-expression' => array( + 'pretty_version' => 'v3.3.2', + 'version' => '3.3.2.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../dragonmantank/cron-expression', + 'aliases' => array(), + 'reference' => '782ca5968ab8b954773518e9e49a6f892a34b2a8', 'dev_requirement' => false, ), 'erusev/parsedown' => array( @@ -55,6 +64,21 @@ 'reference' => '32c5ba748d661a9654c190bf70ce2854eaf5ad22', 'dev_requirement' => false, ), + 'mtdowling/cron-expression' => array( + 'dev_requirement' => false, + 'replaced' => array( + 0 => '^1.0', + ), + ), + 'peppeocchi/php-cron-scheduler' => array( + 'pretty_version' => 'v4.0', + 'version' => '4.0.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../peppeocchi/php-cron-scheduler', + 'aliases' => array(), + 'reference' => '0acfa032e60f0ea22a27b96a6b15a673a31d3448', + 'dev_requirement' => false, + ), 'php-curl-class/php-curl-class' => array( 'pretty_version' => '9.13.1', 'version' => '9.13.1.0', @@ -100,5 +124,14 @@ 'reference' => 'cfa0ae98841b9e461207c13ab093d76b0fa7bace', 'dev_requirement' => false, ), + 'webmozart/assert' => array( + 'pretty_version' => '1.11.0', + 'version' => '1.11.0.0', + 'type' => 'library', + 'install_path' => __DIR__ . '/../webmozart/assert', + 'aliases' => array(), + 'reference' => '11cb2199493b2f8a3b53e7f19068fc6aac760991', + 'dev_requirement' => false, + ), ), ); diff --git a/web/app/vendor/composer/platform_check.php b/web/app/vendor/composer/platform_check.php index a8b98d5..92370c5 100644 --- a/web/app/vendor/composer/platform_check.php +++ b/web/app/vendor/composer/platform_check.php @@ -4,8 +4,8 @@ $issues = array(); -if (!(PHP_VERSION_ID >= 70205)) { - $issues[] = 'Your Composer dependencies require a PHP version ">= 7.2.5". You are running ' . PHP_VERSION . '.'; +if (!(PHP_VERSION_ID >= 70300)) { + $issues[] = 'Your Composer dependencies require a PHP version ">= 7.3.0". You are running ' . PHP_VERSION . '.'; } if ($issues) { diff --git a/web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md b/web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md new file mode 100644 index 0000000..7b6df4b --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/CHANGELOG.md @@ -0,0 +1,228 @@ +# Change Log + +## [3.3.2] - 2022-09-19 + +### Added +- N/A + +### Changed +- Skip some daylight savings time tests for PHP 8.1 daylight savings time weirdness (#146) + +### Fixed +- Changed string interpolations to work better with PHP 8.2 (#142) + +## [3.3.1] - 2022-01-18 + +### Added +- N/A + +### Changed +- N/A + +### Fixed +- Fixed issue when timezones had no transition, which can occur over very short timespans (#134) + +## [3.3.0] - 2022-01-13 + +### Added +- Added ability to register your own expression aliases (#132) + +### Changed +- Changed how Day of Week and Day of Month resolve when one or the other is `*` or `?` + +### Fixed +- PHPStan should no longer error out + +## [3.2.4] - 2022-01-12 + +### Added +- N/A + +### Changed +- Changed how Day of Week increment/decrement to help with DST changes (#131) + +### Fixed +- N/A + +## [3.2.3] - 2022-01-05 + +### Added +- N/A + +### Changed +- Changed how minutes and hours increment/decrement to help with DST changes (#131) + +### Fixed +- N/A + +## [3.2.2] - 2022-01-05 + +### Added +- N/A + +### Changed +- Marked some methods `@internal` (#124) + +### Fixed +- Fixed issue with small ranges and large steps that caused an error with `range()` (#88) +- Fixed issue where wraparound logic incorrectly considered high bound on range (#89) + +## [3.2.1] - 2022-01-04 + +### Added +- N/A + +### Changed +- Added PHP 8.1 to testing (#125) + +### Fixed +- Allow better mixture of ranges, steps, and lists (#122) +- Fixed return order when multiple dates are requested and inverted (#121) +- Better handling over DST (#115) +- Fixed PHPStan tests (#130) + +## [3.2.0] - 2022-01-04 + +### Added +- Added alias for `@midnight` (#117) + +### Changed +- Improved testing for instance of field in tests (#105) +- Optimization for determining multiple run dates (#75) +- `CronExpression` properties changed from private to protected (#106) + +### Fixed +- N/A + +## [3.1.0] - 2020-11-24 + +### Added +- Added `CronExpression::getParts()` method to get parts of the expression as an array (#83) + +### Changed +- Changed to Interfaces for some type hints (#97, #86) +- Dropped minimum PHP version to 7.2 +- Few syntax changes for phpstan compatibility (#93) + +### Fixed +- N/A + +### Deprecated +- Deprecated `CronExpression::factory` in favor of the constructor (#56) +- Deprecated `CronExpression::YEAR` as a formality, the functionality is already removed (#87) + +## [3.0.1] - 2020-10-12 +### Added +- Added support for PHP 8 (#92) +### Changed +- N/A +### Fixed +- N/A + +## [3.0.0] - 2020-03-25 + +**MAJOR CHANGE** - In previous versions of this library, setting both a "Day of Month" and a "Day of Week" would be interpreted as an `AND` statement, not an `OR` statement. For example: + +`30 0 1 * 1` + +would evaluate to "Run 30 minutes after the 0 hour when the Day Of Month is 1 AND a Monday" instead of "Run 30 minutes after the 0 hour on Day Of Month 1 OR a Monday", where the latter is more inline with most cron systems. This means that if your cron expression has both of these fields set, you may see your expression fire more often starting with v3.0.0. + +### Added +- Additional docblocks for IDE and documentation +- Added phpstan as a development dependency +- Added a `Cron\FieldFactoryInterface` to make migrations easier (#38) +### Changed +- Changed some DI testing during TravisCI runs +- `\Cron\CronExpression::determineTimezone()` now checks for `\DateTimeInterface` instead of just `\DateTime` +- Errors with fields now report a more human-understandable error and are 1-based instead of 0-based +- Better support for `\DateTimeImmutable` across the library by typehinting for `\DateTimeInterface` now +- Literals should now be less case-sensative across the board +- Changed logic for when both a Day of Week and a Day of Month are supplied to now be an OR statement, not an AND +### Fixed +- Fixed infinite loop when determining last day of week from literals +- Fixed bug where single number ranges were allowed (ex: `1/10`) +- Fixed nullable FieldFactory in CronExpression where no factory could be supplied +- Fixed issue where logic for dropping seconds to 0 could lead to a timezone change + +## [2.3.1] - 2020-10-12 +### Added +- Added support for PHP 8 (#92) +### Changed +- N/A +### Fixed +- N/A + +## [2.3.0] - 2019-03-30 +### Added +- Added support for DateTimeImmutable via DateTimeInterface +- Added support for PHP 7.3 +- Started listing projects that use the library +### Changed +- Errors should now report a human readable position in the cron expression, instead of starting at 0 +### Fixed +- N/A + +## [2.2.0] - 2018-06-05 +### Added +- Added support for steps larger than field ranges (#6) +## Changed +- N/A +### Fixed +- Fixed validation for numbers with leading 0s (#12) + +## [2.1.0] - 2018-04-06 +### Added +- N/A +### Changed +- Upgraded to PHPUnit 6 (#2) +### Fixed +- Refactored timezones to deal with some inconsistent behavior (#3) +- Allow ranges and lists in same expression (#5) +- Fixed regression where literals were not converted to their numerical counterpart (#) + +## [2.0.0] - 2017-10-12 +### Added +- N/A + +### Changed +- Dropped support for PHP 5.x +- Dropped support for the YEAR field, as it was not part of the cron standard + +### Fixed +- Reworked validation for all the field types +- Stepping should now work for 1-indexed fields like Month (#153) + +## [1.2.0] - 2017-01-22 +### Added +- Added IDE, CodeSniffer, and StyleCI.IO support + +### Changed +- Switched to PSR-4 Autoloading + +### Fixed +- 0 step expressions are handled better +- Fixed `DayOfMonth` validation to be more strict +- Typos + +## [1.1.0] - 2016-01-26 +### Added +- Support for non-hourly offset timezones +- Checks for valid expressions + +### Changed +- Max Iterations no longer hardcoded for `getRunDate()` +- Supports DateTimeImmutable for newer PHP verions + +### Fixed +- Fixed looping bug for PHP 7 when determining the last specified weekday of a month + +## [1.0.3] - 2013-11-23 +### Added +- Now supports expressions with any number of extra spaces, tabs, or newlines + +### Changed +- Using static instead of self in `CronExpression::factory` + +### Fixed +- Fixes issue [#28](https://github.com/mtdowling/cron-expression/issues/28) where PHP increments of ranges were failing due to PHP casting hyphens to 0 +- Only set default timezone if the given $currentTime is not a DateTime instance ([#34](https://github.com/mtdowling/cron-expression/issues/34)) diff --git a/web/app/vendor/dragonmantank/cron-expression/LICENSE b/web/app/vendor/dragonmantank/cron-expression/LICENSE new file mode 100644 index 0000000..3e38bbc --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/LICENSE @@ -0,0 +1,19 @@ +Copyright (c) 2011 Michael Dowling , 2016 Chris Tankersley , and contributors + +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. diff --git a/web/app/vendor/dragonmantank/cron-expression/README.md b/web/app/vendor/dragonmantank/cron-expression/README.md new file mode 100644 index 0000000..e853ad4 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/README.md @@ -0,0 +1,87 @@ +PHP Cron Expression Parser +========================== + +[![Latest Stable Version](https://poser.pugx.org/dragonmantank/cron-expression/v/stable.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Total Downloads](https://poser.pugx.org/dragonmantank/cron-expression/downloads.png)](https://packagist.org/packages/dragonmantank/cron-expression) [![Build Status](https://secure.travis-ci.org/dragonmantank/cron-expression.png)](http://travis-ci.org/dragonmantank/cron-expression) [![StyleCI](https://github.styleci.io/repos/103715337/shield?branch=master)](https://github.styleci.io/repos/103715337) + +The PHP cron expression parser can parse a CRON expression, determine if it is +due to run, calculate the next run date of the expression, and calculate the previous +run date of the expression. You can calculate dates far into the future or past by +skipping **n** number of matching dates. + +The parser can handle increments of ranges (e.g. */12, 2-59/3), intervals (e.g. 0-9), +lists (e.g. 1,2,3), **W** to find the nearest weekday for a given day of the month, **L** to +find the last day of the month, **L** to find the last given weekday of a month, and hash +(#) to find the nth weekday of a given month. + +More information about this fork can be found in the blog post [here](http://ctankersley.com/2017/10/12/cron-expression-update/). tl;dr - v2.0.0 is a major breaking change, and @dragonmantank can better take care of the project in a separate fork. + +Installing +========== + +Add the dependency to your project: + +```bash +composer require dragonmantank/cron-expression +``` + +Usage +===== +```php +isDue(); +echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); +echo $cron->getPreviousRunDate()->format('Y-m-d H:i:s'); + +// Works with complex expressions +$cron = new Cron\CronExpression('3-59/15 6-12 */15 1 2-5'); +echo $cron->getNextRunDate()->format('Y-m-d H:i:s'); + +// Calculate a run date two iterations into the future +$cron = new Cron\CronExpression('@daily'); +echo $cron->getNextRunDate(null, 2)->format('Y-m-d H:i:s'); + +// Calculate a run date relative to a specific time +$cron = new Cron\CronExpression('@monthly'); +echo $cron->getNextRunDate('2010-01-12 00:00:00')->format('Y-m-d H:i:s'); +``` + +CRON Expressions +================ + +A CRON expression is a string representing the schedule for a particular command to execute. The parts of a CRON schedule are as follows: + + * * * * * + - - - - - + | | | | | + | | | | | + | | | | +----- day of week (0 - 7) (Sunday=0 or 7) + | | | +---------- month (1 - 12) + | | +--------------- day of month (1 - 31) + | +-------------------- hour (0 - 23) + +------------------------- min (0 - 59) + +This library also supports a few macros: + +* `@yearly`, `@annually` - Run once a year, midnight, Jan. 1 - `0 0 1 1 *` +* `@monthly` - Run once a month, midnight, first of month - `0 0 1 * *` +* `@weekly` - Run once a week, midnight on Sun - `0 0 * * 0` +* `@daily`, `@midnight` - Run once a day, midnight - `0 0 * * *` +* `@hourly` - Run once an hour, first minute - `0 * * * *` + +Requirements +============ + +- PHP 7.2+ +- PHPUnit is required to run the unit tests +- Composer is required to run the unit tests + +Projects that Use cron-expression +================================= +* Part of the [Laravel Framework](https://github.com/laravel/framework/) +* Available as a [Symfony Bundle - setono/cron-expression-bundle](https://github.com/Setono/CronExpressionBundle) +* Framework agnostic, PHP-based job scheduler - [Crunz](https://github.com/lavary/crunz) diff --git a/web/app/vendor/dragonmantank/cron-expression/composer.json b/web/app/vendor/dragonmantank/cron-expression/composer.json new file mode 100644 index 0000000..657a5b4 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/composer.json @@ -0,0 +1,47 @@ +{ + "name": "dragonmantank/cron-expression", + "type": "library", + "description": "CRON for PHP: Calculate the next or previous run date and determine if a CRON expression is due", + "keywords": ["cron", "schedule"], + "license": "MIT", + "authors": [ + { + "name": "Chris Tankersley", + "email": "chris@ctankersley.com", + "homepage": "https://github.com/dragonmantank" + } + ], + "require": { + "php": "^7.2|^8.0", + "webmozart/assert": "^1.0" + }, + "require-dev": { + "phpstan/phpstan": "^1.0", + "phpunit/phpunit": "^7.0|^8.0|^9.0", + "phpstan/phpstan-webmozart-assert": "^1.0", + "phpstan/extension-installer": "^1.0" + }, + "autoload": { + "psr-4": { + "Cron\\": "src/Cron/" + } + }, + "autoload-dev": { + "psr-4": { + "Cron\\Tests\\": "tests/Cron/" + } + }, + "replace": { + "mtdowling/cron-expression": "^1.0" + }, + "scripts": { + "phpstan": "./vendor/bin/phpstan analyze", + "test": "phpunit" + }, + "config": { + "allow-plugins": { + "ocramius/package-versions": true, + "phpstan/extension-installer": true + } + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/phpstan.neon b/web/app/vendor/dragonmantank/cron-expression/phpstan.neon new file mode 100644 index 0000000..bea9cb0 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/phpstan.neon @@ -0,0 +1,15 @@ +parameters: + checkMissingIterableValueType: false + + ignoreErrors: + - '#Call to an undefined method DateTimeInterface::add\(\)#' + - '#Call to an undefined method DateTimeInterface::modify\(\)#' + - '#Call to an undefined method DateTimeInterface::setDate\(\)#' + - '#Call to an undefined method DateTimeInterface::setTime\(\)#' + - '#Call to an undefined method DateTimeInterface::setTimezone\(\)#' + - '#Call to an undefined method DateTimeInterface::sub\(\)#' + + level: max + + paths: + - src/ diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php new file mode 100644 index 0000000..df2848d --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/AbstractField.php @@ -0,0 +1,346 @@ +fullRange = range($this->rangeStart, $this->rangeEnd); + } + + /** + * Check to see if a field is satisfied by a value. + * + * @internal + * @param int $dateValue Date value to check + * @param string $value Value to test + * + * @return bool + */ + public function isSatisfied(int $dateValue, string $value): bool + { + if ($this->isIncrementsOfRanges($value)) { + return $this->isInIncrementsOfRanges($dateValue, $value); + } + + if ($this->isRange($value)) { + return $this->isInRange($dateValue, $value); + } + + return '*' === $value || $dateValue === (int) $value; + } + + /** + * Check if a value is a range. + * + * @internal + * @param string $value Value to test + * + * @return bool + */ + public function isRange(string $value): bool + { + return false !== strpos($value, '-'); + } + + /** + * Check if a value is an increments of ranges. + * + * @internal + * @param string $value Value to test + * + * @return bool + */ + public function isIncrementsOfRanges(string $value): bool + { + return false !== strpos($value, '/'); + } + + /** + * Test if a value is within a range. + * + * @internal + * @param int $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInRange(int $dateValue, $value): bool + { + $parts = array_map( + function ($value) { + $value = trim($value); + + return $this->convertLiterals($value); + }, + explode('-', $value, 2) + ); + + return $dateValue >= $parts[0] && $dateValue <= $parts[1]; + } + + /** + * Test if a value is within an increments of ranges (offset[-to]/step size). + * + * @internal + * @param int $dateValue Set date value + * @param string $value Value to test + * + * @return bool + */ + public function isInIncrementsOfRanges(int $dateValue, string $value): bool + { + $chunks = array_map('trim', explode('/', $value, 2)); + $range = $chunks[0]; + $step = $chunks[1] ?? 0; + + // No step or 0 steps aren't cool + /** @phpstan-ignore-next-line */ + if (null === $step || '0' === $step || 0 === $step) { + return false; + } + + // Expand the * to a full range + if ('*' === $range) { + $range = $this->rangeStart . '-' . $this->rangeEnd; + } + + // Generate the requested small range + $rangeChunks = explode('-', $range, 2); + $rangeStart = (int) $rangeChunks[0]; + $rangeEnd = $rangeChunks[1] ?? $rangeStart; + $rangeEnd = (int) $rangeEnd; + + if ($rangeStart < $this->rangeStart || $rangeStart > $this->rangeEnd || $rangeStart > $rangeEnd) { + throw new \OutOfRangeException('Invalid range start requested'); + } + + if ($rangeEnd < $this->rangeStart || $rangeEnd > $this->rangeEnd || $rangeEnd < $rangeStart) { + throw new \OutOfRangeException('Invalid range end requested'); + } + + // Steps larger than the range need to wrap around and be handled + // slightly differently than smaller steps + + // UPDATE - This is actually false. The C implementation will allow a + // larger step as valid syntax, it never wraps around. It will stop + // once it hits the end. Unfortunately this means in future versions + // we will not wrap around. However, because the logic exists today + // per the above documentation, fixing the bug from #89 + if ($step > $this->rangeEnd) { + $thisRange = [$this->fullRange[$step % \count($this->fullRange)]]; + } else { + if ($step > ($rangeEnd - $rangeStart)) { + $thisRange[$rangeStart] = (int) $rangeStart; + } else { + $thisRange = range($rangeStart, $rangeEnd, (int) $step); + } + } + + return \in_array($dateValue, $thisRange, true); + } + + /** + * Returns a range of values for the given cron expression. + * + * @param string $expression The expression to evaluate + * @param int $max Maximum offset for range + * + * @return array + */ + public function getRangeForExpression(string $expression, int $max): array + { + $values = []; + $expression = $this->convertLiterals($expression); + + if (false !== strpos($expression, ',')) { + $ranges = explode(',', $expression); + $values = []; + foreach ($ranges as $range) { + $expanded = $this->getRangeForExpression($range, $this->rangeEnd); + $values = array_merge($values, $expanded); + } + + return $values; + } + + if ($this->isRange($expression) || $this->isIncrementsOfRanges($expression)) { + if (!$this->isIncrementsOfRanges($expression)) { + [$offset, $to] = explode('-', $expression); + $offset = $this->convertLiterals($offset); + $to = $this->convertLiterals($to); + $stepSize = 1; + } else { + $range = array_map('trim', explode('/', $expression, 2)); + $stepSize = $range[1] ?? 0; + $range = $range[0]; + $range = explode('-', $range, 2); + $offset = $range[0]; + $to = $range[1] ?? $max; + } + $offset = '*' === $offset ? $this->rangeStart : $offset; + if ($stepSize >= $this->rangeEnd) { + $values = [$this->fullRange[$stepSize % \count($this->fullRange)]]; + } else { + for ($i = $offset; $i <= $to; $i += $stepSize) { + $values[] = (int) $i; + } + } + sort($values); + } else { + $values = [$expression]; + } + + return $values; + } + + /** + * Convert literal. + * + * @param string $value + * + * @return string + */ + protected function convertLiterals(string $value): string + { + if (\count($this->literals)) { + $key = array_search(strtoupper($value), $this->literals, true); + if (false !== $key) { + return (string) $key; + } + } + + return $value; + } + + /** + * Checks to see if a value is valid for the field. + * + * @param string $value + * + * @return bool + */ + public function validate(string $value): bool + { + $value = $this->convertLiterals($value); + + // All fields allow * as a valid value + if ('*' === $value) { + return true; + } + + // Validate each chunk of a list individually + if (false !== strpos($value, ',')) { + foreach (explode(',', $value) as $listItem) { + if (!$this->validate($listItem)) { + return false; + } + } + + return true; + } + + if (false !== strpos($value, '/')) { + [$range, $step] = explode('/', $value); + + // Don't allow numeric ranges + if (is_numeric($range)) { + return false; + } + + return $this->validate($range) && filter_var($step, FILTER_VALIDATE_INT); + } + + if (false !== strpos($value, '-')) { + if (substr_count($value, '-') > 1) { + return false; + } + + $chunks = explode('-', $value); + $chunks[0] = $this->convertLiterals($chunks[0]); + $chunks[1] = $this->convertLiterals($chunks[1]); + + if ('*' === $chunks[0] || '*' === $chunks[1]) { + return false; + } + + return $this->validate($chunks[0]) && $this->validate($chunks[1]); + } + + if (!is_numeric($value)) { + return false; + } + + if (false !== strpos($value, '.')) { + return false; + } + + // We should have a numeric by now, so coerce this into an integer + $value = (int) $value; + + return \in_array($value, $this->fullRange, true); + } + + protected function timezoneSafeModify(DateTimeInterface $dt, string $modification): DateTimeInterface + { + $timezone = $dt->getTimezone(); + $dt = $dt->setTimezone(new \DateTimeZone("UTC")); + $dt = $dt->modify($modification); + $dt = $dt->setTimezone($timezone); + return $dt; + } + + protected function setTimeHour(DateTimeInterface $date, bool $invert, int $originalTimestamp): DateTimeInterface + { + $date = $date->setTime((int)$date->format('H'), ($invert ? 59 : 0)); + + // setTime caused the offset to change, moving time in the wrong direction + $actualTimestamp = $date->format('U'); + if ((! $invert) && ($actualTimestamp <= $originalTimestamp)) { + $date = $this->timezoneSafeModify($date, "+1 hour"); + } elseif ($invert && ($actualTimestamp >= $originalTimestamp)) { + $date = $this->timezoneSafeModify($date, "-1 hour"); + } + + return $date; + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php new file mode 100644 index 0000000..d5337cc --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/CronExpression.php @@ -0,0 +1,568 @@ + '0 0 1 1 *', + '@annually' => '0 0 1 1 *', + '@monthly' => '0 0 1 * *', + '@weekly' => '0 0 * * 0', + '@daily' => '0 0 * * *', + '@midnight' => '0 0 * * *', + '@hourly' => '0 * * * *', + ]; + + /** + * @var array CRON expression parts + */ + protected $cronParts; + + /** + * @var FieldFactoryInterface CRON field factory + */ + protected $fieldFactory; + + /** + * @var int Max iteration count when searching for next run date + */ + protected $maxIterationCount = 1000; + + /** + * @var array Order in which to test of cron parts + */ + protected static $order = [ + self::YEAR, + self::MONTH, + self::DAY, + self::WEEKDAY, + self::HOUR, + self::MINUTE, + ]; + + /** + * @var array + */ + private static $registeredAliases = self::MAPPINGS; + + /** + * Registered a user defined CRON Expression Alias. + * + * @throws LogicException If the expression or the alias name are invalid + * or if the alias is already registered. + */ + public static function registerAlias(string $alias, string $expression): void + { + try { + new self($expression); + } catch (InvalidArgumentException $exception) { + throw new LogicException("The expression `$expression` is invalid", 0, $exception); + } + + $shortcut = strtolower($alias); + if (1 !== preg_match('/^@\w+$/', $shortcut)) { + throw new LogicException("The alias `$alias` is invalid. It must start with an `@` character and contain alphanumeric (letters, numbers, regardless of case) plus underscore (_)."); + } + + if (isset(self::$registeredAliases[$shortcut])) { + throw new LogicException("The alias `$alias` is already registered."); + } + + self::$registeredAliases[$shortcut] = $expression; + } + + /** + * Unregistered a user defined CRON Expression Alias. + * + * @throws LogicException If the user tries to unregister a built-in alias + */ + public static function unregisterAlias(string $alias): bool + { + $shortcut = strtolower($alias); + if (isset(self::MAPPINGS[$shortcut])) { + throw new LogicException("The alias `$alias` is a built-in alias; it can not be unregistered."); + } + + if (!isset(self::$registeredAliases[$shortcut])) { + return false; + } + + unset(self::$registeredAliases[$shortcut]); + + return true; + } + + /** + * Tells whether a CRON Expression alias is registered. + */ + public static function supportsAlias(string $alias): bool + { + return isset(self::$registeredAliases[strtolower($alias)]); + } + + /** + * Returns all registered aliases as an associated array where the aliases are the key + * and their associated expressions are the values. + * + * @return array + */ + public static function getAliases(): array + { + return self::$registeredAliases; + } + + /** + * @deprecated since version 3.0.2, use __construct instead. + */ + public static function factory(string $expression, FieldFactoryInterface $fieldFactory = null): CronExpression + { + /** @phpstan-ignore-next-line */ + return new static($expression, $fieldFactory); + } + + /** + * Validate a CronExpression. + * + * @param string $expression the CRON expression to validate + * + * @return bool True if a valid CRON expression was passed. False if not. + */ + public static function isValidExpression(string $expression): bool + { + try { + new CronExpression($expression); + } catch (InvalidArgumentException $e) { + return false; + } + + return true; + } + + /** + * Parse a CRON expression. + * + * @param string $expression CRON expression (e.g. '8 * * * *') + * @param null|FieldFactoryInterface $fieldFactory Factory to create cron fields + */ + public function __construct(string $expression, FieldFactoryInterface $fieldFactory = null) + { + $shortcut = strtolower($expression); + $expression = self::$registeredAliases[$shortcut] ?? $expression; + + $this->fieldFactory = $fieldFactory ?: new FieldFactory(); + $this->setExpression($expression); + } + + /** + * Set or change the CRON expression. + * + * @param string $value CRON expression (e.g. 8 * * * *) + * + * @throws \InvalidArgumentException if not a valid CRON expression + * + * @return CronExpression + */ + public function setExpression(string $value): CronExpression + { + $split = preg_split('/\s/', $value, -1, PREG_SPLIT_NO_EMPTY); + Assert::isArray($split); + + $this->cronParts = $split; + if (\count($this->cronParts) < 5) { + throw new InvalidArgumentException( + $value . ' is not a valid CRON expression' + ); + } + + foreach ($this->cronParts as $position => $part) { + $this->setPart($position, $part); + } + + return $this; + } + + /** + * Set part of the CRON expression. + * + * @param int $position The position of the CRON expression to set + * @param string $value The value to set + * + * @throws \InvalidArgumentException if the value is not valid for the part + * + * @return CronExpression + */ + public function setPart(int $position, string $value): CronExpression + { + if (!$this->fieldFactory->getField($position)->validate($value)) { + throw new InvalidArgumentException( + 'Invalid CRON field value ' . $value . ' at position ' . $position + ); + } + + $this->cronParts[$position] = $value; + + return $this; + } + + /** + * Set max iteration count for searching next run dates. + * + * @param int $maxIterationCount Max iteration count when searching for next run date + * + * @return CronExpression + */ + public function setMaxIterationCount(int $maxIterationCount): CronExpression + { + $this->maxIterationCount = $maxIterationCount; + + return $this; + } + + /** + * Get a next run date relative to the current date or a specific date + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning a + * matching next run date. 0, the default, will return the + * current date and time if the next run date falls on the + * current date and time. Setting this value to 1 will + * skip the first match and go to the second match. + * Setting this value to 2 will skip the first 2 + * matches and so on. + * @param bool $allowCurrentDate Set to TRUE to return the current date if + * it matches the cron expression. + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @throws \RuntimeException on too many iterations + * @throws \Exception + * + * @return \DateTime + */ + public function getNextRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime + { + return $this->getRunDate($currentTime, $nth, false, $allowCurrentDate, $timeZone); + } + + /** + * Get a previous run date relative to the current date or a specific date. + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @throws \RuntimeException on too many iterations + * @throws \Exception + * + * @return \DateTime + * + * @see \Cron\CronExpression::getNextRunDate + */ + public function getPreviousRunDate($currentTime = 'now', int $nth = 0, bool $allowCurrentDate = false, $timeZone = null): DateTime + { + return $this->getRunDate($currentTime, $nth, true, $allowCurrentDate, $timeZone); + } + + /** + * Get multiple run dates starting at the current date or a specific date. + * + * @param int $total Set the total number of dates to calculate + * @param string|\DateTimeInterface|null $currentTime Relative calculation date + * @param bool $invert Set to TRUE to retrieve previous dates + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @return \DateTime[] Returns an array of run dates + */ + public function getMultipleRunDates(int $total, $currentTime = 'now', bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): array + { + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ('now' === $currentTime) { + $currentTime = new DateTime(); + } elseif ($currentTime instanceof DateTime) { + $currentTime = clone $currentTime; + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); + } elseif (\is_string($currentTime)) { + $currentTime = new DateTime($currentTime); + } + + Assert::isInstanceOf($currentTime, DateTime::class); + $currentTime->setTimezone(new DateTimeZone($timeZone)); + + $matches = []; + for ($i = 0; $i < $total; ++$i) { + try { + $result = $this->getRunDate($currentTime, 0, $invert, $allowCurrentDate, $timeZone); + } catch (RuntimeException $e) { + break; + } + + $allowCurrentDate = false; + $currentTime = clone $result; + $matches[] = $result; + } + + return $matches; + } + + /** + * Get all or part of the CRON expression. + * + * @param int|string|null $part specify the part to retrieve or NULL to get the full + * cron schedule string + * + * @return null|string Returns the CRON expression, a part of the + * CRON expression, or NULL if the part was specified but not found + */ + public function getExpression($part = null): ?string + { + if (null === $part) { + return implode(' ', $this->cronParts); + } + + if (array_key_exists($part, $this->cronParts)) { + return $this->cronParts[$part]; + } + + return null; + } + + /** + * Gets the parts of the cron expression as an array. + * + * @return string[] + * The array of parts that make up this expression. + */ + public function getParts() + { + return $this->cronParts; + } + + /** + * Helper method to output the full expression. + * + * @return string Full CRON expression + */ + public function __toString(): string + { + return (string) $this->getExpression(); + } + + /** + * Determine if the cron is due to run based on the current date or a + * specific date. This method assumes that the current number of + * seconds are irrelevant, and should be called once per minute. + * + * @param string|\DateTimeInterface $currentTime Relative calculation date + * @param null|string $timeZone TimeZone to use instead of the system default + * + * @return bool Returns TRUE if the cron is due to run or FALSE if not + */ + public function isDue($currentTime = 'now', $timeZone = null): bool + { + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ('now' === $currentTime) { + $currentTime = new DateTime(); + } elseif ($currentTime instanceof DateTime) { + $currentTime = clone $currentTime; + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentTime = DateTime::createFromFormat('U', $currentTime->format('U')); + } elseif (\is_string($currentTime)) { + $currentTime = new DateTime($currentTime); + } + + Assert::isInstanceOf($currentTime, DateTime::class); + $currentTime->setTimezone(new DateTimeZone($timeZone)); + + // drop the seconds to 0 + $currentTime->setTime((int) $currentTime->format('H'), (int) $currentTime->format('i'), 0); + + try { + return $this->getNextRunDate($currentTime, 0, true)->getTimestamp() === $currentTime->getTimestamp(); + } catch (Exception $e) { + return false; + } + } + + /** + * Get the next or previous run date of the expression relative to a date. + * + * @param string|\DateTimeInterface|null $currentTime Relative calculation date + * @param int $nth Number of matches to skip before returning + * @param bool $invert Set to TRUE to go backwards in time + * @param bool $allowCurrentDate Set to TRUE to return the + * current date if it matches the cron expression + * @param string|null $timeZone TimeZone to use instead of the system default + * + * @throws \RuntimeException on too many iterations + * @throws Exception + * + * @return \DateTime + */ + protected function getRunDate($currentTime = null, int $nth = 0, bool $invert = false, bool $allowCurrentDate = false, $timeZone = null): DateTime + { + $timeZone = $this->determineTimeZone($currentTime, $timeZone); + + if ($currentTime instanceof DateTime) { + $currentDate = clone $currentTime; + } elseif ($currentTime instanceof DateTimeImmutable) { + $currentDate = DateTime::createFromFormat('U', $currentTime->format('U')); + } elseif (\is_string($currentTime)) { + $currentDate = new DateTime($currentTime); + } else { + $currentDate = new DateTime('now'); + } + + Assert::isInstanceOf($currentDate, DateTime::class); + $currentDate->setTimezone(new DateTimeZone($timeZone)); + // Workaround for setTime causing an offset change: https://bugs.php.net/bug.php?id=81074 + $currentDate = DateTime::createFromFormat("!Y-m-d H:iO", $currentDate->format("Y-m-d H:iP"), $currentDate->getTimezone()); + if ($currentDate === false) { + throw new \RuntimeException('Unable to create date from format'); + } + $currentDate->setTimezone(new DateTimeZone($timeZone)); + + $nextRun = clone $currentDate; + + // We don't have to satisfy * or null fields + $parts = []; + $fields = []; + foreach (self::$order as $position) { + $part = $this->getExpression($position); + if (null === $part || '*' === $part) { + continue; + } + $parts[$position] = $part; + $fields[$position] = $this->fieldFactory->getField($position); + } + + if (isset($parts[self::DAY]) && isset($parts[self::WEEKDAY])) { + $domExpression = sprintf('%s %s %s %s *', $this->getExpression(0), $this->getExpression(1), $this->getExpression(2), $this->getExpression(3)); + $dowExpression = sprintf('%s %s * %s %s', $this->getExpression(0), $this->getExpression(1), $this->getExpression(3), $this->getExpression(4)); + + $domExpression = new self($domExpression); + $dowExpression = new self($dowExpression); + + $domRunDates = $domExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); + $dowRunDates = $dowExpression->getMultipleRunDates($nth + 1, $currentTime, $invert, $allowCurrentDate, $timeZone); + + if ($parts[self::DAY] === '?' || $parts[self::DAY] === '*') { + $domRunDates = []; + } + + if ($parts[self::WEEKDAY] === '?' || $parts[self::WEEKDAY] === '*') { + $dowRunDates = []; + } + + $combined = array_merge($domRunDates, $dowRunDates); + usort($combined, function ($a, $b) { + return $a->format('Y-m-d H:i:s') <=> $b->format('Y-m-d H:i:s'); + }); + if ($invert) { + $combined = array_reverse($combined); + } + + return $combined[$nth]; + } + + // Set a hard limit to bail on an impossible date + for ($i = 0; $i < $this->maxIterationCount; ++$i) { + foreach ($parts as $position => $part) { + $satisfied = false; + // Get the field object used to validate this part + $field = $fields[$position]; + // Check if this is singular or a list + if (false === strpos($part, ',')) { + $satisfied = $field->isSatisfiedBy($nextRun, $part, $invert); + } else { + foreach (array_map('trim', explode(',', $part)) as $listPart) { + if ($field->isSatisfiedBy($nextRun, $listPart, $invert)) { + $satisfied = true; + + break; + } + } + } + + // If the field is not satisfied, then start over + if (!$satisfied) { + $field->increment($nextRun, $invert, $part); + + continue 2; + } + } + + // Skip this match if needed + if ((!$allowCurrentDate && $nextRun == $currentDate) || --$nth > -1) { + $this->fieldFactory->getField(self::MINUTE)->increment($nextRun, $invert, $parts[self::MINUTE] ?? null); + continue; + } + + return $nextRun; + } + + // @codeCoverageIgnoreStart + throw new RuntimeException('Impossible CRON expression'); + // @codeCoverageIgnoreEnd + } + + /** + * Workout what timeZone should be used. + * + * @param string|\DateTimeInterface|null $currentTime Relative calculation date + * @param string|null $timeZone TimeZone to use instead of the system default + * + * @return string + */ + protected function determineTimeZone($currentTime, ?string $timeZone): string + { + if (null !== $timeZone) { + return $timeZone; + } + + if ($currentTime instanceof DateTimeInterface) { + return $currentTime->getTimezone()->getName(); + } + + return date_default_timezone_get(); + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php new file mode 100644 index 0000000..39ff597 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfMonthField.php @@ -0,0 +1,164 @@ + + */ +class DayOfMonthField extends AbstractField +{ + /** + * {@inheritdoc} + */ + protected $rangeStart = 1; + + /** + * {@inheritdoc} + */ + protected $rangeEnd = 31; + + /** + * Get the nearest day of the week for a given day in a month. + * + * @param int $currentYear Current year + * @param int $currentMonth Current month + * @param int $targetDay Target day of the month + * + * @return \DateTime|null Returns the nearest date + */ + private static function getNearestWeekday(int $currentYear, int $currentMonth, int $targetDay): ?DateTime + { + $tday = str_pad((string) $targetDay, 2, '0', STR_PAD_LEFT); + $target = DateTime::createFromFormat('Y-m-d', "{$currentYear}-{$currentMonth}-{$tday}"); + + if ($target === false) { + return null; + } + + $currentWeekday = (int) $target->format('N'); + + if ($currentWeekday < 6) { + return $target; + } + + $lastDayOfMonth = $target->format('t'); + foreach ([-1, 1, -2, 2] as $i) { + $adjusted = $targetDay + $i; + if ($adjusted > 0 && $adjusted <= $lastDayOfMonth) { + $target->setDate($currentYear, $currentMonth, $adjusted); + + if ((int) $target->format('N') < 6 && (int) $target->format('m') === $currentMonth) { + return $target; + } + } + } + + return null; + } + + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool + { + // ? states that the field value is to be skipped + if ('?' === $value) { + return true; + } + + $fieldValue = $date->format('d'); + + // Check to see if this is the last day of the month + if ('L' === $value) { + return $fieldValue === $date->format('t'); + } + + // Check to see if this is the nearest weekday to a particular value + if ($wPosition = strpos($value, 'W')) { + // Parse the target day + $targetDay = (int) substr($value, 0, $wPosition); + // Find out if the current day is the nearest day of the week + $nearest = self::getNearestWeekday( + (int) $date->format('Y'), + (int) $date->format('m'), + $targetDay + ); + if ($nearest) { + return $date->format('j') === $nearest->format('j'); + } + + throw new \RuntimeException('Unable to return nearest weekday'); + } + + return $this->isSatisfied((int) $date->format('d'), $value); + } + + /** + * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable $date + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface + { + if (! $invert) { + $date = $date->add(new \DateInterval('P1D')); + $date = $date->setTime(0, 0); + } else { + $date = $date->sub(new \DateInterval('P1D')); + $date = $date->setTime(23, 59); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate(string $value): bool + { + $basicChecks = parent::validate($value); + + // Validate that a list don't have W or L + if (false !== strpos($value, ',') && (false !== strpos($value, 'W') || false !== strpos($value, 'L'))) { + return false; + } + + if (!$basicChecks) { + if ('?' === $value) { + return true; + } + + if ('L' === $value) { + return true; + } + + if (preg_match('/^(.*)W$/', $value, $matches)) { + return $this->validate($matches[1]); + } + + return false; + } + + return $basicChecks; + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php new file mode 100644 index 0000000..b9bbf48 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/DayOfWeekField.php @@ -0,0 +1,194 @@ + 'MON', 2 => 'TUE', 3 => 'WED', 4 => 'THU', 5 => 'FRI', 6 => 'SAT', 7 => 'SUN']; + + /** + * Constructor + */ + public function __construct() + { + $this->nthRange = range(1, 5); + parent::__construct(); + } + + /** + * @inheritDoc + */ + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool + { + if ('?' === $value) { + return true; + } + + // Convert text day of the week values to integers + $value = $this->convertLiterals($value); + + $currentYear = (int) $date->format('Y'); + $currentMonth = (int) $date->format('m'); + $lastDayOfMonth = (int) $date->format('t'); + + // Find out if this is the last specific weekday of the month + if ($lPosition = strpos($value, 'L')) { + $weekday = $this->convertLiterals(substr($value, 0, $lPosition)); + $weekday %= 7; + + $daysInMonth = (int) $date->format('t'); + $remainingDaysInMonth = $daysInMonth - (int) $date->format('d'); + return (($weekday === (int) $date->format('w')) && ($remainingDaysInMonth < 7)); + } + + // Handle # hash tokens + if (strpos($value, '#')) { + [$weekday, $nth] = explode('#', $value); + + if (!is_numeric($nth)) { + throw new InvalidArgumentException("Hashed weekdays must be numeric, {$nth} given"); + } else { + $nth = (int) $nth; + } + + // 0 and 7 are both Sunday, however 7 matches date('N') format ISO-8601 + if ('0' === $weekday) { + $weekday = 7; + } + + $weekday = (int) $this->convertLiterals((string) $weekday); + + // Validate the hash fields + if ($weekday < 0 || $weekday > 7) { + throw new InvalidArgumentException("Weekday must be a value between 0 and 7. {$weekday} given"); + } + + if (!\in_array($nth, $this->nthRange, true)) { + throw new InvalidArgumentException("There are never more than 5 or less than 1 of a given weekday in a month, {$nth} given"); + } + + // The current weekday must match the targeted weekday to proceed + if ((int) $date->format('N') !== $weekday) { + return false; + } + + $tdate = clone $date; + $tdate = $tdate->setDate($currentYear, $currentMonth, 1); + $dayCount = 0; + $currentDay = 1; + while ($currentDay < $lastDayOfMonth + 1) { + if ((int) $tdate->format('N') === $weekday) { + if (++$dayCount >= $nth) { + break; + } + } + $tdate = $tdate->setDate($currentYear, $currentMonth, ++$currentDay); + } + + return (int) $date->format('j') === $currentDay; + } + + // Handle day of the week values + if (false !== strpos($value, '-')) { + $parts = explode('-', $value); + if ('7' === $parts[0]) { + $parts[0] = 0; + } elseif ('0' === $parts[1]) { + $parts[1] = 7; + } + $value = implode('-', $parts); + } + + // Test to see which Sunday to use -- 0 == 7 == Sunday + $format = \in_array(7, array_map(function ($value) { + return (int) $value; + }, str_split($value)), true) ? 'N' : 'w'; + $fieldValue = (int) $date->format($format); + + return $this->isSatisfied($fieldValue, $value); + } + + /** + * @inheritDoc + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface + { + if (! $invert) { + $date = $date->add(new \DateInterval('P1D')); + $date = $date->setTime(0, 0); + } else { + $date = $date->sub(new \DateInterval('P1D')); + $date = $date->setTime(23, 59); + } + + return $this; + } + + /** + * {@inheritdoc} + */ + public function validate(string $value): bool + { + $basicChecks = parent::validate($value); + + if (!$basicChecks) { + if ('?' === $value) { + return true; + } + + // Handle the # value + if (false !== strpos($value, '#')) { + $chunks = explode('#', $value); + $chunks[0] = $this->convertLiterals($chunks[0]); + + if (parent::validate($chunks[0]) && is_numeric($chunks[1]) && \in_array((int) $chunks[1], $this->nthRange, true)) { + return true; + } + } + + if (preg_match('/^(.*)L$/', $value, $matches)) { + return $this->validate($matches[1]); + } + + return false; + } + + return $basicChecks; + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php new file mode 100644 index 0000000..839b275 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactory.php @@ -0,0 +1,52 @@ +fields[$position] ?? $this->fields[$position] = $this->instantiateField($position); + } + + private function instantiateField(int $position): FieldInterface + { + switch ($position) { + case CronExpression::MINUTE: + return new MinutesField(); + case CronExpression::HOUR: + return new HoursField(); + case CronExpression::DAY: + return new DayOfMonthField(); + case CronExpression::MONTH: + return new MonthField(); + case CronExpression::WEEKDAY: + return new DayOfWeekField(); + } + + throw new InvalidArgumentException( + ($position + 1) . ' is not a valid position' + ); + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php new file mode 100644 index 0000000..8bd3c65 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/FieldFactoryInterface.php @@ -0,0 +1,8 @@ +format('H'); + $retval = $this->isSatisfied($checkValue, $value); + if ($retval) { + return $retval; + } + + // Are we on the edge of a transition + $lastTransition = $this->getPastTransition($date); + if (($lastTransition !== null) && ($lastTransition["ts"] > ((int) $date->format('U') - 3600))) { + $dtLastOffset = clone $date; + $this->timezoneSafeModify($dtLastOffset, "-1 hour"); + $lastOffset = $dtLastOffset->getOffset(); + + $dtNextOffset = clone $date; + $this->timezoneSafeModify($dtNextOffset, "+1 hour"); + $nextOffset = $dtNextOffset->getOffset(); + + $offsetChange = $nextOffset - $lastOffset; + if ($offsetChange >= 3600) { + $checkValue -= 1; + return $this->isSatisfied($checkValue, $value); + } + if ((! $invert) && ($offsetChange <= -3600)) { + $checkValue += 1; + return $this->isSatisfied($checkValue, $value); + } + } + + return $retval; + } + + public function getPastTransition(DateTimeInterface $date): ?array + { + $currentTimestamp = (int) $date->format('U'); + if ( + ($this->transitions === null) + || ($this->transitionsStart < ($currentTimestamp + 86400)) + || ($this->transitionsEnd > ($currentTimestamp - 86400)) + ) { + // We start a day before current time so we can differentiate between the first transition entry + // and a change that happens now + $dtLimitStart = clone $date; + $dtLimitStart = $dtLimitStart->modify("-12 months"); + $dtLimitEnd = clone $date; + $dtLimitEnd = $dtLimitEnd->modify('+12 months'); + + $this->transitions = $date->getTimezone()->getTransitions( + $dtLimitStart->getTimestamp(), + $dtLimitEnd->getTimestamp() + ); + if (empty($this->transitions)) { + return null; + } + $this->transitionsStart = $dtLimitStart->getTimestamp(); + $this->transitionsEnd = $dtLimitEnd->getTimestamp(); + } + + $nextTransition = null; + foreach ($this->transitions as $transition) { + if ($transition["ts"] > $currentTimestamp) { + continue; + } + + if (($nextTransition !== null) && ($transition["ts"] < $nextTransition["ts"])) { + continue; + } + + $nextTransition = $transition; + } + + return ($nextTransition ?? null); + } + + /** + * {@inheritdoc} + * + * @param string|null $parts + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface + { + $originalTimestamp = (int) $date->format('U'); + + // Change timezone to UTC temporarily. This will + // allow us to go back or forwards and hour even + // if DST will be changed between the hours. + if (null === $parts || '*' === $parts) { + if ($invert) { + $date = $date->sub(new \DateInterval('PT1H')); + } else { + $date = $date->add(new \DateInterval('PT1H')); + } + + $date = $this->setTimeHour($date, $invert, $originalTimestamp); + return $this; + } + + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; + $hours = []; + foreach ($parts as $part) { + $hours = array_merge($hours, $this->getRangeForExpression($part, 23)); + } + + $current_hour = (int) $date->format('H'); + $position = $invert ? \count($hours) - 1 : 0; + $countHours = \count($hours); + if ($countHours > 1) { + for ($i = 0; $i < $countHours - 1; ++$i) { + if ((!$invert && $current_hour >= $hours[$i] && $current_hour < $hours[$i + 1]) || + ($invert && $current_hour > $hours[$i] && $current_hour <= $hours[$i + 1])) { + $position = $invert ? $i : $i + 1; + + break; + } + } + } + + $target = (int) $hours[$position]; + $originalHour = (int)$date->format('H'); + + $originalDay = (int)$date->format('d'); + $previousOffset = $date->getOffset(); + + if (! $invert) { + if ($originalHour >= $target) { + $distance = 24 - $originalHour; + $date = $this->timezoneSafeModify($date, "+{$distance} hours"); + + $actualDay = (int)$date->format('d'); + $actualHour = (int)$date->format('H'); + if (($actualDay !== ($originalDay + 1)) && ($actualHour !== 0)) { + $offsetChange = ($previousOffset - $date->getOffset()); + $date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds"); + } + + $originalHour = (int)$date->format('H'); + } + + $distance = $target - $originalHour; + $date = $this->timezoneSafeModify($date, "+{$distance} hours"); + } else { + if ($originalHour <= $target) { + $distance = ($originalHour + 1); + $date = $this->timezoneSafeModify($date, "-" . $distance . " hours"); + + $actualDay = (int)$date->format('d'); + $actualHour = (int)$date->format('H'); + if (($actualDay !== ($originalDay - 1)) && ($actualHour !== 23)) { + $offsetChange = ($previousOffset - $date->getOffset()); + $date = $this->timezoneSafeModify($date, "+{$offsetChange} seconds"); + } + + $originalHour = (int)$date->format('H'); + } + + $distance = $originalHour - $target; + $date = $this->timezoneSafeModify($date, "-{$distance} hours"); + } + + $date = $this->setTimeHour($date, $invert, $originalTimestamp); + + $actualHour = (int)$date->format('H'); + if ($invert && ($actualHour === ($target - 1) || (($actualHour === 23) && ($target === 0)))) { + $date = $this->timezoneSafeModify($date, "+1 hour"); + } + + return $this; + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php new file mode 100644 index 0000000..eda9109 --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MinutesField.php @@ -0,0 +1,96 @@ +isSatisfied((int)$date->format('i'), $value); + } + + /** + * {@inheritdoc} + * {@inheritDoc} + * + * @param string|null $parts + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface + { + if (is_null($parts)) { + $date = $this->timezoneSafeModify($date, ($invert ? "-" : "+") ."1 minute"); + return $this; + } + + $current_minute = (int) $date->format('i'); + + $parts = false !== strpos($parts, ',') ? explode(',', $parts) : [$parts]; + $minutes = []; + foreach ($parts as $part) { + $minutes = array_merge($minutes, $this->getRangeForExpression($part, 59)); + } + + $position = $invert ? \count($minutes) - 1 : 0; + if (\count($minutes) > 1) { + for ($i = 0; $i < \count($minutes) - 1; ++$i) { + if ((!$invert && $current_minute >= $minutes[$i] && $current_minute < $minutes[$i + 1]) || + ($invert && $current_minute > $minutes[$i] && $current_minute <= $minutes[$i + 1])) { + $position = $invert ? $i : $i + 1; + + break; + } + } + } + + $target = (int) $minutes[$position]; + $originalMinute = (int) $date->format("i"); + + if (! $invert) { + if ($originalMinute >= $target) { + $distance = 60 - $originalMinute; + $date = $this->timezoneSafeModify($date, "+{$distance} minutes"); + + $originalMinute = (int) $date->format("i"); + } + + $distance = $target - $originalMinute; + $date = $this->timezoneSafeModify($date, "+{$distance} minutes"); + } else { + if ($originalMinute <= $target) { + $distance = ($originalMinute + 1); + $date = $this->timezoneSafeModify($date, "-{$distance} minutes"); + + $originalMinute = (int) $date->format("i"); + } + + $distance = $originalMinute - $target; + $date = $this->timezoneSafeModify($date, "-{$distance} minutes"); + } + + return $this; + } +} diff --git a/web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php new file mode 100644 index 0000000..5a15fbb --- /dev/null +++ b/web/app/vendor/dragonmantank/cron-expression/src/Cron/MonthField.php @@ -0,0 +1,61 @@ + 'JAN', 2 => 'FEB', 3 => 'MAR', 4 => 'APR', 5 => 'MAY', 6 => 'JUN', 7 => 'JUL', + 8 => 'AUG', 9 => 'SEP', 10 => 'OCT', 11 => 'NOV', 12 => 'DEC', ]; + + /** + * {@inheritdoc} + */ + public function isSatisfiedBy(DateTimeInterface $date, $value, bool $invert): bool + { + if ($value === '?') { + return true; + } + + $value = $this->convertLiterals($value); + + return $this->isSatisfied((int) $date->format('m'), $value); + } + + /** + * @inheritDoc + * + * @param \DateTime|\DateTimeImmutable $date + */ + public function increment(DateTimeInterface &$date, $invert = false, $parts = null): FieldInterface + { + if (! $invert) { + $date = $date->modify('first day of next month'); + $date = $date->setTime(0, 0); + } else { + $date = $date->modify('last day of previous month'); + $date = $date->setTime(23, 59); + } + + return $this; + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml b/web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml new file mode 100644 index 0000000..4eecff5 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/.coveralls.yml @@ -0,0 +1,3 @@ +service_name: travis-ci +coverage_clover: clover.xml +json_path: coveralls-upload.json diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore b/web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore new file mode 100644 index 0000000..11e7bbf --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/.gitignore @@ -0,0 +1,10 @@ +.DS_Store + +/vendor +composer.lock + +/examples +*.old + +# PHPUnit coverage file +clover.xml diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml b/web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml new file mode 100644 index 0000000..0b462f2 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/.travis.yml @@ -0,0 +1,23 @@ +language: php +php: + - '7.3' + - '7.4' + - '8.0' + - hhvm + +matrix: + allow_failures: + - php: hhvm + fast_finish: true + +sudo: false + +install: + - curl -s http://getcomposer.org/installer | php + - php composer.phar install --no-interaction + +script: + - XDEBUG_MODE=coverage php vendor/bin/phpunit -c phpunit.xml --coverage-clover clover.xml + +after_success: + - travis_retry php vendor/bin/php-coveralls -v diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md b/web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..b34773d --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/CODE_OF_CONDUCT.md @@ -0,0 +1,46 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. + +## Our Standards + +Examples of behavior that contributes to creating a positive environment include: + +* Using welcoming and inclusive language +* Being respectful of differing viewpoints and experiences +* Gracefully accepting constructive criticism +* Focusing on what is best for the community +* Showing empathy towards other community members + +Examples of unacceptable behavior by participants include: + +* The use of sexualized language or imagery and unwelcome sexual attention or advances +* Trolling, insulting/derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or electronic address, without explicit permission +* Other conduct which could reasonably be considered inappropriate in a professional setting + +## Our Responsibilities + +Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. + +Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. + +## Scope + +This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at peppeocchi@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. + +Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] + +[homepage]: http://contributor-covenant.org +[version]: http://contributor-covenant.org/version/1/4/ diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE b/web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE new file mode 100644 index 0000000..e77fea8 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Giuseppe Occhipinti + +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. diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/README.md b/web/app/vendor/peppeocchi/php-cron-scheduler/README.md new file mode 100644 index 0000000..f594990 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/README.md @@ -0,0 +1,502 @@ +PHP Cron Scheduler +== + +[![Latest Stable Version](https://poser.pugx.org/peppeocchi/php-cron-scheduler/v/stable)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [![License](https://poser.pugx.org/peppeocchi/php-cron-scheduler/license)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) [![Build Status](https://travis-ci.org/peppeocchi/php-cron-scheduler.svg)](https://travis-ci.org/peppeocchi/php-cron-scheduler) [![Coverage Status](https://coveralls.io/repos/github/peppeocchi/php-cron-scheduler/badge.svg?branch=v3.x)](https://coveralls.io/github/peppeocchi/php-cron-scheduler?branch=v3.x) [![StyleCI](https://styleci.io/repos/38302733/shield)](https://styleci.io/repos/38302733) [![Total Downloads](https://poser.pugx.org/peppeocchi/php-cron-scheduler/downloads)](https://packagist.org/packages/peppeocchi/php-cron-scheduler) + +This is a framework agnostic cron jobs scheduler that can be easily integrated with your project or run as a standalone command scheduler. +The idea was originally inspired by the [Laravel Task Scheduling](http://laravel.com/docs/5.1/scheduling). + +## Installing via Composer +The recommended way is to install the php-cron-scheduler is through [Composer](https://getcomposer.org/). +Please refer to [Getting Started](https://getcomposer.org/doc/00-intro.md) on how to download and install Composer. + +After you have downloaded/installed Composer, run + +`php composer.phar require peppeocchi/php-cron-scheduler` + +or add the package to your `composer.json` +```json +{ + "require": { + "peppeocchi/php-cron-scheduler": "3.*" + } +} +``` + +Scheduler V4 requires php >= 7.3, please use the [v3 branch](https://github.com/peppeocchi/php-cron-scheduler/tree/v3.x) for php versions < 7.3 and > 7.1 or the [v2 branch](https://github.com/peppeocchi/php-cron-scheduler/tree/v2.x) for php versions < 7.1. + +## How it works + +Create a `scheduler.php` file in the root your project with the following content. +```php +run(); +``` + +Then add a new entry to your crontab to run `scheduler.php` every minute. + +```` +* * * * * path/to/phpbin path/to/scheduler.php 1>> /dev/null 2>&1 +```` + +That's it! Your scheduler is up and running, now you can add your jobs without worring anymore about the crontab. + +## Scheduling jobs + +By default all your jobs will try to run in background. +PHP scripts and raw commands will run in background by default, while functions will always run in foreground. +You can force a command to run in foreground by calling the `inForeground()` method. +**Jobs that have to send the output to email, will run foreground**. + +### Schedule a php script + +```php +$scheduler->php('path/to/my/script.php'); +``` +The `php` method accepts 4 arguments: +- The path to your php script +- The PHP binary to use +- Arguments to be passed to the script (**NOTE**: You need to have **register_argc_argv** enable in your php.ini for this to work ([ref](https://github.com/peppeocchi/php-cron-scheduler/issues/88)). Don't worry it's enabled by default, so unlessy you've intentionally disabled it or your host has it disabled by default, you can ignore it.) +- Identifier +```php +$scheduler->php( + 'path/to/my/script.php', // The script to execute + 'path/to/my/custom/bin/php', // The PHP bin + [ + '-c' => 'ignore', + '--merge' => null, + ], + 'myCustomIdentifier' +); +``` + +### Schedule a raw command + +```php +$scheduler->raw('ps aux | grep httpd'); +``` +The `raw` method accepts 3 arguments: +- Your command +- Arguments to be passed to the command +- Identifier +```php +$scheduler->raw( + 'mycommand | myOtherCommand', + [ + '-v' => '6', + '--silent' => null, + ], + 'myCustomIdentifier' +); +``` + +### Schedule a function + +```php +$scheduler->call(function () { + return true; +}); +``` +The `call` method accepts 3 arguments: +- Your function +- Arguments to be passed to the function +- Identifier +```php +$scheduler->call( + function ($args) { + return $args['user']; + }, + [ + ['user' => $user], + ], + 'myCustomIdentifier' +); +``` + +All of the arguments you pass in the array will be injected to your function. +For example + +```php +$scheduler->call( + function ($firstName, $lastName) { + return implode(' ', [$firstName, $lastName]); + }, + [ + 'John', + 'last_name' => 'Doe', // The keys are being ignored + ], + 'myCustomIdentifier' +); +``` + +If you want to pass a key => value pair, please pass an array within the arguments array + +```php +$scheduler->call( + function ($user, $role) { + return implode(' ', [$user['first_name'], $user['last_name']]) . " has role: '{$role}'"; + }, + [ + [ + 'first_name' => 'John', + 'last_name' => 'Doe', + ], + 'Admin' + ], + 'myCustomIdentifier' +); +``` + +### Schedules execution time + +There are a few methods to help you set the execution time of your schedules. +If you don't call any of this method, the job will run every minute (* * * * *). + +- `at` - This method accepts any expression supported by [dragonmantank/cron-expression](https://github.com/dragonmantank/cron-expression) + ```php + $scheduler->php('script.php')->at('* * * * *'); + ``` +- `everyMinute` - Run every minute. You can optionally pass a `$minute` to specify the job runs every `$minute` minutes. + ```php + $scheduler->php('script.php')->everyMinute(); + $scheduler->php('script.php')->everyMinute(5); + ``` +- `hourly` - Run once per hour. You can optionally pass the `$minute` you want to run, by default it will run every hour at minute '00'. + ```php + $scheduler->php('script.php')->hourly(); + $scheduler->php('script.php')->hourly(53); + ``` +- `daily` - Run once per day. You can optionally pass `$hour` and `$minute` to have more granular control (or a string `hour:minute`) + ```php + $scheduler->php('script.php')->daily(); + $scheduler->php('script.php')->daily(22, 03); + $scheduler->php('script.php')->daily('22:03'); + ``` + +There are additional helpers for weekdays (all accepting optionals hour and minute - defaulted at 00:00) +- `sunday` +- `monday` +- `tuesday` +- `wednesday` +- `thursday` +- `friday` +- `saturday` + +```php +$scheduler->php('script.php')->saturday(); +$scheduler->php('script.php')->friday(18); +$scheduler->php('script.php')->sunday(12, 30); +``` + +And additional helpers for months (all accepting optionals day, hour and minute - defaulted to the 1st of the month at 00:00) +- `january` +- `february` +- `march` +- `april` +- `may` +- `june` +- `july` +- `august` +- `september` +- `october` +- `november` +- `december` + +```php +$scheduler->php('script.php')->january(); +$scheduler->php('script.php')->december(25); +$scheduler->php('script.php')->august(15, 20, 30); +``` + +You can also specify a `date` for when the job should run. +The date can be specified as string or as instance of `DateTime`. In both cases you can specify the date only (e.g. 2018-01-01) or the time as well (e.g. 2018-01-01 10:30), if you don't specify the time it will run at 00:00 on that date. +If you're providing a date in a "non standard" format, it is strongly adviced to pass an instance of `DateTime`. If you're using `createFromFormat` without specifying a time, and you want to default it to 00:00, just make sure to add a `!` to the date format, otherwise the time would be the current time. [Read more](http://php.net/manual/en/datetime.createfromformat.php) + +```php +$scheduler->php('script.php')->date('2018-01-01 12:20'); +$scheduler->php('script.php')->date(new DateTime('2018-01-01')); +$scheduler->php('script.php')->date(DateTime::createFromFormat('!d/m Y', '01/01 2018')); +``` + +### Send output to file/s + +You can define one or multiple files where you want the output of your script/command/function execution to be sent to. + +```php +$scheduler->php('script.php')->output([ + 'my_file1.log', 'my_file2.log' +]); + +// The scheduler catches both stdout and function return and send +// those values to the output file +$scheduler->call(function () { + echo "Hello"; + + return " world!"; +})->output('my_file.log'); +``` + +### Send output to email/s + +You can define one or multiple email addresses where you want the output of your script/command/function execution to be sent to. +In order for the email to be sent, the output of the job needs to be sent first to a file. +In fact, the files will be attached to your email address. +In order for this to work, you need to install [swiftmailer/swiftmailer](https://github.com/swiftmailer/swiftmailer) + +```php +$scheduler->php('script.php')->output([ + // If you specify multiple files, both will be attached to the email + 'my_file1.log', 'my_file2.log' +])->email([ + 'someemail@mail.com' => 'My custom name', + 'someotheremail@mail.com' +]); +``` + +You can optionally customize the `Swift_Mailer` instance with a custom `Swift_Transport`. +You can configure: +- `subject` - The subject of the email sent +- `from` - The email address set as sender +- `body` - The body of the email +- `transport` - The transport to use. For example if you want to use your gmail account or any other SMTP account. The value should be an instance of `Swift_Tranport` +- `ignore_empty_output` - If this is set to `true`, jobs that return no output won't fire any email. + +The configuration can be set "globally" for all the scheduler commands, when creating the scheduler. + +```php +$scheduler = new Scheduler([ + 'email' => [ + 'subject' => 'Visitors count', + 'from' => 'cron@email.com', + 'body' => 'This is the daily visitors count', + 'transport' => Swift_SmtpTransport::newInstance('smtp.gmail.com', 465, 'ssl') + ->setUsername('username') + ->setPassword('password'), + 'ignore_empty_output' => false, + ] +]); +``` + +Or can be set on a job per job basis. + +```php +$scheduler = new Scheduler(); + +$scheduler->php('myscript.php')->configure([ + 'email' => [ + 'subject' => 'Visitors count', + ] +]); + +$scheduler->php('my_other_script.php')->configure([ + 'email' => [ + 'subject' => 'Page views count', + ] +]); +``` + +### Schedule conditional execution + +Sometimes you might want to execute a schedule not only when the execution is due, but also depending on some other condition. + +You can delegate the execution of a cronjob to a truthful test with the method `when`. + +```php +$scheduler->php('script.php')->when(function () { + // The job will run (if due) only when + // this function returns true + return true; +}); +``` + +### Schedules execution order + +The jobs that are due to run are being ordered by their execution: jobs that can run in **background** will be executed **first**. + +### Schedules overlapping + +To prevent the execution of a schedule while the previous execution is still in progress, use the method `onlyOne`. To avoid overlapping, the Scheduler needs to create **lock files**. +By default it will be used the directory path used for temporary files. + +You can specify a custom directory path globally, when creating a new Scheduler instance. + +```php +$scheduler = new Scheduler([ + 'tempDir' => 'path/to/my/tmp/dir' +]); + +$scheduler->php('script.php')->onlyOne(); +``` + +Or you can define the directory path on a job per job basis. + +```php +$scheduler = new Scheduler(); + +// This will use the default directory path +$scheduler->php('script.php')->onlyOne(); + +$scheduler->php('script.php')->onlyOne('path/to/my/tmp/dir'); +$scheduler->php('other_script.php')->onlyOne('path/to/my/other/tmp/dir'); +``` + +In some cases you might want to run the job also if it's overlapping. +For example if the last execution was more that 5 minutes ago. +You can pass a function as a second parameter, the last execution time will be injected. +The job will not run until this function returns `false`. If it returns `true`, the job will run if overlapping. + +```php +$scheduler->php('script.php')->onlyOne(null, function ($lastExecutionTime) { + return (time() - $lastExecutionTime) > (60 * 5); +}); +``` + +### Before job execution + +In some cases you might want to run some code, if the job is due to run, before it's being executed. +For example you might want to add a log entry, ping a url or anything else. +To do so, you can call the `before` like the example below. + +```php +// $logger here is your own implementation +$scheduler->php('script.php')->before(function () use ($logger) { + $logger->info("script.php started at " . time()); +}); +``` + +### After job execution + +Sometime you might wish to do something after a job runs. The `then` methods provides you the flexibility to do anything you want after the job execution. The output of the job will be injected to this function. +For example you might want to add an entry to you logs, ping a url etc... +By default, the job will be forced to run in foreground (because the output is injected to the function), if you don't need the output, you can pass `true` as a second parameter to allow the execution in background (in this case `$output` will be empty). + +```php +// $logger and $messenger here are your own implementation +$scheduler->php('script.php')->then(function ($output) use ($logger, $messenger) { + $logger->info($output); + + $messenger->ping('myurl.com', $output); +}); + +$scheduler->php('script.php')->then(function ($output) use ($logger) { + $logger->info('Job executed!'); +}, true); +``` + +#### Using "before" and "then" together + +```php +// $logger here is your own implementation +$scheduler->php('script.php') + ->before(function () use ($logger) { + $logger->info("script.php started at " . time()); + }) + ->then(function ($output) use ($logger) { + $logger->info("script.php completed at " . time(), [ + 'output' => $output, + ]); + }); +``` + +### Multiple scheduler runs +In some cases you might need to run the scheduler multiple times in the same script. +Although this is not a common case, the following methods will allow you to re-use the same instance of the scheduler. +```php +# some code +$scheduler->run(); +# ... + +// Reset the scheduler after a previous run +$scheduler->resetRun() + ->run(); // now we can run it again +``` + +Another handy method if you are re-using the same instance of the scheduler with different jobs (e.g. job coming from an external source - db, file ...) on every run, is to clear the current scheduled jobs. +```php +$scheduler->clearJobs(); + +$jobsFromDb = $db->query(/*...*/); +foreach ($jobsFromDb as $job) { + $scheduler->php($job->script)->at($job->schedule); +} + +$scheduler->resetRun() + ->run(); +``` + +### Faking scheduler run time +When running the scheduler you might pass an `DateTime` to fake the scheduler run time. +The resons for this feature are described [here](https://github.com/peppeocchi/php-cron-scheduler/pull/28); + +``` +// ... +$fakeRunTime = new DateTime('2017-09-13 00:00:00'); +$scheduler->run($fakeRunTime); +``` + +### Job failures +If some job fails, you can access list of failed jobs and reasons for failures. + +```php +// get all failed jobs and select first +$failedJob = $scheduler->getFailedJobs()[0]; + +// exception that occurred during job +$exception = $failedJob->getException(); + +// job that failed +$job = $failedJob->getJob(); +``` + +### Worker +You can simulate a cronjob by starting a worker. Let's see a simple example +```php +$scheduler = new Scheduler(); +$scheduler->php('some/script.php'); +$scheduler->work(); +``` +The above code starts a worker that will run your job/s every minute. +This is meant to be a testing/debugging tool, but you're free to use it however you like. +You can optionally pass an array of "seconds" of when you want the worker to run your jobs, for example by passing `[0, 30]`, the worker will run your jobs at second **0** and at second **30** of the minute. +```php +$scheduler->work([0, 10, 25, 50, 55]); +``` + +It is highly advisable that you run your worker separately from your scheduler, although you can run the worker within your scheduler. The problem comes when your scheduler has one or more synchronous job, and the worker will have to wait for your job to complete before continuing the loop. For example +```php +$scheduler->call(function () { + sleep(120); +}); +$scheduler->work(); +``` +The above will skip more than one execution, so it won't run anymore every minute but it will run probably every 2 or 3 minutes. +Instead the preferred approach would be to separate the worker from your scheduler. +```php +// File scheduler.php +$scheduler = new Scheduler(); +$scheduler->call(function () { + sleep(120); +}); +$scheduler->run(); +``` +```php +// File worker.php +$scheduler = new Scheduler(); +$scheduler->php('scheduler.php'); +$scheduler->work(); +``` +Then in your command line run `php worker.php`. This will start a foreground process that you can kill by simply exiting the command. + +The worker is not meant to collect any data about your runs, and as already said it is meant to be a testing/debugging tool. + +## License +[The MIT License (MIT)](LICENSE) diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/composer.json b/web/app/vendor/peppeocchi/php-cron-scheduler/composer.json new file mode 100644 index 0000000..b848b9f --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/composer.json @@ -0,0 +1,41 @@ +{ + "name": "peppeocchi/php-cron-scheduler", + "description": "PHP Cron Job Scheduler", + "license": "MIT", + "keywords": ["cron job", "scheduler"], + "authors": [ + { + "name": "Giuseppe Occhipinti", + "email": "peppeocchi@gmail.com" + }, + { + "name": "Carsten Windler", + "email": "carsten@carstenwindler.de", + "homepage": "http://carstenwindler.de", + "role": "Contributor" + } + ], + "minimum-stability": "dev", + "require": { + "php": "^7.3 || ^8.0", + "dragonmantank/cron-expression": "^3.0" + }, + "require-dev": { + "phpunit/phpunit": "~9.5", + "php-coveralls/php-coveralls": "^2.4", + "swiftmailer/swiftmailer": "~5.4 || ^6.0" + }, + "suggest": { + "swiftmailer/swiftmailer": "Required to send the output of a job to email address/es (~5.4 || ^6.0)." + }, + "autoload": { + "psr-4": { + "GO\\": "src/GO/" + } + }, + "autoload-dev": { + "psr-4": { + "Tests\\": "tests/GO/" + } + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml b/web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml new file mode 100644 index 0000000..c29142a --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/phpunit.xml @@ -0,0 +1,20 @@ + + + + + src/GO + + + + + + + + ./tests/ + + + + + + + diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php new file mode 100644 index 0000000..776f825 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/FailedJob.php @@ -0,0 +1,32 @@ +job = $job; + $this->exception = $exception; + } + + public function getJob(): Job + { + return $this->job; + } + + public function getException(): Exception + { + return $this->exception; + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php new file mode 100644 index 0000000..b666930 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Job.php @@ -0,0 +1,590 @@ +id = $id; + } else { + if (is_string($command)) { + $this->id = md5($command); + } elseif (is_array($command)) { + $this->id = md5(serialize($command)); + } else { + /* @var object $command */ + $this->id = spl_object_hash($command); + } + } + + $this->creationTime = new DateTime('now'); + + // initialize the directory path for lock files + $this->tempDir = sys_get_temp_dir(); + + $this->command = $command; + $this->args = $args; + } + + /** + * Get the Job id. + * + * @return string + */ + public function getId() + { + return $this->id; + } + + /** + * Check if the Job is due to run. + * It accepts as input a DateTime used to check if + * the job is due. Defaults to job creation time. + * It also defaults the execution time if not previously defined. + * + * @param DateTime $date + * @return bool + */ + public function isDue(DateTime $date = null) + { + // The execution time is being defaulted if not defined + if (! $this->executionTime) { + $this->at('* * * * *'); + } + + $date = $date !== null ? $date : $this->creationTime; + + if ($this->executionYear && $this->executionYear !== $date->format('Y')) { + return false; + } + + return $this->executionTime->isDue($date); + } + + /** + * Check if the Job is overlapping. + * + * @return bool + */ + public function isOverlapping() + { + return $this->lockFile && + file_exists($this->lockFile) && + call_user_func($this->whenOverlapping, filemtime($this->lockFile)) === false; + } + + /** + * Force the Job to run in foreground. + * + * @return self + */ + public function inForeground() + { + $this->runInBackground = false; + + return $this; + } + + /** + * Check if the Job can run in background. + * + * @return bool + */ + public function canRunInBackground() + { + if (is_callable($this->command) || $this->runInBackground === false) { + return false; + } + + return true; + } + + /** + * This will prevent the Job from overlapping. + * It prevents another instance of the same Job of + * being executed if the previous is still running. + * The job id is used as a filename for the lock file. + * + * @param string $tempDir The directory path for the lock files + * @param callable $whenOverlapping A callback to ignore job overlapping + * @return self + */ + public function onlyOne($tempDir = null, callable $whenOverlapping = null) + { + if ($tempDir === null || ! is_dir($tempDir)) { + $tempDir = $this->tempDir; + } + + $this->lockFile = implode('/', [ + trim($tempDir), + trim($this->id) . '.lock', + ]); + + if ($whenOverlapping) { + $this->whenOverlapping = $whenOverlapping; + } else { + $this->whenOverlapping = function () { + return false; + }; + } + + return $this; + } + + /** + * Compile the Job command. + * + * @return mixed + */ + public function compile() + { + $compiled = $this->command; + + // If callable, return the function itself + if (is_callable($compiled)) { + return $compiled; + } + + // Augment with any supplied arguments + foreach ($this->args as $key => $value) { + $compiled .= ' ' . escapeshellarg($key); + if ($value !== null) { + $compiled .= ' ' . escapeshellarg($value); + } + } + + // Add the boilerplate to redirect the output to file/s + if (count($this->outputTo) > 0) { + $compiled .= ' | tee '; + $compiled .= $this->outputMode === 'a' ? '-a ' : ''; + foreach ($this->outputTo as $file) { + $compiled .= $file . ' '; + } + + $compiled = trim($compiled); + } + + // Add boilerplate to remove lockfile after execution + if ($this->lockFile) { + $compiled .= '; rm ' . $this->lockFile; + } + + // Add boilerplate to run in background + if ($this->canRunInBackground()) { + // Parentheses are need execute the chain of commands in a subshell + // that can then run in background + $compiled = '(' . $compiled . ') > /dev/null 2>&1 &'; + } + + return trim($compiled); + } + + /** + * Configure the job. + * + * @param array $config + * @return self + */ + public function configure(array $config = []) + { + if (isset($config['email'])) { + if (! is_array($config['email'])) { + throw new InvalidArgumentException('Email configuration should be an array.'); + } + $this->emailConfig = $config['email']; + } + + // Check if config has defined a tempDir + if (isset($config['tempDir']) && is_dir($config['tempDir'])) { + $this->tempDir = $config['tempDir']; + } + + return $this; + } + + /** + * Truth test to define if the job should run if due. + * + * @param callable $fn + * @return self + */ + public function when(callable $fn) + { + $this->truthTest = $fn(); + + return $this; + } + + /** + * Run the job. + * + * @return bool + */ + public function run() + { + // If the truthTest failed, don't run + if ($this->truthTest !== true) { + return false; + } + + // If overlapping, don't run + if ($this->isOverlapping()) { + return false; + } + + $compiled = $this->compile(); + + // Write lock file if necessary + $this->createLockFile(); + + if (is_callable($this->before)) { + call_user_func($this->before); + } + + if (is_callable($compiled)) { + $this->output = $this->exec($compiled); + } else { + exec($compiled, $this->output, $this->returnCode); + } + + $this->finalise(); + + return true; + } + + /** + * Create the job lock file. + * + * @param mixed $content + * @return void + */ + private function createLockFile($content = null) + { + if ($this->lockFile) { + if ($content === null || ! is_string($content)) { + $content = $this->getId(); + } + + file_put_contents($this->lockFile, $content); + } + } + + /** + * Remove the job lock file. + * + * @return void + */ + private function removeLockFile() + { + if ($this->lockFile && file_exists($this->lockFile)) { + unlink($this->lockFile); + } + } + + /** + * Execute a callable job. + * + * @param callable $fn + * @throws Exception + * @return string + */ + private function exec(callable $fn) + { + ob_start(); + + try { + $returnData = call_user_func_array($fn, $this->args); + } catch (Exception $e) { + ob_end_clean(); + throw $e; + } + + $outputBuffer = ob_get_clean(); + + foreach ($this->outputTo as $filename) { + if ($outputBuffer) { + file_put_contents($filename, $outputBuffer, $this->outputMode === 'a' ? FILE_APPEND : 0); + } + + if ($returnData) { + file_put_contents($filename, $returnData, FILE_APPEND); + } + } + + $this->removeLockFile(); + + return $outputBuffer . (is_string($returnData) ? $returnData : ''); + } + + /** + * Set the file/s where to write the output of the job. + * + * @param string|array $filename + * @param bool $append + * @return self + */ + public function output($filename, $append = false) + { + $this->outputTo = is_array($filename) ? $filename : [$filename]; + $this->outputMode = $append === false ? 'w' : 'a'; + + return $this; + } + + /** + * Get the job output. + * + * @return mixed + */ + public function getOutput() + { + return $this->output; + } + + /** + * Set the emails where the output should be sent to. + * The Job should be set to write output to a file + * for this to work. + * + * @param string|array $email + * @return self + */ + public function email($email) + { + if (! is_string($email) && ! is_array($email)) { + throw new InvalidArgumentException('The email can be only string or array'); + } + + $this->emailTo = is_array($email) ? $email : [$email]; + + // Force the job to run in foreground + $this->inForeground(); + + return $this; + } + + /** + * Finilise the job after execution. + * + * @return void + */ + private function finalise() + { + // Send output to email + $this->emailOutput(); + + // Call any callback defined + if (is_callable($this->after)) { + call_user_func($this->after, $this->output, $this->returnCode); + } + } + + /** + * Email the output of the job, if any. + * + * @return bool + */ + private function emailOutput() + { + if (! count($this->outputTo) || ! count($this->emailTo)) { + return false; + } + + if (isset($this->emailConfig['ignore_empty_output']) && + $this->emailConfig['ignore_empty_output'] === true && + empty($this->output) + ) { + return false; + } + + $this->sendToEmails($this->outputTo); + + return true; + } + + /** + * Set function to be called before job execution + * Job object is injected as a parameter to callable function. + * + * @param callable $fn + * @return self + */ + public function before(callable $fn) + { + $this->before = $fn; + + return $this; + } + + /** + * Set a function to be called after job execution. + * By default this will force the job to run in foreground + * because the output is injected as a parameter of this + * function, but it could be avoided by passing true as a + * second parameter. The job will run in background if it + * meets all the other criteria. + * + * @param callable $fn + * @param bool $runInBackground + * @return self + */ + public function then(callable $fn, $runInBackground = false) + { + $this->after = $fn; + + // Force the job to run in foreground + if ($runInBackground === false) { + $this->inForeground(); + } + + return $this; + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php new file mode 100644 index 0000000..0c5fba6 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Scheduler.php @@ -0,0 +1,327 @@ +config = $config; + } + + /** + * Queue a job for execution in the correct queue. + * + * @param Job $job + * @return void + */ + private function queueJob(Job $job) + { + $this->jobs[] = $job; + } + + /** + * Prioritise jobs in background. + * + * @return array + */ + private function prioritiseJobs() + { + $background = []; + $foreground = []; + + foreach ($this->jobs as $job) { + if ($job->canRunInBackground()) { + $background[] = $job; + } else { + $foreground[] = $job; + } + } + + return array_merge($background, $foreground); + } + + /** + * Get the queued jobs. + * + * @return array + */ + public function getQueuedJobs() + { + return $this->prioritiseJobs(); + } + + /** + * Queues a function execution. + * + * @param callable $fn The function to execute + * @param array $args Optional arguments to pass to the php script + * @param string $id Optional custom identifier + * @return Job + */ + public function call(callable $fn, $args = [], $id = null) + { + $job = new Job($fn, $args, $id); + + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Queues a php script execution. + * + * @param string $script The path to the php script to execute + * @param string $bin Optional path to the php binary + * @param array $args Optional arguments to pass to the php script + * @param string $id Optional custom identifier + * @return Job + */ + public function php($script, $bin = null, $args = [], $id = null) + { + if (! is_string($script)) { + throw new InvalidArgumentException('The script should be a valid path to a file.'); + } + + $bin = $bin !== null && is_string($bin) && file_exists($bin) ? + $bin : (PHP_BINARY === '' ? '/usr/bin/php' : PHP_BINARY); + + $job = new Job($bin . ' ' . $script, $args, $id); + + if (! file_exists($script)) { + $this->pushFailedJob( + $job, + new InvalidArgumentException('The script should be a valid path to a file.') + ); + } + + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Queue a raw shell command. + * + * @param string $command The command to execute + * @param array $args Optional arguments to pass to the command + * @param string $id Optional custom identifier + * @return Job + */ + public function raw($command, $args = [], $id = null) + { + $job = new Job($command, $args, $id); + + $this->queueJob($job->configure($this->config)); + + return $job; + } + + /** + * Run the scheduler. + * + * @param DateTime $runTime Optional, run at specific moment + * @return array Executed jobs + */ + public function run(Datetime $runTime = null) + { + $jobs = $this->getQueuedJobs(); + + if (is_null($runTime)) { + $runTime = new DateTime('now'); + } + + foreach ($jobs as $job) { + if ($job->isDue($runTime)) { + try { + $job->run(); + $this->pushExecutedJob($job); + } catch (\Exception $e) { + $this->pushFailedJob($job, $e); + } + } + } + + return $this->getExecutedJobs(); + } + + /** + * Reset all collected data of last run. + * + * Call before run() if you call run() multiple times. + */ + public function resetRun() + { + // Reset collected data of last run + $this->executedJobs = []; + $this->failedJobs = []; + $this->outputSchedule = []; + + return $this; + } + + /** + * Add an entry to the scheduler verbose output array. + * + * @param string $string + * @return void + */ + private function addSchedulerVerboseOutput($string) + { + $now = '[' . (new DateTime('now'))->format('c') . '] '; + $this->outputSchedule[] = $now . $string; + + // Print to stdoutput in light gray + // echo "\033[37m{$string}\033[0m\n"; + } + + /** + * Push a succesfully executed job. + * + * @param Job $job + * @return Job + */ + private function pushExecutedJob(Job $job) + { + $this->executedJobs[] = $job; + + $compiled = $job->compile(); + + // If callable, log the string Closure + if (is_callable($compiled)) { + $compiled = 'Closure'; + } + + $this->addSchedulerVerboseOutput("Executing {$compiled}"); + + return $job; + } + + /** + * Get the executed jobs. + * + * @return array + */ + public function getExecutedJobs() + { + return $this->executedJobs; + } + + /** + * Push a failed job. + * + * @param Job $job + * @param Exception $e + * @return Job + */ + private function pushFailedJob(Job $job, Exception $e) + { + $this->failedJobs[] = new FailedJob($job, $e); + + $compiled = $job->compile(); + + // If callable, log the string Closure + if (is_callable($compiled)) { + $compiled = 'Closure'; + } + + $this->addSchedulerVerboseOutput("{$e->getMessage()}: {$compiled}"); + + return $job; + } + + /** + * Get the failed jobs. + * + * @return FailedJob[] + */ + public function getFailedJobs() + { + return $this->failedJobs; + } + + /** + * Get the scheduler verbose output. + * + * @param string $type Allowed: text, html, array + * @return mixed The return depends on the requested $type + */ + public function getVerboseOutput($type = 'text') + { + switch ($type) { + case 'text': + return implode("\n", $this->outputSchedule); + case 'html': + return implode('
', $this->outputSchedule); + case 'array': + return $this->outputSchedule; + default: + throw new InvalidArgumentException('Invalid output type'); + } + } + + /** + * Remove all queued Jobs. + */ + public function clearJobs() + { + $this->jobs = []; + + return $this; + } + + /** + * Start a worker. + * + * @param array $seconds - When the scheduler should run + */ + public function work(array $seconds = [0]) + { + while (true) { + if (in_array((int) date('s'), $seconds)) { + $this->run(); + sleep(1); + } + } + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php new file mode 100644 index 0000000..c80a4d9 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Interval.php @@ -0,0 +1,418 @@ +executionTime = CronExpression::factory($expression); + + return $this; + } + + /** + * Run the Job at a specific date. + * + * @param string/DateTime $date + * @return self + */ + public function date($date) + { + if (! $date instanceof DateTime) { + $date = new DateTime($date); + } + + $this->executionYear = $date->format('Y'); + + return $this->at("{$date->format('i')} {$date->format('H')} {$date->format('d')} {$date->format('m')} *"); + } + + /** + * Set the execution time to every minute. + * + * @param int|string|null When set, specifies that the job will be run every $minute minutes + * + * @return self + */ + public function everyMinute($minute = null) + { + $minuteExpression = '*'; + if ($minute !== null) { + $c = $this->validateCronSequence($minute); + $minuteExpression = '*/' . $c['minute']; + } + + return $this->at($minuteExpression . ' * * * *'); + } + + /** + * Set the execution time to every hour. + * + * @param int|string $minute + * @return self + */ + public function hourly($minute = 0) + { + $c = $this->validateCronSequence($minute); + + return $this->at("{$c['minute']} * * * *"); + } + + /** + * Set the execution time to once a day. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function daily($hour = 0, $minute = 0) + { + if (is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = isset($parts[1]) ? $parts[1] : '0'; + } + + $c = $this->validateCronSequence($minute, $hour); + + return $this->at("{$c['minute']} {$c['hour']} * * *"); + } + + /** + * Set the execution time to once a week. + * + * @param int|string $weekday + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function weekly($weekday = 0, $hour = 0, $minute = 0) + { + if (is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = isset($parts[1]) ? $parts[1] : '0'; + } + + $c = $this->validateCronSequence($minute, $hour, null, null, $weekday); + + return $this->at("{$c['minute']} {$c['hour']} * * {$c['weekday']}"); + } + + /** + * Set the execution time to once a month. + * + * @param int|string $month + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function monthly($month = '*', $day = 1, $hour = 0, $minute = 0) + { + if (is_string($hour)) { + $parts = explode(':', $hour); + $hour = $parts[0]; + $minute = isset($parts[1]) ? $parts[1] : '0'; + } + + $c = $this->validateCronSequence($minute, $hour, $day, $month); + + return $this->at("{$c['minute']} {$c['hour']} {$c['day']} {$c['month']} *"); + } + + /** + * Set the execution time to every Sunday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function sunday($hour = 0, $minute = 0) + { + return $this->weekly(0, $hour, $minute); + } + + /** + * Set the execution time to every Monday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function monday($hour = 0, $minute = 0) + { + return $this->weekly(1, $hour, $minute); + } + + /** + * Set the execution time to every Tuesday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function tuesday($hour = 0, $minute = 0) + { + return $this->weekly(2, $hour, $minute); + } + + /** + * Set the execution time to every Wednesday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function wednesday($hour = 0, $minute = 0) + { + return $this->weekly(3, $hour, $minute); + } + + /** + * Set the execution time to every Thursday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function thursday($hour = 0, $minute = 0) + { + return $this->weekly(4, $hour, $minute); + } + + /** + * Set the execution time to every Friday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function friday($hour = 0, $minute = 0) + { + return $this->weekly(5, $hour, $minute); + } + + /** + * Set the execution time to every Saturday. + * + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function saturday($hour = 0, $minute = 0) + { + return $this->weekly(6, $hour, $minute); + } + + /** + * Set the execution time to every January. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function january($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(1, $day, $hour, $minute); + } + + /** + * Set the execution time to every February. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function february($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(2, $day, $hour, $minute); + } + + /** + * Set the execution time to every March. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function march($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(3, $day, $hour, $minute); + } + + /** + * Set the execution time to every April. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function april($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(4, $day, $hour, $minute); + } + + /** + * Set the execution time to every May. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function may($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(5, $day, $hour, $minute); + } + + /** + * Set the execution time to every June. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function june($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(6, $day, $hour, $minute); + } + + /** + * Set the execution time to every July. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function july($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(7, $day, $hour, $minute); + } + + /** + * Set the execution time to every August. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function august($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(8, $day, $hour, $minute); + } + + /** + * Set the execution time to every September. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function september($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(9, $day, $hour, $minute); + } + + /** + * Set the execution time to every October. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function october($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(10, $day, $hour, $minute); + } + + /** + * Set the execution time to every November. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function november($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(11, $day, $hour, $minute); + } + + /** + * Set the execution time to every December. + * + * @param int|string $day + * @param int|string $hour + * @param int|string $minute + * @return self + */ + public function december($day = 1, $hour = 0, $minute = 0) + { + return $this->monthly(12, $day, $hour, $minute); + } + + /** + * Validate sequence of cron expression. + * + * @param int|string $minute + * @param int|string $hour + * @param int|string $day + * @param int|string $month + * @param int|string $weekday + * @return array + */ + private function validateCronSequence($minute = null, $hour = null, $day = null, $month = null, $weekday = null) + { + return [ + 'minute' => $this->validateCronRange($minute, 0, 59), + 'hour' => $this->validateCronRange($hour, 0, 23), + 'day' => $this->validateCronRange($day, 1, 31), + 'month' => $this->validateCronRange($month, 1, 12), + 'weekday' => $this->validateCronRange($weekday, 0, 6), + ]; + } + + /** + * Validate sequence of cron expression. + * + * @param int|string $value + * @param int $min + * @param int $max + * @return mixed + */ + private function validateCronRange($value, $min, $max) + { + if ($value === null || $value === '*') { + return '*'; + } + + if (! is_numeric($value) || + ! ($value >= $min && $value <= $max) + ) { + throw new InvalidArgumentException( + "Invalid value: it should be '*' or between {$min} and {$max}." + ); + } + + return (int) $value; + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php new file mode 100644 index 0000000..27b009e --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/src/GO/Traits/Mailer.php @@ -0,0 +1,62 @@ +emailConfig['subject']) || + ! is_string($this->emailConfig['subject']) + ) { + $this->emailConfig['subject'] = 'Cronjob execution'; + } + + if (! isset($this->emailConfig['from'])) { + $this->emailConfig['from'] = ['cronjob@server.my' => 'My Email Server']; + } + + if (! isset($this->emailConfig['body']) || + ! is_string($this->emailConfig['body']) + ) { + $this->emailConfig['body'] = 'Cronjob output attached'; + } + + if (! isset($this->emailConfig['transport']) || + ! ($this->emailConfig['transport'] instanceof \Swift_Transport) + ) { + $this->emailConfig['transport'] = new \Swift_SendmailTransport(); + } + + return $this->emailConfig; + } + + /** + * Send files to emails. + * + * @param array $files + * @return void + */ + private function sendToEmails(array $files) + { + $config = $this->getEmailConfig(); + + $mailer = new \Swift_Mailer($config['transport']); + + $message = (new \Swift_Message()) + ->setSubject($config['subject']) + ->setFrom($config['from']) + ->setTo($this->emailTo) + ->setBody($config['body']) + ->addPart('Cronjob output attached', 'text/html'); + + foreach ($files as $filename) { + $message->attach(\Swift_Attachment::fromPath($filename)); + } + + $mailer->send($message); + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php new file mode 100644 index 0000000..9519a73 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/IntervalTest.php @@ -0,0 +1,274 @@ +assertTrue($job->everyMinute()->isDue(\DateTime::createFromFormat('H:i', '00:00'))); + } + + public function testShouldRunHourly() + { + $job = new Job('ls'); + + // Default run is at minute 00 every hour + $this->assertTrue($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '10:00'))); + $this->assertFalse($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '10:01'))); + $this->assertTrue($job->hourly()->isDue(\DateTime::createFromFormat('H:i', '11:00'))); + } + + public function testShouldRunHourlyWithCustomInput() + { + $job = new Job('ls'); + + $this->assertTrue($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '10:19'))); + $this->assertTrue($job->hourly('07')->isDue(\DateTime::createFromFormat('H:i', '10:07'))); + $this->assertFalse($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '10:01'))); + $this->assertTrue($job->hourly(19)->isDue(\DateTime::createFromFormat('H:i', '11:19'))); + } + + public function testShouldThrowExceptionWithInvalidHourlyMinuteInput() + { + $this->expectException(\InvalidArgumentException::class); + + $job = new Job('ls'); + $job->hourly('abc'); + } + + public function testShouldRunDaily() + { + $job = new Job('ls'); + + // Default run is at 00:00 every day + $this->assertTrue($job->daily()->isDue(\DateTime::createFromFormat('H:i', '00:00'))); + } + + public function testShouldRunDailyWithCustomInput() + { + $job = new Job('ls'); + + $this->assertTrue($job->daily(19)->isDue(\DateTime::createFromFormat('H:i', '19:00'))); + $this->assertTrue($job->daily(19, 53)->isDue(\DateTime::createFromFormat('H:i', '19:53'))); + $this->assertFalse($job->daily(19)->isDue(\DateTime::createFromFormat('H:i', '18:00'))); + $this->assertFalse($job->daily(19, 53)->isDue(\DateTime::createFromFormat('H:i', '19:52'))); + + // A string is also acceptable + $this->assertTrue($job->daily('19')->isDue(\DateTime::createFromFormat('H:i', '19:00'))); + $this->assertTrue($job->daily('19:53')->isDue(\DateTime::createFromFormat('H:i', '19:53'))); + } + + public function testShouldThrowExceptionWithInvalidDailyHourInput() + { + $this->expectException(\InvalidArgumentException::class); + + $job = new Job('ls'); + $job->daily('abc'); + } + + public function testShouldThrowExceptionWithInvalidDailyMinuteInput() + { + $this->expectException(\InvalidArgumentException::class); + + $job = new Job('ls'); + $job->daily(2, 'abc'); + } + + public function testShouldRunWeekly() + { + $job = new Job('ls'); + + // Default run is every Sunday at 00:00 + $this->assertTrue($job->weekly()->isDue( + new \DateTime('Sunday')) + ); + + $this->assertFalse($job->weekly()->isDue( + new \DateTime('Tuesday')) + ); + } + + public function testShouldRunWeeklyOnCustomDay() + { + $job = new Job('ls'); + + $this->assertTrue($job->weekly(6)->isDue( + new \DateTime('Saturday')) + ); + + // Testing also the helpers to run weekly on custom day + $this->assertTrue($job->monday()->isDue( + new \DateTime('Monday')) + ); + $this->assertFalse($job->monday()->isDue( + new \DateTime('Saturday')) + ); + + $this->assertTrue($job->tuesday()->isDue( + new \DateTime('Tuesday')) + ); + $this->assertTrue($job->wednesday()->isDue( + new \DateTime('Wednesday')) + ); + $this->assertTrue($job->thursday()->isDue( + new \DateTime('Thursday')) + ); + $this->assertTrue($job->friday()->isDue( + new \DateTime('Friday')) + ); + $this->assertTrue($job->saturday()->isDue( + new \DateTime('Saturday')) + ); + $this->assertTrue($job->sunday()->isDue( + new \DateTime('Sunday')) + ); + } + + public function testShouldRunWeeklyOnCustomDayAndTime() + { + $job = new Job('ls'); + + $date1 = new \DateTime('Saturday 03:45'); + $date2 = new \DateTime('Saturday 03:46'); + + $this->assertTrue($job->weekly(6, 3, 45)->isDue($date1)); + $this->assertTrue($job->weekly(6, '03:45')->isDue($date1)); + $this->assertFalse($job->weekly(6, '03:45')->isDue($date2)); + } + + public function testShouldRunMonthly() + { + $job = new Job('ls'); + + // Default run is every 1st of the month at 00:00 + $this->assertTrue($job->monthly()->isDue( + new \DateTime('01 January')) + ); + $this->assertTrue($job->monthly()->isDue( + new \DateTime('01 December')) + ); + + $this->assertFalse($job->monthly()->isDue( + new \DateTime('02 January')) + ); + } + + public function testShouldRunMonthlyOnCustomMonth() + { + $job = new Job('ls'); + + $this->assertTrue($job->monthly()->isDue( + new \DateTime('01 January')) + ); + + // Testing also the helpers to run weekly on custom day + $this->assertTrue($job->january()->isDue( + new \DateTime('01 January')) + ); + $this->assertFalse($job->january()->isDue( + new \DateTime('01 February')) + ); + + $this->assertTrue($job->february()->isDue( + new \DateTime('01 February')) + ); + + $this->assertTrue($job->march()->isDue( + new \DateTime('01 March')) + ); + $this->assertTrue($job->april()->isDue( + new \DateTime('01 April')) + ); + $this->assertTrue($job->may()->isDue( + new \DateTime('01 May')) + ); + $this->assertTrue($job->june()->isDue( + new \DateTime('01 June')) + ); + $this->assertTrue($job->july()->isDue( + new \DateTime('01 July')) + ); + $this->assertTrue($job->august()->isDue( + new \DateTime('01 August')) + ); + $this->assertTrue($job->september()->isDue( + new \DateTime('01 September')) + ); + $this->assertTrue($job->october()->isDue( + new \DateTime('01 October')) + ); + $this->assertTrue($job->november()->isDue( + new \DateTime('01 November')) + ); + $this->assertTrue($job->december()->isDue( + new \DateTime('01 December')) + ); + } + + public function testShouldRunMonthlyOnCustomDayAndTime() + { + $job = new Job('ls'); + + $date1 = new \DateTime('May 15 12:21'); + $date2 = new \DateTime('February 15 12:21'); + $date3 = new \DateTime('February 16 12:21'); + + $this->assertTrue($job->monthly(5, 15, 12, 21)->isDue($date1)); + $this->assertTrue($job->monthly(5, 15, '12:21')->isDue($date1)); + $this->assertFalse($job->monthly(5, 15, '12:21')->isDue($date2)); + // Every 15th at 12:21 + $this->assertTrue($job->monthly(null, 15, '12:21')->isDue($date1)); + $this->assertTrue($job->monthly(null, 15, '12:21')->isDue($date2)); + $this->assertFalse($job->monthly(null, 15, '12:21')->isDue($date3)); + } + + public function testShouldRunAtSpecificDate() + { + $job = new Job('ls'); + + $date = '2018-01-01'; + + // As instance of datetime + $this->assertTrue($job->date(new \DateTime($date))->isDue(new \DateTime($date))); + // As date string + $this->assertTrue($job->date($date)->isDue(new \DateTime($date))); + // Fail for different day + $this->assertFalse($job->date($date)->isDue(new \DateTime('2018-01-02'))); + } + + public function testShouldRunAtSpecificDateTime() + { + $job = new Job('ls'); + + $date = '2018-01-01 12:20'; + + // As instance of datetime + $this->assertTrue($job->date(new \DateTime($date))->isDue(new \DateTime($date))); + // As date string + $this->assertTrue($job->date($date)->isDue(new \DateTime($date))); + // Fail for different time + $this->assertFalse($job->date($date)->isDue(new \DateTime('2018-01-01 12:21'))); + } + + public function testShouldFailIfDifferentYear() + { + $job = new Job('ls'); + + // As instance of datetime + $this->assertFalse($job->date('2018-01-01')->isDue(new \DateTime('2019-01-01'))); + } + + public function testEveryMinuteWithParameter() + { + $job = new Job('ls'); + + // Job should run at 10:00, 10:05, 10:10 etc., but not at 10:02 + $this->assertTrue($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:00'))); + $this->assertFalse($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:02'))); + $this->assertTrue($job->everyMinute(5)->isDue(\DateTime::createFromFormat('H:i', '10:05'))); + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php new file mode 100644 index 0000000..dad4c8d --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobOutputFilesTest.php @@ -0,0 +1,201 @@ +assertFalse(file_exists($outputFile)); + $job->output($outputFile)->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile)); + + // Content should be 'hi' + $this->assertEquals('hi', file_get_contents($outputFile)); + + unlink($outputFile); + } + + public function testShouldWriteCommandOutputToMultipleFiles() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php'; + $job = new Job($command); + $outputFile1 = __DIR__ . '/../tmp/output1.log'; + $outputFile2 = __DIR__ . '/../tmp/output2.log'; + $outputFile3 = __DIR__ . '/../tmp/output3.log'; + + @unlink($outputFile1); + @unlink($outputFile2); + @unlink($outputFile3); + + // Test fist that the file doesn't exist yet + $this->assertFalse(file_exists($outputFile1)); + $this->assertFalse(file_exists($outputFile2)); + $this->assertFalse(file_exists($outputFile3)); + $job->output([ + $outputFile1, + $outputFile2, + $outputFile3, + ])->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile1)); + $this->assertTrue(file_exists($outputFile2)); + $this->assertTrue(file_exists($outputFile3)); + + $this->assertEquals('hi', file_get_contents($outputFile1)); + $this->assertEquals('hi', file_get_contents($outputFile2)); + $this->assertEquals('hi', file_get_contents($outputFile3)); + + unlink($outputFile1); + unlink($outputFile2); + unlink($outputFile3); + } + + public function testShouldWriteFunctionOutputToSingleFile() + { + $job = new Job(function () { + echo 'Hello '; + + return 'World!'; + }); + $outputFile = __DIR__ . '/../tmp/output.log'; + + @unlink($outputFile); + + // Test fist that the file doesn't exist yet + $this->assertFalse(file_exists($outputFile)); + $job->output($outputFile)->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile)); + + $this->assertEquals('Hello World!', file_get_contents($outputFile)); + + unlink($outputFile); + } + + public function testShouldWriteFunctionOutputToMultipleFiles() + { + $job = new Job(function () { + echo 'Hello'; + }); + $outputFile1 = __DIR__ . '/../tmp/output1.log'; + $outputFile2 = __DIR__ . '/../tmp/output2.log'; + $outputFile3 = __DIR__ . '/../tmp/output3.log'; + + @unlink($outputFile1); + @unlink($outputFile2); + @unlink($outputFile3); + + // Test fist that the file doesn't exist yet + $this->assertFalse(file_exists($outputFile1)); + $this->assertFalse(file_exists($outputFile2)); + $this->assertFalse(file_exists($outputFile3)); + $job->output([ + $outputFile1, + $outputFile2, + $outputFile3, + ])->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile1)); + $this->assertTrue(file_exists($outputFile2)); + $this->assertTrue(file_exists($outputFile3)); + + $this->assertEquals('Hello', file_get_contents($outputFile1)); + $this->assertEquals('Hello', file_get_contents($outputFile2)); + $this->assertEquals('Hello', file_get_contents($outputFile3)); + + unlink($outputFile1); + unlink($outputFile2); + unlink($outputFile3); + } + + public function testShouldWriteFunctionReturnToSingleFile() + { + $job = new Job(function () { + return 'Hello World!'; + }); + $outputFile = __DIR__ . '/../tmp/output1.log'; + + // Test fist that the file doesn't exist yet + $this->assertFalse(file_exists($outputFile)); + $job->output($outputFile)->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile)); + + $this->assertEquals('Hello World!', file_get_contents($outputFile)); + + unlink($outputFile); + } + + public function testShouldWriteFunctionReturnToMultipleFiles() + { + $job = new Job(function () { + return ['Hello ', 'World!']; + }); + $outputFile1 = __DIR__ . '/../tmp/output1.log'; + $outputFile2 = __DIR__ . '/../tmp/output2.log'; + $outputFile3 = __DIR__ . '/../tmp/output3.log'; + + @unlink($outputFile1); + @unlink($outputFile2); + @unlink($outputFile3); + + // Test fist that the file doesn't exist yet + $this->assertFalse(file_exists($outputFile1)); + $this->assertFalse(file_exists($outputFile2)); + $this->assertFalse(file_exists($outputFile3)); + $job->output([ + $outputFile1, + $outputFile2, + $outputFile3, + ])->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile1)); + $this->assertTrue(file_exists($outputFile2)); + $this->assertTrue(file_exists($outputFile3)); + + $this->assertEquals('Hello World!', file_get_contents($outputFile1)); + $this->assertEquals('Hello World!', file_get_contents($outputFile2)); + $this->assertEquals('Hello World!', file_get_contents($outputFile3)); + + unlink($outputFile1); + unlink($outputFile2); + unlink($outputFile3); + } + + public function testShouldWriteFunctionOutputAndReturnToFile() + { + $job = new Job(function () { + echo 'Hello '; + + return 'World!'; + }); + $outputFile = __DIR__ . '/../tmp/output1.log'; + + // Test fist that the file doesn't exist yet + $this->assertFalse(file_exists($outputFile)); + $job->output($outputFile)->run(); + + sleep(2); + $this->assertTrue(file_exists($outputFile)); + + $this->assertEquals('Hello World!', file_get_contents($outputFile)); + + unlink($outputFile); + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php new file mode 100644 index 0000000..1b84cb2 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/JobTest.php @@ -0,0 +1,470 @@ +assertTrue(is_string($job1->getId())); + + $job2 = new Job(function () { + return true; + }); + $this->assertTrue(is_string($job2->getId())); + + $job3 = new Job(['MyClass', 'myMethod']); + $this->assertTrue(is_string($job3->getId())); + } + + public function testShouldGenerateIdFromSignature() + { + $job1 = new Job('ls'); + $this->assertEquals(md5('ls'), $job1->getId()); + + $job2 = new Job('whoami'); + $this->assertNotEquals($job1->getId(), $job2->getId()); + + $job3 = new Job(['MyClass', 'myMethod']); + $this->assertNotEquals($job1->getId(), $job3->getId()); + } + + public function testShouldAllowCustomId() + { + $job = new Job('ls', [], 'aCustomId'); + + $this->assertNotEquals(md5('ls'), $job->getId()); + $this->assertEquals('aCustomId', $job->getId()); + + $job2 = new Job(['MyClass', 'myMethod'], null, 'myCustomId'); + $this->assertEquals('myCustomId', $job2->getId()); + } + + public function testShouldKnowIfDue() + { + $job1 = new Job('ls'); + $this->assertTrue($job1->isDue()); + + $job2 = new Job('ls'); + $job2->at('* * * * *'); + $this->assertTrue($job2->isDue()); + + $job3 = new Job('ls'); + $job3->at('10 * * * *'); + $this->assertTrue($job3->isDue(\DateTime::createFromFormat('i', '10'))); + $this->assertFalse($job3->isDue(\DateTime::createFromFormat('i', '12'))); + } + + public function testShouldKnowIfCanRunInBackground() + { + $job = new Job('ls'); + $this->assertTrue($job->canRunInBackground()); + + $job2 = new Job(function () { + return "I can't run in background"; + }); + $this->assertFalse($job2->canRunInBackground()); + } + + public function testShouldForceTheJobToRunInForeground() + { + $job = new Job('ls'); + + $this->assertTrue($job->canRunInBackground()); + $this->assertFalse($job->inForeground()->canRunInBackground()); + } + + public function testShouldReturnCompiledJobCommand() + { + $job1 = new Job('ls'); + $this->assertEquals('ls', $job1->inForeground()->compile()); + + $fn = function () { + return true; + }; + $job2 = new Job($fn); + $this->assertEquals($fn, $job2->compile()); + } + + public function testShouldCompileWithArguments() + { + $job = new Job('ls', [ + '-l' => null, + '-arg' => 'value', + ]); + + $this->assertEquals("ls '-l' '-arg' 'value'", $job->inForeground()->compile()); + } + + public function testShouldCompileCommandInBackground() + { + $job1 = new Job('ls'); + $job1->at('* * * * *'); + + $this->assertEquals('(ls) > /dev/null 2>&1 &', $job1->compile()); + } + + public function testShouldRunInBackground() + { + // This script has a 5 seconds sleep + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $startTime = microtime(true); + $job->at('* * * * *')->run(); + $endTime = microtime(true); + + $this->assertTrue(5 > ($endTime - $startTime)); + + $startTime = microtime(true); + $job->at('* * * * *')->inForeground()->run(); + $endTime = microtime(true); + + $this->assertTrue(($endTime - $startTime) >= 5); + } + + public function testShouldRunInForegroundIfSendsEmails() + { + $job = new Job('ls'); + $job->email('test@mail.com'); + + $this->assertFalse($job->canRunInBackground()); + } + + public function testShouldAcceptSingleOrMultipleEmails() + { + $job = new Job('ls'); + + $this->assertInstanceOf(Job::class, $job->email('test@mail.com')); + $this->assertInstanceOf(Job::class, $job->email(['test@mail.com', 'other@mail.com'])); + } + + public function testShouldFailIfEmailInputIsNotStringOrArray() + { + $this->expectException(\InvalidArgumentException::class); + + $job = new Job('ls'); + + $job->email(1); + } + + public function testShouldAcceptEmailConfigurationAndItShouldBeChainable() + { + $job = new Job('ls'); + $this->assertInstanceOf(Job::class, $job->configure([ + 'email' => [], + ])); + } + + public function testShouldFailIfEmailConfigurationIsNotArray() + { + $this->expectException(\InvalidArgumentException::class); + + $job = new Job('ls'); + $job->configure([ + 'email' => 123, + ]); + } + + public function testShouldCreateLockFileIfOnlyOne() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + // Default temp dir + $tmpDir = sys_get_temp_dir(); + $lockFile = $tmpDir . '/' . $job->getId() . '.lock'; + + @unlink($lockFile); + + $this->assertFalse(file_exists($lockFile)); + + $job->onlyOne()->run(); + + $this->assertTrue(file_exists($lockFile)); + } + + public function testShouldCreateLockFilesInCustomPath() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + // Default temp dir + $tmpDir = __DIR__ . '/../tmp'; + $lockFile = $tmpDir . '/' . $job->getId() . '.lock'; + + @unlink($lockFile); + + $this->assertFalse(file_exists($lockFile)); + + $job->onlyOne($tmpDir)->run(); + + $this->assertTrue(file_exists($lockFile)); + } + + public function testShouldRemoveLockFileAfterRunningClosures() + { + $job = new Job(function () { + sleep(3); + }); + + // Default temp dir + $tmpDir = __DIR__ . '/../tmp'; + $lockFile = $tmpDir . '/' . $job->getId() . '.lock'; + + $job->onlyOne($tmpDir)->run(); + + $this->assertFalse(file_exists($lockFile)); + } + + public function testShouldRemoveLockFileAfterRunningCommands() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + // Default temp dir + $tmpDir = __DIR__ . '/../tmp'; + $lockFile = $tmpDir . '/' . $job->getId() . '.lock'; + + $job->onlyOne($tmpDir)->run(); + + sleep(1); + + $this->assertTrue(file_exists($lockFile)); + + sleep(5); + + $this->assertFalse(file_exists($lockFile)); + } + + public function testShouldKnowIfOverlapping() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $this->assertFalse($job->isOverlapping()); + + $tmpDir = __DIR__ . '/../tmp'; + + $job->onlyOne($tmpDir)->run(); + + sleep(1); + + $this->assertTrue($job->isOverlapping()); + + sleep(5); + + $this->assertFalse($job->isOverlapping()); + } + + public function testShouldNotRunIfOverlapping() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $this->assertFalse($job->isOverlapping()); + + $tmpDir = __DIR__ . '/../tmp'; + + $job->onlyOne($tmpDir); + + sleep(1); + + $this->assertTrue($job->run()); + $this->assertFalse($job->run()); + + sleep(6); + $this->assertTrue($job->run()); + } + + public function testShouldRunIfOverlappingCallbackReturnsTrue() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $this->assertFalse($job->isOverlapping()); + + $tmpDir = __DIR__ . '/../tmp'; + + $job->onlyOne($tmpDir, function ($lastExecution) { + return time() - $lastExecution > 2; + })->run(); + + // The job should not run as it is overlapping + $this->assertFalse($job->run()); + sleep(3); + // The job should run now as the function should now return true, + // while it's still being executed + $lockFile = $tmpDir . '/' . $job->getId() . '.lock'; + $this->assertTrue(file_exists($lockFile)); + $this->assertTrue($job->run()); + } + + public function testShouldAcceptTempDirInConfiguration() + { + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $tmpDir = __DIR__ . '/../tmp'; + + $job->configure([ + 'tempDir' => $tmpDir, + ])->onlyOne()->run(); + + sleep(1); + + $this->assertTrue(file_exists($tmpDir . '/' . $job->getId() . '.lock')); + } + + public function testWhenMethodShouldBeChainable() + { + $job = new Job('ls'); + + $this->assertInstanceOf(Job::class, $job->when(function () { + return true; + })); + } + + public function testShouldNotRunIfTruthTestFails() + { + $job = new Job('ls'); + + $this->assertFalse($job->when(function () { + return false; + })->run()); + + $this->assertTrue($job->when(function () { + return true; + })->run()); + } + + public function testShouldReturnOutputOfJobExecution() + { + $job1 = new Job(function () { + echo 'hi'; + }); + $job1->run(); + $this->assertEquals('hi', $job1->getOutput()); + + $job2 = new Job(function () { + return 'hello'; + }); + $job2->run(); + $this->assertEquals('hello', $job2->getOutput()); + + $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php'; + $job3 = new Job($command); + $job3->inForeground()->run(); + $this->assertEquals(['hi'], $job3->getOutput()); + } + + public function testShouldRunCallbackBeforeJobExecution() + { + $job = new Job(function () { + return 'Job for testing before function'; + }); + + $callbackWasExecuted = false; + $outputWasSet = false; + + $job->before(function () use ($job, &$callbackWasExecuted, &$outputWasSet) { + $callbackWasExecuted = true; + $outputWasSet = ! is_null($job->getOutput()); + })->run(); + + $this->assertTrue($callbackWasExecuted); + $this->assertFalse($outputWasSet); + } + + public function testShouldRunCallbackAfterJobExecution() + { + $job = new Job(function () { + $visitors = 1000; + + return 'Daily visitors: ' . $visitors; + }); + + $jobResult = null; + + $job->then(function ($output) use (&$jobResult) { + $jobResult = $output; + })->run(); + + $this->assertEquals($jobResult, $job->getOutput()); + + $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php'; + $job2 = new Job($command); + + $job2Result = null; + + $job2->then(function ($output) use (&$job2Result) { + $job2Result = $output; + }, true)->run(); + + // Commands in background should return an empty string + $this->assertTrue(empty($job2Result)); + + $job2Result = null; + $job2->then(function ($output) use (&$job2Result) { + $job2Result = $output; + })->inForeground()->run(); + $this->assertTrue(! empty($job2Result) && + $job2Result === $job2->getOutput()); + } + + public function testThenMethodShouldPassReturnCode() + { + $command_success = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php'; + $command_fail = $command_success . ' fail'; + + $run = function ($command) { + $job = new Job($command); + $testReturnCode = null; + + $job->then(function ($output, $returnCode) use (&$testReturnCode, &$testOutput) { + $testReturnCode = $returnCode; + })->run(); + + return $testReturnCode; + }; + + $this->assertEquals(0, $run($command_success)); + $this->assertNotEquals(0, $run($command_fail)); + } + + public function testThenMethodShouldBeChainable() + { + $job = new Job('ls'); + + $this->assertInstanceOf(Job::class, $job->then(function () { + return true; + })); + } + + public function testShouldDefaultExecutionInForegroundIfMethodThenIsDefined() + { + $job = new Job('ls'); + + $job->then(function () { + return true; + }); + + $this->assertFalse($job->canRunInBackground()); + } + + public function testShouldAllowForcingTheJobToRunInBackgroundIfMethodThenIsDefined() + { + // This is a use case when you want to execute a callback every time your + // job is executed, but you don't care about the output of the job + + $job = new Job('ls'); + + $job->then(function () { + return true; + }, true); + + $this->assertTrue($job->canRunInBackground()); + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php new file mode 100644 index 0000000..3ae1f60 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/MailerTest.php @@ -0,0 +1,166 @@ +getEmailConfig(); + + $this->assertTrue(isset($config['subject'])); + $this->assertTrue(isset($config['from'])); + $this->assertTrue(isset($config['body'])); + $this->assertTrue(isset($config['transport'])); + } + + public function testShouldAllowCustomTransportWhenSendingEmails() + { + $job = new Job(function () { + return 'hi'; + }); + + $job->configure([ + 'email' => [ + 'transport' => new \Swift_NullTransport(), + ], + ]); + + $this->assertInstanceOf(\Swift_NullTransport::class, $job->getEmailConfig()['transport']); + } + + public function testEmailTransportShouldAlwaysBeInstanceOfSwift_Transport() + { + $job = new Job(function () { + return 'hi'; + }); + + $job->configure([ + 'email' => [ + 'transport' => 'Something not allowed', + ], + ]); + + $this->assertInstanceOf(\Swift_Transport::class, $job->getEmailConfig()['transport']); + } + + public function testShouldSendJobOutputToEmail() + { + $emailAddress = 'local@localhost.com'; + $command = PHP_BINARY . ' ' . __DIR__ . '/../test_job.php'; + $job1 = new Job($command); + + $job2 = new Job(function () { + return 'Hello World!'; + }); + + $nullTransportConfig = [ + 'email' => [ + 'transport' => new \Swift_NullTransport(), + ], + ]; + $job1->configure($nullTransportConfig); + $job2->configure($nullTransportConfig); + + $outputFile1 = __DIR__ . '/../tmp/output001.log'; + $this->assertTrue($job1->output($outputFile1)->email($emailAddress)->run()); + $outputFile2 = __DIR__ . '/../tmp/output002.log'; + $this->assertTrue($job2->output($outputFile2)->email($emailAddress)->run()); + + unlink($outputFile1); + unlink($outputFile2); + } + + public function testShouldSendMultipleFilesToEmail() + { + $emailAddress = 'local@localhost.com'; + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $outputFile1 = __DIR__ . '/../tmp/output003.log'; + $outputFile2 = __DIR__ . '/../tmp/output004.log'; + + $nullTransportConfig = [ + 'email' => [ + 'transport' => new \Swift_NullTransport(), + ], + ]; + $job->configure($nullTransportConfig); + + $this->assertTrue($job->output([ + $outputFile1, $outputFile2, + ])->email([$emailAddress])->run()); + + unlink($outputFile1); + unlink($outputFile2); + } + + public function testShouldSendToMultipleEmails() + { + $emailAddress1 = 'local@localhost.com'; + $emailAddress2 = 'local1@localhost.com'; + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $outputFile = __DIR__ . '/../tmp/output005.log'; + + $nullTransportConfig = [ + 'email' => [ + 'transport' => new \Swift_NullTransport(), + ], + ]; + $job->configure($nullTransportConfig); + + $this->assertTrue($job->output($outputFile)->email([ + $emailAddress1, $emailAddress2, + ])->run()); + + unlink($outputFile); + } + + public function testShouldAcceptCustomEmailConfig() + { + $emailAddress = 'local@localhost.com'; + $command = PHP_BINARY . ' ' . __DIR__ . '/../async_job.php'; + $job = new Job($command); + + $outputFile = __DIR__ . '/../tmp/output6.log'; + + $this->assertTrue( + $job->output($outputFile)->email($emailAddress) + ->configure([ + 'email' => [ + 'subject' => 'My custom subject', + 'from' => 'my@custom.from', + 'body' => 'My custom body', + 'transport' => new \Swift_NullTransport(), + ], + ])->run() + ); + + unlink($outputFile); + } + + public function testShouldIgnoreEmailIfSpecifiedInConfig() + { + $job = new Job(function () { + $tot = 1 + 2; + // Return nothing.... + }); + + $nullTransportConfig = [ + 'email' => [ + 'transport' => new \Swift_NullTransport(), + 'ignore_empty_output' => true, + ], + ]; + $job->configure($nullTransportConfig); + + $outputFile = __DIR__ . '/../tmp/output.log'; + $this->assertTrue($job->output($outputFile)->email('local@localhost.com')->run()); + + @unlink($outputFile); + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php new file mode 100644 index 0000000..9f2204d --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/GO/SchedulerTest.php @@ -0,0 +1,381 @@ +assertEquals(count($scheduler->getQueuedJobs()), 0); + + $scheduler->raw('ls'); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 1); + } + + public function testShouldQueueAPhpScript() + { + $scheduler = new Scheduler(); + + $script = __DIR__ . '/../test_job.php'; + + $this->assertEquals(count($scheduler->getQueuedJobs()), 0); + + $scheduler->php($script); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 1); + } + + public function testShouldAllowCustomPhpBin() + { + $scheduler = new Scheduler(); + $script = __DIR__ . '/../test_job.php'; + + // Create fake bin + $bin = __DIR__ . '/../custom_bin'; + touch($bin); + + $job = $scheduler->php($script, $bin)->inForeground(); + + unlink($bin); + + $this->assertEquals($bin . ' ' . $script, $job->compile()); + } + + public function testShouldUseSystemPhpBinIfCustomBinDoesNotExist() + { + $scheduler = new Scheduler(); + $script = __DIR__ . '/../test_job.php'; + + // Create fake bin + $bin = '/my/custom/php/bin'; + + $job = $scheduler->php($script, $bin)->inForeground(); + + $this->assertNotEquals($bin . ' ' . $script, $job->compile()); + $this->assertEquals(PHP_BINARY . ' ' . $script, $job->compile()); + } + + public function testShouldThrowExceptionIfScriptIsNotAString() + { + $this->expectException(\InvalidArgumentException::class); + + $scheduler = new Scheduler(); + $scheduler->php(function () { + return false; + }); + + $scheduler->run(); + } + + public function testShouldMarkJobAsFailedIfScriptPathIsInvalid() + { + $scheduler = new Scheduler(); + $scheduler->php('someInvalidPathToAScript'); + + $scheduler->run(); + $fail = $scheduler->getFailedJobs(); + $this->assertCount(1, $fail); + $this->assertContainsOnlyInstancesOf(FailedJob::class, $fail); + } + + public function testShouldQueueAShellCommand() + { + $scheduler = new Scheduler(); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 0); + + $scheduler->raw('ls'); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 1); + } + + public function testShouldQueueAFunction() + { + $scheduler = new Scheduler(); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 0); + + $scheduler->call(function () { + return true; + }); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 1); + } + + public function testShouldKeepTrackOfExecutedJobs() + { + $scheduler = new Scheduler(); + + $scheduler->call(function () { + return true; + }); + + $this->assertEquals(count($scheduler->getQueuedJobs()), 1); + $this->assertEquals(count($scheduler->getExecutedJobs()), 0); + + $scheduler->run(); + + $this->assertEquals(count($scheduler->getExecutedJobs()), 1); + } + + public function testShouldPassParametersToAFunction() + { + $scheduler = new Scheduler(); + + $outputFile = __DIR__ . '/../tmp/output.txt'; + $scheduler->call(function ($phrase) { + return $phrase; + }, [ + 'Hello World!', + ])->output($outputFile); + + @unlink($outputFile); + + $this->assertFalse(file_exists($outputFile)); + + $scheduler->run(); + + $this->assertNotEquals('Hello', file_get_contents($outputFile)); + $this->assertEquals('Hello World!', file_get_contents($outputFile)); + + @unlink($outputFile); + } + + public function testShouldKeepTrackOfFailedJobs() + { + $scheduler = new Scheduler(); + + $exception = new \Exception('Something failed'); + $scheduler->call(function () use ($exception) { + throw $exception; + }); + + $this->assertEquals(count($scheduler->getFailedJobs()), 0); + + $scheduler->run(); + + $this->assertEquals(count($scheduler->getExecutedJobs()), 0); + $this->assertEquals(count($scheduler->getFailedJobs()), 1); + $failedJob = $scheduler->getFailedJobs()[0]; + $this->assertInstanceOf(FailedJob::class, $failedJob); + $this->assertSame($exception, $failedJob->getException()); + $this->assertInstanceOf(Job::class, $failedJob->getJob()); + } + + public function testShouldKeepExecutingJobsIfOneFails() + { + $scheduler = new Scheduler(); + + $scheduler->call(function () { + throw new \Exception('Something failed'); + }); + + $scheduler->call(function () { + return true; + }); + + $scheduler->run(); + + $this->assertEquals(count($scheduler->getExecutedJobs()), 1); + $this->assertEquals(count($scheduler->getFailedJobs()), 1); + } + + public function testShouldInjectConfigToTheJobs() + { + $schedulerConfig = [ + 'email' => [ + 'subject' => 'My custom subject', + ], + ]; + $scheduler = new Scheduler($schedulerConfig); + + $job = $scheduler->raw('ls'); + + $this->assertEquals($job->getEmailConfig()['subject'], $schedulerConfig['email']['subject']); + } + + public function testShouldPrioritizeJobConfigOverSchedulerConfig() + { + $schedulerConfig = [ + 'email' => [ + 'subject' => 'My custom subject', + ], + ]; + $scheduler = new Scheduler($schedulerConfig); + + $jobConfig = [ + 'email' => [ + 'subject' => 'My job subject', + ], + ]; + $job = $scheduler->raw('ls')->configure($jobConfig); + + $this->assertNotEquals($job->getEmailConfig()['subject'], $schedulerConfig['email']['subject']); + $this->assertEquals($job->getEmailConfig()['subject'], $jobConfig['email']['subject']); + } + + public function testShouldShowClosuresVerboseOutputAsText() + { + $scheduler = new Scheduler(); + + $scheduler->call(function ($phrase) { + return $phrase; + }, [ + 'Hello World!', + ]); + + $scheduler->run(); + + $this->assertMatchesRegularExpression('/ Executing Closure$/', $scheduler->getVerboseOutput()); + $this->assertMatchesRegularExpression('/ Executing Closure$/', $scheduler->getVerboseOutput('text')); + } + + public function testShouldShowClosuresVerboseOutputAsHtml() + { + $scheduler = new Scheduler(); + + $scheduler->call(function ($phrase) { + return $phrase; + }, [ + 'Hello World!', + ]); + + $scheduler->call(function () { + return true; + }); + + $scheduler->run(); + + $this->assertMatchesRegularExpression('/
/', $scheduler->getVerboseOutput('html')); + } + + public function testShouldShowClosuresVerboseOutputAsArray() + { + $scheduler = new Scheduler(); + + $scheduler->call(function ($phrase) { + return $phrase; + }, [ + 'Hello World!', + ]); + + $scheduler->call(function () { + return true; + }); + + $scheduler->run(); + + $this->assertTrue(is_array($scheduler->getVerboseOutput('array'))); + $this->assertEquals(count($scheduler->getVerboseOutput('array')), 2); + } + + public function testShouldThrowExceptionWithInvalidOutputType() + { + $this->expectException(\InvalidArgumentException::class); + + $scheduler = new Scheduler(); + + $scheduler->call(function ($phrase) { + return $phrase; + }, [ + 'Hello World!', + ]); + + $scheduler->call(function () { + return true; + }); + + $scheduler->run(); + + $scheduler->getVerboseOutput('multiline'); + } + + public function testShouldPrioritizeJobsInBackround() + { + $scheduler = new Scheduler(); + + $scheduler->php(__DIR__ . '/../async_job.php', null, null, 'async_foreground')->then(function () { + return true; + }); + + $scheduler->php(__DIR__ . '/../async_job.php', null, null, 'async_background'); + + $jobs = $scheduler->getQueuedJobs(); + + $this->assertEquals('async_background', $jobs[0]->getId()); + $this->assertEquals('async_foreground', $jobs[1]->getId()); + } + + public function testCouldRunTwice() + { + $scheduler = new Scheduler(); + + $scheduler->call(function () { + return true; + }); + + $scheduler->run(); + + $this->assertCount(1, $scheduler->getExecutedJobs(), 'Number of executed jobs'); + + $scheduler->resetRun(); + $scheduler->run(); + + $this->assertCount(1, $scheduler->getExecutedJobs(), 'Number of executed jobs'); + } + + public function testClearJobs() + { + $scheduler = new Scheduler(); + + $scheduler->call(function () { + return true; + }); + + $this->assertCount(1, $scheduler->getQueuedJobs(), 'Number of queued jobs'); + + $scheduler->clearJobs(); + + $this->assertCount(0, $scheduler->getQueuedJobs(), 'Number of queued jobs'); + } + + public function testShouldRunDelayedJobsIfDueWhenCreated() + { + $scheduler = new Scheduler(); + $currentTime = date('H:i'); + + $scheduler->call(function () { + $s = (int) date('s'); + sleep(60 - $s + 1); + })->daily($currentTime); + + $scheduler->call(function () { + // do nothing + })->daily($currentTime); + + $executed = $scheduler->run(); + + $this->assertEquals(2, count($executed)); + } + + public function testShouldRunAtSpecificTime() + { + $scheduler = new Scheduler(); + $runTime = new DateTime('2017-09-13 00:00:00'); + + $scheduler->call(function () { + // do nothing + })->daily('00:00'); + + $executed = $scheduler->run($runTime); + + $this->assertEquals(1, count($executed)); + } +} diff --git a/web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php new file mode 100644 index 0000000..8e77317 --- /dev/null +++ b/web/app/vendor/peppeocchi/php-cron-scheduler/tests/async_job.php @@ -0,0 +1,4 @@ + Webmozart\Assert\InvalidArgumentException: +// The employee ID must be an integer. Got: string + +new Employee(-10); +// => Webmozart\Assert\InvalidArgumentException: +// The employee ID must be a positive integer. Got: -10 +``` + +Assertions +---------- + +The [`Assert`] class provides the following assertions: + +### Type Assertions + +Method | Description +-------------------------------------------------------- | -------------------------------------------------- +`string($value, $message = '')` | Check that a value is a string +`stringNotEmpty($value, $message = '')` | Check that a value is a non-empty string +`integer($value, $message = '')` | Check that a value is an integer +`integerish($value, $message = '')` | Check that a value casts to an integer +`positiveInteger($value, $message = '')` | Check that a value is a positive (non-zero) integer +`float($value, $message = '')` | Check that a value is a float +`numeric($value, $message = '')` | Check that a value is numeric +`natural($value, $message= ''')` | Check that a value is a non-negative integer +`boolean($value, $message = '')` | Check that a value is a boolean +`scalar($value, $message = '')` | Check that a value is a scalar +`object($value, $message = '')` | Check that a value is an object +`resource($value, $type = null, $message = '')` | Check that a value is a resource +`isCallable($value, $message = '')` | Check that a value is a callable +`isArray($value, $message = '')` | Check that a value is an array +`isTraversable($value, $message = '')` (deprecated) | Check that a value is an array or a `\Traversable` +`isIterable($value, $message = '')` | Check that a value is an array or a `\Traversable` +`isCountable($value, $message = '')` | Check that a value is an array or a `\Countable` +`isInstanceOf($value, $class, $message = '')` | Check that a value is an `instanceof` a class +`isInstanceOfAny($value, array $classes, $message = '')` | Check that a value is an `instanceof` at least one class on the array of classes +`notInstanceOf($value, $class, $message = '')` | Check that a value is not an `instanceof` a class +`isAOf($value, $class, $message = '')` | Check that a value is of the class or has one of its parents +`isAnyOf($value, array $classes, $message = '')` | Check that a value is of at least one of the classes or has one of its parents +`isNotA($value, $class, $message = '')` | Check that a value is not of the class or has not one of its parents +`isArrayAccessible($value, $message = '')` | Check that a value can be accessed as an array +`uniqueValues($values, $message = '')` | Check that the given array contains unique values + +### Comparison Assertions + +Method | Description +----------------------------------------------- | ------------------------------------------------------------------ +`true($value, $message = '')` | Check that a value is `true` +`false($value, $message = '')` | Check that a value is `false` +`notFalse($value, $message = '')` | Check that a value is not `false` +`null($value, $message = '')` | Check that a value is `null` +`notNull($value, $message = '')` | Check that a value is not `null` +`isEmpty($value, $message = '')` | Check that a value is `empty()` +`notEmpty($value, $message = '')` | Check that a value is not `empty()` +`eq($value, $value2, $message = '')` | Check that a value equals another (`==`) +`notEq($value, $value2, $message = '')` | Check that a value does not equal another (`!=`) +`same($value, $value2, $message = '')` | Check that a value is identical to another (`===`) +`notSame($value, $value2, $message = '')` | Check that a value is not identical to another (`!==`) +`greaterThan($value, $value2, $message = '')` | Check that a value is greater than another +`greaterThanEq($value, $value2, $message = '')` | Check that a value is greater than or equal to another +`lessThan($value, $value2, $message = '')` | Check that a value is less than another +`lessThanEq($value, $value2, $message = '')` | Check that a value is less than or equal to another +`range($value, $min, $max, $message = '')` | Check that a value is within a range +`inArray($value, array $values, $message = '')` | Check that a value is one of a list of values +`oneOf($value, array $values, $message = '')` | Check that a value is one of a list of values (alias of `inArray`) + +### String Assertions + +You should check that a value is a string with `Assert::string()` before making +any of the following assertions. + +Method | Description +--------------------------------------------------- | ----------------------------------------------------------------- +`contains($value, $subString, $message = '')` | Check that a string contains a substring +`notContains($value, $subString, $message = '')` | Check that a string does not contain a substring +`startsWith($value, $prefix, $message = '')` | Check that a string has a prefix +`notStartsWith($value, $prefix, $message = '')` | Check that a string does not have a prefix +`startsWithLetter($value, $message = '')` | Check that a string starts with a letter +`endsWith($value, $suffix, $message = '')` | Check that a string has a suffix +`notEndsWith($value, $suffix, $message = '')` | Check that a string does not have a suffix +`regex($value, $pattern, $message = '')` | Check that a string matches a regular expression +`notRegex($value, $pattern, $message = '')` | Check that a string does not match a regular expression +`unicodeLetters($value, $message = '')` | Check that a string contains Unicode letters only +`alpha($value, $message = '')` | Check that a string contains letters only +`digits($value, $message = '')` | Check that a string contains digits only +`alnum($value, $message = '')` | Check that a string contains letters and digits only +`lower($value, $message = '')` | Check that a string contains lowercase characters only +`upper($value, $message = '')` | Check that a string contains uppercase characters only +`length($value, $length, $message = '')` | Check that a string has a certain number of characters +`minLength($value, $min, $message = '')` | Check that a string has at least a certain number of characters +`maxLength($value, $max, $message = '')` | Check that a string has at most a certain number of characters +`lengthBetween($value, $min, $max, $message = '')` | Check that a string has a length in the given range +`uuid($value, $message = '')` | Check that a string is a valid UUID +`ip($value, $message = '')` | Check that a string is a valid IP (either IPv4 or IPv6) +`ipv4($value, $message = '')` | Check that a string is a valid IPv4 +`ipv6($value, $message = '')` | Check that a string is a valid IPv6 +`email($value, $message = '')` | Check that a string is a valid e-mail address +`notWhitespaceOnly($value, $message = '')` | Check that a string contains at least one non-whitespace character + +### File Assertions + +Method | Description +----------------------------------- | -------------------------------------------------- +`fileExists($value, $message = '')` | Check that a value is an existing path +`file($value, $message = '')` | Check that a value is an existing file +`directory($value, $message = '')` | Check that a value is an existing directory +`readable($value, $message = '')` | Check that a value is a readable path +`writable($value, $message = '')` | Check that a value is a writable path + +### Object Assertions + +Method | Description +----------------------------------------------------- | -------------------------------------------------- +`classExists($value, $message = '')` | Check that a value is an existing class name +`subclassOf($value, $class, $message = '')` | Check that a class is a subclass of another +`interfaceExists($value, $message = '')` | Check that a value is an existing interface name +`implementsInterface($value, $class, $message = '')` | Check that a class implements an interface +`propertyExists($value, $property, $message = '')` | Check that a property exists in a class/object +`propertyNotExists($value, $property, $message = '')` | Check that a property does not exist in a class/object +`methodExists($value, $method, $message = '')` | Check that a method exists in a class/object +`methodNotExists($value, $method, $message = '')` | Check that a method does not exist in a class/object + +### Array Assertions + +Method | Description +-------------------------------------------------- | ------------------------------------------------------------------ +`keyExists($array, $key, $message = '')` | Check that a key exists in an array +`keyNotExists($array, $key, $message = '')` | Check that a key does not exist in an array +`validArrayKey($key, $message = '')` | Check that a value is a valid array key (int or string) +`count($array, $number, $message = '')` | Check that an array contains a specific number of elements +`minCount($array, $min, $message = '')` | Check that an array contains at least a certain number of elements +`maxCount($array, $max, $message = '')` | Check that an array contains at most a certain number of elements +`countBetween($array, $min, $max, $message = '')` | Check that an array has a count in the given range +`isList($array, $message = '')` | Check that an array is a non-associative list +`isNonEmptyList($array, $message = '')` | Check that an array is a non-associative list, and not empty +`isMap($array, $message = '')` | Check that an array is associative and has strings as keys +`isNonEmptyMap($array, $message = '')` | Check that an array is associative and has strings as keys, and is not empty + +### Function Assertions + +Method | Description +------------------------------------------- | ----------------------------------------------------------------------------------------------------- +`throws($closure, $class, $message = '')` | Check that a function throws a certain exception. Subclasses of the exception class will be accepted. + +### Collection Assertions + +All of the above assertions can be prefixed with `all*()` to test the contents +of an array or a `\Traversable`: + +```php +Assert::allIsInstanceOf($employees, 'Acme\Employee'); +``` + +### Nullable Assertions + +All of the above assertions can be prefixed with `nullOr*()` to run the +assertion only if it the value is not `null`: + +```php +Assert::nullOrString($middleName, 'The middle name must be a string or null. Got: %s'); +``` + +### Extending Assert + +The `Assert` class comes with a few methods, which can be overridden to change the class behaviour. You can also extend it to +add your own assertions. + +#### Overriding methods + +Overriding the following methods in your assertion class allows you to change the behaviour of the assertions: + +* `public static function __callStatic($name, $arguments)` + * This method is used to 'create' the `nullOr` and `all` versions of the assertions. +* `protected static function valueToString($value)` + * This method is used for error messages, to convert the value to a string value for displaying. You could use this for representing a value object with a `__toString` method for example. +* `protected static function typeToString($value)` + * This method is used for error messages, to convert the a value to a string representing its type. +* `protected static function strlen($value)` + * This method is used to calculate string length for relevant methods, using the `mb_strlen` if available and useful. +* `protected static function reportInvalidArgument($message)` + * This method is called when an assertion fails, with the specified error message. Here you can throw your own exception, or log something. + +## Static analysis support + +Where applicable, assertion functions are annotated to support Psalm's +[Assertion syntax](https://psalm.dev/docs/annotating_code/assertion_syntax/). +A dedicated [PHPStan Plugin](https://github.com/phpstan/phpstan-webmozart-assert) is +required for proper type support. + +Authors +------- + +* [Bernhard Schussek] a.k.a. [@webmozart] +* [The Community Contributors] + +Contribute +---------- + +Contributions to the package are always welcome! + +* Report any bugs or issues you find on the [issue tracker]. +* You can grab the source code at the package's [Git repository]. + +License +------- + +All contents of this package are licensed under the [MIT license]. + +[beberlei/assert]: https://github.com/beberlei/assert +[assert package]: https://github.com/beberlei/assert +[Composer]: https://getcomposer.org +[Bernhard Schussek]: https://webmozarts.com +[The Community Contributors]: https://github.com/webmozart/assert/graphs/contributors +[issue tracker]: https://github.com/webmozart/assert/issues +[Git repository]: https://github.com/webmozart/assert +[@webmozart]: https://twitter.com/webmozart +[MIT license]: LICENSE +[`Assert`]: src/Assert.php diff --git a/web/app/vendor/webmozart/assert/composer.json b/web/app/vendor/webmozart/assert/composer.json new file mode 100644 index 0000000..b340452 --- /dev/null +++ b/web/app/vendor/webmozart/assert/composer.json @@ -0,0 +1,43 @@ +{ + "name": "webmozart/assert", + "description": "Assertions to validate method input/output with nice error messages.", + "license": "MIT", + "keywords": [ + "assert", + "check", + "validate" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "require": { + "php": "^7.2 || ^8.0", + "ext-ctype": "*" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "autoload-dev": { + "psr-4": { + "Webmozart\\Assert\\Tests\\": "tests/", + "Webmozart\\Assert\\Bin\\": "bin/src" + } + }, + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + } +} diff --git a/web/app/vendor/webmozart/assert/src/Assert.php b/web/app/vendor/webmozart/assert/src/Assert.php new file mode 100644 index 0000000..db1f3a5 --- /dev/null +++ b/web/app/vendor/webmozart/assert/src/Assert.php @@ -0,0 +1,2080 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webmozart\Assert; + +use ArrayAccess; +use BadMethodCallException; +use Closure; +use Countable; +use DateTime; +use DateTimeImmutable; +use Exception; +use ResourceBundle; +use SimpleXMLElement; +use Throwable; +use Traversable; + +/** + * Efficient assertions to validate the input/output of your methods. + * + * @since 1.0 + * + * @author Bernhard Schussek + */ +class Assert +{ + use Mixin; + + /** + * @psalm-pure + * @psalm-assert string $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function string($value, $message = '') + { + if (!\is_string($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a string. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert non-empty-string $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function stringNotEmpty($value, $message = '') + { + static::string($value, $message); + static::notEq($value, '', $message); + } + + /** + * @psalm-pure + * @psalm-assert int $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function integer($value, $message = '') + { + if (!\is_int($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an integer. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert numeric $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function integerish($value, $message = '') + { + if (!\is_numeric($value) || $value != (int) $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an integerish value. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert positive-int $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function positiveInteger($value, $message = '') + { + if (!(\is_int($value) && $value > 0)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a positive integer. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert float $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function float($value, $message = '') + { + if (!\is_float($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a float. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert numeric $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function numeric($value, $message = '') + { + if (!\is_numeric($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a numeric. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert positive-int|0 $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function natural($value, $message = '') + { + if (!\is_int($value) || $value < 0) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non-negative integer. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert bool $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function boolean($value, $message = '') + { + if (!\is_bool($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a boolean. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert scalar $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function scalar($value, $message = '') + { + if (!\is_scalar($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a scalar. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert object $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function object($value, $message = '') + { + if (!\is_object($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an object. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert resource $value + * + * @param mixed $value + * @param string|null $type type of resource this should be. @see https://www.php.net/manual/en/function.get-resource-type.php + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function resource($value, $type = null, $message = '') + { + if (!\is_resource($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a resource. Got: %s', + static::typeToString($value) + )); + } + + if ($type && $type !== \get_resource_type($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a resource of type %2$s. Got: %s', + static::typeToString($value), + $type + )); + } + } + + /** + * @psalm-pure + * @psalm-assert callable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isCallable($value, $message = '') + { + if (!\is_callable($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a callable. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert array $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isArray($value, $message = '') + { + if (!\is_array($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @deprecated use "isIterable" or "isInstanceOf" instead + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isTraversable($value, $message = '') + { + @\trigger_error( + \sprintf( + 'The "%s" assertion is deprecated. You should stop using it, as it will soon be removed in 2.0 version. Use "isIterable" or "isInstanceOf" instead.', + __METHOD__ + ), + \E_USER_DEPRECATED + ); + + if (!\is_array($value) && !($value instanceof Traversable)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a traversable. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert array|ArrayAccess $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isArrayAccessible($value, $message = '') + { + if (!\is_array($value) && !($value instanceof ArrayAccess)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array accessible. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert countable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isCountable($value, $message = '') + { + if ( + !\is_array($value) + && !($value instanceof Countable) + && !($value instanceof ResourceBundle) + && !($value instanceof SimpleXMLElement) + ) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a countable. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isIterable($value, $message = '') + { + if (!\is_array($value) && !($value instanceof Traversable)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an iterable. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert ExpectedType $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isInstanceOf($value, $class, $message = '') + { + if (!($value instanceof $class)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of %2$s. Got: %s', + static::typeToString($value), + $class + )); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert !ExpectedType $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notInstanceOf($value, $class, $message = '') + { + if ($value instanceof $class) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance other than %2$s. Got: %s', + static::typeToString($value), + $class + )); + } + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param mixed $value + * @param array $classes + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isInstanceOfAny($value, array $classes, $message = '') + { + foreach ($classes as $class) { + if ($value instanceof $class) { + return; + } + } + + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an instance of any of %2$s. Got: %s', + static::typeToString($value), + \implode(', ', \array_map(array(static::class, 'valueToString'), $classes)) + )); + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert ExpectedType|class-string $value + * + * @param object|string $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isAOf($value, $class, $message = '') + { + static::string($class, 'Expected class as a string. Got: %s'); + + if (!\is_a($value, $class, \is_string($value))) { + static::reportInvalidArgument(sprintf( + $message ?: 'Expected an instance of this class or to this class among its parents "%2$s". Got: %s', + static::valueToString($value), + $class + )); + } + } + + /** + * @psalm-pure + * @psalm-template UnexpectedType of object + * @psalm-param class-string $class + * @psalm-assert !UnexpectedType $value + * @psalm-assert !class-string $value + * + * @param object|string $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isNotA($value, $class, $message = '') + { + static::string($class, 'Expected class as a string. Got: %s'); + + if (\is_a($value, $class, \is_string($value))) { + static::reportInvalidArgument(sprintf( + $message ?: 'Expected an instance of this class or to this class among its parents other than "%2$s". Got: %s', + static::valueToString($value), + $class + )); + } + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param object|string $value + * @param string[] $classes + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isAnyOf($value, array $classes, $message = '') + { + foreach ($classes as $class) { + static::string($class, 'Expected class as a string. Got: %s'); + + if (\is_a($value, $class, \is_string($value))) { + return; + } + } + + static::reportInvalidArgument(sprintf( + $message ?: 'Expected an instance of any of this classes or any of those classes among their parents "%2$s". Got: %s', + static::valueToString($value), + \implode(', ', $classes) + )); + } + + /** + * @psalm-pure + * @psalm-assert empty $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isEmpty($value, $message = '') + { + if (!empty($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an empty value. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert !empty $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notEmpty($value, $message = '') + { + if (empty($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non-empty value. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function null($value, $message = '') + { + if (null !== $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected null. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert !null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notNull($value, $message = '') + { + if (null === $value) { + static::reportInvalidArgument( + $message ?: 'Expected a value other than null.' + ); + } + } + + /** + * @psalm-pure + * @psalm-assert true $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function true($value, $message = '') + { + if (true !== $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be true. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert false $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function false($value, $message = '') + { + if (false !== $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be false. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert !false $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notFalse($value, $message = '') + { + if (false === $value) { + static::reportInvalidArgument( + $message ?: 'Expected a value other than false.' + ); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function ip($value, $message = '') + { + if (false === \filter_var($value, \FILTER_VALIDATE_IP)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be an IP. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function ipv4($value, $message = '') + { + if (false === \filter_var($value, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV4)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be an IPv4. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function ipv6($value, $message = '') + { + if (false === \filter_var($value, \FILTER_VALIDATE_IP, \FILTER_FLAG_IPV6)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be an IPv6. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function email($value, $message = '') + { + if (false === \filter_var($value, FILTER_VALIDATE_EMAIL)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to be a valid e-mail address. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * Does non strict comparisons on the items, so ['3', 3] will not pass the assertion. + * + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function uniqueValues(array $values, $message = '') + { + $allValues = \count($values); + $uniqueValues = \count(\array_unique($values)); + + if ($allValues !== $uniqueValues) { + $difference = $allValues - $uniqueValues; + + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array of unique values, but %s of them %s duplicated', + $difference, + (1 === $difference ? 'is' : 'are') + )); + } + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function eq($value, $expect, $message = '') + { + if ($expect != $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value equal to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($expect) + )); + } + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notEq($value, $expect, $message = '') + { + if ($expect == $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a different value than %s.', + static::valueToString($expect) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function same($value, $expect, $message = '') + { + if ($expect !== $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value identical to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($expect) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notSame($value, $expect, $message = '') + { + if ($expect === $value) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value not identical to %s.', + static::valueToString($expect) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function greaterThan($value, $limit, $message = '') + { + if ($value <= $limit) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value greater than %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function greaterThanEq($value, $limit, $message = '') + { + if ($value < $limit) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value greater than or equal to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function lessThan($value, $limit, $message = '') + { + if ($value >= $limit) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value less than %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function lessThanEq($value, $limit, $message = '') + { + if ($value > $limit) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value less than or equal to %2$s. Got: %s', + static::valueToString($value), + static::valueToString($limit) + )); + } + } + + /** + * Inclusive range, so Assert::(3, 3, 5) passes. + * + * @psalm-pure + * + * @param mixed $value + * @param mixed $min + * @param mixed $max + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function range($value, $min, $max, $message = '') + { + if ($value < $min || $value > $max) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value between %2$s and %3$s. Got: %s', + static::valueToString($value), + static::valueToString($min), + static::valueToString($max) + )); + } + } + + /** + * A more human-readable alias of Assert::inArray(). + * + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function oneOf($value, array $values, $message = '') + { + static::inArray($value, $values, $message); + } + + /** + * Does strict comparison, so Assert::inArray(3, ['3']) does not pass the assertion. + * + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function inArray($value, array $values, $message = '') + { + if (!\in_array($value, $values, true)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected one of: %2$s. Got: %s', + static::valueToString($value), + \implode(', ', \array_map(array(static::class, 'valueToString'), $values)) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function contains($value, $subString, $message = '') + { + if (false === \strpos($value, $subString)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain %2$s. Got: %s', + static::valueToString($value), + static::valueToString($subString) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notContains($value, $subString, $message = '') + { + if (false !== \strpos($value, $subString)) { + static::reportInvalidArgument(\sprintf( + $message ?: '%2$s was not expected to be contained in a value. Got: %s', + static::valueToString($value), + static::valueToString($subString) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notWhitespaceOnly($value, $message = '') + { + if (\preg_match('/^\s*$/', $value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a non-whitespace string. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function startsWith($value, $prefix, $message = '') + { + if (0 !== \strpos($value, $prefix)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to start with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($prefix) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notStartsWith($value, $prefix, $message = '') + { + if (0 === \strpos($value, $prefix)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value not to start with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($prefix) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function startsWithLetter($value, $message = '') + { + static::string($value); + + $valid = isset($value[0]); + + if ($valid) { + $locale = \setlocale(LC_CTYPE, 0); + \setlocale(LC_CTYPE, 'C'); + $valid = \ctype_alpha($value[0]); + \setlocale(LC_CTYPE, $locale); + } + + if (!$valid) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to start with a letter. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function endsWith($value, $suffix, $message = '') + { + if ($suffix !== \substr($value, -\strlen($suffix))) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to end with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($suffix) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notEndsWith($value, $suffix, $message = '') + { + if ($suffix === \substr($value, -\strlen($suffix))) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value not to end with %2$s. Got: %s', + static::valueToString($value), + static::valueToString($suffix) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function regex($value, $pattern, $message = '') + { + if (!\preg_match($pattern, $value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The value %s does not match the expected pattern.', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function notRegex($value, $pattern, $message = '') + { + if (\preg_match($pattern, $value, $matches, PREG_OFFSET_CAPTURE)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The value %s matches the pattern %s (at offset %d).', + static::valueToString($value), + static::valueToString($pattern), + $matches[0][1] + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function unicodeLetters($value, $message = '') + { + static::string($value); + + if (!\preg_match('/^\p{L}+$/u', $value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain only Unicode letters. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function alpha($value, $message = '') + { + static::string($value); + + $locale = \setlocale(LC_CTYPE, 0); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_alpha($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain only letters. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function digits($value, $message = '') + { + $locale = \setlocale(LC_CTYPE, 0); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_digit($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain digits only. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function alnum($value, $message = '') + { + $locale = \setlocale(LC_CTYPE, 0); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_alnum($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain letters and digits only. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert lowercase-string $value + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function lower($value, $message = '') + { + $locale = \setlocale(LC_CTYPE, 0); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_lower($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain lowercase characters only. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-assert !lowercase-string $value + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function upper($value, $message = '') + { + $locale = \setlocale(LC_CTYPE, 0); + \setlocale(LC_CTYPE, 'C'); + $valid = !\ctype_upper($value); + \setlocale(LC_CTYPE, $locale); + + if ($valid) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain uppercase characters only. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * + * @param string $value + * @param int $length + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function length($value, $length, $message = '') + { + if ($length !== static::strlen($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain %2$s characters. Got: %s', + static::valueToString($value), + $length + )); + } + } + + /** + * Inclusive min. + * + * @psalm-pure + * + * @param string $value + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function minLength($value, $min, $message = '') + { + if (static::strlen($value) < $min) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain at least %2$s characters. Got: %s', + static::valueToString($value), + $min + )); + } + } + + /** + * Inclusive max. + * + * @psalm-pure + * + * @param string $value + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function maxLength($value, $max, $message = '') + { + if (static::strlen($value) > $max) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain at most %2$s characters. Got: %s', + static::valueToString($value), + $max + )); + } + } + + /** + * Inclusive , so Assert::lengthBetween('asd', 3, 5); passes the assertion. + * + * @psalm-pure + * + * @param string $value + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function lengthBetween($value, $min, $max, $message = '') + { + $length = static::strlen($value); + + if ($length < $min || $length > $max) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a value to contain between %2$s and %3$s characters. Got: %s', + static::valueToString($value), + $min, + $max + )); + } + } + + /** + * Will also pass if $value is a directory, use Assert::file() instead if you need to be sure it is a file. + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function fileExists($value, $message = '') + { + static::string($value); + + if (!\file_exists($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The file %s does not exist.', + static::valueToString($value) + )); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function file($value, $message = '') + { + static::fileExists($value, $message); + + if (!\is_file($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not a file.', + static::valueToString($value) + )); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function directory($value, $message = '') + { + static::fileExists($value, $message); + + if (!\is_dir($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is no directory.', + static::valueToString($value) + )); + } + } + + /** + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function readable($value, $message = '') + { + if (!\is_readable($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not readable.', + static::valueToString($value) + )); + } + } + + /** + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function writable($value, $message = '') + { + if (!\is_writable($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'The path %s is not writable.', + static::valueToString($value) + )); + } + } + + /** + * @psalm-assert class-string $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function classExists($value, $message = '') + { + if (!\class_exists($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an existing class name. Got: %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert class-string|ExpectedType $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function subclassOf($value, $class, $message = '') + { + if (!\is_subclass_of($value, $class)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected a sub-class of %2$s. Got: %s', + static::valueToString($value), + static::valueToString($class) + )); + } + } + + /** + * @psalm-assert class-string $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function interfaceExists($value, $message = '') + { + if (!\interface_exists($value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an existing interface name. got %s', + static::valueToString($value) + )); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $interface + * @psalm-assert class-string $value + * + * @param mixed $value + * @param mixed $interface + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function implementsInterface($value, $interface, $message = '') + { + if (!\in_array($interface, \class_implements($value))) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an implementation of %2$s. Got: %s', + static::valueToString($value), + static::valueToString($interface) + )); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object $classOrObject + * + * @param string|object $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function propertyExists($classOrObject, $property, $message = '') + { + if (!\property_exists($classOrObject, $property)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the property %s to exist.', + static::valueToString($property) + )); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object $classOrObject + * + * @param string|object $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function propertyNotExists($classOrObject, $property, $message = '') + { + if (\property_exists($classOrObject, $property)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the property %s to not exist.', + static::valueToString($property) + )); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object $classOrObject + * + * @param string|object $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function methodExists($classOrObject, $method, $message = '') + { + if (!(\is_string($classOrObject) || \is_object($classOrObject)) || !\method_exists($classOrObject, $method)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the method %s to exist.', + static::valueToString($method) + )); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object $classOrObject + * + * @param string|object $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function methodNotExists($classOrObject, $method, $message = '') + { + if ((\is_string($classOrObject) || \is_object($classOrObject)) && \method_exists($classOrObject, $method)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the method %s to not exist.', + static::valueToString($method) + )); + } + } + + /** + * @psalm-pure + * + * @param array $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function keyExists($array, $key, $message = '') + { + if (!(isset($array[$key]) || \array_key_exists($key, $array))) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the key %s to exist.', + static::valueToString($key) + )); + } + } + + /** + * @psalm-pure + * + * @param array $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function keyNotExists($array, $key, $message = '') + { + if (isset($array[$key]) || \array_key_exists($key, $array)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected the key %s to not exist.', + static::valueToString($key) + )); + } + } + + /** + * Checks if a value is a valid array key (int or string). + * + * @psalm-pure + * @psalm-assert array-key $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function validArrayKey($value, $message = '') + { + if (!(\is_int($value) || \is_string($value))) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected string or integer. Got: %s', + static::typeToString($value) + )); + } + } + + /** + * Does not check if $array is countable, this can generate a warning on php versions after 7.2. + * + * @param Countable|array $array + * @param int $number + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function count($array, $number, $message = '') + { + static::eq( + \count($array), + $number, + \sprintf( + $message ?: 'Expected an array to contain %d elements. Got: %d.', + $number, + \count($array) + ) + ); + } + + /** + * Does not check if $array is countable, this can generate a warning on php versions after 7.2. + * + * @param Countable|array $array + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function minCount($array, $min, $message = '') + { + if (\count($array) < $min) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array to contain at least %2$d elements. Got: %d', + \count($array), + $min + )); + } + } + + /** + * Does not check if $array is countable, this can generate a warning on php versions after 7.2. + * + * @param Countable|array $array + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function maxCount($array, $max, $message = '') + { + if (\count($array) > $max) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array to contain at most %2$d elements. Got: %d', + \count($array), + $max + )); + } + } + + /** + * Does not check if $array is countable, this can generate a warning on php versions after 7.2. + * + * @param Countable|array $array + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function countBetween($array, $min, $max, $message = '') + { + $count = \count($array); + + if ($count < $min || $count > $max) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Expected an array to contain between %2$d and %3$d elements. Got: %d', + $count, + $min, + $max + )); + } + } + + /** + * @psalm-pure + * @psalm-assert list $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isList($array, $message = '') + { + if (!\is_array($array)) { + static::reportInvalidArgument( + $message ?: 'Expected list - non-associative array.' + ); + } + + if ($array === \array_values($array)) { + return; + } + + $nextKey = -1; + foreach ($array as $k => $v) { + if ($k !== ++$nextKey) { + static::reportInvalidArgument( + $message ?: 'Expected list - non-associative array.' + ); + } + } + } + + /** + * @psalm-pure + * @psalm-assert non-empty-list $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isNonEmptyList($array, $message = '') + { + static::isList($array, $message); + static::notEmpty($array, $message); + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param mixed|array $array + * @psalm-assert array $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isMap($array, $message = '') + { + if ( + !\is_array($array) || + \array_keys($array) !== \array_filter(\array_keys($array), '\is_string') + ) { + static::reportInvalidArgument( + $message ?: 'Expected map - associative array with string keys.' + ); + } + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param mixed|array $array + * @psalm-assert array $array + * @psalm-assert !empty $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function isNonEmptyMap($array, $message = '') + { + static::isMap($array, $message); + static::notEmpty($array, $message); + } + + /** + * @psalm-pure + * + * @param string $value + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function uuid($value, $message = '') + { + $value = \str_replace(array('urn:', 'uuid:', '{', '}'), '', $value); + + // The nil UUID is special form of UUID that is specified to have all + // 128 bits set to zero. + if ('00000000-0000-0000-0000-000000000000' === $value) { + return; + } + + if (!\preg_match('/^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$/', $value)) { + static::reportInvalidArgument(\sprintf( + $message ?: 'Value %s is not a valid UUID.', + static::valueToString($value) + )); + } + } + + /** + * @psalm-param class-string $class + * + * @param Closure $expression + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + */ + public static function throws(Closure $expression, $class = 'Exception', $message = '') + { + static::string($class); + + $actual = 'none'; + + try { + $expression(); + } catch (Exception $e) { + $actual = \get_class($e); + if ($e instanceof $class) { + return; + } + } catch (Throwable $e) { + $actual = \get_class($e); + if ($e instanceof $class) { + return; + } + } + + static::reportInvalidArgument($message ?: \sprintf( + 'Expected to throw "%s", got "%s"', + $class, + $actual + )); + } + + /** + * @throws BadMethodCallException + */ + public static function __callStatic($name, $arguments) + { + if ('nullOr' === \substr($name, 0, 6)) { + if (null !== $arguments[0]) { + $method = \lcfirst(\substr($name, 6)); + \call_user_func_array(array(static::class, $method), $arguments); + } + + return; + } + + if ('all' === \substr($name, 0, 3)) { + static::isIterable($arguments[0]); + + $method = \lcfirst(\substr($name, 3)); + $args = $arguments; + + foreach ($arguments[0] as $entry) { + $args[0] = $entry; + + \call_user_func_array(array(static::class, $method), $args); + } + + return; + } + + throw new BadMethodCallException('No such method: '.$name); + } + + /** + * @param mixed $value + * + * @return string + */ + protected static function valueToString($value) + { + if (null === $value) { + return 'null'; + } + + if (true === $value) { + return 'true'; + } + + if (false === $value) { + return 'false'; + } + + if (\is_array($value)) { + return 'array'; + } + + if (\is_object($value)) { + if (\method_exists($value, '__toString')) { + return \get_class($value).': '.self::valueToString($value->__toString()); + } + + if ($value instanceof DateTime || $value instanceof DateTimeImmutable) { + return \get_class($value).': '.self::valueToString($value->format('c')); + } + + return \get_class($value); + } + + if (\is_resource($value)) { + return 'resource'; + } + + if (\is_string($value)) { + return '"'.$value.'"'; + } + + return (string) $value; + } + + /** + * @param mixed $value + * + * @return string + */ + protected static function typeToString($value) + { + return \is_object($value) ? \get_class($value) : \gettype($value); + } + + protected static function strlen($value) + { + if (!\function_exists('mb_detect_encoding')) { + return \strlen($value); + } + + if (false === $encoding = \mb_detect_encoding($value)) { + return \strlen($value); + } + + return \mb_strlen($value, $encoding); + } + + /** + * @param string $message + * + * @throws InvalidArgumentException + * + * @psalm-pure this method is not supposed to perform side-effects + * @psalm-return never + */ + protected static function reportInvalidArgument($message) + { + throw new InvalidArgumentException($message); + } + + private function __construct() + { + } +} diff --git a/web/app/vendor/webmozart/assert/src/InvalidArgumentException.php b/web/app/vendor/webmozart/assert/src/InvalidArgumentException.php new file mode 100644 index 0000000..9d95a58 --- /dev/null +++ b/web/app/vendor/webmozart/assert/src/InvalidArgumentException.php @@ -0,0 +1,16 @@ + + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Webmozart\Assert; + +class InvalidArgumentException extends \InvalidArgumentException +{ +} diff --git a/web/app/vendor/webmozart/assert/src/Mixin.php b/web/app/vendor/webmozart/assert/src/Mixin.php new file mode 100644 index 0000000..0f0a75e --- /dev/null +++ b/web/app/vendor/webmozart/assert/src/Mixin.php @@ -0,0 +1,5089 @@ + $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allString($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::string($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrString($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::string($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert non-empty-string|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrStringNotEmpty($value, $message = '') + { + null === $value || static::stringNotEmpty($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allStringNotEmpty($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::stringNotEmpty($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrStringNotEmpty($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::stringNotEmpty($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert int|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrInteger($value, $message = '') + { + null === $value || static::integer($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allInteger($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::integer($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrInteger($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::integer($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert numeric|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIntegerish($value, $message = '') + { + null === $value || static::integerish($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIntegerish($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::integerish($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIntegerish($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::integerish($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert positive-int|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrPositiveInteger($value, $message = '') + { + null === $value || static::positiveInteger($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allPositiveInteger($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::positiveInteger($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrPositiveInteger($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::positiveInteger($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert float|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrFloat($value, $message = '') + { + null === $value || static::float($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allFloat($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::float($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrFloat($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::float($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert numeric|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNumeric($value, $message = '') + { + null === $value || static::numeric($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNumeric($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::numeric($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNumeric($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::numeric($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert positive-int|0|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNatural($value, $message = '') + { + null === $value || static::natural($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNatural($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::natural($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNatural($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::natural($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert bool|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrBoolean($value, $message = '') + { + null === $value || static::boolean($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allBoolean($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::boolean($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrBoolean($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::boolean($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert scalar|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrScalar($value, $message = '') + { + null === $value || static::scalar($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allScalar($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::scalar($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrScalar($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::scalar($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert object|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrObject($value, $message = '') + { + null === $value || static::object($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allObject($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::object($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrObject($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::object($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert resource|null $value + * + * @param mixed $value + * @param string|null $type type of resource this should be. @see https://www.php.net/manual/en/function.get-resource-type.php + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrResource($value, $type = null, $message = '') + { + null === $value || static::resource($value, $type, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string|null $type type of resource this should be. @see https://www.php.net/manual/en/function.get-resource-type.php + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allResource($value, $type = null, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::resource($entry, $type, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string|null $type type of resource this should be. @see https://www.php.net/manual/en/function.get-resource-type.php + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrResource($value, $type = null, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::resource($entry, $type, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert callable|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsCallable($value, $message = '') + { + null === $value || static::isCallable($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsCallable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isCallable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsCallable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isCallable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert array|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsArray($value, $message = '') + { + null === $value || static::isArray($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsArray($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isArray($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsArray($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isArray($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable|null $value + * + * @deprecated use "isIterable" or "isInstanceOf" instead + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsTraversable($value, $message = '') + { + null === $value || static::isTraversable($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @deprecated use "isIterable" or "isInstanceOf" instead + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsTraversable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isTraversable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @deprecated use "isIterable" or "isInstanceOf" instead + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsTraversable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isTraversable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert array|ArrayAccess|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsArrayAccessible($value, $message = '') + { + null === $value || static::isArrayAccessible($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsArrayAccessible($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isArrayAccessible($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsArrayAccessible($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isArrayAccessible($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert countable|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsCountable($value, $message = '') + { + null === $value || static::isCountable($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsCountable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isCountable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsCountable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isCountable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsIterable($value, $message = '') + { + null === $value || static::isIterable($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsIterable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isIterable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsIterable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isIterable($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert ExpectedType|null $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsInstanceOf($value, $class, $message = '') + { + null === $value || static::isInstanceOf($value, $class, $message); + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsInstanceOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isInstanceOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsInstanceOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isInstanceOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotInstanceOf($value, $class, $message = '') + { + null === $value || static::notInstanceOf($value, $class, $message); + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotInstanceOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notInstanceOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotInstanceOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notInstanceOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param mixed $value + * @param array $classes + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsInstanceOfAny($value, $classes, $message = '') + { + null === $value || static::isInstanceOfAny($value, $classes, $message); + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param mixed $value + * @param array $classes + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsInstanceOfAny($value, $classes, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isInstanceOfAny($entry, $classes, $message); + } + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param mixed $value + * @param array $classes + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsInstanceOfAny($value, $classes, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isInstanceOfAny($entry, $classes, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert ExpectedType|class-string|null $value + * + * @param object|string|null $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsAOf($value, $class, $message = '') + { + null === $value || static::isAOf($value, $class, $message); + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable> $value + * + * @param iterable $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsAOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isAOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable|null> $value + * + * @param iterable $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsAOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isAOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template UnexpectedType of object + * @psalm-param class-string $class + * + * @param object|string|null $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsNotA($value, $class, $message = '') + { + null === $value || static::isNotA($value, $class, $message); + } + + /** + * @psalm-pure + * @psalm-template UnexpectedType of object + * @psalm-param class-string $class + * + * @param iterable $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsNotA($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isNotA($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template UnexpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable $value + * @psalm-assert iterable|null> $value + * + * @param iterable $value + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsNotA($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isNotA($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param object|string|null $value + * @param string[] $classes + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsAnyOf($value, $classes, $message = '') + { + null === $value || static::isAnyOf($value, $classes, $message); + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param iterable $value + * @param string[] $classes + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsAnyOf($value, $classes, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isAnyOf($entry, $classes, $message); + } + } + + /** + * @psalm-pure + * @psalm-param array $classes + * + * @param iterable $value + * @param string[] $classes + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsAnyOf($value, $classes, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isAnyOf($entry, $classes, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert empty $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsEmpty($value, $message = '') + { + null === $value || static::isEmpty($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsEmpty($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::isEmpty($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsEmpty($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::isEmpty($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotEmpty($value, $message = '') + { + null === $value || static::notEmpty($value, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotEmpty($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notEmpty($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotEmpty($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notEmpty($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNull($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::null($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotNull($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notNull($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert true|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrTrue($value, $message = '') + { + null === $value || static::true($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allTrue($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::true($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrTrue($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::true($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert false|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrFalse($value, $message = '') + { + null === $value || static::false($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allFalse($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::false($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrFalse($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::false($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotFalse($value, $message = '') + { + null === $value || static::notFalse($value, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotFalse($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notFalse($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotFalse($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notFalse($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIp($value, $message = '') + { + null === $value || static::ip($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIp($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::ip($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIp($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::ip($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIpv4($value, $message = '') + { + null === $value || static::ipv4($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIpv4($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::ipv4($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIpv4($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::ipv4($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIpv6($value, $message = '') + { + null === $value || static::ipv6($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIpv6($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::ipv6($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIpv6($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::ipv6($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrEmail($value, $message = '') + { + null === $value || static::email($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allEmail($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::email($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrEmail($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::email($entry, $message); + } + } + + /** + * @param array|null $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrUniqueValues($values, $message = '') + { + null === $values || static::uniqueValues($values, $message); + } + + /** + * @param iterable $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allUniqueValues($values, $message = '') + { + static::isIterable($values); + + foreach ($values as $entry) { + static::uniqueValues($entry, $message); + } + } + + /** + * @param iterable $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrUniqueValues($values, $message = '') + { + static::isIterable($values); + + foreach ($values as $entry) { + null === $entry || static::uniqueValues($entry, $message); + } + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrEq($value, $expect, $message = '') + { + null === $value || static::eq($value, $expect, $message); + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allEq($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::eq($entry, $expect, $message); + } + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrEq($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::eq($entry, $expect, $message); + } + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotEq($value, $expect, $message = '') + { + null === $value || static::notEq($value, $expect, $message); + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotEq($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notEq($entry, $expect, $message); + } + } + + /** + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotEq($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notEq($entry, $expect, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrSame($value, $expect, $message = '') + { + null === $value || static::same($value, $expect, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allSame($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::same($entry, $expect, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrSame($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::same($entry, $expect, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotSame($value, $expect, $message = '') + { + null === $value || static::notSame($value, $expect, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotSame($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notSame($entry, $expect, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $expect + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotSame($value, $expect, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notSame($entry, $expect, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrGreaterThan($value, $limit, $message = '') + { + null === $value || static::greaterThan($value, $limit, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allGreaterThan($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::greaterThan($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrGreaterThan($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::greaterThan($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrGreaterThanEq($value, $limit, $message = '') + { + null === $value || static::greaterThanEq($value, $limit, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allGreaterThanEq($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::greaterThanEq($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrGreaterThanEq($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::greaterThanEq($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrLessThan($value, $limit, $message = '') + { + null === $value || static::lessThan($value, $limit, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allLessThan($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::lessThan($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrLessThan($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::lessThan($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrLessThanEq($value, $limit, $message = '') + { + null === $value || static::lessThanEq($value, $limit, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allLessThanEq($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::lessThanEq($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $limit + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrLessThanEq($value, $limit, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::lessThanEq($entry, $limit, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $min + * @param mixed $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrRange($value, $min, $max, $message = '') + { + null === $value || static::range($value, $min, $max, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $min + * @param mixed $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allRange($value, $min, $max, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::range($entry, $min, $max, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param mixed $min + * @param mixed $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrRange($value, $min, $max, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::range($entry, $min, $max, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrOneOf($value, $values, $message = '') + { + null === $value || static::oneOf($value, $values, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allOneOf($value, $values, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::oneOf($entry, $values, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrOneOf($value, $values, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::oneOf($entry, $values, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrInArray($value, $values, $message = '') + { + null === $value || static::inArray($value, $values, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allInArray($value, $values, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::inArray($entry, $values, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param array $values + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrInArray($value, $values, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::inArray($entry, $values, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrContains($value, $subString, $message = '') + { + null === $value || static::contains($value, $subString, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allContains($value, $subString, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::contains($entry, $subString, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrContains($value, $subString, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::contains($entry, $subString, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotContains($value, $subString, $message = '') + { + null === $value || static::notContains($value, $subString, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotContains($value, $subString, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notContains($entry, $subString, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $subString + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotContains($value, $subString, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notContains($entry, $subString, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotWhitespaceOnly($value, $message = '') + { + null === $value || static::notWhitespaceOnly($value, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotWhitespaceOnly($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notWhitespaceOnly($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotWhitespaceOnly($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notWhitespaceOnly($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrStartsWith($value, $prefix, $message = '') + { + null === $value || static::startsWith($value, $prefix, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allStartsWith($value, $prefix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::startsWith($entry, $prefix, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrStartsWith($value, $prefix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::startsWith($entry, $prefix, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotStartsWith($value, $prefix, $message = '') + { + null === $value || static::notStartsWith($value, $prefix, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotStartsWith($value, $prefix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notStartsWith($entry, $prefix, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $prefix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotStartsWith($value, $prefix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notStartsWith($entry, $prefix, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrStartsWithLetter($value, $message = '') + { + null === $value || static::startsWithLetter($value, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allStartsWithLetter($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::startsWithLetter($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrStartsWithLetter($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::startsWithLetter($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrEndsWith($value, $suffix, $message = '') + { + null === $value || static::endsWith($value, $suffix, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allEndsWith($value, $suffix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::endsWith($entry, $suffix, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrEndsWith($value, $suffix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::endsWith($entry, $suffix, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotEndsWith($value, $suffix, $message = '') + { + null === $value || static::notEndsWith($value, $suffix, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotEndsWith($value, $suffix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notEndsWith($entry, $suffix, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $suffix + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotEndsWith($value, $suffix, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notEndsWith($entry, $suffix, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrRegex($value, $pattern, $message = '') + { + null === $value || static::regex($value, $pattern, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allRegex($value, $pattern, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::regex($entry, $pattern, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrRegex($value, $pattern, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::regex($entry, $pattern, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrNotRegex($value, $pattern, $message = '') + { + null === $value || static::notRegex($value, $pattern, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNotRegex($value, $pattern, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::notRegex($entry, $pattern, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $pattern + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrNotRegex($value, $pattern, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::notRegex($entry, $pattern, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrUnicodeLetters($value, $message = '') + { + null === $value || static::unicodeLetters($value, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allUnicodeLetters($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::unicodeLetters($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrUnicodeLetters($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::unicodeLetters($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrAlpha($value, $message = '') + { + null === $value || static::alpha($value, $message); + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allAlpha($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::alpha($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrAlpha($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::alpha($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrDigits($value, $message = '') + { + null === $value || static::digits($value, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allDigits($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::digits($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrDigits($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::digits($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrAlnum($value, $message = '') + { + null === $value || static::alnum($value, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allAlnum($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::alnum($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrAlnum($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::alnum($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert lowercase-string|null $value + * + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrLower($value, $message = '') + { + null === $value || static::lower($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allLower($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::lower($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrLower($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::lower($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrUpper($value, $message = '') + { + null === $value || static::upper($value, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allUpper($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::upper($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrUpper($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::upper($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param int $length + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrLength($value, $length, $message = '') + { + null === $value || static::length($value, $length, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int $length + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allLength($value, $length, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::length($entry, $length, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int $length + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrLength($value, $length, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::length($entry, $length, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrMinLength($value, $min, $message = '') + { + null === $value || static::minLength($value, $min, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allMinLength($value, $min, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::minLength($entry, $min, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrMinLength($value, $min, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::minLength($entry, $min, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrMaxLength($value, $max, $message = '') + { + null === $value || static::maxLength($value, $max, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allMaxLength($value, $max, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::maxLength($entry, $max, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrMaxLength($value, $max, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::maxLength($entry, $max, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrLengthBetween($value, $min, $max, $message = '') + { + null === $value || static::lengthBetween($value, $min, $max, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allLengthBetween($value, $min, $max, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::lengthBetween($entry, $min, $max, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrLengthBetween($value, $min, $max, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::lengthBetween($entry, $min, $max, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrFileExists($value, $message = '') + { + null === $value || static::fileExists($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allFileExists($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::fileExists($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrFileExists($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::fileExists($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrFile($value, $message = '') + { + null === $value || static::file($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allFile($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::file($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrFile($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::file($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrDirectory($value, $message = '') + { + null === $value || static::directory($value, $message); + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allDirectory($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::directory($entry, $message); + } + } + + /** + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrDirectory($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::directory($entry, $message); + } + } + + /** + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrReadable($value, $message = '') + { + null === $value || static::readable($value, $message); + } + + /** + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allReadable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::readable($entry, $message); + } + } + + /** + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrReadable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::readable($entry, $message); + } + } + + /** + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrWritable($value, $message = '') + { + null === $value || static::writable($value, $message); + } + + /** + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allWritable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::writable($entry, $message); + } + } + + /** + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrWritable($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::writable($entry, $message); + } + } + + /** + * @psalm-assert class-string|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrClassExists($value, $message = '') + { + null === $value || static::classExists($value, $message); + } + + /** + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allClassExists($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::classExists($entry, $message); + } + } + + /** + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrClassExists($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::classExists($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert class-string|ExpectedType|null $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrSubclassOf($value, $class, $message = '') + { + null === $value || static::subclassOf($value, $class, $message); + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable|ExpectedType> $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allSubclassOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::subclassOf($entry, $class, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $class + * @psalm-assert iterable|ExpectedType|null> $value + * + * @param mixed $value + * @param string|object $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrSubclassOf($value, $class, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::subclassOf($entry, $class, $message); + } + } + + /** + * @psalm-assert class-string|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrInterfaceExists($value, $message = '') + { + null === $value || static::interfaceExists($value, $message); + } + + /** + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allInterfaceExists($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::interfaceExists($entry, $message); + } + } + + /** + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrInterfaceExists($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::interfaceExists($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $interface + * @psalm-assert class-string|null $value + * + * @param mixed $value + * @param mixed $interface + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrImplementsInterface($value, $interface, $message = '') + { + null === $value || static::implementsInterface($value, $interface, $message); + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $interface + * @psalm-assert iterable> $value + * + * @param mixed $value + * @param mixed $interface + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allImplementsInterface($value, $interface, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::implementsInterface($entry, $interface, $message); + } + } + + /** + * @psalm-pure + * @psalm-template ExpectedType of object + * @psalm-param class-string $interface + * @psalm-assert iterable|null> $value + * + * @param mixed $value + * @param mixed $interface + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrImplementsInterface($value, $interface, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::implementsInterface($entry, $interface, $message); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object|null $classOrObject + * + * @param string|object|null $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrPropertyExists($classOrObject, $property, $message = '') + { + null === $classOrObject || static::propertyExists($classOrObject, $property, $message); + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allPropertyExists($classOrObject, $property, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + static::propertyExists($entry, $property, $message); + } + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrPropertyExists($classOrObject, $property, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + null === $entry || static::propertyExists($entry, $property, $message); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object|null $classOrObject + * + * @param string|object|null $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrPropertyNotExists($classOrObject, $property, $message = '') + { + null === $classOrObject || static::propertyNotExists($classOrObject, $property, $message); + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allPropertyNotExists($classOrObject, $property, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + static::propertyNotExists($entry, $property, $message); + } + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $property + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrPropertyNotExists($classOrObject, $property, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + null === $entry || static::propertyNotExists($entry, $property, $message); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object|null $classOrObject + * + * @param string|object|null $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrMethodExists($classOrObject, $method, $message = '') + { + null === $classOrObject || static::methodExists($classOrObject, $method, $message); + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allMethodExists($classOrObject, $method, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + static::methodExists($entry, $method, $message); + } + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrMethodExists($classOrObject, $method, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + null === $entry || static::methodExists($entry, $method, $message); + } + } + + /** + * @psalm-pure + * @psalm-param class-string|object|null $classOrObject + * + * @param string|object|null $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrMethodNotExists($classOrObject, $method, $message = '') + { + null === $classOrObject || static::methodNotExists($classOrObject, $method, $message); + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allMethodNotExists($classOrObject, $method, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + static::methodNotExists($entry, $method, $message); + } + } + + /** + * @psalm-pure + * @psalm-param iterable $classOrObject + * + * @param iterable $classOrObject + * @param mixed $method + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrMethodNotExists($classOrObject, $method, $message = '') + { + static::isIterable($classOrObject); + + foreach ($classOrObject as $entry) { + null === $entry || static::methodNotExists($entry, $method, $message); + } + } + + /** + * @psalm-pure + * + * @param array|null $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrKeyExists($array, $key, $message = '') + { + null === $array || static::keyExists($array, $key, $message); + } + + /** + * @psalm-pure + * + * @param iterable $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allKeyExists($array, $key, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::keyExists($entry, $key, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrKeyExists($array, $key, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::keyExists($entry, $key, $message); + } + } + + /** + * @psalm-pure + * + * @param array|null $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrKeyNotExists($array, $key, $message = '') + { + null === $array || static::keyNotExists($array, $key, $message); + } + + /** + * @psalm-pure + * + * @param iterable $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allKeyNotExists($array, $key, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::keyNotExists($entry, $key, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $array + * @param string|int $key + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrKeyNotExists($array, $key, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::keyNotExists($entry, $key, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert array-key|null $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrValidArrayKey($value, $message = '') + { + null === $value || static::validArrayKey($value, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allValidArrayKey($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::validArrayKey($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $value + * + * @param mixed $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrValidArrayKey($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::validArrayKey($entry, $message); + } + } + + /** + * @param Countable|array|null $array + * @param int $number + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrCount($array, $number, $message = '') + { + null === $array || static::count($array, $number, $message); + } + + /** + * @param iterable $array + * @param int $number + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allCount($array, $number, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::count($entry, $number, $message); + } + } + + /** + * @param iterable $array + * @param int $number + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrCount($array, $number, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::count($entry, $number, $message); + } + } + + /** + * @param Countable|array|null $array + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrMinCount($array, $min, $message = '') + { + null === $array || static::minCount($array, $min, $message); + } + + /** + * @param iterable $array + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allMinCount($array, $min, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::minCount($entry, $min, $message); + } + } + + /** + * @param iterable $array + * @param int|float $min + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrMinCount($array, $min, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::minCount($entry, $min, $message); + } + } + + /** + * @param Countable|array|null $array + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrMaxCount($array, $max, $message = '') + { + null === $array || static::maxCount($array, $max, $message); + } + + /** + * @param iterable $array + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allMaxCount($array, $max, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::maxCount($entry, $max, $message); + } + } + + /** + * @param iterable $array + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrMaxCount($array, $max, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::maxCount($entry, $max, $message); + } + } + + /** + * @param Countable|array|null $array + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrCountBetween($array, $min, $max, $message = '') + { + null === $array || static::countBetween($array, $min, $max, $message); + } + + /** + * @param iterable $array + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allCountBetween($array, $min, $max, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::countBetween($entry, $min, $max, $message); + } + } + + /** + * @param iterable $array + * @param int|float $min + * @param int|float $max + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrCountBetween($array, $min, $max, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::countBetween($entry, $min, $max, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert list|null $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsList($array, $message = '') + { + null === $array || static::isList($array, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsList($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::isList($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsList($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::isList($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert non-empty-list|null $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsNonEmptyList($array, $message = '') + { + null === $array || static::isNonEmptyList($array, $message); + } + + /** + * @psalm-pure + * @psalm-assert iterable $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsNonEmptyList($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::isNonEmptyList($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-assert iterable $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsNonEmptyList($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::isNonEmptyList($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param mixed|array|null $array + * @psalm-assert array|null $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsMap($array, $message = '') + { + null === $array || static::isMap($array, $message); + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param iterable> $array + * @psalm-assert iterable> $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsMap($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::isMap($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param iterable|null> $array + * @psalm-assert iterable|null> $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsMap($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::isMap($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param mixed|array|null $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrIsNonEmptyMap($array, $message = '') + { + null === $array || static::isNonEmptyMap($array, $message); + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param iterable> $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allIsNonEmptyMap($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + static::isNonEmptyMap($entry, $message); + } + } + + /** + * @psalm-pure + * @psalm-template T + * @psalm-param iterable|null> $array + * @psalm-assert iterable|null> $array + * @psalm-assert iterable $array + * + * @param mixed $array + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrIsNonEmptyMap($array, $message = '') + { + static::isIterable($array); + + foreach ($array as $entry) { + null === $entry || static::isNonEmptyMap($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param string|null $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrUuid($value, $message = '') + { + null === $value || static::uuid($value, $message); + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allUuid($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + static::uuid($entry, $message); + } + } + + /** + * @psalm-pure + * + * @param iterable $value + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrUuid($value, $message = '') + { + static::isIterable($value); + + foreach ($value as $entry) { + null === $entry || static::uuid($entry, $message); + } + } + + /** + * @psalm-param class-string $class + * + * @param Closure|null $expression + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function nullOrThrows($expression, $class = 'Exception', $message = '') + { + null === $expression || static::throws($expression, $class, $message); + } + + /** + * @psalm-param class-string $class + * + * @param iterable $expression + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allThrows($expression, $class = 'Exception', $message = '') + { + static::isIterable($expression); + + foreach ($expression as $entry) { + static::throws($entry, $class, $message); + } + } + + /** + * @psalm-param class-string $class + * + * @param iterable $expression + * @param string $class + * @param string $message + * + * @throws InvalidArgumentException + * + * @return void + */ + public static function allNullOrThrows($expression, $class = 'Exception', $message = '') + { + static::isIterable($expression); + + foreach ($expression as $entry) { + null === $entry || static::throws($entry, $class, $message); + } + } +} diff --git a/web/install.sh b/web/install.sh index a87aff5..ed5f577 100644 --- a/web/install.sh +++ b/web/install.sh @@ -79,6 +79,8 @@ initProgress(){ #Start services service ntp restart service apache2 restart + service cron start + echo "* * * * * root /usr/bin/php7.4 /opt/uoj/web/app/scheduler.php" >> /etc/crontab mkdir -p /opt/uoj/web/app/storage/submission mkdir -p /opt/uoj/web/app/storage/tmp mkdir -p /opt/uoj/web/app/storage/image_hosting