[
  {
    "path": ".dockerignore",
    "content": "vendor\n.idea\n.dockerignore\n.git*\n.travis.yml\ncomposer.lock\ndocker-compose.yml\nDockerfile\n"
  },
  {
    "path": ".github/workflows/php.yml",
    "content": "name: PHP Composer\n\non:\n  push:\n    branches: [ master ]\n  pull_request:\n    branches: [ master ]\n\njobs:\n  build:\n\n    runs-on: ubuntu-latest\n\n    steps:\n    - uses: actions/checkout@v2\n      \n    - uses: shivammathur/setup-php@v2\n      with:\n        php-version: '5.4'\n        extensions: mbstring\n\n    - name: Validate composer.json and composer.lock\n      run: composer validate\n\n    - name: Install dependencies\n      run: composer install --prefer-dist --no-progress --no-suggest\n\n    - name: Run test suite\n      run: composer run-script test\n"
  },
  {
    "path": ".gitignore",
    "content": ".idea\nvendor\ncomposer.lock\n"
  },
  {
    "path": ".travis.yml",
    "content": "language: php\n\nphp:\n  - 5.4\n\nenv:\n  global:\n    - DEFAULT_COMPOSER_FLAGS=\"--prefer-dist --no-interaction --no-progress --optimize-autoloader\"\n\ncache:\n  directories:\n    - vendor\n    - $HOME/.composer/cache\n\nbefore_install:\n  - phpenv config-rm xdebug.ini || true\n  - travis_retry composer self-update\n  - composer install $DEFAULT_COMPOSER_FLAGS\n\nscript: vendor/bin/phpunit --verbose\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM composer\n\nFROM php:5.4-cli\n\nRUN apt-get update && \\\n    apt-get install -y \\\n        git \\\n        unzip\n\nRUN docker-php-ext-install mbstring\n\nCOPY --from=composer /usr/bin/composer /usr/bin/composer\n\nRUN composer global require hirak/prestissimo\n\nWORKDIR /app\n\nCOPY composer.json .\n\nRUN composer install --prefer-dist --no-interaction --no-ansi\n\nCOPY . .\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2015 Pavel Agalecky <pavel.agalecky@gmail.com>\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "Makefile",
    "content": "docker_build = (docker-compose build)\n\nbuild:\n\t$(call docker_build)\n\ntest:\n\t$(call docker_build) && docker-compose run app composer run-script test\n"
  },
  {
    "path": "README.md",
    "content": "Schedule extension for Yii2\n===========================\n\nThis extension is the port of Laravel's Schedule component (https://laravel.com/docs/master/scheduling#scheduling-artisan-commands)\n\nInstallation\n------------\n\nThe preferred way to install this extension is through [composer](http://getcomposer.org/download/).\n\nEither run\n\n```\nphp composer.phar require omnilight/yii2-scheduling \"*\"\n```\n\nor add\n\n```json\n\"omnilight/yii2-scheduling\": \"*\"\n```\n\nto the `require` section of your composer.json.\n\nDescription\n-----------\n\nThis project is inspired by the Laravel's Schedule component and tries to bring it's simplicity to the Yii framework.\nQuote from Laravel's documentation:\n\n```\nIn the past, developers have generated a Cron entry for each console command they wished to schedule.\nHowever, this is a headache. Your console schedule is no longer in source control,\nand you must SSH into your server to add the Cron entries. Let's make our lives easier.\n```\n\nAfter installation all you have to do is to put single line into crontab:\n\n```\n* * * * * php /path/to/yii yii schedule/run --scheduleFile=@path/to/schedule.php 1>> /dev/null 2>&1\n```\n\nYou can put your schedule into the `schedule.php` file, or add it withing bootstrapping of your extension or\napplication\n\nSchedule examples\n-----------------\n\nThis extension is support all features of Laravel's Schedule, except environments and maintance mode.\n\n**Scheduling Closures**\n\n```php\n$schedule->call(function()\n{\n    // Do some task...\n\n})->hourly();\n```\n\n**Scheduling Terminal Commands**\n\n```php\n$schedule->exec('composer self-update')->daily();\n```\n\n**Running command of your application**\n\n```php\n$schedule->command('migrate')->cron('* * * * *');\n```\n\n**Frequent Jobs**\n\n```php\n$schedule->command('foo')->everyFiveMinutes();\n\n$schedule->command('foo')->everyTenMinutes();\n\n$schedule->command('foo')->everyThirtyMinutes();\n```\n\n**Daily Jobs**\n\n```php\n$schedule->command('foo')->daily();\n```\n\n**Daily Jobs At A Specific Time (24 Hour Time)**\n\n```php\n$schedule->command('foo')->dailyAt('15:00');\n```\n\n**Twice Daily Jobs**\n\n```php\n$schedule->command('foo')->twiceDaily();\n```\n\n**Job That Runs Every Weekday**\n\n```php\n$schedule->command('foo')->weekdays();\n```\n\n**Weekly Jobs**\n\n```php\n$schedule->command('foo')->weekly();\n\n// Schedule weekly job for specific day (0-6) and time...\n$schedule->command('foo')->weeklyOn(1, '8:00');\n```\n\n**Monthly Jobs**\n\n```php\n$schedule->command('foo')->monthly();\n```\n\n**Job That Runs On Specific Days**\n\n```php\n$schedule->command('foo')->mondays();\n$schedule->command('foo')->tuesdays();\n$schedule->command('foo')->wednesdays();\n$schedule->command('foo')->thursdays();\n$schedule->command('foo')->fridays();\n$schedule->command('foo')->saturdays();\n$schedule->command('foo')->sundays();\n```\n\n**Only Allow Job To Run When Callback Is True**\n\n```php\n$schedule->command('foo')->monthly()->when(function()\n{\n    return true;\n});\n```\n\n**E-mail The Output Of A Scheduled Job**\n\n```php\n$schedule->command('foo')->sendOutputTo($filePath)->emailOutputTo('foo@example.com');\n```\n\n**Preventing Task Overlaps**\n\n```php\n$schedule->command('foo')->withoutOverlapping();\n```\nUsed by default yii\\mutex\\FileMutex or 'mutex' application component (http://www.yiiframework.com/doc-2.0/yii-mutex-mutex.html)\n\n**Running Tasks On One Server**\n\n>To utilize this feature, you must config mutex in the application component, except the FileMutex:  `yii\\mutex\\MysqlMutex`,`yii\\mutex\\PgsqlMutex`,`yii\\mutex\\OracleMutex` or `yii\\redis\\Mutex`. In addition, all servers must be communicating with the same central db/cache server.\n\nBelow shows the redis mutex demo:\n\n```php\n'components' => [\n    'mutex' => [\n        'class' => 'yii\\redis\\Mutex',\n        'redis' => [\n            'hostname' => 'localhost',\n            'port' => 6379,\n            'database' => 0,\n        ]\n    ],\n],\n```\n\n```php\n$schedule->command('report:generate')\n                ->fridays()\n                ->at('17:00')\n                ->onOneServer();\n```\n\nHow to use this extension in your application?\n----------------------------------------------\n\nYou should create the following file under `@console/config/schedule.php` (note: you can create file with any name\nand extension and anywere on your server, simpli ajust the name of the scheduleFile in the command below):\n\n```php\n<?php\n/**\n * @var \\omnilight\\scheduling\\Schedule $schedule\n */\n\n// Place here all of your cron jobs\n\n// This command will execute ls command every five minutes\n$schedule->exec('ls')->everyFiveMinutes();\n\n// This command will execute migration command of your application every hour\n$schedule->command('migrate')->hourly();\n\n// This command will call callback function every day at 10:00\n$schedule->call(function(\\yii\\console\\Application $app) {\n    // Some code here...\n})->dailyAt('10:00');\n\n```\n\nNext your should add the following command to your crontab:\n```\n* * * * * php /path/to/yii yii schedule/run --scheduleFile=@console/config/schedule.php 1>> /dev/null 2>&1\n```\n\nThat's all! Now all your cronjobs will be runned as configured in your schedule.php file.\n\nHow to use this extension in your own extension?\n------------------------------------------------\n\nFirst of all, you should include dependency to the `omnilight\\yii2-scheduling` into your composer.json:\n\n```\n...\n'require': {\n    \"omnilight/yii2-schedule\": \"*\"\n}\n...\n```\n\nNext you should create bootstrapping class for your extension, as described in the http://www.yiiframework.com/doc-2.0/guide-structure-extensions.html#bootstrapping-classes\n\nPlace into your bootstrapping method the following code:\n\n```php\npublic function bootstrap(Application $app)\n{\n    if ($app instanceof \\yii\\console\\Application) {\n        if ($app->has('schedule')) {\n            /** @var omnilight\\scheduling\\Schedule $schedule */\n            $schedule = $app->get('schedule');\n            // Place all your shedule command below\n            $schedule->command('my-extension-command')->dailyAt('12:00');\n        }\n    }\n}\n```\n\nAdd to the README of your extension info for the user to register `schedule` component for the application\nand add `schedule/run` command to the crontab as described upper.\n\nUsing `schedule` component\n--------------------------\n\nYou do not have to use `schedule` component directly or define it in your application if you use schedule only in your application (and do not want to give ability for extensions to register they own cron jobs). But if you what to give extensions ability to register cronjobs, you should define `schedule` component in the application config:\n\n```php\n'schedule' => 'omnilight\\scheduling\\Schedule',\n```\n\nUsing addition functions\n------------------------\n\nIf you want to use `thenPing` method of the Event, you should add the following string to the `composer.json` of your app:\n```\n\"guzzlehttp/guzzle\": \"~5.0\"\n```\n\nNote about timezones\n--------------------\n\nPlease note, that this is PHP extension, so it use timezone defined in php config or in your Yii's configuration file,\nso set them correctly.\n"
  },
  {
    "path": "composer.json",
    "content": "{\n  \"name\": \"omnilight/yii2-scheduling\",\n  \"description\": \"Scheduling extension for Yii2 framework\",\n  \"keywords\": [\n    \"yii\",\n    \"scheduling\",\n    \"cron\"\n  ],\n  \"minimum-stability\": \"dev\",\n  \"authors\": [\n    {\n      \"name\": \"Pavel Agalecky\",\n      \"email\": \"pavel.agalecky@gmail.com\"\n    }\n  ],\n  \"type\": \"yii2-extension\",\n  \"require\": {\n    \"php\": \">=5.4.0\",\n    \"yiisoft/yii2\": \"2.0.*\",\n    \"symfony/process\": \"2.6.* || 3.* || 4.* || ^5.0\",\n    \"dragonmantank/cron-expression\": \"1.*\"\n  },\n  \"require-dev\": {\n    \"phpunit/phpunit\": \"4.8.36\"\n  },\n  \"suggest\": {\n    \"guzzlehttp/guzzle\": \"Required to use the thenPing method on schedules (~5.0).\"\n  },\n  \"autoload\": {\n    \"psr-4\": {\n      \"omnilight\\\\scheduling\\\\\": \"src\"\n    }\n  },\n  \"extra\": {\n    \"bootstrap\": \"omnilight\\\\scheduling\\\\Bootstrap\"\n  },\n  \"scripts\": {\n    \"test\": \"vendor/bin/phpunit\"\n  },\n  \"config\": {\n    \"allow-plugins\": {\n      \"yiisoft/yii2-composer\": true\n    }\n  }\n}\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  app:\n    build: .\n"
  },
  {
    "path": "phpunit.xml.dist",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit backupGlobals=\"false\"\n         backupStaticAttributes=\"false\"\n         beStrictAboutTestsThatDoNotTestAnything=\"false\"\n         bootstrap=\"tests/bootstrap.php\"\n         colors=\"true\"\n         convertErrorsToExceptions=\"true\"\n         convertNoticesToExceptions=\"true\"\n         convertWarningsToExceptions=\"true\"\n         processIsolation=\"false\"\n         stopOnError=\"false\"\n         stopOnFailure=\"false\"\n         verbose=\"true\"\n>\n    <testsuites>\n        <testsuite name=\"omnilight/yii2-scheduling test suite\">\n            <directory suffix=\"Test.php\">./tests</directory>\n        </testsuite>\n    </testsuites>\n</phpunit>"
  },
  {
    "path": "src/Bootstrap.php",
    "content": "<?php\n\nnamespace omnilight\\scheduling;\nuse yii\\base\\BootstrapInterface;\nuse yii\\base\\Application;\nuse yii\\di\\Instance;\n\n\n/**\n * Class Bootstrap\n */\nclass Bootstrap implements BootstrapInterface\n{\n\n    /**\n     * Bootstrap method to be called during application bootstrap stage.\n     * @param Application $app the application currently running\n     */\n    public function bootstrap($app)\n    {\n        if ($app instanceof \\yii\\console\\Application) {\n            if (!isset($app->controllerMap['schedule'])) {\n                $app->controllerMap['schedule'] = 'omnilight\\scheduling\\ScheduleController';\n            }\n        }\n    }\n}"
  },
  {
    "path": "src/CallbackEvent.php",
    "content": "<?php\n\nnamespace omnilight\\scheduling;\nuse Yii;\nuse yii\\base\\Application;\nuse yii\\base\\InvalidParamException;\nuse yii\\mutex\\Mutex;\n\n/**\n * Class CallbackEvent\n */\nclass CallbackEvent extends Event\n{\n    /**\n     * The callback to call.\n     *\n     * @var string\n     */\n    protected $callback;\n    /**\n     * The parameters to pass to the method.\n     *\n     * @var array\n     */\n    protected $parameters;\n\n    /**\n     * Create a new event instance.\n     *\n     * @param Mutex $mutex\n     * @param string $callback\n     * @param array $parameters\n     * @param array $config\n     * @throws InvalidParamException\n     */\n    public function __construct(Mutex $mutex, $callback, array $parameters = [], $config = [])\n    {\n        $this->callback = $callback;\n        $this->parameters = $parameters;\n        $this->_mutex = $mutex;\n\n        if (!empty($config)) {\n            Yii::configure($this, $config);\n        }\n\n        if ( ! is_string($this->callback) && ! is_callable($this->callback))\n        {\n            throw new InvalidParamException(\n                \"Invalid scheduled callback event. Must be string or callable.\"\n            );\n        }\n    }\n\n    /**\n     * Run the given event.\n     *\n     * @param Application $app\n     * @return mixed\n     */\n    public function run(Application $app)\n    {\n        $this->trigger(self::EVENT_BEFORE_RUN);\n        $response = call_user_func_array($this->callback, array_merge($this->parameters, [$app]));\n        parent::callAfterCallbacks($app);\n        $this->trigger(self::EVENT_AFTER_RUN);\n        return $response;\n    }\n\n    /**\n     * Do not allow the event to overlap each other.\n     *\n     * @return $this\n     * @throws InvalidParamException\n     */\n    public function withoutOverlapping()\n    {\n        if (empty($this->_description)) {\n            throw new InvalidParamException(\n                \"A scheduled event name is required to prevent overlapping. Use the 'description' method before 'withoutOverlapping'.\"\n            );\n        }\n\n        return parent::withoutOverlapping();\n    }\n\n    /**\n     * Get the mutex name for the scheduled command.\n     *\n     * @return string\n     */\n    protected function mutexName()\n    {\n        return 'framework/schedule-' . sha1($this->_description);\n    }\n\n    /**\n     * Get the summary of the event for display.\n     *\n     * @return string\n     */\n    public function getSummaryForDisplay()\n    {\n        if (is_string($this->_description)) return $this->_description;\n        return is_string($this->callback) ? $this->callback : 'Closure';\n    }\n\n}\n"
  },
  {
    "path": "src/Event.php",
    "content": "<?php\n\nnamespace omnilight\\scheduling;\n\nuse Cron\\CronExpression;\nuse GuzzleHttp\\Client as HttpClient;\nuse Symfony\\Component\\Process\\Process;\nuse yii\\base\\Application;\nuse yii\\base\\Component;\nuse yii\\base\\InvalidCallException;\nuse yii\\base\\InvalidConfigException;\nuse yii\\mail\\MailerInterface;\nuse yii\\mutex\\Mutex;\nuse yii\\mutex\\FileMutex;\n\n/**\n * Class Event\n */\nclass Event extends Component\n{\n    const EVENT_BEFORE_RUN = 'beforeRun';\n    const EVENT_AFTER_RUN = 'afterRun';\n\n    /**\n     * Command string\n     * @var string\n     */\n    public $command;\n    /**\n     * The cron expression representing the event's frequency.\n     *\n     * @var string\n     */\n    protected $_expression = '* * * * * *';\n    /**\n     * The timezone the date should be evaluated on.\n     *\n     * @var \\DateTimeZone|string\n     */\n    protected $_timezone;\n    /**\n     * The user the command should run as.\n     *\n     * @var string\n     */\n    protected $_user;\n    /**\n     * The filter callback.\n     *\n     * @var \\Closure\n     */\n    protected $_filter;\n    /**\n     * The reject callback.\n     *\n     * @var \\Closure\n     */\n    protected $_reject;\n    /**\n     * The location that output should be sent to.\n     *\n     * @var string\n     */\n    protected $_output = null;\n    /**\n     * The string for redirection.\n     *\n     * @var array\n     */\n    protected $_redirect = ' > ';\n    /**\n     * The array of callbacks to be run after the event is finished.\n     *\n     * @var array\n     */\n    protected $_afterCallbacks = [];\n    /**\n     * The human readable description of the event.\n     *\n     * @var string\n     */\n    protected $_description;\n    /**\n     * The mutex implementation.\n     *\n     * @var \\yii\\mutex\\Mutex\n     */\n    protected $_mutex;\n\n    /**\n     * Decide if errors will be displayed.\n     *\n     * @var bool\n     */\n    protected $_omitErrors = false;\n\n    /**\n     * Create a new event instance.\n     *\n     * @param Mutex $mutex\n     * @param string $command\n     * @param array $config\n     */\n    public function __construct(Mutex $mutex, $command, $config = [])\n    {\n        $this->command = $command;\n        $this->_mutex = $mutex;\n        $this->_output = $this->getDefaultOutput();\n        parent::__construct($config);\n    }\n\n    /**\n     * Run the given event.\n     * @param Application $app\n     */\n    public function run(Application $app)\n    {\n        $this->trigger(self::EVENT_BEFORE_RUN);\n        if (count($this->_afterCallbacks) > 0) {\n            $this->runCommandInForeground($app);\n        } else {\n            $this->runCommandInBackground($app);\n        }\n        $this->trigger(self::EVENT_AFTER_RUN);\n    }\n\n    /**\n     * Get the mutex name for the scheduled command.\n     *\n     * @return string\n     */\n    protected function mutexName()\n    {\n        return 'framework/schedule-' . sha1($this->_expression . $this->command);\n    }\n\n    /**\n     * Run the command in the foreground.\n     *\n     * @param Application $app\n     */\n    protected function runCommandInForeground(Application $app)\n    {\n        $command = trim($this->buildCommand(), '& ');\n        $cwd = dirname($app->request->getScriptFile());\n        if (method_exists(Process::class, 'fromShellCommandline')) {\n            $process = Process::fromShellCommandline($command, $cwd, null, null, null);\n        }\n        else {\n            $process = (new Process($command, $cwd, null, null, null));\n        }\n        $process->run();\n        $this->callAfterCallbacks($app);\n    }\n\n    /**\n     * Build the comand string.\n     *\n     * @return string\n     */\n    public function buildCommand()\n    {\n        $command = $this->command . $this->_redirect . $this->_output . (($this->_omitErrors) ? ' 2>&1 &' : ' &');\n        return $this->_user ? 'sudo -u ' . $this->_user . ' ' . $command : $command;\n    }\n\n    /**\n     * Call all of the \"after\" callbacks for the event.\n     *\n     * @param Application $app\n     */\n    protected function callAfterCallbacks(Application $app)\n    {\n        foreach ($this->_afterCallbacks as $callback) {\n            call_user_func($callback, $app);\n        }\n    }\n\n    /**\n     * Run the command in the background using exec.\n     *\n     * @param Application $app\n     */\n    protected function runCommandInBackground(Application $app)\n    {\n        chdir(dirname($app->request->getScriptFile()));\n        exec($this->buildCommand());\n    }\n\n    /**\n     * Determine if the given event should run based on the Cron expression.\n     *\n     * @param Application $app\n     * @return bool\n     */\n    public function isDue(Application $app)\n    {\n        return $this->expressionPasses() && $this->filtersPass($app);\n    }\n\n    /**\n     * Determine if the Cron expression passes.\n     *\n     * @return bool\n     */\n    protected function expressionPasses()\n    {\n        $date = new \\DateTime('now');\n        if ($this->_timezone) {\n            $date->setTimezone($this->_timezone);\n        }\n        return CronExpression::factory($this->_expression)->isDue($date);\n    }\n\n    /**\n     * Determine if the filters pass for the event.\n     *\n     * @param Application $app\n     * @return bool\n     */\n    protected function filtersPass(Application $app)\n    {\n        if ($this->_filter && !call_user_func($this->_filter, $app) ||\n            $this->_reject && call_user_func($this->_reject, $app)\n        ) {\n            return false;\n        }\n        return true;\n    }\n\n    /**\n     * Schedule the event to run hourly.\n     *\n     * @return $this\n     */\n    public function hourly()\n    {\n        return $this->cron('0 * * * * *');\n    }\n\n    /**\n     * The Cron expression representing the event's frequency.\n     *\n     * @param  string $expression\n     * @return $this\n     */\n    public function cron($expression)\n    {\n        $this->_expression = $expression;\n        return $this;\n    }\n\n    /**\n     * Schedule the event to run daily.\n     *\n     * @return $this\n     */\n    public function daily()\n    {\n        return $this->cron('0 0 * * * *');\n    }\n\n    /**\n     * Schedule the command at a given time.\n     *\n     * @param  string $time\n     * @return $this\n     */\n    public function at($time)\n    {\n        return $this->dailyAt($time);\n    }\n\n    /**\n     * Schedule the event to run daily at a given time (10:00, 19:30, etc).\n     *\n     * @param  string $time\n     * @return $this\n     */\n    public function dailyAt($time)\n    {\n        $segments = explode(':', $time);\n        return $this->spliceIntoPosition(2, (int)$segments[0])\n            ->spliceIntoPosition(1, count($segments) == 2 ? (int)$segments[1] : '0');\n    }\n\n    /**\n     * Splice the given value into the given position of the expression.\n     *\n     * @param  int $position\n     * @param  string $value\n     * @return Event\n     */\n    protected function spliceIntoPosition($position, $value)\n    {\n        $segments = explode(' ', $this->_expression);\n        $segments[$position - 1] = $value;\n        return $this->cron(implode(' ', $segments));\n    }\n\n    /**\n     * Schedule the event to run twice daily.\n     *\n     * @return $this\n     */\n    public function twiceDaily()\n    {\n        return $this->cron('0 1,13 * * * *');\n    }\n\n    /**\n     * Schedule the event to run only on weekdays.\n     *\n     * @return $this\n     */\n    public function weekdays()\n    {\n        return $this->spliceIntoPosition(5, '1-5');\n    }\n\n    /**\n     * Schedule the event to run only on Mondays.\n     *\n     * @return $this\n     */\n    public function mondays()\n    {\n        return $this->days(1);\n    }\n\n    /**\n     * Set the days of the week the command should run on.\n     *\n     * @param  array|int $days\n     * @return $this\n     */\n    public function days($days)\n    {\n        $days = is_array($days) ? $days : func_get_args();\n        return $this->spliceIntoPosition(5, implode(',', $days));\n    }\n\n    /**\n     * Schedule the event to run only on Tuesdays.\n     *\n     * @return $this\n     */\n    public function tuesdays()\n    {\n        return $this->days(2);\n    }\n\n    /**\n     * Schedule the event to run only on Wednesdays.\n     *\n     * @return $this\n     */\n    public function wednesdays()\n    {\n        return $this->days(3);\n    }\n\n    /**\n     * Schedule the event to run only on Thursdays.\n     *\n     * @return $this\n     */\n    public function thursdays()\n    {\n        return $this->days(4);\n    }\n\n    /**\n     * Schedule the event to run only on Fridays.\n     *\n     * @return $this\n     */\n    public function fridays()\n    {\n        return $this->days(5);\n    }\n\n    /**\n     * Schedule the event to run only on Saturdays.\n     *\n     * @return $this\n     */\n    public function saturdays()\n    {\n        return $this->days(6);\n    }\n\n    /**\n     * Schedule the event to run only on Sundays.\n     *\n     * @return $this\n     */\n    public function sundays()\n    {\n        return $this->days(0);\n    }\n\n    /**\n     * Schedule the event to run weekly.\n     *\n     * @return $this\n     */\n    public function weekly()\n    {\n        return $this->cron('0 0 * * 0 *');\n    }\n\n    /**\n     * Schedule the event to run weekly on a given day and time.\n     *\n     * @param  int $day\n     * @param  string $time\n     * @return $this\n     */\n    public function weeklyOn($day, $time = '0:0')\n    {\n        $this->dailyAt($time);\n        return $this->spliceIntoPosition(5, $day);\n    }\n\n    /**\n     * Schedule the event to run monthly.\n     *\n     * @return $this\n     */\n    public function monthly()\n    {\n        return $this->cron('0 0 1 * * *');\n    }\n\n    /**\n     * Schedule the event to run yearly.\n     *\n     * @return $this\n     */\n    public function yearly()\n    {\n        return $this->cron('0 0 1 1 * *');\n    }\n\n    /**\n     * Schedule the event to run every minute.\n     *\n     * @return $this\n     */\n    public function everyMinute()\n    {\n        return $this->cron('* * * * * *');\n    }\n\n    /**\n     * Schedule the event to run every N minutes.\n     *\n     * @param int|string $minutes\n     * @return $this\n     */\n    public function everyNMinutes($minutes)\n    {\n        return $this->cron('*/'.$minutes.' * * * * *');\n    }\n\n    /**\n     * Schedule the event to run every five minutes.\n     *\n     * @return $this\n     */\n    public function everyFiveMinutes()\n    {\n        return $this->everyNMinutes(5);\n    }\n\n    /**\n     * Schedule the event to run every ten minutes.\n     *\n     * @return $this\n     */\n    public function everyTenMinutes()\n    {\n        return $this->everyNMinutes(10);\n    }\n\n    /**\n     * Schedule the event to run every thirty minutes.\n     *\n     * @return $this\n     */\n    public function everyThirtyMinutes()\n    {\n        return $this->cron('0,30 * * * * *');\n    }\n\n    /**\n     * Set the timezone the date should be evaluated on.\n     *\n     * @param  \\DateTimeZone|string $timezone\n     * @return $this\n     */\n    public function timezone($timezone)\n    {\n        $this->_timezone = $timezone;\n        return $this;\n    }\n\n    /**\n     * Set which user the command should run as.\n     *\n     * @param  string $user\n     * @return $this\n     */\n    public function user($user)\n    {\n        $this->_user = $user;\n        return $this;\n    }\n\n    /**\n     * Set if errors should be displayed\n     *\n     * @param  bool $omitErrors\n     * @return $this\n     */\n    public function omitErrors($omitErrors = false)\n    {\n        $this->_omitErrors = $omitErrors;\n        return $this;\n    }\n\n    /**\n     * Do not allow the event to overlap each other.\n     *\n     * @return $this\n     */\n    public function withoutOverlapping()\n    {\n        return $this->then(function() {\n            $this->_mutex->release($this->mutexName());\n        })->skip(function() {\n            return !$this->_mutex->acquire($this->mutexName());\n        });\n    }\n\n    /**\n     * Allow the event to only run on one server for each cron expression.\n     *\n     * @return $this\n     */\n    public function onOneServer()\n    {\n        if ($this->_mutex instanceof FileMutex) {\n            throw new InvalidConfigException('You must config mutex in the application component, except the FileMutex.');\n        }\n\n        return $this->withoutOverlapping();\n    }\n\n    /**\n     * Register a callback to further filter the schedule.\n     *\n     * @param  \\Closure $callback\n     * @return $this\n     */\n    public function when(\\Closure $callback)\n    {\n        $this->_filter = $callback;\n        return $this;\n    }\n\n    /**\n     * Register a callback to further filter the schedule.\n     *\n     * @param  \\Closure $callback\n     * @return $this\n     */\n    public function skip(\\Closure $callback)\n    {\n        $this->_reject = $callback;\n        return $this;\n    }\n\n    /**\n     * Send the output of the command to a given location.\n     *\n     * @param  string $location\n     * @return $this\n     */\n    public function sendOutputTo($location)\n    {\n        $this->_redirect = ' > ';\n        $this->_output = $location;\n        return $this;\n    }\n\n    /**\n     * Append the output of the command to a given location.\n     *\n     * @param  string $location\n     * @return $this\n     */\n    public function appendOutputTo($location)\n    {\n        $this->_redirect = ' >> ';\n        $this->_output = $location;\n        return $this;\n    }\n\n    /**\n     * E-mail the results of the scheduled operation.\n     *\n     * @param  array $addresses\n     * @return $this\n     *\n     * @throws \\LogicException\n     */\n    public function emailOutputTo($addresses)\n    {\n        if (is_null($this->_output) || $this->_output == $this->getDefaultOutput()) {\n            throw new InvalidCallException(\"Must direct output to a file in order to e-mail results.\");\n        }\n        $addresses = is_array($addresses) ? $addresses : func_get_args();\n        return $this->then(function (Application $app) use ($addresses) {\n            $this->emailOutput($app->mailer, $addresses);\n        });\n    }\n\n    /**\n     * Register a callback to be called after the operation.\n     *\n     * @param  \\Closure $callback\n     * @return $this\n     */\n    public function then(\\Closure $callback)\n    {\n        $this->_afterCallbacks[] = $callback;\n        return $this;\n    }\n\n    /**\n     * E-mail the output of the event to the recipients.\n     *\n     * @param MailerInterface $mailer\n     * @param  array $addresses\n     */\n    protected function emailOutput(MailerInterface $mailer, $addresses)\n    {\n        $textBody = file_get_contents($this->_output);\n\n        if (trim($textBody) != '' ) {\n            $mailer->compose()\n                ->setTextBody($textBody)\n                ->setSubject($this->getEmailSubject())\n                ->setTo($addresses)\n                ->send();\n        }\n    }\n\n    /**\n     * Get the e-mail subject line for output results.\n     *\n     * @return string\n     */\n    protected function getEmailSubject()\n    {\n        if ($this->_description) {\n            return 'Scheduled Job Output (' . $this->_description . ')';\n        }\n        return 'Scheduled Job Output';\n    }\n\n    /**\n     * Register a callback to the ping a given URL after the job runs.\n     *\n     * @param  string $url\n     * @return $this\n     */\n    public function thenPing($url)\n    {\n        return $this->then(function () use ($url) {\n            (new HttpClient)->get($url);\n        });\n    }\n\n    /**\n     * Set the human-friendly description of the event.\n     *\n     * @param  string $description\n     * @return $this\n     */\n    public function description($description)\n    {\n        $this->_description = $description;\n        return $this;\n    }\n\n    /**\n     * Get the summary of the event for display.\n     *\n     * @return string\n     */\n    public function getSummaryForDisplay()\n    {\n        if (is_string($this->_description)) return $this->_description;\n        return $this->buildCommand();\n    }\n\n    /**\n     * Get the Cron expression for the event.\n     *\n     * @return string\n     */\n    public function getExpression()\n    {\n        return $this->_expression;\n    }\n\n    public function getDefaultOutput()\n    {\n        if (strtoupper(substr(PHP_OS, 0, 3)) === 'WIN') {\n            return 'NUL';\n        } else {\n            return '/dev/null';\n        }\n    }\n}\n"
  },
  {
    "path": "src/Schedule.php",
    "content": "<?php\n\nnamespace omnilight\\scheduling;\n\nuse Yii;\nuse yii\\base\\Component;\nuse yii\\base\\Application;\nuse yii\\mutex\\FileMutex;\n\n\n/**\n * Class Schedule\n */\nclass Schedule extends Component\n{\n    /**\n     * All of the events on the schedule.\n     *\n     * @var Event[]\n     */\n    protected $_events = [];\n\n    /**\n     * The mutex implementation.\n     *\n     * @var \\yii\\mutex\\Mutex\n     */\n    protected $_mutex;\n\n    /**\n     * @var string The name of cli script\n     */\n    public $cliScriptName = 'yii';\n\n    /**\n     * Schedule constructor.\n     * @param array $config\n     */\n    public function __construct(array $config = [])\n    {\n        $this->_mutex = Yii::$app->has('mutex') ? Yii::$app->get('mutex') : (new FileMutex());\n\n        parent::__construct($config);\n    }\n\n    /**\n     * Add a new callback event to the schedule.\n     *\n     * @param  string  $callback\n     * @param  array   $parameters\n     * @return Event\n     */\n    public function call($callback, array $parameters = array())\n    {\n        $this->_events[] = $event = new CallbackEvent($this->_mutex, $callback, $parameters);\n        return $event;\n    }\n    /**\n     * Add a new cli command event to the schedule.\n     *\n     * @param  string  $command\n     * @return Event\n     */\n    public function command($command)\n    {\n        return $this->exec(PHP_BINARY . ' ' . $this->cliScriptName . ' ' . $command);\n    }\n\n    /**\n     * Add a new command event to the schedule.\n     *\n     * @param  string  $command\n     * @return Event\n     */\n    public function exec($command)\n    {\n        $this->_events[] = $event = new Event($this->_mutex, $command);\n        return $event;\n    }\n\n    public function getEvents()\n    {\n        return $this->_events;\n    }\n\n    /**\n     * Get all of the events on the schedule that are due.\n     *\n     * @param \\yii\\base\\Application $app\n     * @return Event[]\n     */\n    public function dueEvents(Application $app)\n    {\n        return array_filter($this->_events, function(Event $event) use ($app)\n        {\n            return $event->isDue($app);\n        });\n    }\n}\n"
  },
  {
    "path": "src/ScheduleController.php",
    "content": "<?php\n\nnamespace omnilight\\scheduling;\nuse yii\\console\\Controller;\nuse yii\\di\\Instance;\n\n\n/**\n * Run the scheduled commands\n */\nclass ScheduleController extends Controller\n{\n    /**\n     * @var Schedule\n     */\n    public $schedule = 'schedule';\n    /**\n     * @var string Schedule file that will be used to run schedule\n     */\n    public $scheduleFile;\n\n    /**\n     * @var bool set to true to avoid error output\n     */\n    public $omitErrors = false;\n\n    public function options($actionID)\n    {\n        return array_merge(parent::options($actionID),\n            $actionID == 'run' ? ['scheduleFile', 'omitErrors'] : []\n        );\n    }\n\n\n    public function init()\n    {\n        if (\\Yii::$app->has($this->schedule)) {\n            $this->schedule = Instance::ensure($this->schedule, Schedule::className());\n        } else {\n            $this->schedule = \\Yii::createObject(Schedule::className());\n        }\n        parent::init();\n    }\n\n\n    public function actionRun()\n    {\n        $this->importScheduleFile();\n\n        $events = $this->schedule->dueEvents(\\Yii::$app);\n\n        foreach ($events as $event) {\n            $event->omitErrors($this->omitErrors);\n            $this->stdout('Running scheduled command: '.$event->getSummaryForDisplay().\"\\n\");\n            $event->run(\\Yii::$app);\n        }\n\n        if (count($events) === 0)\n        {\n            $this->stdout(\"No scheduled commands are ready to run.\\n\");\n        }\n    }\n\n    protected function importScheduleFile()\n    {\n        if ($this->scheduleFile === null) {\n            return;\n        }\n\n        $scheduleFile = \\Yii::getAlias($this->scheduleFile);\n        if (file_exists($scheduleFile) == false) {\n            $this->stderr('Can not load schedule file '.$this->scheduleFile.\"\\n\");\n            return;\n        }\n\n        $schedule = $this->schedule;\n        call_user_func(function() use ($schedule, $scheduleFile) {\n            include $scheduleFile;\n        });\n    }\n}"
  },
  {
    "path": "tests/EventTest.php",
    "content": "<?php\n\nnamespace omnilight\\scheduling\\Tests;\n\nuse omnilight\\scheduling\\Event;\nuse yii\\mutex\\Mutex;\n\nclass EventTest extends \\PHPUnit_Framework_TestCase\n{\n    public function buildCommandData()\n    {\n        return [\n            [false, 'php -i', '/dev/null', 'php -i > /dev/null'],\n            [false, 'php -i', '/my folder/foo.log', 'php -i > /my folder/foo.log'],\n            [true, 'php -i', '/dev/null', 'php -i > /dev/null 2>&1 &'],\n            [true, 'php -i', '/my folder/foo.log', 'php -i > /my folder/foo.log 2>&1 &'],\n        ];\n    }\n\n    /**\n     * @dataProvider buildCommandData\n     * @param bool $omitErrors\n     * @param string $command\n     * @param string $outputTo\n     * @param string $result\n     */\n    public function testBuildCommandSendOutputTo($omitErrors, $command, $outputTo, $result)\n    {\n        $event = new Event($this->getMock(Mutex::className()), $command);\n        $event->omitErrors($omitErrors);\n        $event->sendOutputTo($outputTo);\n        $this->assertSame($result, $event->buildCommand());\n    }\n}\n"
  },
  {
    "path": "tests/bootstrap.php",
    "content": "<?php\n\nrequire __DIR__ . '/../vendor/autoload.php';\n\ndate_default_timezone_set('UTC');\n"
  }
]