├── LICENSE ├── README.md ├── composer.json └── src └── CrontabTicker.php /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DICrontab 2 | This is a php class implement a timetable service using format string like linux crontab, dependence on swoole-extension. 3 | 4 | Thanks to the project [swoole-crontab](https://github.com/osgochina/swoole-crontab), I learned how to analyze the crontab string. 5 | 6 | DICrontab is not the same as [swoole-crontab](https://github.com/osgochina/swoole-crontab), that's a complete application, and DICrontab is just like a tool, a simple library, a class, you can inclued it and use is in your project. 7 | 8 | # Quick start 9 | ``` 10 | When('0 */20 * * *') 15 | ->Then(function ($userParams) 16 | { 17 | echo 'crontab called'; 18 | return false;//return false if you want to cancle this cron. 19 | }, $userParams 20 | ); 21 | //The callback function will be called every 20 minitues. 22 | ``` 23 | If you just want to test when will the ticker tick next time 24 | Use the Next() function instead, Next() will return the Iterator of the time table. 25 | Also you can use From(startTime) to asume the cron start at what time you need instead of time(). 26 | ``` 27 | $iterator = $crontab->When('* */20 * * *') 28 | //->From( mktime(14, 00, 00, 3, 3, 2016) )//Optional 29 | ->Next(); 30 | 31 | $count = 0;//only print next 10 cron. 32 | foreach($nextTickTime in $iterator) 33 | { 34 | if($count++ < 10) 35 | echo $nextTickTime; 36 | else 37 | break; 38 | } 39 | ``` 40 | ``` 41 | //Simple of crontab-string 42 | //$crontabString : 43 | // * 0 1 2 3 4 5 44 | // * * * * * * * 45 | // * - - - - - - 46 | // * | | | | | | 47 | // * | | | | | +----- day of week (0 - 6) (Sunday=0) 48 | // * | | | | +----- month (1 - 12) 49 | // * | | | +------- day of month (1 - 31) 50 | // * | | +--------- hour (0 - 23) 51 | // * | +----------- min (0 - 59) 52 | // * +------------- sec (0-59) (Optional) 53 | //You can use 5 params like what crontab use in Linux, and use the 6th param as to descript the SECONDS. 54 | ``` 55 | 56 | # Dependence 57 | + My develop enverment is CentOS 7. 58 | + PHP 7.0.3 59 | + [Swoole-extension](https://github.com/swoole/swoole-src/releases) 1.8.3 60 | + I think php5.5+ and swoole 1.7.7+ is Ok, but I can't test it. 61 | + I used swoole_timer_after, so must at least swoole-1.7.7. 62 | + It also used keyword 'yield' in php, so 5.5+ is required. 63 | 64 | # Notes 65 | As this class is base on swoole_timer_after, please notes below while using in swoole_server or asyns-swoole-client: 66 | 67 | 1. If the Process was reloaded, the cron tick will be deleted, so if you want add an resident crontab, please record your cron-string and init the ticker on process start, some swoole callback like 'OnWorkerStart', cause the cron-string descripted the timetable, don't worry about lose the ticker, just set a new one. 68 | 69 | 2. If you want to do something like send 1 message to every user, please notice that every Process can has its own tiker, if you add the tick in OnWorkerStart, asume you have 2 Worker Process and 4 TaskWorker Process and you set the ticker on every worker start, you will have 6 ticker and callback at same time, every client will receive 6 message. So, just use something like "if($server->worker_id==0)" to add only one ticker. 70 | 71 | # Description 72 | While working my on swoole-framework [DIServer-framwork](https://github.com/szyhf/DIServer), I find is not easy to implement some timer job like send a message to every clients on every Monday to Friday, swoole_tick provides a high accuracy tick service, but only can tick every same micro-seconds or tick atfer some micro-seconds from now. 73 | 74 | What I need is something like crontab in Linux, using a format string can easily like "0 0 \* \* mon-fri", and the callback function will called while time's up. 75 | 76 | At first I just want to use it as a plugins in [DIServer-framwork](https://github.com/szyhf/DIServer), but after I finished it I find out it's a independent job, so I create this project. 77 | 78 | # Difference 79 | As the high accuracy provieded from swoole-extension, not like Linux-crontab, we can set up HIGH LEVEL cron like "\*/20 0 0 \* \* fri", while using 6 params, the format will be "second minute hour day month week"; As 5 params is "minute hour day month week", and it will called on second 1 in that minute like linux-crontab. 80 | 81 | # Example 82 | Here is some example I used to test the Ticker, if you find any bugs, please [notes](https://github.com/szyhf/DICrontab/issues/new) me. 83 | ``` 84 | private $cronString = [ 85 | '* */1 * 3 3 *' => 'every seconds in Mar 3th.', 86 | '30 30 21 * * *' => '21:30:30 on every day.', 87 | '15,30,45,0 * 23 * * 6' => 'every 0,15,30,45 seconds in 23 every Saturday.', 88 | '0 0 */1 * * *' => 'every hour at 0 minute and 0 seconds.', 89 | '0 0 4 1 jan *' => 'every 4:00:00 at January 1st.', 90 | '* * 7 * * *' => 'every seconds in 7 o'clock every day.', 91 | '0-59/2 20 0-23/2 * * *' => 'every 2 seconds in every 20 minutes at every 2 hours in a day.', 92 | '0 6-12/3 * 2 *' => 'In February, every 3 hours in 6 o'clock to 12 o'clock.', 93 | '0 17 * * 1-5' => 'every 17:00:01 on Monday to Friday.', 94 | '0 11 4 * mon-wed' => 'Every 11:00:01 on 4th of a month or in Monday to Wednesday', 95 | '10 1 * * 6,0' => 'every 1:10:01 on Saturday and Sunday', 96 | '45 4 1,10,22 * *' => '4:45:01 on every 1st,10th,22th in a month.', 97 | '0,30 18-23 * * *' => 'every 0,30 minutes in 18-23. Tips:23:30 will called.', 98 | '*/1 * * * * *' => 'every seconds', 99 | '* * * * * *' => 'every seconds', 100 | '0 23-7/1 * * *' => 'equals to "0 23,0-7 * * *"', 101 | ]; 102 | ``` 103 | 104 | # Shorthand 105 | While using Month or Week, you can use 'fri' instead of '5' means the friday, the shorthand support was listed below: 106 | ``` 107 | //Case is ignored 108 | const SHORT_MAP = [ 109 | 'sun' => 0, 110 | 'sunday' => 0, 111 | 'mon' => 1, 112 | 'monday' => 1, 113 | 'tues' => 2, 114 | 'tue' => 2, 115 | 'tuesday' => 2, 116 | 'wed' => 3, 117 | 'wednesday' => 3, 118 | 'thur' => 4, 119 | 'thu' => 4, 120 | 'thursday' => 4, 121 | 'fri' => 5, 122 | 'friday' => 5, 123 | 'sat' => 6, 124 | 'saturday' => 6, 125 | 'jan' => 1, 126 | 'january' => 1, 127 | 'feb' => 2, 128 | 'february' => 2, 129 | 'mar' => 3, 130 | 'march' => 4, 131 | 'apr' => 4, 132 | 'april' => 4, 133 | 'may' => 5, 134 | 'jun' => 6, 135 | 'june' => 6, 136 | 'jul' => 7, 137 | 'july' => 7, 138 | 'aug' => 8, 139 | 'august' => 8, 140 | 'sep' => 9, 141 | 'sept' => 9, 142 | 'september' => 9, 143 | 'oct' => 10, 144 | 'october' => 10, 145 | 'nov' => 11, 146 | 'november' => 11, 147 | 'dec' => 12, 148 | 'december' => 12 149 | ]; 150 | ``` 151 | 152 | 153 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "DICrontab", 3 | "description": "A linux-crontab command style ticker libriary in php.", 4 | "keywords": [ 5 | "crontab", 6 | "DIServer", 7 | "swoole", 8 | "php" 9 | ], 10 | "license": "Apache2", 11 | "type": "libriary", 12 | "require": { 13 | "php": ">=5.5.9", 14 | "ext-swoole": ">=1.7.20" 15 | }, 16 | "authors": [ 17 | { 18 | "name": "Back Yu", 19 | "email": "yhf_szb@163.com" 20 | } 21 | ], 22 | "autoload": { 23 | "psr-4": { 24 | "DIServer\\": "src/" 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/CrontabTicker.php: -------------------------------------------------------------------------------- 1 | 0, 32 | 'sunday' => 0, 33 | 'mon' => 1, 34 | 'monday' => 1, 35 | 'tues' => 2, 36 | 'tue' => 2, 37 | 'tuesday' => 2, 38 | 'wed' => 3, 39 | 'wednesday' => 3, 40 | 'thur' => 4, 41 | 'thu' => 4, 42 | 'thursday' => 4, 43 | 'fri' => 5, 44 | 'friday' => 5, 45 | 'sat' => 6, 46 | 'saturday' => 6, 47 | 'jan' => 1, 48 | 'january' => 1, 49 | 'feb' => 2, 50 | 'february' => 2, 51 | 'mar' => 3, 52 | 'march' => 4, 53 | 'apr' => 4, 54 | 'april' => 4, 55 | 'may' => 5, 56 | 'jun' => 6, 57 | 'june' => 6, 58 | 'jul' => 7, 59 | 'july' => 7, 60 | 'aug' => 8, 61 | 'august' => 8, 62 | 'sep' => 9, 63 | 'sept' => 9, 64 | 'september' => 9, 65 | 'oct' => 10, 66 | 'october' => 10, 67 | 'nov' => 11, 68 | 'november' => 11, 69 | 'dec' => 12, 70 | 'december' => 12 71 | ]; 72 | 73 | /** 74 | * 设定Crontab string 75 | * 76 | * @param $crontabString : 77 | * 0 1 2 3 4 5 78 | * * * * * * * 79 | * - - - - - - 80 | * | | | | | | 81 | * | | | | | +----- day of week (0 - 6) (Sunday=0) 82 | * | | | | +----- month (1 - 12) 83 | * | | | +------- day of month (1 - 31) 84 | * | | +--------- hour (0 - 23) 85 | * | +----------- min (0 - 59) 86 | * +------------- sec (0-59) 87 | * 88 | * @return $this 89 | * @throws \Exception 90 | */ 91 | public function When($crontabString) 92 | { 93 | if(!is_string($crontabString)) 94 | { 95 | throw new \Exception("\$crontabString should be a string."); 96 | } 97 | 98 | 99 | //处理空白字符 100 | $crontabString = trim($crontabString); 101 | 102 | //如果包含字母,考虑使用英文单词描述周或者月的情况 103 | if(preg_match('/[A-Za-z]*/', $crontabString)) 104 | { 105 | $crontabString = strtolower($crontabString);//处理成小写 106 | foreach(self::$_short_map as $str => $vol) 107 | { 108 | $crontabString = str_replace($str, $vol, $crontabString); 109 | } 110 | } 111 | 112 | //检查crontab是否符合规范 113 | if(!preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i', 114 | $crontabString) 115 | ) 116 | { 117 | if(!preg_match('/^((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)\s+((\*(\/[0-9]+)?)|[0-9\-\,\/]+)$/i', 118 | $crontabString) 119 | ) 120 | { 121 | throw new \Exception("Invalid crontab string: " . $crontabString); 122 | } 123 | } 124 | $this->_clear(); 125 | $this->_crontab = $crontabString; 126 | 127 | return $this; 128 | } 129 | 130 | /** 131 | * 设定统计时间的起点,如果不设置则默认从time()获取 132 | * 133 | * @param int $time 134 | * 135 | * @return $this 136 | */ 137 | public function From($time) 138 | { 139 | $this->_now = $time; 140 | 141 | return $this; 142 | } 143 | 144 | /** 145 | * 下个触发时间的时间戳列表(迭代器) 146 | * 147 | * @return \Iterator 148 | */ 149 | public function Next() 150 | { 151 | $this->_initLimits();//初始化限制条件 152 | while($this->_now < PHP_INT_MAX) 153 | { 154 | $this->_initStart();//初始化统计起点 155 | $this->_initPeriods();//初始化可用周期 156 | $this->_now = $this->_nextAvailableTime(); 157 | yield $this->_now++; 158 | } 159 | } 160 | 161 | /** 162 | * 定时器触发时执行的回调 163 | * 164 | * @param callable $callback function($tickID,$params=null){} 165 | * @param mixed $params 希望传入回调函数的参数 166 | * 167 | * @return $this 168 | */ 169 | public function Then(callable $callback, $params = null) 170 | { 171 | $this->_nextIterator = self::Next(); 172 | $this->_nextTime = $this->_nextIterator->current(); 173 | $tickFunc = function ($called = false) use (&$tickFunc, &$callback, &$params) 174 | { 175 | if($called) 176 | { 177 | //如果called为true,说明是差距回调,先执行用户方法 178 | if(call_user_func($callback, $params) === false) 179 | { 180 | //如果用户回调函数返回false,则停止当然日程继续回调 181 | return; 182 | } 183 | } 184 | 185 | while($this->_nextTime <= time()) 186 | { 187 | //已知时间超时,则更新已知时间 188 | $this->_nextIterator->next(); 189 | $this->_nextTime = $this->_nextIterator->current(); 190 | } 191 | 192 | $timeAfter = $this->_nextTime - time(); 193 | if($timeAfter > 86400)//timeTick最高支持86400s 194 | { 195 | //下次回调在一天以后,则设置一天后重新统计剩余时间 196 | return swoole_timer_after(86400 * 1000, $tickFunc); 197 | } 198 | else 199 | { 200 | return swoole_timer_after($timeAfter * 1000, $tickFunc, true); 201 | } 202 | }; 203 | 204 | return $tickFunc(false); 205 | } 206 | 207 | private function _clear() 208 | { 209 | $this->_start = 0; 210 | $this->_periods = []; 211 | $this->_nextPeriods = []; 212 | $this->_limits = []; 213 | $this->_parse = ''; 214 | $this->_availableTimes = []; 215 | $this->_nextTime = 0; 216 | $this->_nextIterator = null; 217 | } 218 | 219 | /** 220 | * @param $s 221 | * @param $min 222 | * @param $max 223 | * 224 | * @return array 225 | */ 226 | private function _parseCrontabNumber($s, $min, $max) 227 | { 228 | $result = []; 229 | $v1 = explode(",", $s); 230 | foreach($v1 as $v2) 231 | { 232 | $v3 = explode("/", $v2); 233 | $step = empty($v3[1]) ? 1 : $v3[1]; 234 | $v4 = explode("-", $v3[0]); 235 | if(count($v4) == 2)//涉及分阶段的计算 236 | { 237 | if($v4[0] <= $v4[1]) 238 | { 239 | $_min = $v4[0]; 240 | $_max = $v4[1]; 241 | } 242 | else//形如22-7实际表示22-23,0-7 243 | { 244 | //即$v4[0]-$max/$step,$min-$v4[1]/$step,重新拼接字符串计算 245 | $s = "{$v4[0]}-$max/$step,$min-{$v4[1]}/$step"; 246 | 247 | return $this->_parseCrontabNumber($s, $min, $max); 248 | } 249 | } 250 | else 251 | { 252 | $_min = ($v3[0] == "*" ? $min : $v3[0]); 253 | $_max = ($v3[0] == "*" ? $max : $v3[0]); 254 | } 255 | for($i = $_min; $i <= $_max; $i += $step) 256 | { 257 | $result[$i] = intval($i); 258 | } 259 | } 260 | ksort($result); 261 | 262 | return $result; 263 | } 264 | 265 | /** 266 | * Linux crontab风格的对*的处理 267 | * 如 * 1 * * * * 处理为*\/1 1 * * * * 268 | */ 269 | private function _initLimits() 270 | { 271 | $cron = preg_split("/[\s]+/i", trim($this->_crontab)); 272 | if(count($cron) == 5) 273 | { 274 | //5个参数的时候自动补秒约束1 275 | array_unshift($cron, '1'); 276 | } 277 | $this->_availableTimes[self::MONTH] = self::_parseCrontabNumber($cron[4], 1, 12); 278 | $this->_availableTimes[self::WEEK] = self::_parseCrontabNumber($cron[5], 0, 6); 279 | $this->_availableTimes[self::DAY] = self::_parseCrontabNumber($cron[3], 1, 31); 280 | $this->_availableTimes[self::HOUR] = self::_parseCrontabNumber($cron[2], 0, 23); 281 | $this->_availableTimes[self::MINUTE] = self::_parseCrontabNumber($cron[1], 0, 59); 282 | $this->_availableTimes[self::SECOND] = self::_parseCrontabNumber($cron[0], 0, 59); 283 | 284 | if(count($this->_availableTimes[self::MONTH]) < 12) 285 | { 286 | $this->_limits[self::MONTH] = true; 287 | } 288 | if(count($this->_availableTimes[self::WEEK]) < 7) 289 | { 290 | $this->_limits[self::WEEK] = true; 291 | } 292 | if(count($this->_availableTimes[self::DAY]) < 31) 293 | { 294 | $this->_limits[self::DAY] = true; 295 | } 296 | if(count($this->_availableTimes[self::HOUR]) < 24) 297 | { 298 | $this->_limits[self::HOUR] = true; 299 | } 300 | if(count($this->_availableTimes[self::MINUTE]) < 60) 301 | { 302 | $this->_limits[self::MINUTE] = true; 303 | } 304 | if(count($this->_availableTimes[self::SECOND]) < 60) 305 | { 306 | $this->_limits[self::SECOND] = true; 307 | } 308 | if(count($this->_limits) == 0) 309 | { 310 | //如果没有任何一个约束,说明就是等价于* * * * * *的情况 311 | $this->_limits[self::SECOND] = true; 312 | } 313 | } 314 | 315 | /** 316 | * 对start的各个周期属性进行分析并存储 317 | */ 318 | private function _initPeriods() 319 | { 320 | $this->_nextPeriods = [];//清理上一次遗留的数据 321 | $this->_periods[self::YEAR] = date(self::YEAR, $this->_start); 322 | $this->_periods[self::MONTH] = date(self::MONTH, $this->_start); 323 | $this->_periods[self::DAY] = date(self::DAY, $this->_start); 324 | $this->_periods[self::HOUR] = date(self::HOUR, $this->_start); 325 | $this->_periods[self::MINUTE] = date(self::MINUTE, $this->_start); 326 | $this->_periods[self::SECOND] = date(self::SECOND, $this->_start); 327 | $this->_periods[self::WEEK] = date(self::WEEK, $this->_start); 328 | $this->_nextPeriods[self::YEAR] = $this->_periods[self::YEAR]; 329 | //$this->_nextPeriods = $this->_periods; 330 | } 331 | 332 | private function _initStart() 333 | { 334 | $this->_now = $this->_now ?: time(); 335 | if($this->_hasEnoughTime()) 336 | { 337 | $this->_start = $this->_now; 338 | } 339 | else 340 | { 341 | //今天剩余时间不足的话,直接以明天为起点进行统计 342 | $this->_start = $this->_getWeeTime($this->_now + 86400); 343 | } 344 | } 345 | 346 | /** 347 | * 检查start时间所在的日中是否还有可能满足命令需求的时间点存在 348 | */ 349 | private function _hasEnoughTime() 350 | { 351 | $lastHour = $this->_isLimited(self::HOUR) ? max($this->_availableTimes[self::HOUR]) : 23; 352 | $lastMinute = $this->_isLimited(self::MINUTE) ? max($this->_availableTimes[self::MINUTE]) : 59; 353 | $lastSecond = $this->_isLimited(self::SECOND) ? max($this->_availableTimes[self::SECOND]) : 59; 354 | 355 | /** @var int $passedInStartDay 从当前时间的凌晨开始算起已经经过的时间 */ 356 | $passedInStartDay = $this->_now - $this->_getWeeTime($this->_now); 357 | /** @var int $lastSeconds 理论上今天最晚可触发的时间点到凌晨的时间 */ 358 | $lastSeconds = $lastHour * 3600 + $lastMinute * 60 + $lastSecond; 359 | 360 | return $passedInStartDay <= $lastSeconds; 361 | } 362 | 363 | /** 364 | * 从可选的升序时间点中获得距离所需时间点最近的较大点(循环判定) 365 | * 366 | * @param array $availableDates 可选的升序时间点 367 | * @param int $num 当前的时间点 368 | * @param bool|false $nextPeriodOutput 是否进入了下一个循环周期 369 | * 370 | * @return bool|mixed 371 | */ 372 | private function _nextMatch(array $availableDates, $num, &$nextPeriodOutput = false) 373 | { 374 | $next = false; 375 | foreach($availableDates as $availableDate) 376 | { 377 | if($availableDate >= $num) 378 | { 379 | $next = $availableDate; 380 | break; 381 | } 382 | } 383 | $nextPeriodOutput = $next === false;//0是合法的 384 | $next = $nextPeriodOutput ? min($availableDates) : $next; 385 | 386 | return $next; 387 | } 388 | 389 | private function _countNextMonth() 390 | { 391 | $nextMonth = $this->_periods[self::MONTH]; 392 | 393 | if($this->_isLimited(self::MONTH) || $this->_nextPeriods[self::MONTH]) 394 | { 395 | $nextMonth = $this->_nextMatch($this->_availableTimes[self::MONTH], 396 | $this->_periods[self::MONTH] + $this->_nextPeriods[self::MONTH], 397 | $nextPeriod); 398 | //月溢出则增加一年 399 | $this->_nextPeriods[self::YEAR] += $nextPeriod ? 1 : 0; 400 | } 401 | $this->_nextPeriods[self::MONTH] = $nextMonth; 402 | } 403 | 404 | private function _countNextDay() 405 | { 406 | $nextDay = $this->_periods[self::DAY]; 407 | if($this->_isLimited(self::DAY)) 408 | { 409 | $nextDay = $this->_nextMatch($this->_availableTimes[self::DAY], 410 | $this->_periods[self::DAY], 411 | $nextPeriod); 412 | //当前月已无足够的日,可能的月溢出放在_countNextMonth中处理 413 | $this->_nextPeriods[self::MONTH] = $nextPeriod ? 1 : 0; 414 | } 415 | $this->_nextPeriods[self::DAY] = $nextDay; 416 | } 417 | 418 | private function _countNextHour() 419 | { 420 | $nextHour = $this->_periods[self::HOUR]; 421 | if($this->_isLimited(self::HOUR) || $this->_nextPeriods[self::HOUR]) 422 | { 423 | $nextHour = $this->_nextMatch($this->_availableTimes[self::HOUR], 424 | $this->_periods[self::HOUR] + $this->_nextPeriods[self::HOUR], 425 | $nextRound); 426 | if($nextRound) 427 | { 428 | //算法异常,因为已经验证过当天时间的充分性 429 | //如果还出现下个周期说明算法有问题。 430 | throw new \Exception("Algorithm error, next hour has overflow."); 431 | } 432 | 433 | if($nextHour != $this->_periods[self::HOUR]) 434 | { 435 | //进入到新的Hour,则Minute和Second处理为可选的最小值。 436 | $this->_nextPeriods[self::MINUTE] = min($this->_availableTimes[self::MINUTE]); 437 | $this->_nextPeriods[self::SECOND] = min($this->_availableTimes[self::SECOND]); 438 | } 439 | } 440 | $this->_nextPeriods[self::HOUR] = $nextHour; 441 | } 442 | 443 | private function _countNextMinute() 444 | { 445 | $nextMinute = $this->_periods[self::MINUTE]; 446 | if($this->_isLimited(self::MINUTE) || $this->_nextPeriods[self::MINUTE]) 447 | { 448 | $nextMinute = $this->_nextMatch($this->_availableTimes[self::MINUTE], 449 | $this->_periods[self::MINUTE] + $this->_nextPeriods[self::MINUTE], 450 | $nextRound); 451 | 452 | $this->_nextPeriods[self::HOUR] = $nextRound ? 1 : 0; 453 | if($nextMinute != $this->_periods[self::MINUTE]) 454 | { 455 | //进入新的Minute,则Second处理为可选的最小值。 456 | $this->_nextPeriods[self::SECOND] = min($this->_availableTimes[self::SECOND]); 457 | } 458 | } 459 | $this->_nextPeriods[self::MINUTE] = $nextMinute; 460 | } 461 | 462 | private function _countNextSecond() 463 | { 464 | $nextSecond = $this->_periods[self::SECOND]; 465 | if($this->_isLimited(self::SECOND)) 466 | { 467 | $nextSecond = 468 | $this->_nextMatch($this->_availableTimes[self::SECOND], $this->_periods[self::SECOND], $nextRound); 469 | //当前分钟内已无足够秒,进入下个可用分钟(可能导致小时不足进入下个小时,延后处理) 470 | $this->_nextPeriods[self::MINUTE] = $nextRound ? 1 : 0; 471 | } 472 | $this->_nextPeriods[self::SECOND] = $nextSecond; 473 | } 474 | 475 | /** 476 | * 按week条件获取下一个可能的激活日 477 | * 478 | * @return bool|int int为下个可能激活日的0点0分0秒;false表示无week条件。 479 | * @throws \Exception 480 | */ 481 | private function _nextWeekTime() 482 | { 483 | //默认根据当日凌晨零点截取时间 484 | $nextWeekTime = $this->_getWeeTime($this->_start); 485 | //存在week约束时 486 | if($this->_isLimited(self::WEEK)) 487 | { 488 | $nextWeek = $this->_nextMatch($this->_availableTimes[self::WEEK], 489 | $this->_periods[self::WEEK]); 490 | //不在当天激活,则将时间调整为下个激活日的凌晨 491 | if($nextWeek != $this->_periods[self::WEEK]) 492 | { 493 | $nextWeekName = $this->_getWeekName($nextWeek); 494 | $nextWeekTime = strtotime("next $nextWeekName", $this->_start); 495 | } 496 | } 497 | 498 | return $nextWeekTime; 499 | } 500 | 501 | /** 502 | * 按照Day&Month的条件获取下个可能激活日的时间 503 | */ 504 | private function _nextDayTime() 505 | { 506 | $this->_countNextDay(); 507 | $this->_countNextMonth(); 508 | //根据月&&日的条件推算的最近触发日 509 | $nextDayTime = mktime(0, 510 | 0, 511 | 0, 512 | $this->_nextPeriods[self::MONTH], 513 | $this->_nextPeriods[self::DAY], 514 | $this->_nextPeriods[self::YEAR]); 515 | 516 | return $nextDayTime; 517 | } 518 | 519 | /** 520 | * 获取下一个可用日的凌晨时间点 521 | * 522 | * @return int 523 | */ 524 | private function _nextAvailableDay() 525 | { 526 | /** @var bool $dayMonthLimit 日月约束 */ 527 | $dayMonthLimit = $this->_isLimited(self::MONTH) || $this->_isLimited(self::DAY); 528 | /** @var bool $weekLimit 周约束 */ 529 | $weekLimit = $this->_isLimited(self::WEEK); 530 | 531 | //如果既没有日月约束也没有周约束,则以$dayTime作为可行日(此时$dayTime==$weekTime) 532 | //如果只有日月约束,则以$dayTime作为可行日 533 | //如果只有周约束,则以$weekTime作为可行日 534 | //如果既有周约束又有日月约束,则以MIN($dayTime,$weekTime)作为可行日 535 | 536 | if(!$weekLimit) 537 | { 538 | return $this->_nextDayTime(); 539 | } 540 | elseif(!$dayMonthLimit && $weekLimit) 541 | { 542 | $weekTime = $this->_nextWeekTime(); 543 | //根据weekTime重置nextPeriods 544 | $this->_nextPeriods[self::YEAR] = date(self::YEAR, $weekTime); 545 | $this->_nextPeriods[self::MONTH] = date(self::MONTH, $weekTime); 546 | $this->_nextPeriods[self::DAY] = date(self::DAY, $weekTime); 547 | 548 | return $weekTime; 549 | } 550 | else 551 | { 552 | //既有 553 | $dayTime = $this->_nextDayTime(); 554 | $weekTime = $this->_nextWeekTime(); 555 | 556 | if($weekTime < $dayTime) 557 | { 558 | //根据weekTime重置nextPeriods 559 | $this->_nextPeriods[self::YEAR] = date(self::YEAR, $weekTime); 560 | $this->_nextPeriods[self::MONTH] = date(self::MONTH, $weekTime); 561 | $this->_nextPeriods[self::DAY] = date(self::DAY, $weekTime); 562 | 563 | return $weekTime; 564 | } 565 | else 566 | { 567 | return $dayTime; 568 | } 569 | } 570 | } 571 | 572 | /** 573 | * 下个激活时间点的时间戳 574 | * 575 | * @return int 576 | * @throws \Exception 577 | */ 578 | private function _nextAvailableTime() 579 | { 580 | $this->_nextAvailableDay(); 581 | if($this->_isSameDay()) 582 | { 583 | //按照秒、分、时的方式进行计算(以处理进位) 584 | $this->_countNextSecond(); 585 | $this->_countNextMinute(); 586 | $this->_countNextHour(); 587 | } 588 | else 589 | { 590 | //不是同一天,直接获取可行的H:i:s中的最小时间点 591 | $this->_nextPeriods[self::HOUR] = min($this->_availableTimes[self::HOUR]); 592 | $this->_nextPeriods[self::MINUTE] = min($this->_availableTimes[self::MINUTE]); 593 | $this->_nextPeriods[self::SECOND] = min($this->_availableTimes[self::SECOND]); 594 | } 595 | 596 | return mktime($this->_nextPeriods[self::HOUR], 597 | $this->_nextPeriods[self::MINUTE], 598 | $this->_nextPeriods[self::SECOND], 599 | $this->_nextPeriods[self::MONTH], 600 | $this->_nextPeriods[self::DAY], 601 | $this->_nextPeriods[self::YEAR]); 602 | } 603 | 604 | /** 605 | * 工具:根据时间戳获取当天的凌晨0点0分0秒 606 | * 607 | * @param $time 608 | * 609 | * @return int 610 | */ 611 | private function _getWeeTime($time) 612 | { 613 | return strtotime(date('Ymd', $time)); 614 | } 615 | 616 | /** 617 | * 工具:获取周x的英文名称 618 | * 619 | * @param $week 620 | * 621 | * @return string 622 | * @throws \Exception 623 | */ 624 | private function _getWeekName($week) 625 | { 626 | $weekName = ''; 627 | switch($week) 628 | { 629 | case 0: 630 | { 631 | $weekName = 'Sunday'; 632 | break; 633 | } 634 | case 1: 635 | { 636 | $weekName = 'Monday'; 637 | break; 638 | } 639 | case 2: 640 | { 641 | $weekName = 'Tuesday'; 642 | break; 643 | } 644 | case 3: 645 | { 646 | $weekName = 'Wednesday'; 647 | break; 648 | } 649 | case 4: 650 | { 651 | $weekName = 'Thursday'; 652 | break; 653 | } 654 | case 5: 655 | { 656 | $weekName = 'Friday'; 657 | break; 658 | } 659 | case 6: 660 | { 661 | $weekName = 'Saturday'; 662 | break; 663 | } 664 | default: 665 | { 666 | throw new \Exception("\$Week should between [0,6], $week given."); 667 | } 668 | } 669 | 670 | return $weekName; 671 | } 672 | 673 | /** 674 | * 检测指定的周期是否被约束了 675 | * 676 | * @param $span 677 | * 678 | * @return bool 679 | */ 680 | private function _isLimited($span) 681 | { 682 | return isset($this->_limits[$span]) && $this->_limits[$span]; 683 | } 684 | 685 | /** 686 | * 下个激活时间是否还在当天 687 | * 688 | * @return bool 689 | */ 690 | private function _isSameDay() 691 | { 692 | return $this->_periods[self::YEAR] == $this->_nextPeriods[self::YEAR] && 693 | $this->_periods[self::DAY] == $this->_nextPeriods[self::DAY] && 694 | $this->_periods[self::MONTH] == $this->_nextPeriods[self::MONTH]; 695 | } 696 | } 697 | --------------------------------------------------------------------------------