├── .coveralls.yml ├── .github ├── CONTRIBUTING.md ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── build └── phpunit.xml ├── composer.json ├── src ├── CrontabValidator.php └── Exceptions │ └── InvalidExpressionException.php └── tests ├── Unit ├── CrontabValidatorTest.php └── DataProviders │ ├── InvalidExpressions.php │ └── ValidExpressions.php └── bootstrap.php /.coveralls.yml: -------------------------------------------------------------------------------- 1 | service_name: travis-ci 2 | coverage_clover: build/logs/clover.xml 3 | json_path: build/logs/coveralls-upload.json 4 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | Contributions are **welcome** and will be fully **credited**. 4 | 5 | We accept contributions via pull requests on [GitHub](https://github.com/hollodotme/crontab-validator). 6 | 7 | 8 | ## Pull Requests 9 | 10 | - **Add tests!** - Your patch will not be accepted if it does not have tests. 11 | 12 | - **Document any change in behaviour** - Make sure the documentation in `README.md` is kept up-to-date. 13 | 14 | - **Consider our release cycle** - We follow [SemVer v2.0.0](http://semver.org/). Randomly breaking public APIs is not an option. 15 | 16 | - **Create topic branches** - Do not ask us to pull from your master branch. 17 | 18 | - **One pull request per feature** - If you want to do more than one thing, please send multiple pull requests. 19 | 20 | - **Send coherent history** - Make sure each individual commit in your pull request is meaningful. If you had to make multiple intermediate commits while developing, please squash them before submitting. 21 | 22 | 23 | ## Running Tests 24 | 25 | ```bash 26 | $ vendor/bin/phpunit -c build/ 27 | ``` 28 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ## Expected Behavior 2 | 3 | 4 | ## Actual Behavior 5 | 6 | 7 | ## Steps to Reproduce the Problem 8 | 9 | 1. 10 | 2. 11 | 3. 12 | 13 | ## Specifications 14 | 15 | - Package version: 16 | - PHP version: 17 | - Platform/OS: 18 | - Subsystem: 19 | 20 | ## Further comments 21 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | Fixes #`` 2 | 3 | ## Proposed Changes 4 | 5 | - 6 | - 7 | - 8 | 9 | ## Further comments 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # vagrant 4 | .vagrant/ 5 | vagrant 6 | 7 | # composer 8 | vendor/ 9 | composer.lock 10 | 11 | # locally generated docs 12 | build/api/ 13 | build/phpdox/ 14 | build/logs/ -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 7.1 5 | - 7.2 6 | - 7.3 7 | - 7.4snapshot 8 | 9 | branches: 10 | only: 11 | - master 12 | - development 13 | - /^feature\/.+$/ 14 | 15 | before_script: 16 | - git checkout $TRAVIS_BRANCH 17 | - composer self-update 18 | - mkdir -p vendor/bin 19 | - composer install -o --prefer-dist --no-interaction 20 | - mkdir build/logs 21 | 22 | script: 23 | - php vendor/bin/phpunit.phar -c build/ 24 | 25 | after_success: 26 | - travis_retry php vendor/bin/coveralls.phar -v --exclude-no-stmt 27 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | All notable changes to this project will be documented in this file. 4 | This project adheres to [Semantic Versioning](http://semver.org) and [Keep a CHANGELOG](http://keepachangelog.com). 5 | 6 | ## [2.0.1] - 2017-12-12 7 | 8 | ### Fixed 9 | 10 | * Day of month field allows expressions like "5L" - [#1] 11 | 12 | ## [2.0.0] - 2017-12-11 13 | 14 | ### Added 15 | 16 | * Support for the `L` modifier in the day of month section 17 | * Support for the `W` modifier in the day of month section 18 | * Support for the `L` modifier in the day of week section 19 | * Support for the `#[1-5]` modifier in the day of week section 20 | * Support for the `?` instead of an `*` in day of month section 21 | * Support for the `?` instead of an `*` in day of week section 22 | * Support for whitespaces at the beginning and end of the expression 23 | * Stricter step value checking for each section 24 | 25 | ### Changed 26 | 27 | * Terminology from "crontab interval" to "crontab expression" 28 | * Renamed exception class `InvalidCrontabInterval` to `InvalidExpressionException` 29 | * Class `InvalidExpressionException` now extends `\InvalidArgumentException` instead of `\Exception` 30 | * Fixed `InvalidExpressionException`'s message to match the documentation 31 | * Renamed `CrontabValidator->isIntervalValid()` to `CrontabValidator->isExpressionValid()` 32 | * Renamed `CrontabValidator->guardIntervalIsValid()` to `CrontabValidator->guardExpressionIsValid()` 33 | * Renamed parameters and method names from "interval" to "expression" 34 | 35 | ### Removed 36 | 37 | * Support for PHP 7.0 (Supported versions: PHP >= 7.1) 38 | * Package base exception `CrontabValidatorException` 39 | 40 | ## [1.0.1] - 2017-12-05 41 | 42 | ### Fixed 43 | 44 | * Unit test compatibility with PHPUnit 6+ 45 | * Usage of alias function `join` 46 | 47 | ## [1.0.0] - 2016-08-29 48 | 49 | ### Added 50 | 51 | * Interval validation in boolean and/or guarding manner 52 | 53 | [2.0.1]: https://github.com/hollodotme/crontab-validator/compare/v2.0.0...v2.0.1 54 | [2.0.0]: https://github.com/hollodotme/crontab-validator/compare/v1.0.1...v2.0.0 55 | [1.0.1]: https://github.com/hollodotme/crontab-validator/compare/v1.0.0...v1.0.1 56 | [1.0.0]: https://github.com/hollodotme/crontab-validator/tree/v1.0.0 57 | 58 | [#1]: https://github.com/hollodotme/crontab-validator/issues/1 59 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016-2017 Holger Woltersdorf and contributors 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Build Status](https://travis-ci.org/hollodotme/crontab-validator.svg?branch=master)](https://travis-ci.org/hollodotme/crontab-validator) 2 | [![Coverage Status](https://coveralls.io/repos/github/hollodotme/crontab-validator/badge.svg?branch=master)](https://coveralls.io/github/hollodotme/crontab-validator?branch=master) 3 | [![Latest Stable Version](https://poser.pugx.org/hollodotme/crontab-validator/v/stable)](https://packagist.org/packages/hollodotme/crontab-validator) 4 | [![Total Downloads](https://poser.pugx.org/hollodotme/crontab-validator/downloads)](https://packagist.org/packages/hollodotme/crontab-validator) 5 | [![License](https://poser.pugx.org/hollodotme/crontab-validator/license)](https://packagist.org/packages/hollodotme/crontab-validator) 6 | 7 | # CrontabValidator 8 | 9 | A validator for crontab expressions. 10 | 11 | Sources used to determine the allowed expressions: 12 | 13 | * https://crontab.guru/ 14 | * https://en.wikipedia.org/wiki/Cron 15 | 16 | ## Features 17 | 18 | * Validation of crontab expressions like 6,21,36,51 7-23/1 * FEB-NOV/2 *. 19 | 20 | ## Requirements 21 | 22 | * PHP >= 7.1 23 | 24 | ## Installation 25 | 26 | ``` 27 | composer require "hollodotme/crontab-validator" 28 | ``` 29 | 30 | ## Usage 31 | 32 | ### Boolean validation 33 | 34 | ```php 35 | isExpressionValid( '6,21,36,51 7-23/1 * FEB-NOV/2 *' ) ) 44 | { 45 | echo 'Expression is valid.'; 46 | } 47 | else 48 | { 49 | echo 'Expression is invalid.'; 50 | } 51 | ``` 52 | 53 | ### Guarding 54 | 55 | ```php 56 | All fine, execution continues 68 | $validator->guardExpressionIsValid( '6,21,36,51 7-23/1 * FEB-NOV/2 *' ); 69 | 70 | # => This will raise an InvalidExpressionException 71 | $validator->guardExpressionIsValid( 'this is not a valid interval' ); 72 | } 73 | catch ( InvalidExpressionException $e ) 74 | { 75 | echo $e->getMessage(); 76 | } 77 | ``` 78 | 79 | **Prints:** 80 | 81 | Invalid crontab expression: "this is not a valid interval" 82 | 83 | 84 | --- 85 | 86 | Feedback and contributions welcome! 87 | -------------------------------------------------------------------------------- /build/phpunit.xml: -------------------------------------------------------------------------------- 1 | 10 | 11 | 12 | ../tests/Unit 13 | 14 | 15 | 16 | 17 | ../src 18 | 19 | 20 | ../tests/Unit 21 | ../tests/Unit/Fixtures 22 | ../vendor 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hollodotme/crontab-validator", 3 | "description": "A validator for crontab expressions.", 4 | "minimum-stability": "dev", 5 | "prefer-stable": true, 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Holger Woltersdorf (hollodotme)", 10 | "email": "hw@hollo.me" 11 | } 12 | ], 13 | "support": { 14 | "source": "https://github.com/hollodotme/crontab-validator" 15 | }, 16 | "require": { 17 | "php": ">=7.1" 18 | }, 19 | "require-dev": { 20 | "tm/tooly-composer-script": "^1.0" 21 | }, 22 | "autoload": { 23 | "psr-4": { 24 | "hollodotme\\CrontabValidator\\": "src/" 25 | } 26 | }, 27 | "autoload-dev": { 28 | "psr-4": { 29 | "hollodotme\\CrontabValidator\\Tests\\": "tests/" 30 | } 31 | }, 32 | "scripts": { 33 | "post-install-cmd": "Tooly\\ScriptHandler::installPharTools", 34 | "post-update-cmd": "Tooly\\ScriptHandler::installPharTools" 35 | }, 36 | "extra": { 37 | "tools": { 38 | "phpunit": { 39 | "url": "https://phar.phpunit.de/phpunit-6.5.4.phar", 40 | "only-dev": true 41 | }, 42 | "coveralls": { 43 | "url": "https://github.com/satooshi/php-coveralls/releases/download/v1.0.1/coveralls.phar", 44 | "only-dev": true 45 | } 46 | } 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/CrontabValidator.php: -------------------------------------------------------------------------------- 1 | expressionCheckRegExp = $this->buildExpressionCheckRegExp(); 22 | } 23 | 24 | /** 25 | * @return string 26 | */ 27 | private function buildExpressionCheckRegExp() : string 28 | { 29 | $numbers = [ 30 | 'min' => '[0-5]?\d', 31 | 'hour' => '[01]?\d|2[0-3]', 32 | 'dayOfMonth' => '((0?[1-9]|[12]\d|3[01])W?|L|\?)', 33 | 'month' => '0?[1-9]|1[012]|jan|feb|mar|apr|may|jun|jul|aug|sep|oct|nov|dec', 34 | 'dayOfWeek' => '([0-7]|mon|tue|wed|thu|fri|sat|sun|\?)(L|\#[1-5])?', 35 | ]; 36 | 37 | $steps = [ 38 | 'min' => '(0?[1-9]|[1-5]\d)', 39 | 'hour' => '(0?[1-9]|1\d|2[0-3])', 40 | 'dayOfMonth' => '(0?[1-9]|[12]\d|3[01])', 41 | 'month' => '(0?[1-9]|1[012])', 42 | 'dayOfWeek' => '[1-7]', 43 | ]; 44 | 45 | $sections = []; 46 | foreach ( $numbers as $section => $number ) 47 | { 48 | $step = $steps[ $section ]; 49 | $range = "({$number})(-({$number})(/{$step})?)?"; 50 | $sections[ $section ] = "\*(/{$step})?|{$number}(/{$step})?|{$range}(,{$range})*"; 51 | } 52 | 53 | $joinedSections = '(' . implode( ')\s+(', $sections ) . ')'; 54 | $replacements = '@reboot|@yearly|@annually|@monthly|@weekly|@daily|@midnight|@hourly'; 55 | 56 | return "^\s*({$joinedSections}|({$replacements}))\s*$"; 57 | } 58 | 59 | /** 60 | * @param string $expression 61 | * 62 | * @return bool 63 | */ 64 | public function isExpressionValid( string $expression ) : bool 65 | { 66 | return (bool)preg_match( "#{$this->expressionCheckRegExp}#i", $expression ); 67 | } 68 | 69 | /** 70 | * @param string $expression 71 | * 72 | * @throws InvalidExpressionException 73 | */ 74 | public function guardExpressionIsValid( string $expression ) : void 75 | { 76 | if ( !$this->isExpressionValid( $expression ) ) 77 | { 78 | throw (new InvalidExpressionException())->withExpression( $expression ); 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/Exceptions/InvalidExpressionException.php: -------------------------------------------------------------------------------- 1 | expression; 20 | } 21 | 22 | public function withExpression( string $expression ) : InvalidExpressionException 23 | { 24 | $this->message = sprintf( 'Invalid crontab expression: "%s"', $expression ); 25 | $this->expression = $expression; 26 | 27 | return $this; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /tests/Unit/CrontabValidatorTest.php: -------------------------------------------------------------------------------- 1 | assertTrue( $validator->isExpressionValid( $expression ) ); 28 | 29 | $validator->guardExpressionIsValid( $expression ); 30 | } 31 | 32 | public function validExpressionsProvider() : array 33 | { 34 | return require __DIR__ . '/DataProviders/ValidExpressions.php'; 35 | } 36 | 37 | /** 38 | * @param string $expression 39 | * 40 | * @dataProvider invalidExpressionsProvider 41 | */ 42 | public function testExpressionIsInvalid( string $expression ) : void 43 | { 44 | $validator = new CrontabValidator(); 45 | 46 | $this->assertFalse( $validator->isExpressionValid( $expression ) ); 47 | } 48 | 49 | public function invalidExpressionsProvider() : array 50 | { 51 | return require __DIR__ . '/DataProviders/InvalidExpressions.php'; 52 | } 53 | 54 | /** 55 | * @param string $expression 56 | * 57 | * @dataProvider invalidExpressionsProvider 58 | */ 59 | public function testInvalidExpressionThrowsException( string $expression ) : void 60 | { 61 | try 62 | { 63 | (new CrontabValidator())->guardExpressionIsValid( $expression ); 64 | } 65 | catch ( InvalidExpressionException $e ) 66 | { 67 | $this->assertSame( 'Invalid crontab expression: "' . $expression . '"', $e->getMessage() ); 68 | $this->assertSame( $expression, $e->getExpression() ); 69 | } 70 | } 71 | 72 | public function testExpressionAllowsTheLastModifierInDayOfMonth() : void 73 | { 74 | $validator = new CrontabValidator(); 75 | 76 | $this->assertTrue( $validator->isExpressionValid( '* * L * *' ) ); 77 | $this->assertTrue( $validator->isExpressionValid( '* * 5W,L * *' ) ); 78 | } 79 | 80 | public function testExpressionAllowsTheLastModifierInDayOfWeek() : void 81 | { 82 | $validator = new CrontabValidator(); 83 | 84 | $this->assertTrue( $validator->isExpressionValid( '* * * * 5L' ) ); 85 | $this->assertTrue( $validator->isExpressionValid( '* * * * WEDL' ) ); 86 | $this->assertTrue( $validator->isExpressionValid( '* * * * 3L,5L' ) ); 87 | $this->assertTrue( $validator->isExpressionValid( '* * * * 2L-5L' ) ); 88 | } 89 | 90 | public function testExpressionAllowsTheHashtagModifierInDayOfWeek() : void 91 | { 92 | $validator = new CrontabValidator(); 93 | 94 | $this->assertTrue( $validator->isExpressionValid( '* * * * 5#1' ) ); 95 | $this->assertTrue( $validator->isExpressionValid( '* * * * 4#2' ) ); 96 | $this->assertTrue( $validator->isExpressionValid( '* * * * 3#3' ) ); 97 | $this->assertTrue( $validator->isExpressionValid( '* * * * 2#4' ) ); 98 | $this->assertTrue( $validator->isExpressionValid( '* * * * 1#5' ) ); 99 | $this->assertTrue( $validator->isExpressionValid( '* * * * MON#5' ) ); 100 | $this->assertTrue( $validator->isExpressionValid( '* * * * TUE#4' ) ); 101 | $this->assertTrue( $validator->isExpressionValid( '* * * * WED#3' ) ); 102 | $this->assertTrue( $validator->isExpressionValid( '* * * * thu#2' ) ); 103 | $this->assertTrue( $validator->isExpressionValid( '* * * * Fri#1' ) ); 104 | } 105 | 106 | public function testInvalidHashtagModifierIsNotAllowed() : void 107 | { 108 | $validator = new CrontabValidator(); 109 | 110 | $this->assertFalse( $validator->isExpressionValid( '* * * * 1#6' ) ); 111 | $this->assertFalse( $validator->isExpressionValid( '* * * * Wed#0' ) ); 112 | } 113 | 114 | public function testCanUseQuestionmarkInsteadOfAsteriskForDayOfMonth() : void 115 | { 116 | $validator = new CrontabValidator(); 117 | 118 | $this->assertTrue( $validator->isExpressionValid( '* * ? * *' ) ); 119 | } 120 | 121 | public function testInvalidStepsAreNotAllowedInMinuteSection() : void 122 | { 123 | $validator = new CrontabValidator(); 124 | 125 | # Invalid minute steps 126 | $this->assertFalse( $validator->isExpressionValid( '*/-1 * * * *' ) ); 127 | $this->assertFalse( $validator->isExpressionValid( '*/0 * * * *' ) ); 128 | $this->assertFalse( $validator->isExpressionValid( '*/60 * * * *' ) ); 129 | $this->assertFalse( $validator->isExpressionValid( '*/61 * * * *' ) ); 130 | } 131 | 132 | public function testInvalidStepsAreNotAllowedInHourSection() : void 133 | { 134 | $validator = new CrontabValidator(); 135 | 136 | # Invalid hour steps 137 | $this->assertFalse( $validator->isExpressionValid( '* */-1 * * *' ) ); 138 | $this->assertFalse( $validator->isExpressionValid( '* */0 * * *' ) ); 139 | $this->assertFalse( $validator->isExpressionValid( '* */24 * * *' ) ); 140 | $this->assertFalse( $validator->isExpressionValid( '* */26 * * *' ) ); 141 | } 142 | 143 | public function testInvalidStepsAreNotAllowedInDayOfMonthSection() : void 144 | { 145 | $validator = new CrontabValidator(); 146 | 147 | # Invalid day of month steps 148 | $this->assertFalse( $validator->isExpressionValid( '* * */-1 * *' ) ); 149 | $this->assertFalse( $validator->isExpressionValid( '* * */0 * *' ) ); 150 | $this->assertFalse( $validator->isExpressionValid( '* * */32 * *' ) ); 151 | $this->assertFalse( $validator->isExpressionValid( '* * */35 * *' ) ); 152 | } 153 | 154 | public function testInvalidStepsAreNotAllowedInMonthSection() : void 155 | { 156 | $validator = new CrontabValidator(); 157 | 158 | # Invalid month steps 159 | $this->assertFalse( $validator->isExpressionValid( '* * * */-1 *' ) ); 160 | $this->assertFalse( $validator->isExpressionValid( '* * * */0 *' ) ); 161 | $this->assertFalse( $validator->isExpressionValid( '* * * */13 *' ) ); 162 | $this->assertFalse( $validator->isExpressionValid( '* * * */15 *' ) ); 163 | } 164 | 165 | public function testInvalidStepsAreNotAllowedInDayOfWeekSection() : void 166 | { 167 | $validator = new CrontabValidator(); 168 | 169 | # Invalid day of week steps 170 | $this->assertFalse( $validator->isExpressionValid( '* * * * */-1' ) ); 171 | $this->assertFalse( $validator->isExpressionValid( '* * * * */0' ) ); 172 | $this->assertFalse( $validator->isExpressionValid( '* * * * */8' ) ); 173 | $this->assertFalse( $validator->isExpressionValid( '* * * * */10' ) ); 174 | } 175 | } 176 | -------------------------------------------------------------------------------- /tests/Unit/DataProviders/InvalidExpressions.php: -------------------------------------------------------------------------------- 1 |