[
  {
    "path": ".editorconfig",
    "content": "root = true\n\n[*]\nend_of_line = lf\ninsert_final_newline = true\nindent_style = space\nindent_size = 4\ncharset = utf-8\n"
  },
  {
    "path": ".gitattributes",
    "content": "* text=auto\n\n/tests export-ignore\n.gitattributes export-ignore\n.gitignore export-ignore\n.editorconfig export-ignore\nREADME.md export-ignore\nLICENSE.md export-ignore\nCHANGELOG.md export-ignore\nUPGRADE.md export-ignore\n.travis.yml export-ignore\nappveyor.yml export-ignore\nphpunit.xml export-ignore\n/.github export-ignore\n.php-cs-fixer.dist.php export-ignore\ndocker-compose.yml export-ignore\n/docker export-ignore\nphpstan.neon export-ignore\ncomposer-install.php export-ignore\nbootstrap.php export-ignore\n/resources/docs export-ignore\nphpstan-baseline.neon export-ignore\n"
  },
  {
    "path": ".github/FUNDING.yml",
    "content": "github: PabloKowalczyk\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/1_Bug_report.md",
    "content": "---\nname: \"\\U0001F41B Bug Report\"\nabout: Report errors and problems\n\n---\n\n**Crunz version**: x.y.z\n\n**PHP version**: x.y.z\n\n**Operating system type and version**:\n\n**Description**  \n<!-- A clear and concise description of the problem. -->\n\n**How to reproduce**  \n<!-- Code and/or config needed to reproduce the problem. -->\n\n**Possible Solution**  \n<!--- Optional: only if you have suggestions on a fix/reason for the bug -->\n\n**Additional context**  \n<!-- Optional: any other context about the problem: log messages, screenshots, etc. -->\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/2_Feature_request.md",
    "content": "---\nname: \"\\U0001F680 Feature Request\"\nabout: RFC and ideas for new features and improvements\n\n---\n\n**Description**  \n<!-- A clear and concise description of the new feature. -->\n\n**Example**  \n<!-- A simple example of the new feature in action (include PHP code, etc.)\n     If the new feature changes an existing feature, include a simple before/after comparison. -->\n"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "| Q             | A\n| ------------- | ---\n| Fixed tickets | #...   <!-- #-prefixed issue number(s), if any -->\n\n<!--\nWrite a short README entry for your feature/bugfix here (replace this comment block.)\nThis will help people understand your PR and can be used as a start of the Doc PR.\n-->\n"
  },
  {
    "path": ".github/workflows/php.yaml",
    "content": "name: PHP\n\non:\n    pull_request:\n        branches:\n            - '3.9'\n            - '3.10'\n    push: null\n\npermissions: {}\n\nconcurrency:\n    group: '${{ github.workflow }}-${{ github.ref }}'\n    cancel-in-progress: true\n\njobs:\n    tests:\n        name: '${{ matrix.php }} / Symfony ${{ matrix.symfony_version }} / ${{ matrix.dependencies }} / ${{ matrix.os }}'\n        strategy:\n            matrix:\n                os:\n                    - 'ubuntu-22.04'\n                php:\n                    - '8.2'\n                    - '8.3'\n                    - '8.4'\n                    - '8.5'\n                dependencies:\n                    - 'lowest'\n                    - 'highest'\n                symfony_version:\n                    - '~6.4.0'\n                    - '~7.4.0'\n                    - '~8.0.0'\n                include:\n                    - os: 'windows-2022'\n                      php: '8.2'\n                      dependencies: 'highest'\n                      symfony_version: '~6.4.0'\n                exclude:\n                    - os: 'ubuntu-22.04'\n                      php: '8.2'\n                      dependencies: 'lowest'\n                      symfony_version: '~8.0.0'\n                    - os: 'ubuntu-22.04'\n                      php: '8.2'\n                      dependencies: 'highest'\n                      symfony_version: '~8.0.0'\n                    - os: 'ubuntu-22.04'\n                      php: '8.3'\n                      dependencies: 'lowest'\n                      symfony_version: '~8.0.0'\n                    - os: 'ubuntu-22.04'\n                      php: '8.3'\n                      dependencies: 'highest'\n                      symfony_version: '~8.0.0'\n        runs-on: ${{ matrix.os }}\n\n        steps:\n            -   uses: actions/checkout@v4\n\n            -   uses: shivammathur/setup-php@v2\n                with:\n                    php-version: ${{ matrix.php }}\n                    coverage: none\n\n            -   id: symfony_packages\n                shell: bash\n                run: |\n                    jq --raw-output '\"with_string=\" + ([\"--with=\" + ((.\"require\" + .\"require-dev\") | keys[] | select(contains(\"symfony/\"))) + \":${{ matrix.symfony_version }}\"] | join(\" \"))' composer.json \\\n                        >>\"$GITHUB_OUTPUT\"\n\n            -   uses: ramsey/composer-install@v3\n                with:\n                    dependency-versions: ${{ matrix.dependencies }}\n                    composer-options: ${{ steps.symfony_packages.outputs.with_string }}\n\n            -   run: composer exec -- phpunit --testsuite EndToEnd\n\n            -   run: composer exec -- phpunit --testsuite Integration\n\n            -   run: composer exec -- phpunit --testsuite Unit\n\n    static_analysis:\n        name: Static analysis\n        strategy:\n            matrix:\n                include:\n                    - php: '8.2'\n                      symfony_version: '~6.4.0'\n                      dependencies: 'highest'\n        runs-on: ubuntu-22.04\n\n        steps:\n            -   uses: actions/checkout@v4\n\n            -   uses: shivammathur/setup-php@v2\n                with:\n                    php-version: ${{ matrix.php }}\n                    coverage: none\n\n            -   id: symfony_packages\n                shell: bash\n                run: |\n                    jq --raw-output '\"with_string=\" + ([\"--with=\" + ((.\"require\" + .\"require-dev\") | keys[] | select(contains(\"symfony/\"))) + \":${{ matrix.symfony_version }}\"] | join(\" \"))' composer.json \\\n                        >>\"$GITHUB_OUTPUT\"\n\n            -   uses: ramsey/composer-install@v3\n                with:\n                    dependency-versions: ${{ matrix.dependencies }}\n                    composer-options: ${{ steps.symfony_packages.outputs.with_string }}\n\n            -   run: composer normalize --dry-run\n\n            -   uses: actions/cache@v4\n                with:\n                    path: .php-cs-fixer.cache\n                    key: php-cs-fixer-cache\n\n            -   uses: actions/cache@v4\n                with:\n                    path: /tmp/phpstan\n                    key: phpstan-cache\n\n            -   run: composer run crunz:analyze\n"
  },
  {
    "path": ".gitignore",
    "content": "/vendor\n/tasks\ncomposer.phar\n/composer.lock\n.DS_Store\n*.log\n.idea/\nvar/\n.php-cs-fixer.cache\n/crunz.phar\n/crunz.yml\n/.phpunit.result.cache\n"
  },
  {
    "path": ".php-cs-fixer.dist.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse PhpCsFixer\\Runner\\Parallel\\ParallelConfigFactory;\n\nreturn (new PhpCsFixer\\Config())\n    ->setParallelConfig(ParallelConfigFactory::detect())\n    ->setRules([\n        '@Symfony' => true,\n        'array_syntax' => ['syntax' => 'short'],\n        'protected_to_private' => false,\n        'combine_consecutive_unsets' => true,\n        'combine_consecutive_issets' => true,\n        'compact_nullable_type_declaration' => true,\n        'declare_strict_types' => true,\n        'dir_constant' => true,\n        'ereg_to_preg' => true,\n        'explicit_indirect_variable' => true,\n        'explicit_string_variable' => true,\n        'function_to_constant' => true,\n        'is_null' => true,\n        'modernize_types_casting' => true,\n        'linebreak_after_opening_tag' => true,\n        'list_syntax' => ['syntax' => 'short'],\n        'mb_str_functions' => true,\n        'native_function_invocation' => [\n            'include' => ['@all'],\n        ],\n        'no_alias_functions' => true,\n        'no_homoglyph_names' => true,\n        'no_php4_constructor' => true,\n        'no_useless_else' => true,\n        'no_useless_return' => true,\n        'ordered_class_elements' => true,\n        'ordered_imports' => true,\n        'php_unit_construct' => true,\n        'php_unit_dedicate_assert' => true,\n        'php_unit_expectation' => true,\n        'php_unit_mock' => true,\n        'php_unit_namespaced' => true,\n        'php_unit_method_casing' => ['case' => 'snake_case'],\n        'random_api_migration' => true,\n        'strict_comparison' => true,\n        'strict_param' => true,\n        'ternary_to_null_coalescing' => true,\n        'void_return' => true,\n        'concat_space' => [\n            'spacing' => 'one',\n        ],\n        'single_line_throw' => false,\n        'php_unit_test_case_static_method_calls' => [\n            'call_type' => 'self',\n        ],\n    ])\n    ->setRiskyAllowed(true)\n    ->setFinder(\n        PhpCsFixer\\Finder::create()\n            ->in(__DIR__ . '/src')\n            ->in(__DIR__ . '/tests')\n            ->in(__DIR__ . '/config')\n            ->append(\n                [\n                    __FILE__,\n                    __DIR__ . '/composer-install.php',\n                ]\n            )\n    )\n;\n"
  },
  {
    "path": "CHANGELOG.md",
    "content": "# Changelog\nAll notable changes to this project will be documented in this file.\n\nThe format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)\nand this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).\n\n## Unreleased\n\n## [v3.7.0] - 2024-08-12\n\n### Changed\n\n- [crunzphp#77] Require at least Symfony 6.4\n- [crunzphp#79] Update PHPCSFixer\n- [crunzphp#78] Allow Symfony v7\n\n## [v3.6.0] - 2023-11-10\n\n### Added\n\n- [crunzphp#68] Getter for 'from', 'to' event's configuration, thanks to [@lucatacconi]\n\n## [v3.5.1] - 2023-11-03\n\n### Fixed\n\n- [crunzphp#62] Task Life Time functions don't respect the timezone\n\n## [v3.5.0] - 2023-10-15\n\n### Added\n\n- [crunzphp#39] Add PHP v8.2 support\n- [crunzphp#64] Test Symfony v6.3\n- [crunzphp#66] Add PHP v8.3 support\n\n### Removed\n\n- [crunzphp#38] Drop PHP v7.4 support\n- [crunzphp#54] Drop Symfony v6.0 support\n- [crunzphp#63] Drop Symfony v6.1 support\n- [crunzphp#65] Drop Symfony v6.2 support\n\n## [v3.4.1] - 2022-11-01\n\n### Fixed\n\n- [crunzphp#44] Do not require BlockingStoreInterface in preventOverlapping()\n\n## [v3.4.0] - 2022-10-03\n\n### Added\n\n- [crunzphp#31] Add format option to `schedule:list`\n\n## [v3.3.0] - 2022-06-19\n\n### Deprecated\n\n- [crunzphp#24] Deprecate non string parameters\n\n## [v3.2.2] - 2022-05-28\n\n### Fixed\n\n- [crunzphp#5] Fix Symfony 6.1 deprecations\n\n## [v3.2.1] - 2022-01-09\n\n### Fixed\n\n- [#401] Fix version number\n\n## [v3.2.0] - 2022-01-09\n\n### Added\n\n- [#398] Add hourlyAt()\n- [#390] Symfony 6 Support, thanks to [@bashgeek]\n\n### Changed\n\n- [#396] Remove `opis/closure` and use `laravel/serializable-closure` instead\n\n### Removed\n\n- [#393] Drop Symfony 5.2 support\n- [#394] Drop Symfony 5.3 support\n\n## [v3.1.0] - 2021-11-24\n\n### Changed\n\n- [#385] Replace `Swiftmailer` with `symfony/mailer`\n\n## [v3.0.1] - 2021-05-25\n\n### Fixed\n\n- [#361] Log to specific event log file, thanks to [@drjayvee]\n\n## [v2.3.1] - 2021-05-25\n\n### Fixed\n\n- [#361] Log to specific event log file, thanks to [@drjayvee]\n\n## [v3.0.0] - 2021-04-25\n\n### Changed\n\n- [#349] Require at least PHP v7.4\n- [#356] Require package \"dragonmantank/cron-expression\" at least \"v3.1\"\n\n### Removed\n\n- [#351] Drop Symfony v3.4 support\n- [#352] Drop Symfony v5.1 support\n- [#354] Remove most \"Crunz\\Event::every*\" methods\n\n## [v2.3.0] - 2021-03-14\n\n### Deprecated\n\n- [#344] Deprecate most \"Event::every*\" methods\n\n### Removed\n\n- [#323] Drop Symfony 4.3 support\n- [#324] Drop Symfony 5.0 support\n\n## [v2.2.4] - 2020-12-18\n\n### Fixed\n\n- [#333] Include symlinks in Finder, thanks to [@iluuu1994]\n\n## [v2.2.3] - 2020-11-29\n\n### Fixed\n\n- [#334] Fix disabling logger not working\n\n## [v2.2.2] - 2020-10-12\n\n### Fixed\n\n- [#326] Fix lock key on closures\n\n## [v2.2.1] - 2020-10-08\n\n### Fixed\n\n- [#321] Add PHP8 support\n\n## [v2.2.0] - 2020-06-18\n\n### Added\n\n- [#287] Add `task:debug` command\n- [#233] Add option to ignore empty context in monolog, thanks to [@rrushton]\n- [#298] Add `logger_factory` config option\n\n### Removed\n\n- [#292] Drop Symfony 4.2 support\n\n## [v2.1.0] - 2020-02-02\n\n### Added\n\n- [#274] Symfony 5 support\n\n### Changed\n\n- [#240] cron-expression package, thanks to [@mareksuscak]\n- [#280] Hide `closure:run` command\n\n## [v2.0.4] - 2019-12-08\n\n### Fixed\n\n- [#268] Fix Symfony 4.4 deprecations\n\n## [v1.12.4] - 2019-12-08\n\n### Fixed\n\n- [#268] Fix Symfony 4.4 deprecations\n\n## [v2.0.3] - 2019-11-17\n\n### Fixed\n\n- [#261] Release lock on error\n- [#264] Revert converting closure result to `int`\n\n## [v1.12.3] - 2019-11-17\n\n### Fixed\n\n- [#261] Release lock on error\n\n## [v2.0.2] - 2019-10-06\n\n### Fixed\n\n- [#251] Update PHPUnit to avoid PHP7.4 deprecations \n\n## [v1.12.2] - 2019-10-05\n\n### Fixed\n\n- [#243] Sandbox task loading\n- [#245] Fix PHP 7.4 compatibility\n\n## [v2.0.1] - 2019-05-10\n\n### Fixed\n\n- [#229] Fix recursive tasks scan\n\n## [v1.12.1] - 2019-05-01\n\n### Fixed\n\n- [#229] Fix recursive tasks scan\n\n## [v2.0.0] - 2019-04-24\n\n### Changed\n\n- [#101] Throw exception on empty timezone\n- [#204] More than five parts cron expressions will throw exception\n- [#221] Throw `Crunz\\Task\\WrongTaskInstanceException` when task is not `Schedule` instance\n- [#222] Make `\\Crunz\\Event::setProcess` private\n- [#225] Bump dependencies\n\n### Removed\n\n- [#103] Removed `Crunz\\Output\\VerbosityAwareOutput` class\n- [#206] Remove legacy paths recognition\n- [#224] Remove `mail` transport\n\n## [v1.12.0] - 2019-04-07\n\n### Added\n\n- [#178], [#217] `timezone_log` configuration option to decide whether\nconfigured `timezone` should be used for logs, thanks to [@SadeghPM]\n\n### Deprecated\n\n- Using `\\Crunz\\Event::setProcess` is deprecated, this method was intended to be `private`,\nbut for some reason is `public`.\nIn `v2.0` this method will became private and result in exception if you call it.\n- [#199] Not returning `\\Crunz\\Schedule` instance from your task is deprecated.\nIn `v2` this will result in exception.\n\n## [v1.11.2] - 2019-03-16\n\n### Fixed\n\n- [#209], [#210] Composer installs crunz executable to vendor/bin instead of symlink\n\n## [v1.11.1] - 2019-01-27\n\n### Fixed\n\n- [#190] Fix Crunz bin path when running closures\n\n## [v1.11.0] - 2019-01-24\n\n### Fixed\n\n- [#181] Fix missing tasks source\n- [#180] Fix deprecation messages not showing\n\n### Deprecated\n\n- Relying on tasks' source/config file recognition related to Crunz bin \n\n## [v1.11.0-rc.1] - 2018-12-22\n\n### Fixed\n\n- [#171] Fix lock storage bug\n- [#173] Remove Symfony 4.2 deprecations\n- [#166] Improve task collection debugging\n\n## [v1.11.0-beta.2] - 2018-11-10\n\n### Fixed\n\n- [#162] Fix command error output [closes [#161]]\n\n## [v1.11.0-beta.1] - 2018-10-23\n\n### Added\n\n- [#153] Add support for `symfony/lock`, Thanks to [@digilist]\n\n### Fixed\n\n- [#146] Make paths relative to current working directory - \"cwd\".\n- [#158] Accept only string task number.\n\n## [v1.10.1] - 2018-09-22\n\n### Fixed\n\n- [#139] Do not require `cURL` extension\n\n## [v1.10.0] - 2018-09-22\n\n### Fixed\n\n- [#137] Treat whole output of failed command as \"error output\".\n\n### Removed\n\n- [#136] Remove guzzle\n\n## [v1.9.0] - 2018-08-18\n\n### Changed\n\n- [#132] Improved container caching in shared servers\n\n### Fixed\n\n- [#131] Crunz can be used with `dragonmantank/cron-expression` package\n\n### Deprecated\n\n- Passing more than five parts (e.g `* * * * * *`) to `Crunz\\Event::cron()`\n\n## [v1.8.0] - 2018-08-15\n\n### Added\n\n- [#120] Added `--force` option to `schedule:run` command\n- [#129] Add `--task` option for `schedule:run` command\n\n### Fixed\n\n- [#123] Spellfix: `comand` -> `command`, Thanks to [@FallDi]\n\n## [v1.7.3] - 2018-06-15\n\n- [#118] Undefined index: year in `vendor/lavary/crunz/src/Event.php` on line 370, Thanks to [@mindcreations]\n\n## [v1.7.2] - 2018-06-13\n\n### Fixed\n\n- [#116] Do not replace Symfony's polyfills.\n\n## [v1.7.1] - 2018-06-01\n\n### Fixed\n\n- [#110] Fixed config file path guessing.\n\n## [v1.7.0] - 2018-05-27\n\n### Added\n\n- [#94] Added timezone option\n\n### Deprecated\n- `timezone` option in config file is now required, lack of it will result in Exception in version `2.0`\n\n### Removed\n\n- [#104] Remove splitCamel helper.\n\n## [v1.6.1] - 2018-05-13\n\n### Fixed\n\n- [#90] Send output by email only if it is not empty.\n\n## [v1.6.0] - 2018-04-22\n\n### Added\n\n- [#69] Option for allowing line breaks in logs, Thanks to [@TomasDuda]\n- [#79] Introduce DI container\n\n### Fixed\n\n- [#43] Typos stopping email transport of 'mail', Thanks to [@m-hume]\n- [#46] sendOutputTo and appendOutputTo fix, Thanks to [@m-hume]\n- [#80] Fixed prevent overlapping on windows\n- [#81] Fix Event::in on windows\n- [#84] Make comparing date segments strict.\n- [#86] Fix closure running on windows\n- [#85] Fix changing user\n- [#87] Remove error handler\n\n## [v1.5.1] - 2018-04-12\n\n### Added\n\n- [#76] Introduce editorconfig\n- [#75] Added changelog file.\n\n### Fixed\n\n- [#77] Fix high cpu usage\n\n[#401]: https://github.com/lavary/crunz/pull/401\n[#398]: https://github.com/lavary/crunz/pull/398\n[#396]: https://github.com/lavary/crunz/pull/396\n[#394]: https://github.com/lavary/crunz/pull/394\n[#393]: https://github.com/lavary/crunz/pull/393\n[#390]: https://github.com/lavary/crunz/pull/390\n[#385]: https://github.com/lavary/crunz/pull/385\n[#361]: https://github.com/lavary/crunz/pull/361\n[#356]: https://github.com/lavary/crunz/pull/356\n[#354]: https://github.com/lavary/crunz/pull/354\n[#352]: https://github.com/lavary/crunz/pull/352\n[#351]: https://github.com/lavary/crunz/pull/351\n[#349]: https://github.com/lavary/crunz/pull/349\n[#344]: https://github.com/lavary/crunz/pull/344\n[#334]: https://github.com/lavary/crunz/pull/334\n[#333]: https://github.com/lavary/crunz/pull/333\n[#326]: https://github.com/lavary/crunz/pull/326\n[#324]: https://github.com/lavary/crunz/pull/324\n[#323]: https://github.com/lavary/crunz/pull/323\n[#321]: https://github.com/lavary/crunz/pull/321\n[#298]: https://github.com/lavary/crunz/pull/298\n[#292]: https://github.com/lavary/crunz/pull/292\n[#287]: https://github.com/lavary/crunz/pull/287\n[#280]: https://github.com/lavary/crunz/pull/280\n[#274]: https://github.com/lavary/crunz/pull/274\n[#268]: https://github.com/lavary/crunz/pull/268\n[#264]: https://github.com/lavary/crunz/pull/264\n[#261]: https://github.com/lavary/crunz/pull/261\n[#251]: https://github.com/lavary/crunz/pull/251\n[#245]: https://github.com/lavary/crunz/pull/245\n[#243]: https://github.com/lavary/crunz/pull/243\n[#240]: https://github.com/lavary/crunz/pull/240\n[#233]: https://github.com/lavary/crunz/pull/233\n[#229]: https://github.com/lavary/crunz/pull/229\n[#225]: https://github.com/lavary/crunz/pull/225\n[#224]: https://github.com/lavary/crunz/pull/224\n[#222]: https://github.com/lavary/crunz/pull/222\n[#221]: https://github.com/lavary/crunz/pull/221\n[#217]: https://github.com/lavary/crunz/pull/217\n[#210]: https://github.com/lavary/crunz/pull/210\n[#209]: https://github.com/lavary/crunz/pull/209\n[#206]: https://github.com/lavary/crunz/pull/206\n[#204]: https://github.com/lavary/crunz/pull/204\n[#199]: https://github.com/lavary/crunz/pull/199\n[#190]: https://github.com/lavary/crunz/pull/190\n[#181]: https://github.com/lavary/crunz/pull/181\n[#180]: https://github.com/lavary/crunz/pull/180\n[#178]: https://github.com/lavary/crunz/pull/178\n[#173]: https://github.com/lavary/crunz/pull/173  \n[#171]: https://github.com/lavary/crunz/pull/171\n[#166]: https://github.com/lavary/crunz/pull/166\n[#164]: https://github.com/lavary/crunz/pull/164\n[#163]: https://github.com/lavary/crunz/pull/163\n[#162]: https://github.com/lavary/crunz/pull/162\n[#161]: https://github.com/lavary/crunz/pull/161\n[#159]: https://github.com/lavary/crunz/pull/159\n[#158]: https://github.com/lavary/crunz/pull/158\n[#157]: https://github.com/lavary/crunz/pull/157\n[#155]: https://github.com/lavary/crunz/pull/155\n[#154]: https://github.com/lavary/crunz/pull/154\n[#153]: https://github.com/lavary/crunz/pull/153\n[#151]: https://github.com/lavary/crunz/pull/151\n[#150]: https://github.com/lavary/crunz/pull/150\n[#149]: https://github.com/lavary/crunz/pull/149\n[#148]: https://github.com/lavary/crunz/pull/148\n[#147]: https://github.com/lavary/crunz/pull/147\n[#146]: https://github.com/lavary/crunz/pull/146\n[#142]: https://github.com/lavary/crunz/pull/142\n[#141]: https://github.com/lavary/crunz/pull/141\n[#140]: https://github.com/lavary/crunz/pull/140\n[#139]: https://github.com/lavary/crunz/pull/139\n[#138]: https://github.com/lavary/crunz/pull/138\n[#137]: https://github.com/lavary/crunz/pull/137\n[#136]: https://github.com/lavary/crunz/pull/136\n[#133]: https://github.com/lavary/crunz/pull/133\n[#132]: https://github.com/lavary/crunz/pull/132\n[#131]: https://github.com/lavary/crunz/pull/131\n[#130]: https://github.com/lavary/crunz/pull/130\n[#129]: https://github.com/lavary/crunz/pull/129\n[#123]: https://github.com/lavary/crunz/pull/123\n[#120]: https://github.com/lavary/crunz/pull/120\n[#119]: https://github.com/lavary/crunz/pull/119\n[#118]: https://github.com/lavary/crunz/pull/118\n[#117]: https://github.com/lavary/crunz/pull/117\n[#116]: https://github.com/lavary/crunz/pull/116\n[#113]: https://github.com/lavary/crunz/pull/113\n[#112]: https://github.com/lavary/crunz/pull/112\n[#111]: https://github.com/lavary/crunz/pull/111\n[#110]: https://github.com/lavary/crunz/pull/110\n[#109]: https://github.com/lavary/crunz/pull/109\n[#107]: https://github.com/lavary/crunz/pull/107\n[#105]: https://github.com/lavary/crunz/pull/105\n[#104]: https://github.com/lavary/crunz/pull/104\n[#103]: https://github.com/lavary/crunz/pull/103\n[#102]: https://github.com/lavary/crunz/pull/102\n[#101]: https://github.com/lavary/crunz/pull/101\n[#100]: https://github.com/lavary/crunz/pull/100\n[#98]: https://github.com/lavary/crunz/pull/98\n[#97]: https://github.com/lavary/crunz/pull/97\n[#96]: https://github.com/lavary/crunz/pull/96\n[#95]: https://github.com/lavary/crunz/pull/95\n[#94]: https://github.com/lavary/crunz/pull/94\n[#92]: https://github.com/lavary/crunz/pull/92\n[#90]: https://github.com/lavary/crunz/pull/90\n[#89]: https://github.com/lavary/crunz/pull/89\n[#88]: https://github.com/lavary/crunz/pull/88\n[#87]: https://github.com/lavary/crunz/pull/87\n[#86]: https://github.com/lavary/crunz/pull/86\n[#85]: https://github.com/lavary/crunz/pull/85\n[#84]: https://github.com/lavary/crunz/pull/84\n[#82]: https://github.com/lavary/crunz/pull/82\n[#81]: https://github.com/lavary/crunz/pull/81\n[#80]: https://github.com/lavary/crunz/pull/80\n[#79]: https://github.com/lavary/crunz/pull/79\n[#77]: https://github.com/lavary/crunz/pull/77\n[#76]: https://github.com/lavary/crunz/pull/76\n[#75]: https://github.com/lavary/crunz/pull/75\n[#74]: https://github.com/lavary/crunz/pull/74\n[#73]: https://github.com/lavary/crunz/pull/73\n[#72]: https://github.com/lavary/crunz/pull/72\n[#69]: https://github.com/lavary/crunz/pull/69\n[#50]: https://github.com/lavary/crunz/pull/50\n[#46]: https://github.com/lavary/crunz/pull/46\n[#43]: https://github.com/lavary/crunz/pull/43\n[#36]: https://github.com/lavary/crunz/pull/36\n[#25]: https://github.com/lavary/crunz/pull/25\n[#24]: https://github.com/lavary/crunz/pull/24\n[#23]: https://github.com/lavary/crunz/pull/23\n[#17]: https://github.com/lavary/crunz/pull/17\n[#16]: https://github.com/lavary/crunz/pull/16\n[crunzphp#5]: https://github.com/crunzphp/crunz/pull/5\n[crunzphp#24]: https://github.com/crunzphp/crunz/pull/24\n[crunzphp#31]: https://github.com/crunzphp/crunz/pull/31\n[crunzphp#38]: https://github.com/crunzphp/crunz/pull/38\n[crunzphp#39]: https://github.com/crunzphp/crunz/pull/39\n[crunzphp#44]: https://github.com/crunzphp/crunz/pull/44\n[crunzphp#54]: https://github.com/crunzphp/crunz/pull/54\n[crunzphp#62]: https://github.com/crunzphp/crunz/pull/62\n[crunzphp#63]: https://github.com/crunzphp/crunz/pull/63\n[crunzphp#64]: https://github.com/crunzphp/crunz/pull/64\n[crunzphp#65]: https://github.com/crunzphp/crunz/pull/65\n[crunzphp#66]: https://github.com/crunzphp/crunz/pull/66\n[crunzphp#68]: https://github.com/crunzphp/crunz/pull/68\n[crunzphp#77]: https://github.com/crunzphp/crunz/pull/77\n[crunzphp#78]: https://github.com/crunzphp/crunz/pull/78\n[crunzphp#79]: https://github.com/crunzphp/crunz/pull/79\n[v1.5.1]: https://github.com/crunzphp/crunz/compare/v1.5.0...v1.5.1\n[v1.6.0]: https://github.com/crunzphp/crunz/compare/v1.5.1...v1.6.0\n[v1.6.1]: https://github.com/crunzphp/crunz/compare/v1.6.0...v1.6.1\n[v1.7.0]: https://github.com/crunzphp/crunz/compare/v1.6.1...v1.7.0\n[v1.7.1]: https://github.com/crunzphp/crunz/compare/v1.7.0...v1.7.1\n[v1.7.2]: https://github.com/crunzphp/crunz/compare/v1.7.1...v1.7.2\n[v1.7.3]: https://github.com/crunzphp/crunz/compare/v1.7.2...v1.7.3\n[v1.8.0]: https://github.com/crunzphp/crunz/compare/v1.7.3...v1.8.0\n[v1.9.0]: https://github.com/crunzphp/crunz/compare/v1.8.0...v1.9.0\n[v1.10.0]: https://github.com/crunzphp/crunz/compare/v1.9.0...v1.10.0\n[v1.10.1]: https://github.com/crunzphp/crunz/compare/v1.10.0...v1.10.1\n[v1.11.0-beta.1]: https://github.com/crunzphp/crunz/compare/v1.10.1...v1.11.0-beta.1\n[v1.11.0-beta.2]: https://github.com/crunzphp/crunz/compare/v1.11.0-beta.1...v1.11.0-beta.2\n[v1.11.0-rc.1]: https://github.com/crunzphp/crunz/compare/v1.11.0-beta.2...v1.11.0-rc.1\n[v1.11.0]: https://github.com/crunzphp/crunz/compare/v1.11.0-rc.1...v1.11.0\n[v1.11.1]: https://github.com/crunzphp/crunz/compare/v1.11.0...v1.11.1\n[v1.11.2]: https://github.com/crunzphp/crunz/compare/v1.11.1...v1.11.2\n[v1.12.0]: https://github.com/crunzphp/crunz/compare/v1.11.2...v1.12.0\n[v1.12.1]: https://github.com/crunzphp/crunz/compare/v1.12.0...v1.12.1\n[v1.12.2]: https://github.com/crunzphp/crunz/compare/v1.12.1...v1.12.2\n[v1.12.3]: https://github.com/crunzphp/crunz/compare/v1.12.2...v1.12.3\n[v1.12.4]: https://github.com/crunzphp/crunz/compare/v1.12.3...v1.12.4\n[v2.0.0]: https://github.com/crunzphp/crunz/compare/v1.12.0...v2.0.0\n[v2.0.1]: https://github.com/crunzphp/crunz/compare/v2.0.0...v2.0.1\n[v2.0.2]: https://github.com/crunzphp/crunz/compare/v2.0.1...v2.0.2\n[v2.0.3]: https://github.com/crunzphp/crunz/compare/v2.0.2...v2.0.3\n[v2.0.4]: https://github.com/crunzphp/crunz/compare/v2.0.3...v2.0.4\n[v2.1.0]: https://github.com/crunzphp/crunz/compare/v2.0.4...v2.1.0\n[v2.2.0]: https://github.com/crunzphp/crunz/compare/v2.1.0...v2.2.0\n[v2.2.1]: https://github.com/crunzphp/crunz/compare/v2.2.0...v2.2.1\n[v2.2.2]: https://github.com/crunzphp/crunz/compare/v2.2.1...v2.2.2\n[v2.2.3]: https://github.com/crunzphp/crunz/compare/v2.2.2...v2.2.3\n[v2.2.4]: https://github.com/crunzphp/crunz/compare/v2.2.3...v2.2.4\n[v2.3.0]: https://github.com/crunzphp/crunz/compare/v2.2.4...v2.3.0\n[v2.3.1]: https://github.com/crunzphp/crunz/compare/v2.3.0...v2.3.1\n[v3.0.0]: https://github.com/crunzphp/crunz/compare/v2.3.1...v3.0.0\n[v3.0.1]: https://github.com/crunzphp/crunz/compare/v3.0.0...v3.0.1\n[v3.1.0]: https://github.com/crunzphp/crunz/compare/v3.0.1...v3.1.0\n[v3.2.0]: https://github.com/crunzphp/crunz/compare/v3.1.0...v3.2.0\n[v3.2.1]: https://github.com/crunzphp/crunz/compare/v3.2.0...v3.2.1\n[v3.2.2]: https://github.com/crunzphp/crunz/compare/v3.2.1...v3.2.2\n[v3.3.0]: https://github.com/crunzphp/crunz/compare/v3.2.2...v3.3.0\n[v3.4.0]: https://github.com/crunzphp/crunz/compare/v3.3.0...v3.4.0\n[v3.4.1]: https://github.com/crunzphp/crunz/compare/v3.4.0...v3.4.1\n[v3.5.0]: https://github.com/crunzphp/crunz/compare/v3.4.1...v3.5.0\n[v3.5.1]: https://github.com/crunzphp/crunz/compare/v3.5.0...v3.5.1\n[v3.6.0]: https://github.com/crunzphp/crunz/compare/v3.5.1...v3.6.0\n[v3.7.0]: https://github.com/crunzphp/crunz/compare/v3.6.0...v3.7.0\n[@andrewmy]: https://github.com/andrewmy\n[@arthurbarros]: https://github.com/arthurbarros\n[@bashgeek]: https://github.com/bashgeek\n[@codermarcel]: https://github.com/codermarcel\n[@digilist]: https://github.com/digilist\n[@drjayvee]: https://github.com/drjayvee\n[@erfan723]: https://github.com/erfan723\n[@FallDi]: https://github.com/FallDi\n[@iluuu1994]: https://github.com/iluuu1994\n[@jhoughtelin]: https://github.com/jhoughtelin\n[@lucatacconi]: https://github.com/lucatacconi\n[@m-hume]: https://github.com/m-hume\n[@mareksuscak]: https://github.com/mareksuscak\n[@mindcreations]: https://github.com/mindcreations\n[@PhilETaylor]: https://github.com/PhilETaylor\n[@radarhere]: https://github.com/radarhere\n[@rrushton]: https://github.com/rrushton\n[@SadeghPM]: https://github.com/SadeghPM\n[@timurbakarov]: https://github.com/timurbakarov\n[@TomasDuda]: https://github.com/TomasDuda\n[@vinkla]: https://github.com/vinkla\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2018 Moe Reza Lavarian\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": "sh-php:\n\tdocker compose exec --user=www-data php82 sh\n"
  },
  {
    "path": "README.md",
    "content": "# Crunz needs your funding 💲\n\n## Support further Crunz development by [GitHub](https://github.com/sponsors/PabloKowalczyk).\n\nCheck [more info](https://github.com/crunzphp/crunz/issues/111).\n\n# Crunz\n\nInstall a cron job once and for all, manage the rest from the code.\n\nCrunz is a framework-agnostic package to schedule periodic tasks (cron jobs) in PHP using a fluent API.\n\nCrunz is capable of executing any kind of executable command as well as PHP closures.\n\n[![Version](http://img.shields.io/packagist/v/crunzphp/crunz.svg?style=flat-square)](https://packagist.org/packages/crunzphp/crunz)\n[![Packagist](https://img.shields.io/packagist/dt/crunzphp/crunz.svg?style=flat-square)](https://packagist.org/packages/crunzphp/crunz/stats)\n[![Packagist](https://img.shields.io/packagist/dm/crunzphp/crunz.svg?style=flat-square)](https://packagist.org/packages/crunzphp/crunz/stats)\n\n## Roadmap\n| Version | Release date | Active support until | Bug support until | Status         |\n|---------|--------------|----------------------|-------------------|----------------|\n| v1.x    | April 2016   | April 2019           | April 2020        | End of life    |\n| v2.x    | April 2019   | April 2021           | April 2022        | End of life    |\n| v3.x    | April 2021   | TBD                  | TBD               | Active support |\n| v4.x    | TBD          | TBD                  | TBD               | Development    |\n\n## Installation\n\nTo install it:\n\n```bash\ncomposer require crunzphp/crunz\n```\nIf the installation is successful, a command-line utility named **crunz** is symlinked to the `vendor/bin` directory of your project.\n\n## How It Works?\n\nThe idea is very simple: instead of installing cron jobs in a crontab file, we define them in one or several PHP files, by using the Crunz interface. \n\nHere's a basic example:\n\n```php\n<?php\n// tasks/backupTasks.php\n\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n$task = $schedule->run('cp project project-bk');       \n$task->daily();\n\nreturn $schedule;\n```\n\nTo run the tasks, you only need to install an ordinary cron job (a crontab entry) which runs **every minute**, and delegates the responsibility to Crunz' event runner:\n\n```bash\n* * * * * cd /project && vendor/bin/crunz schedule:run\n```\n\nThe command `schedule:run` is responsible for collecting all the PHP task files and run the tasks which are due.\n\n## Task Files\n\nTask files resemble crontab files. Just like crontab files they can contain one or more tasks.\n\nNormally we create our task files in the `tasks/` directory within the project's root directory. \n\n> By default, Crunz assumes all the task files reside in the `tasks/` directory within the project's root directory.\n\nThere are two ways to specify the source directory: 1) Configuration file  2) As a parameter to the event runner command.\n \nWe can explicitly set the source path by passing it to the event runner as a parameter:\n\n```bash\n* * * * * cd /project && vendor/bin/crunz schedule:run /path/to/tasks/directory\n```\n\n### Creating a Simple Task\n\nIn the terminal, change the directory to your project's root directory and run the following commands:\n\n```bash\nmkdir tasks && cd tasks\nnano GeneralTasks.php\n```\n\nThen, add a task as below:\n\n```php\n<?php\n// tasks/FirstTasks.php\n\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n\n$task = $schedule->run('cp project project-bk'); \n$task\n    ->daily()\n    ->description('Create a backup of the project directory.');\n\n// ...\n\n// IMPORTANT: You must return the schedule object\nreturn $schedule; \n```\n\nThere are some conventions for creating a task file, which you need to follow. First of all, the filename should end with `Tasks.php` unless we change this via the configuration settings.  \n\nIn addition to that, we **must** return the instance of `Schedule` class at the end of each file, otherwise, all the tasks inside the file will be skipped by the event runner.\n\nSince Crunz scans the tasks directory recursively, we can either put all the tasks in one file or across different files (or directories) based on their usage. This behavior helps us have a well organized tasks directory.\n\n\n## The Command\n\nWe can run **any** command or script by using `run()`. This method accepts two arguments:  **the command to be executed**, and **the command options** (as an associative array) if there's any.\n\n### Normal Command or Script\n\n```php\n<?php\n\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n$task = $schedule->run(PHP_BINARY . ' backup.php', ['--destination' => 'path/to/destination']);\n$task\n    ->everyMinute()\n    ->description('Copying the project directory');\n\nreturn $schedule;\n```\n\nIn the above example, `--destination` is an option supported by `backup.php` script.\n\n### Closures\n\nWe can also write to a closure instead of a command:\n\n```php\n<?php\n\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n\n$x = 12;\n$task = $schedule->run(function() use ($x) { \n   // Do some cool stuff in here \n});\n\n$task\n    ->everyMinute()\n    ->description('Copying the project directory');\n\nreturn $schedule;\n```\n\n## Frequency of Execution\n\nThere are a variety of ways to specify **when** and **how often** a task should run. We can combine these methods together to get our desired frequencies. \n\n### Units of Time\n\nThere are a group of methods which specify a unit of time (bigger than minute) as frequency. They usually end with `ly` suffix, as in `hourly()`, `daily()`, `weekly`, `monthly()`, `quarterly()`, and  `yearly` .\n\nAll the events scheduled with this set of methods happen at the **beginning** of that time unit. For example `weekly()` will run the event on Sundays, and `monthly()` will run on the first day of each month.\n\nThe task below will run **daily at midnight** (start of the daily time period).\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' backup.php');    \n$task->daily();\n// ...\n```\n\nHere's another one, which runs on the **first day of each month**.\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task->monthly();\n// ...\n```\n\n### Running Events at Certain Times\n\nTo schedule a one-off tasks, you may use `on()` method like this:\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' email.php'); \n$task->on('13:30 2016-03-01');\n// ...\n```\n\nThe above task will run on the first of march 2016 at 01:30 pm. \n\n> `On()` accepts any date format parsed by PHP's [strtotime](http://php.net/manual/en/function.strtotime.php) function.\n\nTo specify the time of a task we use `at()` method:\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' email.php'); \n$task\n    ->daily()\n    ->at('13:30');\n// ...\n```\n\nIf we only pass a time to the `on()` method, it will have the same effect as using `at()`\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' email.php');   \n$task\n    ->daily()\n    ->on('13:30');\n         \n// is the sames as\n$task = $schedule->run(PHP_BINARY . ' email.php');       \n$task\n    ->daily()\n    ->at('13:30');\n// ...\n```\n\nWe can combine the \"Unit of Time\" methods eg. daily(), monthly() with the at() or on() constraint in a single statement if we wish.\n\nThe following task will be run every hour at the 15th minute\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' feedmecookie.php'); \n$task\n    ->hourlyAt('15');\n// ...\n```\n>hourlyOn('15') could have been used instead of hourlyAt('15') with the same result\n\nThe following task will be run Monday at 13:30\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' startofwork.php'); \n$task\n    ->weeklyOn(1,'13:30');\n// ...\n```\n>Sunday is considered day 0 of the week. \n\nIf we wished for the task to run on Tuesday (day 2 of the week) at 09:00 we would have used:\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' startofwork.php'); \n$task\n    ->weeklyOn(2,'09:00');\n// ...\n```\n\n## Task Life Time\n\nIn a crontab entry, we can not easily specify a task's lifetime (the period of time when the task is active). However, it's been made easy in Crunz:\n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->from('12:30 2016-03-04')\n    ->to('04:55 2016-03-10');\n //       \n```\nOr alternatively we can use the `between()` method to accomplish the same result:\n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->between('12:30 2016-03-04', '04:55 2016-03-10');\n\n //       \n```\n\nIf we don't specify the date portion, the task will be active **every** day but only within the specified duration:\n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n     ->everyFiveMinutes()\n     ->between('12:30', '04:55');\n\n //       \n```\n\nThe above task runs **every five minutes** between **12:30 pm** and **4:55 pm** every day.\n\nAn example of restricting a task from running only during a certain range of minutes each hour can be achieved as follows:\n\n```php\n<?php\n//\n\n$hour = date('H');\n$startminute = $hour.':05';\n$endminute = $hour.':15';\n\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n     ->hourly()\n     ->between($startminute, $endminute);\n\n //       \n```\n\nThe above task runs **every hour** between **minutes 5 to 15**\n\n### Weekdays\n\nCrunz also provides a set of methods which specify a certain day in the week. \n* `mondays()`\n* `tuesdays()`\n* `wednesdays()`\n* `thursdays()`\n* `fridays()`\n* `saturdays()`\n* `sundays()`\n* `weekdays()`\n\nThese methods have been designed to be used as a constraint and should not be used alone. The reason is that weekday methods just modify the `Day of Week` field of a cron job expression.\n\nConsider the following example:\n\n```php\n<?php\n// Cron equivalent:  * * * * 1\n$task = $schedule->run(PHP_BINARY . ' startofwork.php');\n$task->mondays();\n```\n\nAt first glance, the task seems to run **every Monday**, but since it only modifies the \"day of week\" field of the cron job expression, the task  runs **every minute on Mondays**.\n\nThis is the correct way of using weekday methods:\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' startofwork.php');\n$task    \n    ->mondays()\n    ->at('13:30');\n\n// ...\n```\n>(An easier to read alternative with a similar result ->weeklyOn(0,'13:30') to that shown in a previously example above)\n\n\n### The Classic Way\n\nWe can also do the scheduling the old way, just like we do in a crontab file:\n\n```php\n<?php\n\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task->cron('30 12 * 5-6,9 Mon,Fri');\n```\n\n### Setting Individual Fields\n\nCrunz's methods are not limited to the ready-made methods explained. We can also set individual fields to compose custom frequencies similar to how a classic crontab would composed them. These methods either accept an array of values, or list arguments separated by commas:\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task       \n    ->minute(['1-30', 45, 55])\n    ->hour('1-5', 7, 8)\n    ->dayOfMonth(12, 15)\n    ->month(1);\n```\n\nOr:\n\n```php\n<?php\n// ...\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->minute('30')\n    ->hour('13')\n    ->month([1,2])\n    ->dayofWeek('Mon', 'Fri', 'Sat');\n\n// ...\n```\n\nBased on our use cases, we can choose and combine the proper set of methods, which are easier to use.\n\n## Running Conditions\n\nAnother thing that we cannot do very easily in a traditional crontab file is to make conditions for running the tasks. This has been made easy by `when()` and `skip()` methods.\n\nConsider the following code:\n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->between('12:30 2016-03-04', '04:55 2016-03-10')\n    ->when(function() {\n        if ((bool) (time() % 2)) {\n            return true;\n        }\n        \n        return false;\n    });\n```\n\nMethod `when()` accepts a callback,  which must return `TRUE` for the task to run. This is really useful when we need to check our resources before performing a resource-hungry task.\n\nWe can also skip a task under certain conditions, by using `skip()` method. If the passed callback returns `TRUE`, the task will be skipped.\n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->between('12:30 2016-03-04', '04:55 2016-03-10')\n    ->skip(function() {\n        if ((bool) (time() % 2)) {\n            return true;\n        }\n        \n        return false;  \n    });\n\n //       \n```\n\nWe can use these methods **several** times for a single task. They are evaluated sequentially.\n\n## Changing Directories\n\nYou can use the `in()` method to change directory before running a command:\n\n```php\n<?php\n\n// ...\n\n$task = $schedule->run('./deploy.sh');\n$task\n    ->in('/home')\n    ->weekly()\n    ->sundays()\n    ->at('12:30')\n    ->appendOutputTo('/var/log/backup.log');\n\n// ...\n\nreturn $schedule;\n```\n\n## Parallelism and the Locking Mechanism\n\nCrunz runs the scheduled events in parallel (in separate processes), so all the events which have the same frequency of execution, will run at the same time asynchronously. To achieve this, Crunz utilizes the [symfony/Process](http://symfony.com/doc/current/components/process.html) library for running the tasks in sub-processes.\n\nIf the execution of a task lasts until the next interval or even beyond that, we say that the same instances of a task are overlapping. In some cases, this is a not a problem. But there are times, when these tasks are modifying database data or files, which might cause unexpected behaviors, or even data loss.\n\nTo prevent critical tasks from overlapping each other, Crunz provides a locking mechanism through `preventOverlapping()` method, which, ensures no task runs if the previous instance is already running. \n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->preventOverlapping();\n //       \n```\n\nBy default, crunz uses file based locking (if no parameters are passed to `preventOverlapping`). For alternative lock mechanisms, crunz uses the [symfony/lock](https://symfony.com/doc/current/components/lock.html) component that provides lock mechanisms with various stores. To use this component, you can pass a store to the `preventOverlapping()` method. In the following example, the file based `FlockStore` is used to provide an alternative lock file path.\n\n```php\n<?php\n\nuse Symfony\\Component\\Lock\\Store\\FlockStore;\n\n$store = new FlockStore(__DIR__ . '/locks');\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->preventOverlapping($store);\n\n```\n\n## Keeping the Output\n\nCron jobs usually have outputs, which is normally emailed to the owner of the crontab file, or the user(s) set by the `MAILTO` environment variable inside the crontab file.\n\nWe can also redirect the standard output to a physical file using `>` or `>>` operators:\n\n```bash\n* * * * * /command/to/run >> /var/log/crons/cron.log\n```\n\nThis kind of output logging has been automated in Crunz. To automatically send each event's output to a log file, we can set `log_output` and `output_log_file` options in the configuration file accordingly:\n\n```yaml\n# Configuration settings\n\n## ...\nlog_output:      true\noutput_log_file: /var/log/crunz.log\n## ...\n```\n\nThis will send the events' output (if executed successfully) to `/var/log/crunz.log` file. However, we need to make sure we are permitted to write to the respective file.\n\nIf we need to log the outputs on an event-basis, we can use `appendOutputTo()` or `sendOutputTo()` methods like this:\n\n```php\n<?php\n//\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->appendOutputTo('/var/log/crunz/emails.log');\n\n //       \n```\n\nMethod `appendOutputTo()` **appends** the output to the specified file. To override the log file with new data after each run, we use `saveOutputTo()` method.\n\nIt is also possible to send the errors as emails to a group of recipients by setting `email_output` and `mailer` settings in the configuration file.\n\n## Error Handling\n\nCrunz makes error handling easy by logging and also allowing you add a set of callbacks in case of an error.\n\n## Error Callbacks\n\nYou can set as many callbacks as needed to run in case of an error:\n\n```php\n<?php\n\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n\n$task = $schedule->run('command/to/run');\n$task->everyFiveMinutes();\n\n$schedule\n->onError(function() {\n   // Send mail\n})\n->onError(function() {\n   // Do something else\n});\n\nreturn $schedule;\n```\n\nIf there's an error the two defined callbacks will be executed.\n\n## Error Logging\n\nTo log the possible errors during each run, we can set `log_error` and `error_log_file` settings in the configuration file as below:\n\n```yaml\n# Configuration settings\n\n# ...\nlog_errors:      true\nerrors_log_file: /var/log/error.log\n# ...\n```\n\nAs a result, if the execution of an event is unsuccessful for some reasons, the error message is appended to the specified error log file. Each entry provides useful information including the time it happened, the event description,  the executed command which caused the error, and the error message itself.\n\nIt is also possible to send the errors as emails to a group of recipients by setting `email_error` and `mailer` settings in the configuration file.\n\n## Custom logger\n\nTo use your own logger create class implementing `\\Crunz\\Application\\Service\\LoggerFactoryInterface`, for example:\n\n```php\n<?php\n\nnamespace Vendor\\Package;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Application\\Service\\LoggerFactoryInterface;\nuse Psr\\Log\\AbstractLogger;\nuse Psr\\Log\\LoggerInterface;\n\nfinal class MyEchoLoggerFactory implements LoggerFactoryInterface\n{\n    public function create(ConfigurationInterface $configuration): LoggerInterface\n    {\n        return new class extends AbstractLogger {\n            /** @inheritDoc */\n            public function log(\n                $level,\n                $message,\n                array $context = array()\n            ) {\n                echo \"crunz.{$level}: {$message}\";   \n            }\n        };\n    }\n}\n```\n\nthen use this class name in config: \n\n```yaml\n# ./crunz.yml file\n \nlogger_factory: 'Vendor\\Package\\MyEchoLoggerFactory'\n```\n\nDone.\n\n## Pre-Process and Post-Process Hooks\n\nThere are times when we want to do some kind of operations before and after an event. This is possible by attaching pre-process and post-process callbacks to the respective event.\n\nTo do this, we use `before()` and `after()` on both `Event` and `Schedule` objects, meaning we can have pre and post hooks on an event-basis as well as schedule basis. The hooks bind to schedule will run before all events, and after all the events are finished.\n\n```php\n<?php\n\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n\n$task = $schedule->run(PHP_BINARY . ' email.php');\n$task\n    ->everyFiveMinutes()\n    ->before(function() { \n        // Do something before the task runs\n    })\n    ->before(function() { \n        // Do something else\n    })\n    ->after(function() {\n        // After the task is run\n    });\n \n$schedule\n    ->before(function () {\n       // Do something before all events\n    })\n    ->after(function () {\n       // Do something after all events are finished\n    })\n    ->before(function () {\n       // Do something before all events\n    });\n```\n\n> We might need to use these methods as many times we need by chaining them.\n\nPost-execution callbacks are only called if the execution of the event has been successful.\n\n## Other Useful Commands\n\nWe've already used a few of `crunz` commands like `schedule:run` and `publish:config`. \n\nTo see all the valid options and arguments of `crunz`, we can run the following command:\n\n```bash\nvendor/bin/crunz --help\n```\n\n### Listing Tasks\n\nOne of these commands is `crunz schedule:list`, which lists the defined tasks (in collected `*.Tasks.php` files) in a tabular format.\n\n```text\nvendor/bin/crunz schedule:list\n\n+---+---------------+-------------+--------------------+\n| # | Task          | Expression  | Command to Run     |\n+---+---------------+-------------+--------------------+\n| 1 | Sample Task   | * * * * 1 * | command/to/execute |\n+---+---------------+-------------+--------------------+\n```\n\nBy default, list is in text format, but format can be changed by `--format` option.\n\nList in `json` format, command:\n```bash\nvendor/bin/crunz schedule:list --format json\n```\n\nwill output:\n\n```json\n[\n    {\n        \"number\": 1,\n        \"task\": \"Sample Task\",\n        \"expression\": \"* * * * 1\",\n        \"command\": \"command/to/execute\"\n    }\n]\n```\n\n### Force run\n\nWhile in development it may be useful to force run all tasks regardless of their actual run time,\nwhich can be achieved by adding `--force` to `schedule:run`:\n\n```bash\nvendor/bin/crunz schedule:run --force\n```\n\nTo force run a single task, use the schedule:list command above to determine the Task number and run as follows:\n\n```bash\nvendor/bin/crunz schedule:run --task 1 --force\n```\n\n### Generating Tasks\n\nThere is also a useful command named `make:task`, which generates a task file skeleton with all the defaults, so we won't have to write them from scratch. We can modify the output file later based on our requirements. \n\nFor example, to create a task, which runs `/var/www/script.php` every hour on Mondays, we run the following command:\n\n```text\nvendor/bin/crunz make:task exampleOne --run scripts.php --in /var/www --frequency everyHour --constraint mondays\nWhere do you want to save the file? (Press enter for the current directory)\n```\n\nWhen we run this command, Crunz will ask about the location we want to save the file. By default, it is our source tasks directory.\n\nAs a result, the event is defined in a file named `exampleOneTasks.php` within the specified tasks directory.\n\nTo see if the event has been created successfully, we list the events:\n\n```text\ncrunz schedule:list\n\n+---+------------------+-------------+----------------+\n| # | Task             | Expression  | Command to Run |\n+---+------------------+-------------+----------------+\n| 1 | Task description | 0 * * * 1 * | scripts.php    |\n+---+------------------+-------------+----------------+\n```\n\nTo see all the options of `make:task` command with all the defaults, we run this:\n\n```bash\nvendor/bin/crunz make:task --help\n```\n\n### Debugging tasks\n\nTo show basic information about task run:\n\n```bash\nvendor/bin/crunz task:debug 1\n```\n\nAbove command should output something like this:\n\n```text\n+----------------------+-----------------------------------+\n| Debug information for task '1'                           |\n+----------------------+-----------------------------------+\n| Command to run       | php -v                            |\n| Description          | Inner task                        |\n| Prevent overlapping  | No                                |\n+----------------------+-----------------------------------+\n| Cron expression      | * * * * *                         |\n| Comparisons timezone | Europe/Warsaw (from config)       |\n+----------------------+-----------------------------------+\n| Example run dates                                        |\n| #1                   | 2020-03-08 09:27:00 Europe/Warsaw |\n| #2                   | 2020-03-08 09:28:00 Europe/Warsaw |\n| #3                   | 2020-03-08 09:29:00 Europe/Warsaw |\n| #4                   | 2020-03-08 09:30:00 Europe/Warsaw |\n| #5                   | 2020-03-08 09:31:00 Europe/Warsaw |\n+----------------------+-----------------------------------+\n```\n\n## Configuration\n\nThere are a few configuration options provided by Crunz in YAML format. To modify the configuration settings, it is highly recommended to have your own copy of the configuration file, instead of modifying the original one. \n\nTo create a copy of the configuration file, first we need to publish the configuration file:\n\n```bash\n/project/vendor/bin/crunz publish:config\nThe configuration file was generated successfully\n```\n\nAs a result, a copy of the configuration file will be created within our project's root directory.\n\n The configuration file looks like this:\n\n```yaml\n# Crunz Configuration Settings\n\n# This option defines where the task files and\n# directories reside.\n# The path is relative to the project's root directory,\n# where the Crunz is installed (Trailing slashes will be ignored).\nsource: tasks\n\n# The suffix is meant to target the task files inside the \":source\" directory.\n# Please note if you change this value, you need\n# to make sure all the existing tasks files are renamed accordingly.\nsuffix: Tasks.php\n\n# Timezone is used to calculate task run time\n# This option is very important and not setting it is deprecated\n# and will result in exception in 2.0 version.\ntimezone: ~\n\n# This option define which timezone should be used for log files\n# If false, system default timezone will be used\n# If true, the timezone in config file that is used to calculate task run time will be used\ntimezone_log: false\n\n# By default the errors are not logged by Crunz\n# You may set the value to true for logging the errors\nlog_errors: false\n\n# This is the absolute path to the errors' log file\n# You need to make sure you have the required permission to write to this file though.\nerrors_log_file:\n\n# By default the output is not logged as they are redirected to the\n# null output.\n# Set this to true if you want to keep the outputs\nlog_output: false\n\n# This is the absolute path to the global output log file\n# The events which have dedicated log files (defined with them), won't be\n# logged to this file though.\noutput_log_file:\n\n# By default line breaks in logs aren't allowed.\n# Set the value to true to allow them.\nlog_allow_line_breaks: false\n\n# By default empty context arrays are shown in the log.\n# Set the value to true to remove them.\nlog_ignore_empty_context: false\n\n# This option determines whether the output should be emailed or not.\nemail_output: false\n\n# This option determines whether the error messages should be emailed or not.\nemail_errors: false\n\n# Global Swift Mailer settings\n#\nmailer:\n    # Possible values: smtp, mail, and sendmail\n    transport: smtp\n    recipients:\n    sender_name:\n    sender_email:\n\n\n# SMTP settings\n#\nsmtp:\n    host:\n    port:\n    username:\n    password:\n    encryption:\n```\n\nAs you can see there are a few options like `source` which is used to specify the source tasks directory. The other options are used for error/output logging/emailing purposes.\n\nEach time we run Crunz commands, it will look into the project's root directory to see if there's any user-modified configuration file. If the configuration file doesn't exists, it will use the one shipped with the package.\n\n## Setting the base cache directory\n\nYou can change the base cache directory of crunz by setting the `CRUNZ_BASE_CACHE_DIR` environment variable.\nThe default base cache directory is `\\sys_get_temp_dir()`. The subdirectory `.crunz` is always added to the base cache directory.\n\n## Development ENV flags\n\nThe following environment flags should be used only while in development.\nTypical end-users do not need to, and should not, change them.\n\n### `CRUNZ_CONTAINER_DEBUG`\n\nFlag used to enable/disable container debug mode, useful only for development.\nEnabled by default in `docker-compose`.\n\n### `CRUNZ_DEPRECATION_HANDLER`\n\nFlag used to enable/disable Crunz deprecation handler, useful only for integration tests.\nDisabled by default for tests.\n\n## Sponsors\n\n[![Blakfire.io logo](resources/docs/blackfire-logo.png)](https://www.blackfire.io/?utm_source=crunz&utm_medium=readme&utm_campaign=free-open-source)\n\n## Support\n\nYou can support further Crunz development by [GitHub](https://github.com/sponsors/PabloKowalczyk). \n\n## Contributing\n\n### Which branch should I choose?\n\nBug fixes and readme changes should target `3.9`, new features should target `3.10`.\n\n## If You Need Help\n\nPlease submit all issues and questions using GitHub issues and I will try to help you.\n\n\n## Credits\n\n* [PabloKowalczyk](https://github.com/PabloKowalczyk)\n* [Reza Lavarian](https://github.com/lavary)\n* [All Contributors](https://github.com/crunzphp/crunz/graphs/contributors)\n\n## License\nCrunz is free software distributed under the terms of the MIT license.\n"
  },
  {
    "path": "UPGRADE.md",
    "content": "# Upgrading from v3.2 to v3.3\n\n## Pass only string parameters to `\\Crunz\\Schedule::run`\n\nConvert this:\n```php\n$schedule = new Schedule();\n$schedule->run('php', ['-v' => true, 2]);\n```\n\ninto this:\n```php\n$schedule = new Schedule();\n$schedule->run('php', ['-v' => '1', '2']);\n```\n\n# Upgrading from v1.12 to v2.0\n\n## Stop using `mail` transport for mailer\n\nAs of `v6.0` SwiftMailer dropped support for `mail` transport,\nso `Crunz` `v2.0` won't support it either,\nplease use `smtp` or `sendmail` transport.\n\n# Upgrading from v1.11 to v1.12\n\n## Always return `\\Crunz\\Schedule` from task files\n\nExample of wrong task file:\n\n```php\n<?php\n\nreturn [];\n```\n\nExample of correct task file:\n```php\n<?php\n\nuse Crunz\\Schedule;\n\n$scheduler = new Schedule();\n\n$scheduler\n    ->run('php -v')\n    ->description('PHP version')\n    ->everyMinute();\n\n// Crunz\\Schedule instance returned\nreturn $scheduler;\n```\n\n## Stop using `\\Crunz\\Event::setProcess`\n\nIf you, for some reason, use above method you should stop it.\nThis method was intended to be `private` and will be in `v2.0`,\nwhich will lead to exception if you call it.\n\nExample of wrong usage\n\n```php\n<?php\n\nuse Crunz\\Schedule;\n\n$process = new \\Symfony\\Component\\Process\\Process('php -i');\n$scheduler = new Schedule();\n$task = $scheduler->run('php -v');\n$task\n    // setProcess is deprecated\n    ->setProcess($process)\n    ->description('PHP version')\n    ->everyMinute()\n;\n\nreturn $scheduler;\n``` \n\n# Upgrading from v1.10 to v1.11\n\n## Run `Crunz` in directory with your `crunz.yml`\n\nSearching for Crunz's config is now related to `cwd`, not to `vendor/bin/crunz`.\n\nFor example, if your `crunz.yml` is in `/var/www/project/crunz.yml`, then run Crunz with `cd` first:\n```bash\ncd /var/www/project && vendor/bin/crunz schedule:list\n```\n\nCron job also should be changed:\n```bash\n* * * * * cd /var/www/project && vendor/bin/crunz schedule:run\n```\n\n# Upgrading from v1.9 to v1.10\n\n### Do not pass more than five parts to `Crunz\\Event::cron()`\n\nExample correct call:\n```yaml\n$event = new Crunz\\Event;\n$event->cron('0 * * * *');\n```\n\n# Upgrading from v1.7 to v1.8\n\n### Add `timezone` to your `crunz.yml`\n\nExample config file:\n```yaml\nsource: tasks\nsuffix: Tasks.php\ntimezone: Europe/Warsaw\n```\n"
  },
  {
    "path": "bootstrap.php",
    "content": "<?php\n\nrequire_once __DIR__ . '/vendor/autoload.php';\n\nif (!\\defined('IS_WINDOWS')) {\n    \\define('IS_WINDOWS', PHP_OS_FAMILY === \"Windows\");\n}\n\n// Disable deprecation helper\n$envFlags = new \\Crunz\\EnvFlags\\EnvFlags();\n$envFlags->disableDeprecationHandler();\n\n// Make sure current working directory is \"tests\"\n$filesystem = new \\Crunz\\Filesystem\\Filesystem();\nif (\\str_contains($filesystem->getCwd(), 'tests')) {\n    return;\n}\n\nif (!\\chdir('tests')) {\n    throw new RuntimeException(\"Unable to change current directory to 'tests'.\");\n}\n"
  },
  {
    "path": "composer.json",
    "content": "{\n    \"name\": \"crunzphp/crunz\",\n    \"description\": \"Schedule your tasks right from the code.\",\n    \"license\": \"MIT\",\n    \"type\": \"library\",\n    \"keywords\": [\n        \"scheduler\",\n        \"cron jobs\",\n        \"cron\",\n        \"Task Scheduler\",\n        \"PHP Task Scheduler\",\n        \"Job Scheduler\",\n        \"Job Manager\",\n        \"Event Runner\"\n    ],\n    \"authors\": [\n        {\n            \"name\": \"Reza M. Lavaryan\",\n            \"email\": \"mrl.8081@gmail.com\"\n        },\n        {\n            \"name\": \"PabloKowalczyk\",\n            \"homepage\": \"https://github.com/PabloKowalczyk\",\n            \"role\": \"Developer\"\n        }\n    ],\n    \"homepage\": \"https://github.com/crunzphp/crunz\",\n    \"support\": {\n        \"issues\": \"https://github.com/crunzphp/crunz/issues\"\n    },\n    \"funding\": [\n        {\n            \"type\": \"github\",\n            \"url\": \"https://github.com/sponsors/PabloKowalczyk\"\n        }\n    ],\n    \"require\": {\n        \"php\": \">=8.2\",\n        \"composer-runtime-api\": \"^2.0\",\n        \"dragonmantank/cron-expression\": \"^3.4.0\",\n        \"laravel/serializable-closure\": \"^2.0\",\n        \"psr/log\": \"^2.0 || ^3.0\",\n        \"symfony/config\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/console\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/dependency-injection\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/filesystem\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/lock\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/mailer\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/process\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/string\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/yaml\": \"^6.4.25 || ^7.4.0 || ^8.0.0\"\n    },\n    \"require-dev\": {\n        \"ext-json\": \"*\",\n        \"ext-mbstring\": \"*\",\n        \"ergebnis/composer-normalize\": \"2.28.3\",\n        \"friendsofphp/php-cs-fixer\": \"3.90.0\",\n        \"phpstan/phpstan\": \"2.0.2\",\n        \"phpstan/phpstan-phpunit\": \"2.0.1\",\n        \"phpstan/phpstan-strict-rules\": \"2.0.0\",\n        \"phpunit/phpunit\": \"10.5.63\",\n        \"symfony/error-handler\": \"^6.4.25 || ^7.4.0 || ^8.0.0\",\n        \"symfony/phpunit-bridge\": \"^6.4.25 || ^7.4.0 || ^8.0.0\"\n    },\n    \"conflict\": {\n        \"laravel/serializable-closure\": \">=2.0.9,<2.0.11\"\n    },\n    \"minimum-stability\": \"beta\",\n    \"prefer-stable\": true,\n    \"autoload\": {\n        \"psr-4\": {\n            \"Crunz\\\\\": \"src/\"\n        }\n    },\n    \"autoload-dev\": {\n        \"psr-4\": {\n            \"Crunz\\\\Tests\\\\\": \"tests/\"\n        }\n    },\n    \"bin\": [\n        \"crunz\"\n    ],\n    \"config\": {\n        \"allow-plugins\": {\n            \"ergebnis/composer-normalize\": true\n        },\n        \"sort-packages\": true\n    },\n    \"scripts\": {\n        \"crunz:analyze\": [\n            \"@php vendor/bin/php-cs-fixer fix --diff --dry-run -v\",\n            \"@phpstan:check\"\n        ],\n        \"crunz:cs-fix\": \"@php vendor/bin/php-cs-fixer fix --diff -v --ansi\",\n        \"phpstan:check\": \"@php vendor/bin/phpstan analyse -c phpstan.neon src tests crunz config bootstrap.php\"\n    }\n}\n"
  },
  {
    "path": "config/services.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Crunz\\Application\\Cron\\CronExpressionFactoryInterface;\nuse Crunz\\Application\\Query\\TaskInformation\\TaskInformationHandler;\nuse Crunz\\Application\\Service\\ClosureSerializerInterface;\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Clock\\Clock;\nuse Crunz\\Clock\\ClockInterface;\nuse Crunz\\Configuration\\Configuration;\nuse Crunz\\Configuration\\ConfigurationParser;\nuse Crunz\\Configuration\\ConfigurationParserInterface;\nuse Crunz\\Configuration\\Definition;\nuse Crunz\\Configuration\\FileParser;\nuse Crunz\\Console\\Command\\ConfigGeneratorCommand;\nuse Crunz\\Console\\Command\\ScheduleListCommand;\nuse Crunz\\Console\\Command\\ScheduleRunCommand;\nuse Crunz\\Console\\Command\\TaskGeneratorCommand;\nuse Crunz\\EventRunner;\nuse Crunz\\Filesystem\\Filesystem as CrunzFilesystem;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Finder\\Finder;\nuse Crunz\\Finder\\FinderInterface;\nuse Crunz\\HttpClient\\CurlHttpClient;\nuse Crunz\\HttpClient\\FallbackHttpClient;\nuse Crunz\\HttpClient\\HttpClientInterface;\nuse Crunz\\HttpClient\\HttpClientLoggerDecorator;\nuse Crunz\\HttpClient\\StreamHttpClient;\nuse Crunz\\Infrastructure\\Dragonmantank\\CronExpression\\DragonmantankCronExpressionFactory;\nuse Crunz\\Infrastructure\\Laravel\\LaravelClosureSerializer;\nuse Crunz\\Invoker;\nuse Crunz\\Logger\\ConsoleLogger;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\nuse Crunz\\Logger\\LoggerFactory;\nuse Crunz\\Mailer;\nuse Crunz\\Output\\OutputFactory;\nuse Crunz\\Schedule\\ScheduleFactory;\nuse Crunz\\Task\\Collection;\nuse Crunz\\Task\\CollectionInterface;\nuse Crunz\\Task\\Loader;\nuse Crunz\\Task\\LoaderInterface;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Timezone\\Provider;\nuse Crunz\\Timezone\\ProviderInterface;\nuse Crunz\\UserInterface\\Cli\\ClosureRunCommand;\nuse Crunz\\UserInterface\\Cli\\DebugTaskCommand;\nuse Symfony\\Component\\Config\\Definition\\Processor;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\ConsoleOutput;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\nuse Symfony\\Component\\DependencyInjection\\ContainerBuilder;\nuse Symfony\\Component\\DependencyInjection\\Reference;\nuse Symfony\\Component\\Filesystem\\Filesystem;\nuse Symfony\\Component\\Yaml\\Yaml;\n\n$simpleServices = [\n    Definition::class,\n    Yaml::class,\n    Processor::class,\n    Invoker::class,\n    ProviderInterface::class => Provider::class,\n    Filesystem::class,\n    ScheduleFactory::class,\n    StreamHttpClient::class,\n    CurlHttpClient::class,\n    FilesystemInterface::class => CrunzFilesystem::class,\n    FinderInterface::class => Finder::class,\n    LoaderInterface::class => Loader::class,\n    CronExpressionFactoryInterface::class => DragonmantankCronExpressionFactory::class,\n    ClosureSerializerInterface::class => LaravelClosureSerializer::class,\n    ClockInterface::class => Clock::class,\n];\n\n/* @var ContainerBuilder $container */\n\n$container\n    ->register(ScheduleRunCommand::class, ScheduleRunCommand::class)\n    ->setPublic(true)\n    ->setArguments(\n        [\n            new Reference(CollectionInterface::class),\n            new Reference(ConfigurationInterface::class),\n            new Reference(EventRunner::class),\n            new Reference(Timezone::class),\n            new Reference(ScheduleFactory::class),\n            new Reference(LoaderInterface::class),\n        ]\n    )\n;\n$container\n    ->register(ClosureRunCommand::class, ClosureRunCommand::class)\n    ->setArguments(\n        [\n            new Reference(ClosureSerializerInterface::class),\n        ]\n    )\n    ->setPublic(true)\n;\n$container\n    ->register(ConfigGeneratorCommand::class, ConfigGeneratorCommand::class)\n    ->setPublic(true)\n    ->setArguments(\n        [\n            new Reference(ProviderInterface::class),\n            new Reference(Filesystem::class),\n            new Reference(FilesystemInterface::class),\n        ]\n    )\n;\n$container\n    ->register(ScheduleListCommand::class, ScheduleListCommand::class)\n    ->setPublic(true)\n    ->setArguments(\n        [\n            new Reference(ConfigurationInterface::class),\n            new Reference(CollectionInterface::class),\n            new Reference(LoaderInterface::class),\n        ]\n    )\n;\n$container\n    ->register(TaskGeneratorCommand::class, TaskGeneratorCommand::class)\n    ->setPublic(true)\n    ->setArguments(\n        [\n            new Reference(ConfigurationInterface::class),\n            new Reference(FilesystemInterface::class),\n        ]\n    )\n;\n$container\n    ->register(DebugTaskCommand::class, DebugTaskCommand::class)\n    ->setPublic(true)\n    ->setArguments(\n        [\n            new Reference(TaskInformationHandler::class),\n        ]\n    )\n;\n$container\n    ->register(CollectionInterface::class, Collection::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(ConfigurationInterface::class),\n            new Reference(FinderInterface::class),\n            new Reference(ConsoleLoggerInterface::class),\n        ]\n    )\n;\n$container\n    ->register(FileParser::class, FileParser::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(Yaml::class),\n        ]\n    )\n;\n$container\n    ->register(ConfigurationInterface::class, Configuration::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(ConfigurationParserInterface::class),\n            new Reference(FilesystemInterface::class),\n        ]\n    )\n;\n$container\n    ->register(Mailer::class, Mailer::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(ConfigurationInterface::class),\n        ]\n    )\n;\n$container\n    ->register(LoggerFactory::class, LoggerFactory::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(ConfigurationInterface::class),\n            new Reference(Timezone::class),\n            new Reference(ConsoleLoggerInterface::class),\n            new Reference(ClockInterface::class),\n        ]\n    )\n;\n$container\n    ->register(EventRunner::class, EventRunner::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(Invoker::class),\n            new Reference(ConfigurationInterface::class),\n            new Reference(Mailer::class),\n            new Reference(LoggerFactory::class),\n            new Reference(HttpClientInterface::class),\n            new Reference(ConsoleLoggerInterface::class),\n        ]\n    )\n;\n$container\n    ->register(Timezone::class, Timezone::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(ConfigurationInterface::class),\n            new Reference(ConsoleLoggerInterface::class),\n        ]\n    )\n;\n$container\n    ->register(OutputInterface::class, ConsoleOutput::class)\n    ->setPublic(true)\n    ->setFactory([new Reference(OutputFactory::class), 'createOutput'])\n;\n$container\n    ->register(OutputFactory::class, OutputFactory::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(InputInterface::class),\n        ]\n    )\n;\n$container\n    ->register(InputInterface::class, ArgvInput::class)\n    ->setPublic(true)\n;\n$container\n    ->register(SymfonyStyle::class, SymfonyStyle::class)\n    ->setPublic(true)\n    ->setArguments(\n        [\n            new Reference(InputInterface::class),\n            new Reference(OutputInterface::class),\n        ]\n    )\n;\n$container\n    ->register(ConsoleLoggerInterface::class, ConsoleLogger::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(SymfonyStyle::class),\n        ]\n    )\n;\n$container\n    ->register(ConsoleLoggerInterface::class, ConsoleLogger::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(SymfonyStyle::class),\n        ]\n    )\n;\n$container\n    ->register(FallbackHttpClient::class, FallbackHttpClient::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(StreamHttpClient::class),\n            new Reference(CurlHttpClient::class),\n            new Reference(ConsoleLoggerInterface::class),\n        ]\n    )\n;\n$container\n    ->register(HttpClientInterface::class, HttpClientLoggerDecorator::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(FallbackHttpClient::class),\n            new Reference(ConsoleLoggerInterface::class),\n        ]\n    )\n;\n$container\n    ->register(ConfigurationParserInterface::class, ConfigurationParser::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(Definition::class),\n            new Reference(Processor::class),\n            new Reference(FileParser::class),\n            new Reference(ConsoleLoggerInterface::class),\n            new Reference(FilesystemInterface::class),\n        ]\n    )\n;\n\n$container\n    ->register(TaskInformationHandler::class, TaskInformationHandler::class)\n    ->setPublic(false)\n    ->setArguments(\n        [\n            new Reference(Timezone::class),\n            new Reference(ConfigurationInterface::class),\n            new Reference(CollectionInterface::class),\n            new Reference(LoaderInterface::class),\n            new Reference(ScheduleFactory::class),\n            new Reference(CronExpressionFactoryInterface::class),\n        ]\n    )\n;\n\nforeach ($simpleServices as $id => $simpleService) {\n    if (!\\is_string($id)) {\n        $id = $simpleService;\n    }\n\n    $container\n        ->register($id, $simpleService)\n        ->setPublic(false)\n    ;\n}\n"
  },
  {
    "path": "crunz",
    "content": "#!/usr/bin/env php\n<?php\n\n/*\n|--------------------------------------------------------------------------\n| Crunz\n|--------------------------------------------------------------------------\n|\n| This file is part of Crunz library.\n| (c) Reza M. Lavaryan <mrl.8081@gmail.com>\n| For the full copyright and license information, please view the LICENSE\n| file that was distributed with this source code.\n|\n*/\n\nuse Composer\\InstalledVersions;\nuse Crunz\\Application;\n\nif (!\\defined('CRUNZ_BIN')) {\n    \\define('CRUNZ_BIN', __FILE__);\n}\n\n$generatePath = static fn(string ...$parts): string => \\implode(DIRECTORY_SEPARATOR, $parts);\n$autoloadPaths = [\n    // Dependency\n    $generatePath(\n        \\dirname(__DIR__, 2),\n        'autoload.php'\n    ),\n    // Vendor/Bin\n    $generatePath(\n        \\dirname(__DIR__),\n        'autoload.php'\n    ),\n    // Local dev\n    $generatePath(\n        __DIR__,\n        'vendor',\n        'autoload.php'\n    ),\n];\n$loadAutoloader = static function () use($autoloadPaths): void {\n    foreach ($autoloadPaths as $autoloadPath) {\n        if (\\file_exists($autoloadPath) === true) {\n            require_once $autoloadPath;\n            return;\n        }\n    }\n\n    throw new RuntimeException(\n        \\sprintf(\n            'Unable to find \"vendor/autoload.php\" in \"%s\" paths.',\n            \\implode('\", \"', $autoloadPaths)\n        )\n    );\n};\n$loadAutoloader();\n\n$application = new Application(\n    'Crunz Command Line Interface',\n    InstalledVersions::getPrettyVersion('crunzphp/crunz') ?? '1.0.x-dev',\n);\n$application->run();\n"
  },
  {
    "path": "docker/php82/Dockerfile",
    "content": "FROM php:8.2.30-cli-alpine\n\nRUN apk add --no-cache \\\n        shadow \\\n        su-exec && \\\n    usermod --non-unique --uid 1000 www-data && \\\n    apk del \\\n        shadow && \\\n    docker-php-ext-install -j$(nproc) \\\n        opcache \\\n        sysvsem\n\nRUN mkdir -p \\\n        /var/log/php \\\n        /var/www/.composer \\\n    && touch /var/log/php/error.log \\\n    && chown www-data:www-data \\\n        /var/log/php/error.log \\\n        /var/www/.composer\n\nCOPY --from=composer/composer:2.9.3-bin /composer /usr/bin/composer\nENV COMPOSER_HOME /var/www/.composer\n"
  },
  {
    "path": "docker/php82/php.ini",
    "content": "realpath_cache_size = 8192k\nrealpath_cache_ttl = 6000\n\nexpose_php = On\nerror_log = /var/log/php/error.log\nerror_reporting = E_ALL\ndisplay_errors = On\ndisplay_startup_errors = On\nlog_errors = On\nreport_memleaks = On\n\nmemory_limit = 80M\n\ndate.timezone = \"UTC\"\n\nzend.assertions = 1\n\nopcache.enable=1\nopcache.enable_cli=0\nopcache.memory_consumption=80\nopcache.interned_strings_buffer=5\nopcache.max_accelerated_files=3000\nopcache.validate_timestamps=1\nopcache.revalidate_freq=0\nopcache.save_comments=1\nopcache.fast_shutdown=1\nopcache.huge_code_pages=1\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "services:\n    php82:\n        build:\n            context: ./docker/php82\n        working_dir: /var/www/html\n        environment:\n            CRUNZ_CONTAINER_DEBUG: 1\n        command: >\n            sh -c \"\n                chown -R www-data:www-data /var/www/.composer && \\\n                echo 'Logs from /var/log/php/error.log:' && \\\n                touch /var/log/php/error.log && \\\n                tail -f /var/log/php/error.log\n            \"\n        volumes:\n            - .:/var/www/html\n            - ./docker/php82/php.ini:/usr/local/etc/php/php.ini:ro\n        stop_grace_period: 1s\n"
  },
  {
    "path": "phpstan-baseline.neon",
    "content": "parameters:\n\tignoreErrors:\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#5 \\\\$timeZone of class Crunz\\\\\\\\Application\\\\\\\\Query\\\\\\\\TaskInformation\\\\\\\\TaskInformationView constructor expects DateTimeZone\\\\|null, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Application/Query/TaskInformation/TaskInformationHandler.php\n\n\t\t-\n\t\t\tmessage: \"#^Variable property access on \\\\$this\\\\(Crunz\\\\\\\\Application\\\\\\\\Query\\\\\\\\TaskInformation\\\\\\\\TaskInformationHandler\\\\)\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Application/Query/TaskInformation/TaskInformationHandler.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$parts of static method Crunz\\\\\\\\Path\\\\\\\\Path\\\\:\\\\:create\\\\(\\\\) expects array\\\\<string\\\\>, array\\\\<int, mixed\\\\> given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Configuration/Configuration.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Configuration\\\\\\\\ConfigurationParser\\\\:\\\\:parseConfig\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Configuration/ConfigurationParser.php\n\n\t\t-\n\t\t\tmessage: \"#^Variable \\\\$cwd on left side of \\\\?\\\\? always exists and is not nullable\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Configuration/ConfigurationParser.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Configuration\\\\\\\\ConfigurationParserInterface\\\\:\\\\:parseConfig\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Configuration/ConfigurationParserInterface.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Configuration\\\\\\\\FileParser\\\\:\\\\:parse\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Configuration/FileParser.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Configuration\\\\\\\\FileParser\\\\:\\\\:parse\\\\(\\\\) should return array\\\\<array\\\\> but returns array\\\\<int, mixed\\\\>\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Configuration/FileParser.php\n\n\t\t-\n\t\t\tmessage: \"#^Property Crunz\\\\\\\\Console\\\\\\\\Command\\\\\\\\Command\\\\:\\\\:\\\\$arguments type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/Command.php\n\n\t\t-\n\t\t\tmessage: \"#^Property Crunz\\\\\\\\Console\\\\\\\\Command\\\\\\\\Command\\\\:\\\\:\\\\$options type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/Command.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Console\\\\\\\\Command\\\\\\\\ConfigGeneratorCommand\\\\:\\\\:askForTimezone\\\\(\\\\) should return string but returns mixed\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/ConfigGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Short ternary operator is not allowed\\\\. Use null coalesce operator if applicable or consider using long ternary\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/ConfigGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in a negated boolean, int\\\\<0, max\\\\> given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/ScheduleListCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in a negated boolean, int\\\\<0, max\\\\> given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Console/Command/ScheduleRunCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Binary operation \\\"\\\\.\\\" between array\\\\|string\\\\|null and mixed results in an error\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to an undefined method Symfony\\\\\\\\Component\\\\\\\\Console\\\\\\\\Helper\\\\\\\\HelperInterface\\\\:\\\\:ask\\\\(\\\\)\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Console\\\\\\\\Command\\\\\\\\TaskGeneratorCommand\\\\:\\\\:type\\\\(\\\\) should return string but returns array\\\\|bool\\\\|float\\\\|int\\\\|string\\\\|null\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, string given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$string of function rtrim expects string, array\\\\|bool\\\\|float\\\\|int\\\\|string\\\\|null given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#2 \\\\$replace of function str_replace expects array\\\\|string, array\\\\|bool\\\\|float\\\\|int\\\\|string\\\\|null given\\\\.$#\"\n\t\t\tcount: 3\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#3 \\\\$subject of function preg_replace expects array\\\\|string, array\\\\|bool\\\\|float\\\\|int\\\\|string\\\\|null given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Return type \\\\(int\\\\|null\\\\) of method Crunz\\\\\\\\Console\\\\\\\\Command\\\\\\\\TaskGeneratorCommand\\\\:\\\\:execute\\\\(\\\\) should be covariant with return type \\\\(int\\\\) of method Symfony\\\\\\\\Component\\\\\\\\Console\\\\\\\\Command\\\\\\\\Command\\\\:\\\\:execute\\\\(\\\\)$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Console/Command/TaskGeneratorCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to function is_string\\\\(\\\\) with string will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Cannot cast mixed to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Instanceof between Closure and Closure will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Instanceof between DateTimeZone and DateTimeZone will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in a negated boolean, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, DateTimeZone\\\\|string given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, mixed given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, string given\\\\.$#\"\n\t\t\tcount: 3\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Property Crunz\\\\\\\\Event\\\\:\\\\:\\\\$lock \\\\(Symfony\\\\\\\\Component\\\\\\\\Lock\\\\\\\\Lock\\\\) does not accept Symfony\\\\\\\\Component\\\\\\\\Lock\\\\\\\\SharedLockInterface\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Short ternary operator is not allowed\\\\. Use null coalesce operator if applicable or consider using long ternary\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Strict comparison using \\\\=\\\\=\\\\= between false and array\\\\<string, mixed\\\\> will always evaluate to false\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Event.php\n\n\t\t-\n\t\t\tmessage: \"#^Construct empty\\\\(\\\\) is not allowed\\\\. Use more strict comparison\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/EventRunner.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in &&, mixed given on the left side\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/EventRunner.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in a negated boolean, int\\\\<0, max\\\\> given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/EventRunner.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, mixed given\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/EventRunner.php\n\n\t\t-\n\t\t\tmessage: \"#^Short ternary operator is not allowed\\\\. Use null coalesce operator if applicable or consider using long ternary\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/EventRunner.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$callback of function array_map expects \\\\(callable\\\\(mixed\\\\)\\\\: mixed\\\\)\\\\|null, Closure\\\\(array\\\\)\\\\: SplFileInfo given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Finder/Finder.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Infrastructure\\\\\\\\Laravel\\\\\\\\LaravelClosureSerializer\\\\:\\\\:extractWrapper\\\\(\\\\) should return Laravel\\\\\\\\SerializableClosure\\\\\\\\SerializableClosure but returns mixed\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Laravel/LaravelClosureSerializer.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\EnabledLoggerDecorator\\\\:\\\\:log\\\\(\\\\) has parameter \\\\$context with no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/EnabledLoggerDecorator.php\n\n\t\t-\n\t\t\tmessage: \"#^Construct empty\\\\(\\\\) is not allowed\\\\. Use more strict comparison\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger\\\\:\\\\:log\\\\(\\\\) has parameter \\\\$context with no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$message of method Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger\\\\:\\\\:replaceNewlines\\\\(\\\\) expects string, string\\\\|Stringable given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$string of function mb_strtoupper expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#3 \\\\$outputStreamPath of class Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger constructor expects string\\\\|null, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#4 \\\\$errorStreamPath of class Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger constructor expects string\\\\|null, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#5 \\\\$ignoreEmptyContext of class Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger constructor expects bool, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#6 \\\\$timezoneLog of class Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger constructor expects bool, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#7 \\\\$allowLineBreaks of class Crunz\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\PsrStreamLogger constructor expects bool, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Infrastructure/Psr/Logger/PsrStreamLoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Logger\\\\\\\\LoggerFactory\\\\:\\\\:createLoggerFactory\\\\(\\\\) should return Crunz\\\\\\\\Application\\\\\\\\Service\\\\\\\\LoggerFactoryInterface but returns object\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Logger/LoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Logger/LoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$class of function class_exists expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Logger/LoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$loggerFactoryClass \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Logger/LoggerFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Argument of an invalid type mixed supplied for foreach, only iterables are supported\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in an if condition, Symfony\\\\\\\\Component\\\\\\\\Mailer\\\\\\\\Mailer\\\\|null given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$address of class Symfony\\\\\\\\Component\\\\\\\\Mime\\\\\\\\Address constructor expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\.\\\\.\\\\.\\\\$addresses of method Symfony\\\\\\\\Component\\\\\\\\Mime\\\\\\\\Email\\\\:\\\\:addTo\\\\(\\\\) expects string\\\\|Symfony\\\\\\\\Component\\\\\\\\Mime\\\\\\\\Address, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#2 \\\\$name of class Symfony\\\\\\\\Component\\\\\\\\Mime\\\\\\\\Address constructor expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$host \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$password \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$port \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$user \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Mailer.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in \\\\|\\\\|, mixed given on the right side\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Output/OutputFactory.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to function is_string\\\\(\\\\) with string will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Schedule.php\n\n\t\t-\n\t\t\tmessage: \"#^Only booleans are allowed in &&, int\\\\<0, max\\\\> given on the right side\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Schedule.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#2 \\\\$suffix of method Crunz\\\\\\\\Finder\\\\\\\\FinderInterface\\\\:\\\\:find\\\\(\\\\) expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Task/Collection.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$suffix \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Task/Collection.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to function is_string\\\\(\\\\) with string will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Task/TaskNumber.php\n\n\t\t-\n\t\t\tmessage: \"#^Construct empty\\\\(\\\\) is not allowed\\\\. Use more strict comparison\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Task/Timezone.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$timezone of class DateTimeZone constructor expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/Task/Timezone.php\n\n\t\t-\n\t\t\tmessage: \"#^Part \\\\$newTimezone \\\\(mixed\\\\) of encapsed string cannot be cast to string\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: src/Task/Timezone.php\n\n\t\t-\n\t\t\tmessage: \"#^Return type \\\\(int\\\\|null\\\\) of method Crunz\\\\\\\\UserInterface\\\\\\\\Cli\\\\\\\\ClosureRunCommand\\\\:\\\\:execute\\\\(\\\\) should be covariant with return type \\\\(int\\\\) of method Symfony\\\\\\\\Component\\\\\\\\Console\\\\\\\\Command\\\\\\\\Command\\\\:\\\\:execute\\\\(\\\\)$#\"\n\t\t\tcount: 1\n\t\t\tpath: src/UserInterface/Cli/ClosureRunCommand.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\EndToEnd\\\\\\\\WrongTaskTest\\\\:\\\\:scheduleInstanceProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/EndToEnd/WrongTaskTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to an undefined method Symfony\\\\\\\\Component\\\\\\\\Console\\\\\\\\Helper\\\\\\\\HelperInterface\\\\:\\\\:setInputStream\\\\(\\\\)\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Functional/TaskGeneratorTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to function method_exists\\\\(\\\\) with Symfony\\\\\\\\Component\\\\\\\\Console\\\\\\\\Tester\\\\\\\\CommandTester and 'setInputs' will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Functional/TaskGeneratorTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Construct empty\\\\(\\\\) is not allowed\\\\. Use more strict comparison\\\\.$#\"\n\t\t\tcount: 2\n\t\t\tpath: tests/TestCase/EndToEnd/Environment/Environment.php\n\n\t\t-\n\t\t\tmessage: \"#^Casting to string something that's already string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/EndToEndTestCase.php\n\n\t\t-\n\t\t\tmessage: \"#^Cannot cast mixed to string\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/FakeConfiguration.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\FakeConfiguration\\\\:\\\\:__construct\\\\(\\\\) has parameter \\\\$config with no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/FakeConfiguration.php\n\n\t\t-\n\t\t\tmessage: \"#^Property Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\FakeConfiguration\\\\:\\\\:\\\\$config \\\\(array\\\\<int\\\\|string, array\\\\|bool\\\\|string\\\\|null\\\\>\\\\) does not accept array\\\\<int\\\\|string, mixed\\\\>\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/FakeConfiguration.php\n\n\t\t-\n\t\t\tmessage: \"#^Property Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\FakeConfiguration\\\\:\\\\:\\\\$config type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/FakeConfiguration.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$timezone of class DateTimeZone constructor expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/Faker.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\Logger\\\\\\\\SpyPsrLogger\\\\:\\\\:getLogs\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/Logger/SpyPsrLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\Logger\\\\\\\\SpyPsrLogger\\\\:\\\\:log\\\\(\\\\) has parameter \\\\$context with no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/Logger/SpyPsrLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Property Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\Logger\\\\\\\\SpyPsrLogger\\\\:\\\\:\\\\$logs type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/Logger/SpyPsrLogger.php\n\n\t\t-\n\t\t\tmessage: \"#^Offset 'uri' on array\\\\{timed_out\\\\: bool, blocked\\\\: bool, eof\\\\: bool, unread_bytes\\\\: int, stream_type\\\\: string, wrapper_type\\\\: string, wrapper_data\\\\: mixed, mode\\\\: string, \\\\.\\\\.\\\\.\\\\} on left side of \\\\?\\\\? always exists and is not nullable\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/TestCase/TemporaryFile.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Application\\\\\\\\Cron\\\\\\\\AbstractCronExpressionTest\\\\:\\\\:multipleRunDatesProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Application/Cron/AbstractCronExpressionTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Application\\\\\\\\Query\\\\\\\\TaskInformation\\\\\\\\TaskInformationHandlerTest\\\\:\\\\:taskInformationProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Application/Query/TaskInformation/TaskInformationHandlerTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Configuration\\\\\\\\ConfigurationTest\\\\:\\\\:createConfiguration\\\\(\\\\) has parameter \\\\$config with no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Configuration/ConfigurationTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\EnvFlags\\\\\\\\EnvFlagsTest\\\\:\\\\:containerDebugProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/EnvFlags/EnvFlagsTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\EnvFlags\\\\\\\\EnvFlagsTest\\\\:\\\\:statusProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/EnvFlags/EnvFlagsTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to method expects\\\\(\\\\) on an unknown class Symfony\\\\\\\\Component\\\\\\\\Lock\\\\\\\\StoreInterface\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/EventRunnerTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\EventTest\\\\:\\\\:deprecatedEveryProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/EventTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\EventTest\\\\:\\\\:everyMethodProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/EventTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\EventTest\\\\:\\\\:hourlyAtInvalidProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/EventTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Filesystem\\\\\\\\FilesystemTest\\\\:\\\\:fileExistsProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Filesystem/FilesystemTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Finder\\\\\\\\FinderTest\\\\:\\\\:tasksProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Finder/FinderTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\EnabledLoggerDecoratorTest\\\\:\\\\:disabledChannelProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Infrastructure/Psr/Logger/EnabledLoggerDecoratorTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Infrastructure\\\\\\\\Psr\\\\\\\\Logger\\\\\\\\EnabledLoggerDecoratorTest\\\\:\\\\:enabledChannelProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Infrastructure/Psr/Logger/EnabledLoggerDecoratorTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$config of class Crunz\\\\\\\\Tests\\\\\\\\TestCase\\\\\\\\FakeConfiguration constructor expects array\\\\<int\\\\|string, array\\\\|bool\\\\|string\\\\|null\\\\>, array\\\\<string, mixed\\\\> given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Logger/LoggerFactoryTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Output\\\\\\\\OutputFactoryTest\\\\:\\\\:inputProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Output/OutputFactoryTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Call to function is_string\\\\(\\\\) with string will always evaluate to true\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Pingable.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Pinger\\\\\\\\PingableTest\\\\:\\\\:nonStringProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Pinger/PingableTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$url of method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Pingable\\\\:\\\\:pingBefore\\\\(\\\\) expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Pinger/PingableTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$url of method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Pingable\\\\:\\\\:thenPing\\\\(\\\\) expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Pinger/PingableTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Task\\\\\\\\TaskNumberTest\\\\:\\\\:nonNumericProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Task/TaskNumberTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Task\\\\\\\\TaskNumberTest\\\\:\\\\:nonStringValueProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Task/TaskNumberTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Method Crunz\\\\\\\\Tests\\\\\\\\Unit\\\\\\\\Task\\\\\\\\TaskNumberTest\\\\:\\\\:numericValueProvider\\\\(\\\\) return type has no value type specified in iterable type array\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Task/TaskNumberTest.php\n\n\t\t-\n\t\t\tmessage: \"#^Parameter \\\\#1 \\\\$value of static method Crunz\\\\\\\\Task\\\\\\\\TaskNumber\\\\:\\\\:fromString\\\\(\\\\) expects string, mixed given\\\\.$#\"\n\t\t\tcount: 1\n\t\t\tpath: tests/Unit/Task/TaskNumberTest.php\n"
  },
  {
    "path": "phpstan.neon",
    "content": "parameters:\n    level: 9\n    reportUnmatchedIgnoredErrors: false\n    inferPrivatePropertyTypeFromConstructor: true\n    ignoreErrors:\n        -\n            message: '#Variable \\$container might not be defined#'\n            path: config/services.php\n        -\n            message: '#Call to an undefined method Symfony\\\\Component\\\\Config\\\\Definition\\\\Builder\\\\NodeDefinition::children\\(\\)#'\n            path: src/Configuration/Definition.php\n        -\n            message: '#Variable \\$configFile might not be defined#'\n            path: src/Configuration/ConfigurationParser.php\n        -\n            message: '#Call to an undefined method Crunz\\\\Event::DummyFrequency\\(\\)#'\n            path: src/Stubs/BasicTask.php\n        -\n            message: '#Parameter \\#1 \\$command of static method Symfony\\\\Component\\\\Process\\\\Process::fromShellCommandline\\(\\) expects string#'\n            path: src/Process/Process.php\n        -\n            message: '#Result of#'\n            path: src/Event.php\n        -\n            message: '#Parameter \\#1 \\$store of class#'\n            path: src/Event.php\n        -\n            message: '#CrunzContainer#'\n            path: src/Application.php\n        -\n            message: '#Parameter \\#2 \\$currentTime#'\n            path: src/Infrastructure/Dragonmantank/CronExpression/DragonmantankCronExpression.php\n\nincludes:\n    - phpstan-baseline.neon\n    - vendor/phpstan/phpstan-phpunit/extension.neon\n    - vendor/phpstan/phpstan-phpunit/rules.neon\n    - vendor/phpstan/phpstan-strict-rules/rules.neon\n"
  },
  {
    "path": "phpunit.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<phpunit\n    xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n    xsi:noNamespaceSchemaLocation=\"https://schema.phpunit.de/9.5/phpunit.xsd\"\n    bootstrap=\"bootstrap.php\"\n    backupGlobals=\"false\"\n    beStrictAboutCoversAnnotation=\"true\"\n    beStrictAboutOutputDuringTests=\"true\"\n    beStrictAboutTestsThatDoNotTestAnything=\"true\"\n    beStrictAboutTodoAnnotatedTests=\"true\"\n    verbose=\"true\"\n    colors=\"true\"\n>\n    <coverage processUncoveredFiles=\"true\">\n        <include>\n            <directory suffix=\".php\">src</directory>\n        </include>\n    </coverage>\n\n    <testsuites>\n        <testsuite name=\"EndToEnd\">\n            <directory suffix=\"Test.php\">tests/EndToEnd</directory>\n        </testsuite>\n        <testsuite name=\"Integration\">\n            <directory suffix=\"Test.php\">tests/Functional</directory>\n        </testsuite>\n        <testsuite name=\"Unit\">\n            <directory suffix=\"Test.php\">tests/Unit</directory>\n        </testsuite>\n    </testsuites>\n\n    <listeners>\n        <listener class=\"Symfony\\Bridge\\PhpUnit\\SymfonyTestsListener\"/>\n    </listeners>\n</phpunit>\n"
  },
  {
    "path": "resources/config/crunz.yml",
    "content": "# Crunz Configuration Settings\n\n# This option defines where the task files and\n# directories reside.\n# The path is relative to this config file.\n# Trailing slashes will be ignored.\nsource: tasks\n\n# The suffix is meant to target the task files inside the \":source\" directory.\n# Please note if you change this value, you need\n# to make sure all the existing tasks files are renamed accordingly.\nsuffix: Tasks.php\n\n# Timezone is used to calculate task run time\n# This option is very important and not setting it is deprecated\n# and will result in exception in 2.0 version.\ntimezone: ~\n\n# This option define which timezone should be used for log files\n# If false, system default timezone will be used\n# If true, the timezone in config file that is used to calculate task run time will be used\ntimezone_log: false\n\n# By default the errors are not logged by Crunz\n# You may set the value to true for logging the errors\nlog_errors: false\n\n# This is the absolute path to the errors' log file\n# You need to make sure you have the required permission to write to this file though.\nerrors_log_file: ~\n\n# By default the output is not logged as they are redirected to the\n# null output.\n# Set this to true if you want to keep the outputs\nlog_output: false\n\n# This is the absolute path to the global output log file\n# The events which have dedicated log files (defined with them), won't be\n# logged to this file though.\noutput_log_file: ~\n\n# By default line breaks in logs aren't allowed.\n# Set the value to true to allow them.\nlog_allow_line_breaks: false\n\n# By default empty context arrays are shown in the log.\n# Set the value to true to remove them.\nlog_ignore_empty_context: false\n\n# This option determines whether the output should be emailed or not.\nemail_output: false\n\n# This option determines whether the error messages should be emailed or not.\nemail_errors: false\n\n# Global Swift Mailer settings\nmailer:\n    # Possible values: smtp, mail, and sendmail\n    transport: smtp\n    recipients:\n    sender_name:\n    sender_email:\n\n\n# SMTP settings\nsmtp:\n    host: ~\n    port: ~\n    username: ~\n    password: ~\n    encryption: ~\n"
  },
  {
    "path": "src/Application/Cron/CronExpressionFactoryInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Cron;\n\ninterface CronExpressionFactoryInterface\n{\n    public function createFromString(string $cronExpression): CronExpressionInterface;\n}\n"
  },
  {
    "path": "src/Application/Cron/CronExpressionInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Cron;\n\ninterface CronExpressionInterface\n{\n    /** @return \\DateTimeImmutable[] */\n    public function multipleRunDates(\n        int $total,\n        \\DateTimeImmutable $now,\n        ?\\DateTimeZone $timeZone = null,\n    ): array;\n}\n"
  },
  {
    "path": "src/Application/Query/TaskInformation/TaskInformation.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Query\\TaskInformation;\n\nuse Crunz\\Task\\TaskNumber;\n\nfinal class TaskInformation\n{\n    public function __construct(private readonly TaskNumber $taskNumber)\n    {\n    }\n\n    public function taskNumber(): TaskNumber\n    {\n        return $this->taskNumber;\n    }\n}\n"
  },
  {
    "path": "src/Application/Query/TaskInformation/TaskInformationHandler.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Query\\TaskInformation;\n\nuse Crunz\\Application\\Cron\\CronExpressionFactoryInterface;\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Event;\nuse Crunz\\Schedule\\ScheduleFactory;\nuse Crunz\\Task\\CollectionInterface;\nuse Crunz\\Task\\LoaderInterface;\nuse Crunz\\Task\\Timezone;\n\nfinal class TaskInformationHandler\n{\n    public function __construct(\n        private readonly Timezone $timezone,\n        private readonly ConfigurationInterface $configuration,\n        private readonly CollectionInterface $taskCollection,\n        private readonly LoaderInterface $taskLoader,\n        private readonly ScheduleFactory $scheduleFactory,\n        private readonly CronExpressionFactoryInterface $cronExpressionFactory,\n    ) {\n    }\n\n    public function handle(TaskInformation $taskInformation): TaskInformationView\n    {\n        $source = $this->configuration\n            ->getSourcePath()\n        ;\n        /** @var \\SplFileInfo[] $files */\n        $files = $this->taskCollection\n            ->all($source)\n        ;\n\n        // List of schedules\n        $schedules = $this->taskLoader\n            ->load(...\\array_values($files))\n        ;\n\n        $timezoneForComparisons = $this->timezone\n            ->timezoneForComparisons()\n        ;\n        $event = $this->scheduleFactory\n            ->singleTask($taskInformation->taskNumber(), ...$schedules)\n        ;\n\n        $cronExpression = $this->cronExpressionFactory\n            ->createFromString($event->getExpression())\n        ;\n        $nextRunTimezone = $timezoneForComparisons;\n        $eventProperties = $this->getEventProperties($event, ['timezone', 'preventOverlapping']);\n        $eventTimezone = $eventProperties['timezone'];\n        if (\\is_string($eventTimezone)) {\n            $eventTimezone = new \\DateTimeZone($eventTimezone);\n            $nextRunTimezone = $eventTimezone;\n        }\n\n        $nextRuns = $cronExpression->multipleRunDates(\n            5,\n            new \\DateTimeImmutable(),\n            $nextRunTimezone\n        );\n\n        return new TaskInformationView(\n            $event->getCommand(),\n            $event->description ?? '',\n            $event->getExpression(),\n            \\filter_var($eventProperties['preventOverlapping'] ?? false, FILTER_VALIDATE_BOOLEAN),\n            $eventTimezone,\n            $timezoneForComparisons,\n            ...$nextRuns\n        );\n    }\n\n    /**\n     * @param string[] $properties\n     *\n     * @return array<string,mixed>\n     */\n    private function getEventProperties(Event $event, array $properties): array\n    {\n        $propertiesExtractor = function () use ($properties, $event): array {\n            $values = [];\n            foreach ($properties as $property) {\n                if (!\\property_exists($event, $property)) {\n                    $class = $event::class;\n\n                    throw new \\RuntimeException(\"Property '{$property}' doesn't exists in '{$class}' class.\");\n                }\n\n                $values[$property] = $this->{$property};\n            }\n\n            return $values;\n        };\n\n        return $propertiesExtractor->bindTo($event, Event::class)();\n    }\n}\n"
  },
  {
    "path": "src/Application/Query/TaskInformation/TaskInformationView.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Query\\TaskInformation;\n\nfinal class TaskInformationView\n{\n    /** @var \\DateTimeImmutable[] */\n    private readonly array $nextRuns;\n\n    public function __construct(\n        private readonly string|object $command,\n        private readonly string $description,\n        private readonly string $cronExpression,\n        private readonly bool $preventOverlapping,\n        private readonly ?\\DateTimeZone $timeZone,\n        private readonly \\DateTimeZone $configTimeZone,\n        \\DateTimeImmutable ...$nextRuns,\n    ) {\n        $this->nextRuns = $nextRuns;\n    }\n\n    public function command(): string|object\n    {\n        return $this->command;\n    }\n\n    public function description(): string\n    {\n        return $this->description;\n    }\n\n    public function cronExpression(): string\n    {\n        return $this->cronExpression;\n    }\n\n    public function timeZone(): ?\\DateTimeZone\n    {\n        return $this->timeZone;\n    }\n\n    public function configTimeZone(): \\DateTimeZone\n    {\n        return $this->configTimeZone;\n    }\n\n    /** @return \\DateTimeImmutable[] */\n    public function nextRuns(): array\n    {\n        return $this->nextRuns;\n    }\n\n    public function preventOverlapping(): bool\n    {\n        return $this->preventOverlapping;\n    }\n}\n"
  },
  {
    "path": "src/Application/Service/ClosureSerializerInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Service;\n\ninterface ClosureSerializerInterface\n{\n    public function serialize(\\Closure $closure): string;\n\n    public function unserialize(string $serializedClosure): \\Closure;\n\n    public function closureCode(\\Closure $closure): string;\n}\n"
  },
  {
    "path": "src/Application/Service/ConfigurationInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Service;\n\ninterface ConfigurationInterface\n{\n    /**\n     * Return a parameter based on a key.\n     */\n    public function get(string $key, mixed $default = null): mixed;\n\n    /**\n     * Set a parameter based on a key.\n     */\n    public function withNewEntry(string $key, mixed $value): ConfigurationInterface;\n\n    public function getSourcePath(): string;\n}\n"
  },
  {
    "path": "src/Application/Service/LoggerFactoryInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Application\\Service;\n\nuse Psr\\Log\\LoggerInterface;\n\n/**\n * @experimental\n */\ninterface LoggerFactoryInterface\n{\n    public function create(ConfigurationInterface $configuration): LoggerInterface;\n}\n"
  },
  {
    "path": "src/Application.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz;\n\nuse Crunz\\CacheDirectoryFactory\\CacheDirectoryFactory;\nuse Crunz\\Console\\Command\\ConfigGeneratorCommand;\nuse Crunz\\Console\\Command\\ScheduleListCommand;\nuse Crunz\\Console\\Command\\ScheduleRunCommand;\nuse Crunz\\Console\\Command\\TaskGeneratorCommand;\nuse Crunz\\EnvFlags\\EnvFlags;\nuse Crunz\\Path\\Path;\nuse Crunz\\UserInterface\\Cli\\DebugTaskCommand;\nuse Symfony\\Component\\Config\\ConfigCache;\nuse Symfony\\Component\\Config\\FileLocator;\nuse Symfony\\Component\\Console\\Application as SymfonyApplication;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\nuse Symfony\\Component\\DependencyInjection\\Container;\nuse Symfony\\Component\\DependencyInjection\\ContainerBuilder;\nuse Symfony\\Component\\DependencyInjection\\Dumper\\PhpDumper;\nuse Symfony\\Component\\DependencyInjection\\Loader\\PhpFileLoader;\n\nclass Application extends SymfonyApplication\n{\n    /**\n     * List of commands to register.\n     *\n     * @var class-string[]\n     */\n    private const COMMANDS = [\n        // This command starts the event runner (vendor/bin/crunz schedule:run)\n        // It takes an optional argument which is the source directory for tasks\n        // If the argument is not provided, the default in the configuratrion file\n        // will be considered as the source path\n        ScheduleRunCommand::class,\n\n        // This command (vendor/bin/schedule:list) lists the scheduled events in different task files\n        // Just like schedule:run it gets the :source argument\n        ScheduleListCommand::class,\n\n        // This command generates a task from the command-line\n        // This is often useful when you want to create a task file and start\n        // adding tasks to it.\n        TaskGeneratorCommand::class,\n\n        // The modify the configuration, the user's own copy should be modified\n        // This command creates a configuration file in Crunz installation directory\n        ConfigGeneratorCommand::class,\n\n        // This command is used by Crunz itself for running serialized closures\n        // It accepts an argument which is the serialized form of the closure to run.\n        UserInterface\\Cli\\ClosureRunCommand::class,\n\n        // Debug task command\n        DebugTaskCommand::class,\n    ];\n\n    private Container $container;\n    private readonly EnvFlags $envFlags;\n    private readonly CacheDirectoryFactory $cacheDirectoryFactory;\n\n    public function __construct(string $appName, string $appVersion)\n    {\n        parent::__construct($appName, $appVersion);\n\n        $this->cacheDirectoryFactory = new CacheDirectoryFactory();\n        $this->envFlags = new EnvFlags();\n\n        $this->initializeContainer();\n        $this->registerDeprecationHandler();\n\n        foreach (self::COMMANDS as $commandClass) {\n            /** @var Command $command */\n            $command = $this->container\n                ->get($commandClass)\n            ;\n\n            // @phpstan-ignore function.alreadyNarrowedType (backward compatibility with Symfony < 7.4)\n            if (\\method_exists($this, 'addCommand')) {\n                $this->addCommand($command);\n            } else {\n                $this->add($command);\n            }\n        }\n    }\n\n    public function run(?InputInterface $input = null, ?OutputInterface $output = null): int\n    {\n        if (null === $output) {\n            /** @var OutputInterface $outputObject */\n            $outputObject = $this->container\n                ->get(OutputInterface::class);\n\n            $output = $outputObject;\n        }\n\n        if (null === $input) {\n            /** @var InputInterface $inputObject */\n            $inputObject = $this->container\n                ->get(InputInterface::class);\n\n            $input = $inputObject;\n        }\n\n        return parent::run($input, $output);\n    }\n\n    private function initializeContainer(): void\n    {\n        $containerCacheDirWritable = $this->createBaseCacheDirectory();\n        $isContainerDebugEnabled = $this->envFlags\n            ->isContainerDebugEnabled();\n\n        if ($containerCacheDirWritable) {\n            $class = 'CrunzContainer';\n            $baseClass = 'Container';\n            $cachePath = Path::create(\n                [\n                    $this->getContainerCacheDir(),\n                    \"{$class}.php\",\n                ]\n            );\n            $cache = new ConfigCache($cachePath->toString(), $isContainerDebugEnabled);\n\n            if (!$cache->isFresh()) {\n                $containerBuilder = $this->buildContainer();\n                $containerBuilder->compile();\n\n                $this->dumpContainer(\n                    $cache,\n                    $containerBuilder,\n                    $class,\n                    $baseClass\n                );\n            }\n\n            require_once $cache->getPath();\n\n            $this->container = new $class();\n\n            return;\n        }\n\n        $containerBuilder = $this->buildContainer();\n        $containerBuilder->compile();\n\n        $this->container = $containerBuilder;\n    }\n\n    /**\n     * @return ContainerBuilder\n     *\n     * @throws \\Exception\n     */\n    private function buildContainer()\n    {\n        $containerBuilder = new ContainerBuilder();\n        $configDir = Path::create(\n            [\n                __DIR__,\n                '..',\n                'config',\n            ]\n        );\n\n        $phpLoader = new PhpFileLoader($containerBuilder, new FileLocator($configDir->toString()));\n        $phpLoader->load('services.php');\n\n        return $containerBuilder;\n    }\n\n    private function dumpContainer(\n        ConfigCache $cache,\n        ContainerBuilder $container,\n        string $class,\n        string $baseClass,\n    ): void {\n        $dumper = new PhpDumper($container);\n\n        /** @var string $content */\n        $content = $dumper->dump(\n            [\n                'class' => $class,\n                'base_class' => $baseClass,\n                'file' => $cache->getPath(),\n            ]\n        );\n\n        $cache->write($content, $container->getResources());\n    }\n\n    /**\n     * @return bool\n     */\n    private function createBaseCacheDirectory()\n    {\n        $baseCacheDir = $this->getBaseCacheDir();\n\n        if (!\\is_dir($baseCacheDir)) {\n            $makeDirResult = \\mkdir(\n                $this->getBaseCacheDir(),\n                0777,\n                true\n            );\n\n            return $makeDirResult\n                && \\is_dir($baseCacheDir)\n                && \\is_writable($baseCacheDir)\n            ;\n        }\n\n        return \\is_writable($baseCacheDir);\n    }\n\n    private function getBaseCacheDir(): string\n    {\n        return $this->cacheDirectoryFactory->generate()->toString();\n    }\n\n    /**\n     * @return string\n     */\n    private function getContainerCacheDir()\n    {\n        $containerCacheDir = Path::create(\n            [\n                $this->getBaseCacheDir(),\n                \\get_current_user(),\n                $this->getVersion(),\n            ]\n        );\n\n        return $containerCacheDir->toString();\n    }\n\n    private function registerDeprecationHandler(): void\n    {\n        $isDeprecationHandlerEnabled = $this->envFlags\n            ->isDeprecationHandlerEnabled();\n\n        if (!$isDeprecationHandlerEnabled) {\n            return;\n        }\n\n        /** @var SymfonyStyle $io */\n        $io = $this->container\n            ->get(SymfonyStyle::class);\n\n        \\set_error_handler(\n            static function (\n                int $errorNumber,\n                string $errorString,\n                string $file,\n                int $line,\n            ) use ($io): bool {\n                $io->block(\n                    \"{$errorString} File {$file}, line {$line}\",\n                    'Deprecation',\n                    'bg=yellow;fg=black',\n                    ' ',\n                    true\n                );\n\n                return true;\n            },\n            E_USER_DEPRECATED\n        );\n    }\n}\n"
  },
  {
    "path": "src/CacheDirectoryFactory/CacheDirectoryFactory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\CacheDirectoryFactory;\n\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Path\\Path;\n\nfinal class CacheDirectoryFactory\n{\n    public const CRUNZ_BASE_CACHE_DIR = 'CRUNZ_BASE_CACHE_DIR';\n\n    /**\n     * @throws CrunzException\n     */\n    public function generate(): Path\n    {\n        $cacheDirectory = '.crunz';\n        /** @var false|string $basePath */\n        $basePath = \\getenv(self::CRUNZ_BASE_CACHE_DIR, true);\n        if (false === $basePath) {\n            return Path::fromStrings(\\sys_get_temp_dir(), $cacheDirectory);\n        }\n\n        $basePath = \\ltrim($basePath);\n        if ('' === $basePath) {\n            throw new CrunzException('Crunz base cache directory path is empty.');\n        }\n\n        return Path::fromStrings($basePath, $cacheDirectory);\n    }\n}\n"
  },
  {
    "path": "src/Clock/Clock.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Clock;\n\nfinal class Clock implements ClockInterface\n{\n    public function now(): \\DateTimeImmutable\n    {\n        return new \\DateTimeImmutable();\n    }\n}\n"
  },
  {
    "path": "src/Clock/ClockInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Clock;\n\ninterface ClockInterface\n{\n    public function now(): \\DateTimeImmutable;\n}\n"
  },
  {
    "path": "src/Configuration/ConfigFileNotExistsException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\nuse Crunz\\Exception\\CrunzException;\n\nfinal class ConfigFileNotExistsException extends CrunzException\n{\n    public static function fromFilePath(string $filePath): self\n    {\n        return new self(\"Configuration file '{$filePath}' not exists.\");\n    }\n}\n"
  },
  {
    "path": "src/Configuration/ConfigFileNotReadableException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\nuse Crunz\\Exception\\CrunzException;\n\nfinal class ConfigFileNotReadableException extends CrunzException\n{\n    public static function fromFilePath(string $filePath): self\n    {\n        return new self(\"Config file '{$filePath}' is not readable.\");\n    }\n}\n"
  },
  {
    "path": "src/Configuration/Configuration.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Path\\Path;\n\nfinal class Configuration implements ConfigurationInterface\n{\n    /** @var array<string,mixed> */\n    private $config;\n\n    public function __construct(\n        private readonly ConfigurationParserInterface $configurationParser,\n        private readonly FilesystemInterface $filesystem,\n    ) {\n    }\n\n    /**\n     * Return a parameter based on a key.\n     */\n    public function get(string $key, mixed $default = null): mixed\n    {\n        if (null === $this->config) {\n            $this->config = $this->configurationParser\n                ->parseConfig();\n        }\n\n        if (\\array_key_exists($key, $this->config)) {\n            return $this->config[$key];\n        }\n\n        $parts = \\explode('.', $key);\n\n        $value = $this->config;\n        foreach ($parts as $part) {\n            if (!\\is_array($value) || !\\array_key_exists($part, $value)) {\n                return $default;\n            }\n\n            $value = $value[$part];\n        }\n\n        return $value;\n    }\n\n    /**\n     * Set a parameter based on key/value.\n     */\n    public function withNewEntry(string $key, mixed $value): ConfigurationInterface\n    {\n        $newConfiguration = clone $this;\n\n        if (null === $newConfiguration->config) {\n            $newConfiguration->config = $newConfiguration->configurationParser\n                ->parseConfig();\n        }\n\n        $parts = \\explode('.', $key);\n\n        if (\\count($parts) > 1) {\n            if (\\array_key_exists($parts[0], $newConfiguration->config) && \\is_array($newConfiguration->config[$parts[0]])) {\n                $newConfiguration->config[$parts[0]][$parts[1]] = $value;\n            } else {\n                $newConfiguration->config[$parts[0]] = [$parts[1] => $value];\n            }\n        } else {\n            $newConfiguration->config[$key] = $value;\n        }\n\n        return $newConfiguration;\n    }\n\n    public function getSourcePath(): string\n    {\n        $sourcePath = Path::create(\n            [\n                $this->filesystem\n                    ->getCwd(),\n                $this->get('source', 'tasks'),\n            ]\n        );\n\n        return $sourcePath->toString();\n    }\n}\n"
  },
  {
    "path": "src/Configuration/ConfigurationParser.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\nuse Crunz\\Console\\Command\\ConfigGeneratorCommand;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\nuse Crunz\\Path\\Path;\nuse Symfony\\Component\\Config\\Definition\\ConfigurationInterface;\nuse Symfony\\Component\\Config\\Definition\\Processor;\n\nfinal class ConfigurationParser implements ConfigurationParserInterface\n{\n    public function __construct(\n        private readonly ConfigurationInterface $configurationDefinition,\n        private readonly Processor $definitionProcessor,\n        private readonly FileParser $fileParser,\n        private readonly ConsoleLoggerInterface $consoleLogger,\n        private readonly FilesystemInterface $filesystem,\n    ) {\n    }\n\n    public function parseConfig(): array\n    {\n        $configFile = null;\n        $parsedConfig = [];\n        $configFileParsed = false;\n\n        try {\n            $configFile = $this->configFilePath();\n            $parsedConfig = $this->fileParser\n                ->parse($configFile);\n\n            $configFileParsed = true;\n        } catch (ConfigFileNotExistsException $exception) {\n            $this->consoleLogger\n                ->debug(\"Config file not found, exception message: '<error>{$exception->getMessage()}</error>'.\");\n        } catch (ConfigFileNotReadableException $exception) {\n            $this->consoleLogger\n                ->debug(\"Config file is not readable, exception message: '<error>{$exception->getMessage()}</error>'.\");\n        }\n\n        if (false === $configFileParsed) {\n            $this->consoleLogger\n                ->verbose('Unable to find/parse config file, fallback to default values.');\n        } else {\n            $this->consoleLogger\n                ->verbose(\"Using config file <info>{$configFile}</info>.\");\n        }\n\n        return $this->definitionProcessor\n            ->processConfiguration(\n                $this->configurationDefinition,\n                $parsedConfig\n            );\n    }\n\n    /** @throws ConfigFileNotExistsException */\n    private function configFilePath(): string\n    {\n        $cwd = $this->filesystem\n            ->getCwd();\n        $configPath = Path::fromStrings($cwd ?? '', ConfigGeneratorCommand::CONFIG_FILE_NAME)->toString();\n        $configExists = $this->filesystem\n            ->fileExists($configPath);\n\n        if ($configExists) {\n            return $configPath;\n        }\n\n        throw new ConfigFileNotExistsException(\n            \\sprintf(\n                'Unable to find config file \"%s\".',\n                $configPath\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "src/Configuration/ConfigurationParserInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\ninterface ConfigurationParserInterface\n{\n    /** @return array<string,int|string|array> */\n    public function parseConfig(): array;\n}\n"
  },
  {
    "path": "src/Configuration/Definition.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\nuse Crunz\\Infrastructure\\Psr\\Logger\\PsrStreamLoggerFactory;\nuse Symfony\\Component\\Config\\Definition\\Builder\\TreeBuilder;\nuse Symfony\\Component\\Config\\Definition\\ConfigurationInterface;\n\nclass Definition implements ConfigurationInterface\n{\n    public function getConfigTreeBuilder(): TreeBuilder\n    {\n        $treeBuilder = new TreeBuilder('crunz');\n\n        $rootNode = $treeBuilder->getRootNode();\n        $rootNode\n\n            ->children()\n\n                ->scalarNode('source')\n                    ->cannotBeEmpty()\n                    ->info('path to the tasks directory' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('suffix')\n                    ->defaultValue('Tasks.php')\n                    ->info('The suffix for filenames' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('timezone')\n                    ->info('Timezone used to calculate task run date')\n                ->end()\n\n                ->booleanNode('timezone_log')\n                    ->defaultFalse()\n                    ->info('Whether configured \"timezone\" will be used for logs')\n                ->end()\n\n                ->scalarNode('logger_factory')\n                    ->defaultValue(PsrStreamLoggerFactory::class)\n                    ->cannotBeEmpty()\n                    ->info(\"Class name implementing 'LoggerFactoryInterface'. Use it to provider your own logger.\")\n                ->end()\n\n                ->booleanNode('log_errors')\n                    ->defaultFalse()\n                    ->info('Flag for logging errors' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('errors_log_file')\n                    ->defaultValue('/dev/null')\n                    ->info('Path to errors log' . PHP_EOL)\n                ->end()\n\n                ->booleanNode('log_output')\n                    ->defaultFalse()\n                    ->info('Flag for logging output' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('output_log_file')\n                    ->defaultValue('/dev/null')\n                    ->info('Path to output logs' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('log_allow_line_breaks')\n                    ->defaultFalse()\n                    ->info('Flag for line breaks in logs' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('log_ignore_empty_context')\n                    ->defaultFalse()\n                    ->info('Flag for empty context in logs' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('email_output')\n                    ->defaultFalse()\n                    ->info('Email the event\\'s output' . PHP_EOL)\n                ->end()\n\n                ->scalarNode('email_errors')\n                    ->defaultFalse()\n                    ->info('Notify by email in case of an error' . PHP_EOL)\n                ->end()\n\n                ->arrayNode('mailer')\n\n                    ->children()\n\n                        ->scalarNode('transport')\n                        ->info('The type the Swift transporter' . PHP_EOL)\n                        ->end()\n\n                        ->arrayNode('recipients')\n                        ->prototype('scalar')->end()\n                        ->info('List of the email recipients' . PHP_EOL)\n                        ->end()\n\n                        ->scalarNode('sender_name')\n                        ->info('The sender name' . PHP_EOL)\n                        ->end()\n\n                        ->scalarNode('sender_email')\n                        ->info('The sender email' . PHP_EOL)\n                        ->end()\n\n                    ->end()\n\n                ->end()\n\n                ->arrayNode('smtp')\n\n                    ->children()\n\n                        ->scalarNode('host')\n                        ->info('SMTP host' . PHP_EOL)\n                        ->end()\n\n                        ->scalarNode('port')\n                        ->info('SMTP port' . PHP_EOL)\n                        ->end()\n\n                        ->scalarNode('username')\n                        ->info('SMTP username' . PHP_EOL)\n                        ->end()\n\n                        ->scalarNode('password')\n                        ->info('SMTP password' . PHP_EOL)\n                        ->end()\n\n                        ->scalarNode('encryption')\n                        ->info('SMTP encryption' . PHP_EOL)\n                        ->end()\n\n                    ->end()\n\n                ->end()\n\n            ->end()\n        ;\n\n        return $treeBuilder;\n    }\n}\n"
  },
  {
    "path": "src/Configuration/FileParser.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Configuration;\n\nuse Symfony\\Component\\Yaml\\Yaml;\n\nclass FileParser\n{\n    public function __construct(private readonly Yaml $yamlParser)\n    {\n    }\n\n    /**\n     * @return array<array>\n     *\n     * @throws ConfigFileNotExistsException\n     * @throws ConfigFileNotReadableException\n     */\n    public function parse(string $configPath): array\n    {\n        if (!\\file_exists($configPath)) {\n            throw ConfigFileNotExistsException::fromFilePath($configPath);\n        }\n\n        if (!\\is_readable($configPath)) {\n            throw ConfigFileNotReadableException::fromFilePath($configPath);\n        }\n\n        $yamlParser = $this->yamlParser;\n        $configContent = \\file_get_contents($configPath);\n\n        if (false === $configContent) {\n            throw ConfigFileNotReadableException::fromFilePath($configPath);\n        }\n\n        return [$yamlParser::parse($configContent)];\n    }\n}\n"
  },
  {
    "path": "src/Console/Command/Command.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Console\\Command;\n\nuse Symfony\\Component\\Console\\Command\\Command as BaseCommand;\n\nclass Command extends BaseCommand\n{\n    /** @var array<string|bool|int|float|array|null> */\n    protected $arguments;\n\n    /** @var array<string|bool|int|float|array|null> */\n    protected $options;\n\n    /**\n     * Input object.\n     *\n     * @var \\Symfony\\Component\\Console\\Input\\InputInterface\n     */\n    protected $input;\n\n    /**\n     * output object.\n     *\n     * @var \\Symfony\\Component\\Console\\Output\\OutputInterface\n     */\n    protected $output;\n}\n"
  },
  {
    "path": "src/Console/Command/ConfigGeneratorCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Console\\Command;\n\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Path\\Path;\nuse Crunz\\Timezone\\ProviderInterface;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Question\\Question;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\nuse Symfony\\Component\\Filesystem\\Filesystem;\n\nfinal class ConfigGeneratorCommand extends Command\n{\n    public const CONFIG_FILE_NAME = 'crunz.yml';\n\n    public function __construct(\n        private readonly ProviderInterface $timezoneProvider,\n        private readonly Filesystem $symfonyFilesystem,\n        private readonly FilesystemInterface $filesystem,\n    ) {\n        parent::__construct();\n    }\n\n    /**\n     * Configures the current command.\n     */\n    protected function configure(): void\n    {\n        $this\n            ->setName('publish:config')\n            ->setDescription(\"Generates a config file within the project's root directory.\")\n            ->setHelp(\"This generates a config file in YML format within the project's root directory.\")\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $symfonyStyleIo = new SymfonyStyle($input, $output);\n        $cwd = $this->filesystem\n            ->getCwd();\n        $path = Path::create([$cwd, self::CONFIG_FILE_NAME])->toString();\n        $destination = \\realpath($path) ?: $path;\n        $configExists = $this->filesystem\n            ->fileExists($destination)\n        ;\n\n        $output->writeln(\n            \"<info>Destination config file: '{$destination}'.</info>\",\n            OutputInterface::VERBOSITY_VERBOSE\n        );\n\n        if ($configExists) {\n            $output->writeln(\n                \"<comment>The configuration file already exists at '{$destination}'.</comment>\"\n            );\n\n            return 0;\n        }\n\n        $projectRoot = $this->filesystem\n            ->projectRootDirectory();\n        $srcPath = Path::fromStrings(\n            $projectRoot,\n            'resources',\n            'config',\n            self::CONFIG_FILE_NAME\n        );\n        $src = $srcPath->toString();\n        $output->writeln(\n            \"<info>Source config file: '{$src}'.</info>\",\n            OutputInterface::VERBOSITY_VERBOSE\n        );\n        $defaultTimezone = $this->askForTimezone($symfonyStyleIo);\n        $output->writeln(\n            \"<info>Provided timezone: '{$defaultTimezone}'.</info>\",\n            OutputInterface::VERBOSITY_VERBOSE\n        );\n\n        $this->updateTimezone(\n            $destination,\n            $src,\n            $defaultTimezone\n        );\n\n        $output->writeln('<info>The configuration file was generated successfully.</info>');\n\n        return 0;\n    }\n\n    /**\n     * @return string\n     */\n    protected function askForTimezone(SymfonyStyle $symfonyStyleIo)\n    {\n        $defaultTimezone = $this->timezoneProvider\n            ->defaultTimezone()\n            ->getName()\n        ;\n        $question = new Question(\n            '<question>Please provide default timezone for task run date calculations</question>',\n            $defaultTimezone\n        );\n        $question->setAutocompleterValues(\\DateTimeZone::listIdentifiers());\n        $question->setValidator(\n            static function ($answer) {\n                try {\n                    new \\DateTimeZone($answer);\n                } catch (\\Exception) {\n                    throw new \\Exception(\"Timezone '{$answer}' is not valid. Please provide valid timezone.\");\n                }\n\n                return $answer;\n            }\n        );\n\n        return $symfonyStyleIo->askQuestion($question);\n    }\n\n    private function updateTimezone(\n        string $destination,\n        string $src,\n        string $timezone,\n    ): void {\n        $this->symfonyFilesystem\n            ->dumpFile(\n                $destination,\n                \\str_replace(\n                    'timezone: ~',\n                    \"timezone: {$timezone}\",\n                    $this->filesystem\n                        ->readContent($src)\n                )\n            )\n        ;\n    }\n}\n"
  },
  {
    "path": "src/Console/Command/ScheduleListCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Console\\Command;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Task\\CollectionInterface;\nuse Crunz\\Task\\LoaderInterface;\nuse Crunz\\Task\\WrongTaskInstanceException;\nuse Symfony\\Component\\Console\\Helper\\Table;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass ScheduleListCommand extends \\Symfony\\Component\\Console\\Command\\Command\n{\n    private const FORMAT_TEXT = 'text';\n    private const FORMAT_JSON = 'json';\n    private const FORMATS = [\n        self::FORMAT_TEXT,\n        self::FORMAT_JSON,\n    ];\n\n    public function __construct(\n        private readonly ConfigurationInterface $configuration,\n        private readonly CollectionInterface $taskCollection,\n        private readonly LoaderInterface $taskLoader,\n    ) {\n        parent::__construct();\n    }\n\n    /**\n     * Configures the current command.\n     */\n    protected function configure(): void\n    {\n        $possibleFormats = \\implode('\", \"', self::FORMATS);\n        $this\n            ->setName('schedule:list')\n            ->setDescription('Displays the list of scheduled tasks.')\n            ->setDefinition(\n                [\n                    new InputArgument(\n                        'source',\n                        InputArgument::OPTIONAL,\n                        'The source directory for collecting the tasks.',\n                        $this->configuration\n                            ->getSourcePath()\n                    ),\n                ]\n            )\n            ->addOption(\n                'format',\n                'f',\n                InputOption::VALUE_REQUIRED,\n                \"Tasks list format, possible formats: \\\"{$possibleFormats}\\\".\",\n                self::FORMAT_TEXT,\n            )\n        ;\n    }\n\n    /**\n     * @throws WrongTaskInstanceException\n     */\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        /** @var string $source */\n        $source = $input->getArgument('source');\n        $format = $this->resolveFormat($input);\n        $tasks = $this->tasks($source);\n        if (!\\count($tasks)) {\n            $output->writeln('<comment>No task found!</comment>');\n\n            return 0;\n        }\n\n        $this->printList(\n            $output,\n            $tasks,\n            $format,\n        );\n\n        return 0;\n    }\n\n    /**\n     * @return array<\n     *     int,\n     *     array{\n     *         number: int,\n     *         task: string,\n     *         expression: string,\n     *         command: string,\n     *     },\n     * >\n     */\n    private function tasks(string $source): array\n    {\n        /** @var \\SplFileInfo[] $tasks */\n        $tasks = $this->taskCollection\n            ->all($source)\n        ;\n        $schedules = $this->taskLoader\n            ->load(...\\array_values($tasks))\n        ;\n\n        $tasksList = [];\n        $number = 0;\n\n        foreach ($schedules as $schedule) {\n            $events = $schedule->events();\n            foreach ($events as $event) {\n                $tasksList[] = [\n                    'number' => ++$number,\n                    'task' => $event->description ?? '',\n                    'expression' => $event->getExpression(),\n                    'command' => $event->getCommandForDisplay(),\n                ];\n            }\n        }\n\n        return $tasksList;\n    }\n\n    private function resolveFormat(InputInterface $input): string\n    {\n        /** @var string $format */\n        $format = $input->getOption('format');\n        $isValidFormat = \\in_array(\n            $format,\n            self::FORMATS,\n            true,\n        );\n\n        if (false === $isValidFormat) {\n            throw new CrunzException(\"Format '{$format}' is not supported.\");\n        }\n\n        return $format;\n    }\n\n    /**\n     * @param array<\n     *     int,\n     *     array{\n     *         number: int,\n     *         task: string,\n     *         expression: string,\n     *         command: string,\n     *     },\n     * > $tasks\n     */\n    private function printList(\n        OutputInterface $output,\n        array $tasks,\n        string $format,\n    ): void {\n        switch ($format) {\n            case self::FORMAT_TEXT:\n                $table = new Table($output);\n                $table->setHeaders(\n                    [\n                        '#',\n                        'Task',\n                        'Expression',\n                        'Command to Run',\n                    ]\n                );\n\n                foreach ($tasks as $task) {\n                    $table->addRow($task);\n                }\n\n                $table->render();\n\n                break;\n\n            case self::FORMAT_JSON:\n                $output->writeln(\n                    \\json_encode(\n                        $tasks,\n                        JSON_THROW_ON_ERROR | JSON_PRETTY_PRINT,\n                    ),\n                );\n\n                break;\n\n            default:\n                throw new CrunzException(\"Unable to print list in format '{$format}'.\");\n        }\n    }\n}\n"
  },
  {
    "path": "src/Console/Command/ScheduleRunCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Console\\Command;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\EventRunner;\nuse Crunz\\Schedule;\nuse Crunz\\Task\\CollectionInterface;\nuse Crunz\\Task\\LoaderInterface;\nuse Crunz\\Task\\TaskNumber;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Task\\WrongTaskInstanceException;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass ScheduleRunCommand extends Command\n{\n    public function __construct(\n        private readonly CollectionInterface $taskCollection,\n        private readonly ConfigurationInterface $configuration,\n        private readonly EventRunner $eventRunner,\n        private readonly Timezone $taskTimezone,\n        private readonly Schedule\\ScheduleFactory $scheduleFactory,\n        private readonly LoaderInterface $taskLoader,\n    ) {\n        parent::__construct();\n    }\n\n    /**\n     * Configures the current command.\n     */\n    protected function configure(): void\n    {\n        $this->setName('schedule:run')\n            ->setDescription('Starts the event runner.')\n            ->setDefinition(\n                [\n                    new InputArgument(\n                        'source',\n                        InputArgument::OPTIONAL,\n                        'The source directory for collecting the task files.',\n                        $this->configuration\n                            ->getSourcePath()\n                    ),\n                ]\n            )\n            ->addOption(\n                'force',\n                'f',\n                InputOption::VALUE_NONE,\n                'Run all tasks regardless of configured run time.'\n            )\n            ->addOption(\n                'task',\n                't',\n                InputOption::VALUE_REQUIRED,\n                'Which task to run. Provide task number from <info>schedule:list</info> command.',\n                null\n            )\n           ->setHelp('This command starts the Crunz event runner.');\n    }\n\n    /**\n     * @throws WrongTaskInstanceException\n     */\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $this->arguments = $input->getArguments();\n        $this->options = $input->getOptions();\n        $task = $this->options['task'];\n        /** @var string $source */\n        $source = $input->getArgument('source') ?? '';\n        /** @var \\SplFileInfo[] $files */\n        $files = $this->taskCollection\n            ->all($source)\n        ;\n\n        if (!\\count($files)) {\n            $output->writeln('<comment>No task found! Please check your source path.</comment>');\n\n            return 0;\n        }\n\n        // List of schedules\n        $schedules = $this->taskLoader\n            ->load(...\\array_values($files))\n        ;\n        $tasksTimezone = $this->taskTimezone\n            ->timezoneForComparisons()\n        ;\n\n        // Is specified task should be invoked?\n        if (\\is_string($task)) {\n            $schedules = $this->scheduleFactory\n                ->singleTaskSchedule(TaskNumber::fromString($task), ...$schedules);\n        }\n\n        $forceRun = \\filter_var($this->options['force'] ?? false, FILTER_VALIDATE_BOOLEAN);\n        $schedules = \\array_map(\n            static function (Schedule $schedule) use ($tasksTimezone, $forceRun) {\n                if (false === $forceRun) {\n                    // We keep the events which are due and dismiss the rest.\n                    $schedule->events(\n                        $schedule->dueEvents(\n                            $tasksTimezone\n                        )\n                    );\n                }\n\n                return $schedule;\n            },\n            $schedules\n        );\n        $schedules = \\array_filter(\n            $schedules,\n            static fn (Schedule $schedule): bool => \\count($schedule->events()) > 0\n        );\n\n        if (!\\count($schedules)) {\n            $output->writeln('<comment>No event is due!</comment>');\n\n            return 0;\n        }\n\n        // Running the events\n        $this->eventRunner\n            ->handle($output, $schedules)\n        ;\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/Console/Command/TaskGeneratorCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Console\\Command;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Path\\Path;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Input\\InputOption;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Console\\Question\\Question;\n\nclass TaskGeneratorCommand extends Command\n{\n    /**\n     * Default option values.\n     *\n     * @var array<string,string>\n     */\n    final public const DEFAULTS = [\n        'frequency' => 'everyThirtyMinutes',\n        'constraint' => 'weekdays',\n        'in' => 'path/to/your/command',\n        'run' => 'command/to/execute',\n        'description' => 'Task description',\n        'type' => 'basic',\n    ];\n    /**\n     * Stub content.\n     *\n     * @var string\n     */\n    protected $stub;\n\n    public function __construct(\n        private readonly ConfigurationInterface $config,\n        private readonly FilesystemInterface $filesystem,\n    ) {\n        parent::__construct();\n    }\n\n    /**\n     * Configures the current command.\n     */\n    protected function configure(): void\n    {\n        $this\n            ->setName('make:task')\n            ->setDescription('Generates a task file with one task.')\n            ->setDefinition(\n                [\n                    new InputArgument(\n                        'taskfile',\n                        InputArgument::REQUIRED,\n                        'The task file name'\n                    ),\n                    new InputOption(\n                        'frequency',\n                        'f',\n                        InputOption::VALUE_OPTIONAL,\n                        \"The task's frequency\",\n                        self::DEFAULTS['frequency']\n                    ),\n                    new InputOption(\n                        'constraint',\n                        'c',\n                        InputOption::VALUE_OPTIONAL,\n                        \"The task's constraint\",\n                        self::DEFAULTS['constraint']\n                    ),\n                    new InputOption(\n                        'in',\n                        'i',\n                        InputOption::VALUE_OPTIONAL,\n                        \"The command's path\",\n                        self::DEFAULTS['in']\n                    ),\n                    new InputOption(\n                        'run',\n                        'r',\n                        InputOption::VALUE_OPTIONAL,\n                        \"The task's command\",\n                        self::DEFAULTS['run']\n                    ),\n                    new InputOption(\n                        'description',\n                        'd',\n                        InputOption::VALUE_OPTIONAL,\n                        \"The task's description\",\n                        self::DEFAULTS['description']\n                    ),\n                    new InputOption(\n                        'type',\n                        't',\n                        InputOption::VALUE_OPTIONAL,\n                        'The task type',\n                        self::DEFAULTS['type']\n                    ),\n                ]\n            )\n            ->setHelp('This command makes a task file skeleton.');\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $this->input = $input;\n        $this->output = $output;\n\n        $this->arguments = $input->getArguments();\n        $this->options = $input->getOptions();\n        $this->stub = $this->getStub();\n\n        if ($this->stub) {\n            $this\n                ->replaceFrequency()\n                ->replaceConstraint()\n                ->replaceCommand()\n                ->replacePath()\n                ->replaceDescription()\n            ;\n        }\n\n        if ($this->save()) {\n            $output->writeln('<info>The task file generated successfully</info>');\n        } else {\n            $output->writeln('<comment>There was a problem when generating the file. Please check your command.</comment>');\n        }\n\n        return 0;\n    }\n\n    /**\n     * Save the generate task skeleton into a file.\n     *\n     * @return bool\n     */\n    protected function save()\n    {\n        $filename = Path::create([$this->outputPath(), $this->outputFile()]);\n\n        return (bool) \\file_put_contents($filename->toString(), $this->stub);\n    }\n\n    /**\n     * Ask a question.\n     *\n     * @param string $question\n     *\n     * @return ?string\n     */\n    protected function ask($question)\n    {\n        $helper = $this->getHelper('question');\n        $question = new Question(\"<question>{$question}</question>\");\n\n        return $helper->ask($this->input, $this->output, $question);\n    }\n\n    /**\n     * Return the output path.\n     *\n     * @return string\n     */\n    protected function outputPath()\n    {\n        $source = $this->config\n            ->getSourcePath()\n        ;\n        $destination = $this->ask('Where do you want to save the file? (Press enter for the current directory)');\n        $outputPath = $destination ?? $source;\n\n        if (!\\file_exists($outputPath)) {\n            \\mkdir($outputPath, 0744, true);\n        }\n\n        return $outputPath;\n    }\n\n    /**\n     * Populate the output filename.\n     *\n     * @return string\n     */\n    protected function outputFile()\n    {\n        /** @var string $suffix */\n        $suffix = $this->config\n            ->get('suffix')\n        ;\n        /** @var string $taskFile */\n        $taskFile = $this->arguments['taskfile'];\n\n        return \\preg_replace('/Tasks|\\.php$/', '', $taskFile) . $suffix;\n    }\n\n    /**\n     * Get the task stub.\n     *\n     * @return string\n     */\n    protected function getStub()\n    {\n        $projectRootDirectory = $this->filesystem\n            ->projectRootDirectory();\n        $path = Path::fromStrings(\n            $projectRootDirectory,\n            'src',\n            'Stubs',\n            \\ucfirst($this->type() . 'Task.php')\n        );\n\n        return $this->filesystem\n            ->readContent($path->toString());\n    }\n\n    /**\n     * Get the task type.\n     *\n     * @return string\n     */\n    protected function type()\n    {\n        return $this->options['type'];\n    }\n\n    /**\n     * Replace frequency.\n     */\n    protected function replaceFrequency(): self\n    {\n        $this->stub = \\str_replace('DummyFrequency', \\rtrim($this->options['frequency'], '()'), $this->stub);\n\n        return $this;\n    }\n\n    /**\n     * Replace constraint.\n     */\n    protected function replaceConstraint(): self\n    {\n        $this->stub = \\str_replace('DummyConstraint', \\rtrim($this->options['constraint'], '()'), $this->stub);\n\n        return $this;\n    }\n\n    protected function replaceCommand(): self\n    {\n        $run = $this->optionString('run');\n        $this->stub = \\str_replace('DummyCommand', $run, $this->stub);\n\n        return $this;\n    }\n\n    protected function replacePath(): self\n    {\n        $in = $this->optionString('in');\n        $this->stub = \\str_replace('DummyPath', $in, $this->stub);\n\n        return $this;\n    }\n\n    protected function replaceDescription(): self\n    {\n        $description = $this->optionString('description');\n        $this->stub = \\str_replace('DummyDescription', $description, $this->stub);\n\n        return $this;\n    }\n\n    private function optionString(string $name): string\n    {\n        $option = $this->options[$name] ?? throw new \\RuntimeException(\"Missing option '{$name}'.\");\n        if (false === \\is_string($option)) {\n            throw new \\RuntimeException(\"Option must be of type 'string'.\");\n        }\n\n        return $option;\n    }\n}\n"
  },
  {
    "path": "src/EnvFlags/EnvFlags.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\EnvFlags;\n\nuse Crunz\\Exception\\CrunzException;\n\nfinal class EnvFlags\n{\n    public const DEPRECATION_HANDLER_FLAG = 'CRUNZ_DEPRECATION_HANDLER';\n    public const CONTAINER_DEBUG_FLAG = 'CRUNZ_CONTAINER_DEBUG';\n\n    /** @return bool */\n    public function isDeprecationHandlerEnabled()\n    {\n        $registerHandlerEnv = \\getenv(self::DEPRECATION_HANDLER_FLAG, true);\n        $registerHandler = true;\n\n        if (false !== $registerHandlerEnv) {\n            $registerHandler = \\filter_var($registerHandlerEnv, FILTER_VALIDATE_BOOLEAN);\n        }\n\n        return $registerHandler;\n    }\n\n    /** @return bool */\n    public function isContainerDebugEnabled()\n    {\n        $containerDebugEnv = \\getenv(self::CONTAINER_DEBUG_FLAG, true);\n        $containerDebug = false;\n\n        if (false !== $containerDebugEnv) {\n            $containerDebug = \\filter_var($containerDebugEnv, FILTER_VALIDATE_BOOLEAN);\n        }\n\n        return $containerDebug;\n    }\n\n    /** @throws CrunzException When disabling deprecation handler fails */\n    public function disableContainerDebug(): void\n    {\n        if (false === \\putenv(self::CONTAINER_DEBUG_FLAG . '=0')) {\n            throw new CrunzException('Disabling container debug failed.');\n        }\n    }\n\n    /** @throws CrunzException When enabling deprecation handler fails */\n    public function enableContainerDebug(): void\n    {\n        if (false === \\putenv(self::CONTAINER_DEBUG_FLAG . '=1')) {\n            throw new CrunzException('Enabling container debug failed.');\n        }\n    }\n\n    /** @throws CrunzException When enabling deprecation handler fails */\n    public function enableDeprecationHandler(): void\n    {\n        if (false === \\putenv(self::DEPRECATION_HANDLER_FLAG . '=1')) {\n            throw new CrunzException('Enabling deprecation handler failed.');\n        }\n    }\n\n    /** @throws CrunzException When disabling deprecation handler fails */\n    public function disableDeprecationHandler(): void\n    {\n        if (false === \\putenv(self::DEPRECATION_HANDLER_FLAG . '=0')) {\n            throw new CrunzException('Disabling deprecation handler failed.');\n        }\n    }\n}\n"
  },
  {
    "path": "src/Event.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz;\n\nuse Closure;\nuse Cron\\CronExpression;\nuse Crunz\\Application\\Service\\ClosureSerializerInterface;\nuse Crunz\\Clock\\Clock;\nuse Crunz\\Clock\\ClockInterface;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Exception\\NotImplementedException;\nuse Crunz\\Infrastructure\\Laravel\\LaravelClosureSerializer;\nuse Crunz\\Logger\\Logger;\nuse Crunz\\Path\\Path;\nuse Crunz\\Pinger\\PingableInterface;\nuse Crunz\\Pinger\\PingableTrait;\nuse Crunz\\Process\\Process;\nuse Crunz\\Task\\TaskException;\nuse Symfony\\Component\\Lock\\Exception\\InvalidArgumentException;\nuse Symfony\\Component\\Lock\\Factory;\nuse Symfony\\Component\\Lock\\Lock;\nuse Symfony\\Component\\Lock\\LockFactory;\nuse Symfony\\Component\\Lock\\PersistingStoreInterface;\nuse Symfony\\Component\\Lock\\Store\\FlockStore;\n\nclass Event implements PingableInterface\n{\n    use PingableTrait;\n\n    private const LOCK_TTL = 120; // seconds\n    private const LOCK_REFRESH_THRESHOLD = 30; // seconds\n\n    /**\n     * The location that output should be sent to.\n     *\n     * @var string\n     */\n    public $output = '/dev/null';\n\n    /**\n     * Indicates whether output should be appended.\n     *\n     * @var bool\n     */\n    public $shouldAppendOutput = false;\n\n    /**\n     * The human readable description of the event.\n     *\n     * @var string|null\n     */\n    public $description;\n\n    /**\n     * Event generated output.\n     *\n     * @var string|null\n     */\n    public $outputStream;\n\n    /**\n     * Event personal logger instance.\n     *\n     * @var Logger\n     */\n    public $logger;\n\n    /** @var string|\\Closure */\n    protected $command;\n\n    /**\n     * Process that runs the event.\n     *\n     * @var Process\n     */\n    protected $process;\n\n    /**\n     * The cron expression representing the event's frequency.\n     *\n     * @var string\n     */\n    protected $expression = '* * * * *';\n\n    /**\n     * The timezone the date should be evaluated on.\n     *\n     * @var \\DateTimeZone|string\n     */\n    protected $timezone;\n\n    /**\n     * Datetime or time since the task is evaluated and possibly executed only for display purposes.\n     */\n    protected \\DateTime|string|null $from = null;\n\n    /**\n     * Datetime or time until the task is evaluated and possibly executed only for display purposes.\n     */\n    protected \\DateTime|string|null $to = null;\n\n    /**\n     * The user the command should run as.\n     *\n     * @var string\n     */\n    protected $user;\n\n    /**\n     * The array of filter callbacks.\n     *\n     * @var \\Closure[]\n     */\n    protected $filters = [];\n\n    /**\n     * The array of reject callbacks.\n     *\n     * @var \\Closure[]\n     */\n    protected $rejects = [];\n\n    /**\n     * The array of callbacks to be run before the event is started.\n     *\n     * @var \\Closure[]\n     */\n    protected $beforeCallbacks = [];\n\n    /**\n     * The array of callbacks to be run after the event is finished.\n     *\n     * @var \\Closure[]\n     */\n    protected $afterCallbacks = [];\n\n    /**\n     * Current working directory.\n     *\n     * @var string\n     */\n    protected $cwd;\n\n    /**\n     * Position of cron fields.\n     *\n     * @var array<string,int>\n     */\n    protected $fieldsPosition = [\n        'minute' => 1,\n        'hour' => 2,\n        'day' => 3,\n        'month' => 4,\n        'week' => 5,\n    ];\n\n    /**\n     * Indicates if the command should not overlap itself.\n     */\n    private bool $preventOverlapping = false;\n    /** @var ClockInterface */\n    private static $clock;\n    private static ?ClosureSerializerInterface $closureSerializer = null;\n\n    /**\n     * The symfony lock factory that is used to acquire locks. If the value is null, but preventOverlapping = true\n     * crunz falls back to filesystem locks.\n     */\n    private ?LockFactory $lockFactory = null;\n    /** @var string[] */\n    private array $wholeOutput = [];\n    /** @var Lock */\n    private $lock;\n    /** @var \\Closure[] */\n    private array $errorCallbacks = [];\n\n    /**\n     * Create a new event instance.\n     *\n     * @param string|\\Closure $command\n     * @param string|int      $id\n     */\n    public function __construct(protected $id, $command)\n    {\n        $this->command = $command;\n        $this->output = $this->getDefaultOutput();\n    }\n\n    /**\n     * Change the current working directory.\n     *\n     * @param string $directory\n     *\n     * @return self\n     */\n    public function in($directory)\n    {\n        $this->cwd = $directory;\n\n        return $this;\n    }\n\n    /**\n     * Determine if the event's output is sent to null.\n     *\n     * @return bool\n     */\n    public function nullOutput()\n    {\n        return 'NUL' === $this->output || '/dev/null' === $this->output;\n    }\n\n    /**\n     * Build the command string.\n     *\n     * @return string\n     */\n    public function buildCommand()\n    {\n        $command = '';\n\n        if ($this->cwd) {\n            if ($this->user) {\n                $command .= $this->sudo($this->user);\n            }\n\n            // Support changing drives in Windows\n            $cdParameter = $this->isWindows() ? '/d ' : '';\n            $andSign = $this->isWindows() ? ' &' : ';';\n\n            $command .= \"cd {$cdParameter}{$this->cwd}{$andSign} \";\n        }\n\n        if ($this->user) {\n            $command .= $this->sudo($this->user);\n        }\n\n        $command .= \\is_string($this->command)\n            ? $this->command\n            : $this->serializeClosure($this->command)\n        ;\n\n        return \\trim($command, '& ');\n    }\n\n    /**\n     * Determine whether the passed value is a closure or not.\n     *\n     * @return bool\n     */\n    public function isClosure()\n    {\n        return \\is_object($this->command) && ($this->command instanceof \\Closure);\n    }\n\n    /**\n     * Determine if the given event should run based on the Cron expression.\n     *\n     * @return bool\n     */\n    public function isDue(\\DateTimeZone $timeZone)\n    {\n        return $this->expressionPasses($timeZone) && $this->filtersPass($timeZone);\n    }\n\n    /**\n     * Determine if the filters pass for the event.\n     *\n     * @return bool\n     */\n    public function filtersPass(\\DateTimeZone $timeZone)\n    {\n        $invoker = new Invoker();\n\n        foreach ($this->filters as $callback) {\n            if (!$invoker->call($callback)) {\n                return false;\n            }\n        }\n\n        foreach ($this->rejects as $callback) {\n            if ($invoker->call($callback, [$timeZone])) {\n                return false;\n            }\n        }\n\n        return true;\n    }\n\n    /** @return string */\n    public function wholeOutput()\n    {\n        return \\implode('', $this->wholeOutput);\n    }\n\n    /**\n     * Start the event execution.\n     *\n     * @return int\n     */\n    public function start()\n    {\n        $command = $this->buildCommand();\n        $process = Process::fromStringCommand($command);\n\n        $this->setProcess($process);\n        $this->getProcess()->start(\n            function ($type, $content): void {\n                $this->wholeOutput[] = $content;\n            }\n        );\n\n        if ($this->preventOverlapping) {\n            $this->lock();\n        }\n\n        /** @var int $pid */\n        $pid = $this->getProcess()\n            ->getPid();\n\n        return $pid;\n    }\n\n    /**\n     * The Cron expression representing the event's frequency.\n     *\n     * @throws TaskException\n     */\n    public function cron(string $expression): self\n    {\n        $parts = \\preg_split(\n            '/\\s/',\n            $expression,\n            -1,\n            PREG_SPLIT_NO_EMPTY\n        );\n        $parts = false === $parts\n            ? []\n            : $parts\n        ;\n\n        if (\\count($parts) > 5) {\n            throw new TaskException(\"Expression '{$expression}' has more than five parts and this is not allowed.\");\n        }\n\n        $this->expression = $expression;\n\n        return $this;\n    }\n\n    /**\n     * Schedule the event to run hourly.\n     */\n    public function hourly(): self\n    {\n        return $this->hourlyAt(0);\n    }\n\n    public function hourlyAt(int $minute): self\n    {\n        if ($minute < 0) {\n            throw new CrunzException(\"Minute cannot be lower than '0'.\");\n        }\n\n        if ($minute > 59) {\n            throw new CrunzException(\"Minute cannot be greater than '59'.\");\n        }\n\n        return $this->cron(\"{$minute} * * * *\");\n    }\n\n    /**\n     * Schedule the event to run daily.\n     */\n    public function daily(): self\n    {\n        return $this->cron('0 0 * * *');\n    }\n\n    /**\n     * Schedule the event to run on a certain date.\n     *\n     * @param string $date\n     *\n     * @return $this\n     */\n    public function on($date)\n    {\n        $parsedDate = \\date_parse($date);\n        if (false === $parsedDate) {\n            $parsedDate = [];\n        }\n\n        $segments = \\array_intersect_key($parsedDate, $this->fieldsPosition);\n\n        if ($parsedDate['year']) {\n            $this->skip(static fn () => (int) \\date('Y') !== $parsedDate['year']);\n        }\n\n        foreach ($segments as $key => $value) {\n            if (false !== $value) {\n                $this->spliceIntoPosition($this->fieldsPosition[$key], (string) $value);\n            }\n        }\n\n        return $this;\n    }\n\n    /**\n     * Schedule the command at a given time.\n     *\n     * @param string $time\n     */\n    public function at($time): self\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     */\n    public function dailyAt($time): self\n    {\n        $segments = \\explode(':', $time);\n        $firstSegment = (int) $segments[0];\n        $secondSegment = \\count($segments) > 1\n            ? (int) $segments[1]\n            : '0'\n        ;\n\n        return $this\n            ->spliceIntoPosition(2, (string) $firstSegment)\n            ->spliceIntoPosition(1, (string) $secondSegment)\n        ;\n    }\n\n    /**\n     * Set Working period.\n     *\n     * @param string $from\n     * @param string $to\n     *\n     * @return self\n     */\n    public function between($from, $to)\n    {\n        return $this->from($from)\n                    ->to($to);\n    }\n\n    /**\n     * Check if event should be on.\n     *\n     * @param string $datetime\n     *\n     * @return self\n     */\n    public function from($datetime)\n    {\n        $this->from = $datetime;\n\n        return $this->skip(\n            fn (\\DateTimeZone $timeZone) => $this->notYet($datetime, $timeZone)\n        );\n    }\n\n    /**\n     * Check if event should be off.\n     *\n     * @param string $datetime\n     *\n     * @return self\n     */\n    public function to($datetime)\n    {\n        $this->to = $datetime;\n\n        return $this->skip(\n            fn (\\DateTimeZone $timeZone) => $this->past($datetime, $timeZone),\n        );\n    }\n\n    /**\n     * Schedule the event to run twice daily.\n     *\n     * @param int $first\n     * @param int $second\n     */\n    public function twiceDaily($first = 1, $second = 13): self\n    {\n        $hours = $first . ',' . $second;\n\n        return $this\n            ->spliceIntoPosition(1, '0')\n            ->spliceIntoPosition(2, $hours)\n        ;\n    }\n\n    /**\n     * Schedule the event to run only on weekdays.\n     */\n    public function weekdays(): self\n    {\n        return $this->spliceIntoPosition(5, '1-5');\n    }\n\n    /**\n     * Schedule the event to run only on Mondays.\n     */\n    public function mondays(): self\n    {\n        return $this->days(1);\n    }\n\n    /**\n     * Schedule the event to run only on Tuesdays.\n     */\n    public function tuesdays(): self\n    {\n        return $this->days(2);\n    }\n\n    /**\n     * Schedule the event to run only on Wednesdays.\n     */\n    public function wednesdays(): self\n    {\n        return $this->days(3);\n    }\n\n    /**\n     * Schedule the event to run only on Thursdays.\n     */\n    public function thursdays(): self\n    {\n        return $this->days(4);\n    }\n\n    /**\n     * Schedule the event to run only on Fridays.\n     */\n    public function fridays(): self\n    {\n        return $this->days(5);\n    }\n\n    /**\n     * Schedule the event to run only on Saturdays.\n     */\n    public function saturdays(): self\n    {\n        return $this->days(6);\n    }\n\n    /**\n     * Schedule the event to run only on Sundays.\n     */\n    public function sundays(): self\n    {\n        return $this->days(0);\n    }\n\n    /**\n     * Schedule the event to run weekly.\n     */\n    public function weekly(): self\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 string $time\n     */\n    public function weeklyOn(int|string $day, $time = '0:0'): self\n    {\n        $this->dailyAt($time);\n\n        return $this->spliceIntoPosition(5, (string) $day);\n    }\n\n    /**\n     * Schedule the event to run monthly.\n     */\n    public function monthly(): self\n    {\n        return $this->cron('0 0 1 * *');\n    }\n\n    /**\n     * Schedule the event to run quarterly.\n     */\n    public function quarterly(): self\n    {\n        return $this->cron('0 0 1 */3 *');\n    }\n\n    /**\n     * Schedule the event to run yearly.\n     */\n    public function yearly(): self\n    {\n        return $this->cron('0 0 1 1 *');\n    }\n\n    /**\n     * Set the days of the week the command should run on.\n     */\n    public function days(mixed $days): self\n    {\n        $days = \\is_array($days) ? $days : \\func_get_args();\n\n        return $this->spliceIntoPosition(5, \\implode(',', $days));\n    }\n\n    /**\n     * Set hour for the cron job.\n     */\n    public function hour(mixed $value): self\n    {\n        $value = \\is_array($value) ? $value : \\func_get_args();\n\n        return $this->spliceIntoPosition(2, \\implode(',', $value));\n    }\n\n    /**\n     * Set minute for the cron job.\n     */\n    public function minute(mixed $value): self\n    {\n        $value = \\is_array($value) ? $value : \\func_get_args();\n\n        return $this->spliceIntoPosition(1, \\implode(',', $value));\n    }\n\n    /**\n     * Set hour for the cron job.\n     */\n    public function dayOfMonth(mixed $value): self\n    {\n        $value = \\is_array($value) ? $value : \\func_get_args();\n\n        return $this->spliceIntoPosition(3, \\implode(',', $value));\n    }\n\n    /**\n     * Set hour for the cron job.\n     */\n    public function month(mixed $value): self\n    {\n        $value = \\is_array($value) ? $value : \\func_get_args();\n\n        return $this->spliceIntoPosition(4, \\implode(',', $value));\n    }\n\n    /**\n     * Set hour for the cron job.\n     */\n    public function dayOfWeek(mixed $value): self\n    {\n        $value = \\is_array($value) ? $value : \\func_get_args();\n\n        return $this->spliceIntoPosition(5, \\implode(',', $value));\n    }\n\n    /**\n     * Set the timezone the date should be evaluated on.\n     *\n     * @return $this\n     */\n    public function timezone(\\DateTimeZone|string $timezone)\n    {\n        $this->timezone = $timezone;\n\n        return $this;\n    }\n\n    /**\n     * Set which user the command should run as.\n     *\n     * @param string $user\n     *\n     * @return $this\n     */\n    public function user($user)\n    {\n        if ($this->isWindows()) {\n            throw new NotImplementedException('Changing user on Windows is not implemented.');\n        }\n\n        $this->user = $user;\n\n        return $this;\n    }\n\n    /**\n     * Do not allow the event to overlap each other.\n     *\n     * By default, the lock is acquired through file system locks. Alternatively, you can pass a symfony lock store\n     * that will be responsible for the locking.\n     *\n     * @param PersistingStoreInterface|object $store\n     *\n     * @return $this\n     */\n    public function preventOverlapping(?object $store = null)\n    {\n        if (null !== $store && !($store instanceof PersistingStoreInterface)) {\n            $expectedClass = PersistingStoreInterface::class;\n            $actualClass = $store::class;\n\n            throw new \\RuntimeException(\n                \"Instance of '{$expectedClass}' is expected, '{$actualClass}' provided\"\n            );\n        }\n\n        $lockStore = $store ?: $this->createDefaultLockStore();\n        $this->preventOverlapping = true;\n        $this->lockFactory = new LockFactory($lockStore);\n\n        // Skip the event if it's locked (processing)\n        $this->skip(function () {\n            $lock = $this->createLockObject();\n            $lock->acquire();\n\n            return !$lock->isAcquired();\n        });\n\n        $releaseCallback = function (): void {\n            $this->releaseLock();\n        };\n\n        // Delete the lock file when the event is completed\n        $this->after($releaseCallback);\n        // Or on error\n        $this->addErrorCallback($releaseCallback);\n\n        return $this;\n    }\n\n    /**\n     * Register a callback to further filter the schedule.\n     *\n     * @return $this\n     */\n    public function when(\\Closure $callback)\n    {\n        $this->filters[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Register a callback to further filter the schedule.\n     *\n     * @return $this\n     */\n    public function skip(\\Closure $callback)\n    {\n        $this->rejects[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Send the output of the command to a given location.\n     *\n     * @param string $location\n     * @param bool   $append\n     *\n     * @return $this\n     */\n    public function sendOutputTo($location, $append = false)\n    {\n        $this->output = $location;\n\n        $this->shouldAppendOutput = $append;\n\n        return $this;\n    }\n\n    /**\n     * Append the output of the command to a given location.\n     *\n     * @param string $location\n     *\n     * @return $this\n     */\n    public function appendOutputTo($location)\n    {\n        return $this->sendOutputTo($location, true);\n    }\n\n    /**\n     * Register a callback to be called before the operation.\n     *\n     * @return $this\n     */\n    public function before(\\Closure $callback)\n    {\n        $this->beforeCallbacks[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Register a callback to be called after the operation.\n     *\n     * @return $this\n     */\n    public function after(\\Closure $callback)\n    {\n        return $this->then($callback);\n    }\n\n    /**\n     * Register a callback to be called after the operation.\n     *\n     * @return $this\n     */\n    public function then(\\Closure $callback)\n    {\n        $this->afterCallbacks[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Set the human-friendly description of the event.\n     *\n     * @param string $description\n     *\n     * @return $this\n     */\n    public function name($description)\n    {\n        return $this->description($description);\n    }\n\n    /**\n     * Return the event's process.\n     *\n     * @return Process $process\n     */\n    public function getProcess()\n    {\n        return $this->process;\n    }\n\n    /**\n     * Set the human-friendly description of the event.\n     *\n     * @param string $description\n     *\n     * @return $this\n     */\n    public function description($description)\n    {\n        $this->description = $description;\n\n        return $this;\n    }\n\n    /**\n     * Another way to the frequency of the cron job.\n     *\n     * @param string         $unit\n     * @param float|int|null $value\n     */\n    public function every($unit = null, $value = null): self\n    {\n        if (null === $unit || !isset($this->fieldsPosition[$unit])) {\n            return $this;\n        }\n\n        $value = (1 === (int) $value) ? '*' : '*/' . $value;\n\n        return $this->spliceIntoPosition($this->fieldsPosition[$unit], $value)\n                    ->applyMask($unit);\n    }\n\n    /**\n     * Return the event's command.\n     */\n    public function getId(): string|int\n    {\n        return $this->id;\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)) {\n            return $this->description;\n        }\n\n        return $this->buildCommand();\n    }\n\n    /**\n     * Get the command for display.\n     *\n     * @return string\n     */\n    public function getCommandForDisplay()\n    {\n        return $this->isClosure() ? 'object(Closure)' : $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    /**\n     * Get the 'from' configuration for the event if present.\n     */\n    public function getFrom(): \\DateTime|string|null\n    {\n        return $this->from;\n    }\n\n    /**\n     * Get the 'to' configuration for the event if present.\n     */\n    public function getTo(): \\DateTime|string|null\n    {\n        return $this->to;\n    }\n\n    /**\n     * Set the event's command.\n     *\n     * @param string $command\n     *\n     * @return $this\n     */\n    public function setCommand($command)\n    {\n        $this->command = $command;\n\n        return $this;\n    }\n\n    /**\n     * Return the event's command.\n     */\n    public function getCommand(): string|\\Closure\n    {\n        return $this->command;\n    }\n\n    /**\n     * Return the current working directory.\n     *\n     * @return string\n     */\n    public function getWorkingDirectory()\n    {\n        return $this->cwd;\n    }\n\n    /**\n     * Return event's full output.\n     *\n     * @return string|null\n     */\n    public function getOutputStream()\n    {\n        return $this->outputStream;\n    }\n\n    /**\n     * Return all registered before callbacks.\n     *\n     * @return \\Closure[]\n     */\n    public function beforeCallbacks()\n    {\n        return $this->beforeCallbacks;\n    }\n\n    /**\n     * Return all registered after callbacks.\n     *\n     * @return \\Closure[]\n     */\n    public function afterCallbacks()\n    {\n        return $this->afterCallbacks;\n    }\n\n    /** @return \\Closure[] */\n    public function errorCallbacks()\n    {\n        return $this->errorCallbacks;\n    }\n\n    /**\n     * If this event is prevented from overlapping, this method should be called regularly to refresh the lock.\n     */\n    public function refreshLock(): void\n    {\n        if (!$this->preventOverlapping) {\n            return;\n        }\n\n        $lock = $this->createLockObject();\n        $remainingLifetime = $lock->getRemainingLifetime();\n\n        // Lock will never expire\n        if (null === $remainingLifetime) {\n            return;\n        }\n\n        $lockRefreshNeeded = $remainingLifetime < self::LOCK_REFRESH_THRESHOLD;\n        if ($lockRefreshNeeded) {\n            $lock->refresh();\n        }\n    }\n\n    public function everyMinute(): self\n    {\n        return $this->cron('* * * * *');\n    }\n\n    public function everyTwoMinutes(): self\n    {\n        return $this->cron('*/2 * * * *');\n    }\n\n    public function everyThreeMinutes(): self\n    {\n        return $this->cron('*/3 * * * *');\n    }\n\n    public function everyFourMinutes(): self\n    {\n        return $this->cron('*/4 * * * *');\n    }\n\n    public function everyFiveMinutes(): self\n    {\n        return $this->cron('*/5 * * * *');\n    }\n\n    public function everyTenMinutes(): self\n    {\n        return $this->cron('*/10 * * * *');\n    }\n\n    public function everyFifteenMinutes(): self\n    {\n        return $this->cron('*/15 * * * *');\n    }\n\n    public function everyThirtyMinutes(): self\n    {\n        return $this->cron('*/30 * * * *');\n    }\n\n    public function everyTwoHours(): self\n    {\n        return $this->cron('0 */2 * * *');\n    }\n\n    public function everyThreeHours(): self\n    {\n        return $this->cron('0 */3 * * *');\n    }\n\n    public function everyFourHours(): self\n    {\n        return $this->cron('0 */4 * * *');\n    }\n\n    public function everySixHours(): self\n    {\n        return $this->cron('0 */6 * * *');\n    }\n\n    /**\n     * Get the symfony lock object for the task.\n     *\n     * @return Lock\n     */\n    protected function createLockObject()\n    {\n        $this->checkLockFactory();\n\n        if (null === $this->lock && null !== $this->lockFactory) {\n            $this->lock = $this->lockFactory\n                ->createLock($this->lockKey(), self::LOCK_TTL);\n        }\n\n        return $this->lock;\n    }\n\n    /**\n     * Release the lock after the command completed.\n     */\n    protected function releaseLock(): void\n    {\n        $this->checkLockFactory();\n\n        $lock = $this->createLockObject();\n        $lock->release();\n    }\n\n    /**\n     * Get the default output depending on the OS.\n     *\n     * @return string\n     */\n    protected function getDefaultOutput()\n    {\n        return (DIRECTORY_SEPARATOR === '\\\\') ? 'NUL' : '/dev/null';\n    }\n\n    /**\n     * Add sudo to the command.\n     *\n     * @param string $user\n     *\n     * @return string\n     */\n    protected function sudo($user)\n    {\n        return \"sudo -u {$user} \";\n    }\n\n    /**\n     * Convert closure to an executable command.\n     *\n     * @return string\n     */\n    protected function serializeClosure(\\Closure $closure)\n    {\n        $closure = $this->closureSerializer()\n            ->serialize($closure)\n        ;\n        $serializedClosure = \\http_build_query([$closure]);\n        $crunzRoot = CRUNZ_BIN;\n\n        return \\escapeshellarg(PHP_BINARY) . ' ' . \\escapeshellarg($crunzRoot) . \" closure:run {$serializedClosure}\";\n    }\n\n    /**\n     * Determine if the Cron expression passes.\n     *\n     * @return bool\n     */\n    protected function expressionPasses(\\DateTimeZone $timeZone)\n    {\n        $now = $this->getClock()\n            ->now();\n        $now = $now->setTimezone($timeZone);\n\n        if ($this->timezone) {\n            $taskTimeZone = \\is_object($this->timezone) && $this->timezone instanceof \\DateTimeZone\n                ? $this->timezone\n                    ->getName()\n                : $this->timezone\n            ;\n\n            $now = $now->setTimezone(\n                new \\DateTimeZone(\n                    $taskTimeZone\n                )\n            );\n        }\n\n        return CronExpression::factory($this->expression)->isDue($now->format('Y-m-d H:i:s'));\n    }\n\n    /**\n     * Check if time hasn't arrived.\n     *\n     * @param string $datetime\n     */\n    protected function notYet($datetime, \\DateTimeZone $timeZone): bool\n    {\n        $timeZonedNow = $this->timeZonedNow($timeZone);\n        $testedDateTime = new \\DateTimeImmutable($datetime, $timeZone);\n\n        return $timeZonedNow < $testedDateTime;\n    }\n\n    /**\n     * Check if the time has passed.\n     *\n     * @param string $datetime\n     */\n    protected function past($datetime, \\DateTimeZone $timeZone): bool\n    {\n        $timeZonedNow = $this->timeZonedNow($timeZone);\n        $testedDateTime = new \\DateTimeImmutable($datetime, $timeZone);\n\n        return $timeZonedNow > $testedDateTime;\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     */\n    protected function spliceIntoPosition($position, $value): self\n    {\n        $segments = \\explode(' ', $this->expression);\n\n        $segments[$position - 1] = $value;\n\n        return $this->cron(\\implode(' ', $segments));\n    }\n\n    /**\n     * Mask a cron expression.\n     *\n     * @param string $unit\n     *\n     * @return self\n     */\n    protected function applyMask($unit)\n    {\n        $cron = \\explode(' ', $this->expression);\n        $mask = ['0', '0', '1', '1', '*', '*'];\n        $fpos = $this->fieldsPosition[$unit] - 1;\n\n        \\array_splice($cron, 0, $fpos, \\array_slice($mask, 0, $fpos));\n\n        return $this->cron(\\implode(' ', $cron));\n    }\n\n    /**\n     * Lock the event.\n     */\n    protected function lock(): void\n    {\n        $lock = $this->createLockObject();\n        $lock->acquire();\n    }\n\n    private function addErrorCallback(\\Closure $closure): void\n    {\n        $this->errorCallbacks[] = $closure;\n    }\n\n    /**\n     * Set the event's process.\n     */\n    private function setProcess(Process $process): void\n    {\n        $this->process = $process;\n    }\n\n    /**\n     * @return FlockStore\n     *\n     * @throws CrunzException\n     */\n    private function createDefaultLockStore()\n    {\n        try {\n            $lockPath = Path::create(\n                [\n                    \\sys_get_temp_dir(),\n                    '.crunz',\n                ]\n            );\n\n            $store = new FlockStore($lockPath->toString());\n        } catch (InvalidArgumentException) {\n            // Fallback to system temp dir\n            $lockPath = Path::create([\\sys_get_temp_dir()]);\n            $store = new FlockStore($lockPath->toString());\n        }\n\n        return $store;\n    }\n\n    private function lockKey(): string\n    {\n        if ($this->isClosure()) {\n            /** @var \\Closure $closure */\n            $closure = $this->command;\n            $command = $this->closureSerializer()\n                ->closureCode($closure)\n            ;\n        } else {\n            $command = $this->buildCommand();\n        }\n\n        return 'crunz-' . \\md5($command);\n    }\n\n    private function checkLockFactory(): void\n    {\n        if (null === $this->lockFactory) {\n            throw new \\BadMethodCallException(\n                'No lock factory. Please call preventOverlapping() first.'\n            );\n        }\n    }\n\n    private function getClock(): ClockInterface\n    {\n        if (null === self::$clock) {\n            self::$clock = new Clock();\n        }\n\n        return self::$clock;\n    }\n\n    private function closureSerializer(): ClosureSerializerInterface\n    {\n        if (null === self::$closureSerializer) {\n            self::$closureSerializer = new LaravelClosureSerializer();\n        }\n\n        return self::$closureSerializer;\n    }\n\n    private function isWindows(): bool\n    {\n        $osCode = \\mb_substr(\n            PHP_OS,\n            0,\n            3\n        );\n\n        return 'WIN' === $osCode;\n    }\n\n    private function timeZonedNow(\\DateTimeZone $timeZone): \\DateTimeImmutable\n    {\n        $clock = $this->getClock();\n        $now = $clock->now();\n\n        return $now->setTimezone($timeZone);\n    }\n}\n"
  },
  {
    "path": "src/EventRunner.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\HttpClient\\HttpClientInterface;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\nuse Crunz\\Logger\\Logger;\nuse Crunz\\Logger\\LoggerFactory;\nuse Crunz\\Pinger\\PingableInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass EventRunner\n{\n    /** @var Schedule[] */\n    protected array $schedules = [];\n    /** @var Logger|null */\n    protected $logger;\n    private ?OutputInterface $output = null;\n\n    public function __construct(\n        protected Invoker $invoker,\n        private readonly ConfigurationInterface $configuration,\n        protected Mailer $mailer,\n        private readonly LoggerFactory $loggerFactory,\n        private readonly HttpClientInterface $httpClient,\n        private readonly ConsoleLoggerInterface $consoleLogger,\n    ) {\n    }\n\n    /** @param Schedule[] $schedules */\n    public function handle(OutputInterface $output, array $schedules = []): void\n    {\n        $this->schedules = $schedules;\n        $this->output = $output;\n\n        foreach ($this->schedules as $schedule) {\n            $this->consoleLogger\n                ->debug(\"Invoke Schedule's ping before\");\n\n            $this->pingBefore($schedule);\n\n            // Running the before-callbacks of the current schedule\n            $this->invoke($schedule->beforeCallbacks());\n\n            $events = $schedule->events();\n            foreach ($events as $event) {\n                $this->start($event);\n            }\n        }\n\n        // Watch events until they are finished\n        $this->manageStartedEvents();\n    }\n\n    protected function start(Event $event): void\n    {\n        $this->logger = $this->loggerFactory\n            ->create()\n        ;\n\n        // if sendOutputTo or appendOutputTo have been specified\n        if (!$event->nullOutput()) {\n            // if sendOutputTo then truncate the log file if it exists\n            if (!$event->shouldAppendOutput) {\n                $f = @\\fopen($event->output, 'r+');\n                if (false !== $f) {\n                    \\ftruncate($f, 0);\n                    \\fclose($f);\n                }\n            }\n            // Create an instance of the Logger specific to the event\n            $event->logger = $this->loggerFactory->createEvent($event->output);\n        }\n\n        $this->consoleLogger\n            ->debug(\"Invoke Event's ping before.\");\n\n        $this->pingBefore($event);\n\n        // Running the before-callbacks\n        $event->outputStream = $this->invoke($event->beforeCallbacks());\n        $event->start();\n    }\n\n    protected function manageStartedEvents(): void\n    {\n        while ($this->schedules) {\n            foreach ($this->schedules as $scheduleKey => $schedule) {\n                $events = $schedule->events();\n                // 10% chance that refresh will be called\n                $refreshLocks = (\\random_int(1, 100) <= 10);\n\n                /** @var Event $event */\n                foreach ($events as $eventKey => $event) {\n                    if ($refreshLocks) {\n                        $event->refreshLock();\n                    }\n\n                    $proc = $event->getProcess();\n                    if ($proc->isRunning()) {\n                        continue;\n                    }\n\n                    $runStatus = '';\n\n                    if ($proc->isSuccessful()) {\n                        $this->consoleLogger\n                            ->debug(\"Invoke Event's ping after.\");\n                        $this->pingAfter($event);\n\n                        $runStatus = '<info>success</info>';\n\n                        $event->outputStream .= $event->wholeOutput();\n                        $event->outputStream .= $this->invoke($event->afterCallbacks());\n\n                        $this->handleOutput($event);\n                    } else {\n                        $runStatus = '<error>fail</error>';\n\n                        // Invoke error callbacks\n                        $this->invoke($event->errorCallbacks());\n                        // Calling registered error callbacks with an instance of $event as argument\n                        $this->invoke($schedule->errorCallbacks(), [$event]);\n                        $this->handleError($event);\n                    }\n\n                    $id = $event->description ?: $event->getId();\n\n                    $this->consoleLogger\n                        ->debug(\"Task <info>{$id}</info> status: {$runStatus}.\");\n\n                    // Dismiss the event if it's finished\n                    $schedule->dismissEvent($eventKey);\n                }\n\n                // If there's no event left for the Schedule instance,\n                // run the schedule's after-callbacks and remove\n                // the Schedule from list of active schedules.                                                                                                                           zzzwwscxqqqAAAQ11\n                if (!\\count($schedule->events())) {\n                    $this->consoleLogger\n                        ->debug(\"Invoke Schedule's ping after.\");\n\n                    $this->pingAfter($schedule);\n                    $this->invoke($schedule->afterCallbacks());\n                    unset($this->schedules[$scheduleKey]);\n                }\n            }\n\n            \\usleep(250000);\n        }\n    }\n\n    /**\n     * @param \\Closure[]         $callbacks\n     * @param array<mixed,mixed> $parameters\n     *\n     * @return string\n     */\n    protected function invoke(array $callbacks = [], array $parameters = [])\n    {\n        $output = '';\n        foreach ($callbacks as $callback) {\n            /** @var string $callResult */\n            $callResult = $this->invoker->call($callback, $parameters, true);\n            // Invoke the callback with buffering enabled\n            $output .= $callResult;\n        }\n\n        return $output;\n    }\n\n    protected function handleOutput(Event $event): void\n    {\n        $logged = false;\n        $logOutput = $this->configuration\n            ->get('log_output')\n        ;\n\n        if (!$event->nullOutput()) {\n            $event->logger->info($this->formatEventOutput($event));\n            $logged = true;\n        }\n\n        if ($logOutput && !$logged) {\n            $this->logger()\n                ->info($this->formatEventOutput($event))\n            ;\n            $logged = true;\n        }\n\n        if (!$logged) {\n            $this->display($event->getOutputStream());\n        }\n\n        $emailOutput = $this->configuration\n            ->get('email_output')\n        ;\n        if ($emailOutput && !empty($event->getOutputStream())) {\n            $this->mailer->send(\n                'Crunz: output for event: ' . ($event->description ?? $event->getId()),\n                $this->formatEventOutput($event)\n            );\n        }\n    }\n\n    protected function handleError(Event $event): void\n    {\n        $logErrors = $this->configuration\n            ->get('log_errors')\n        ;\n        $emailErrors = $this->configuration\n            ->get('email_errors')\n        ;\n\n        if ($logErrors) {\n            $this->logger()\n                ->error($this->formatEventError($event))\n            ;\n        } else {\n            $output = $event->wholeOutput();\n\n            $this->output\n                ?->write(\"<error>{$output}</error>\")\n            ;\n        }\n\n        // Send error as email as configured\n        if ($emailErrors) {\n            $this->mailer->send(\n                'Crunz: reporting error for event:' . ($event->description ?? $event->getId()),\n                $this->formatEventError($event)\n            );\n        }\n    }\n\n    /** @return string */\n    protected function formatEventOutput(Event $event)\n    {\n        return $event->description\n            . '('\n            . $event->getCommandForDisplay()\n            . ') '\n            . PHP_EOL\n            . PHP_EOL\n            . $event->outputStream\n            . PHP_EOL;\n    }\n\n    /** @return string */\n    protected function formatEventError(Event $event)\n    {\n        return $event->description\n            . '('\n            . $event->getCommandForDisplay()\n            . ') '\n            . PHP_EOL\n            . $event->wholeOutput()\n            . PHP_EOL;\n    }\n\n    /** @param string|null $output */\n    protected function display($output): void\n    {\n        $this->output\n            ?->write(\\is_string($output) ? $output : '')\n        ;\n    }\n\n    private function pingBefore(PingableInterface $schedule): void\n    {\n        if (!$schedule->hasPingBefore()) {\n            $this->consoleLogger\n                ->debug('There is no ping before url.');\n\n            return;\n        }\n\n        /** @var non-empty-string $pingBeforeUrl */\n        $pingBeforeUrl = $schedule->getPingBeforeUrl();\n        $this->httpClient\n            ->ping($pingBeforeUrl)\n        ;\n    }\n\n    private function pingAfter(PingableInterface $schedule): void\n    {\n        if (!$schedule->hasPingAfter()) {\n            $this->consoleLogger\n                ->debug('There is no ping after url.');\n\n            return;\n        }\n\n        /** @var non-empty-string $pingAfterUrl */\n        $pingAfterUrl = $schedule->getPingAfterUrl();\n        $this->httpClient\n            ->ping($pingAfterUrl)\n        ;\n    }\n\n    private function logger(): Logger\n    {\n        if (null === $this->logger) {\n            $this->logger = $this->loggerFactory\n                ->create()\n            ;\n        }\n\n        return $this->logger;\n    }\n}\n"
  },
  {
    "path": "src/Exception/CrunzException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Exception;\n\nclass CrunzException extends \\Exception\n{\n}\n"
  },
  {
    "path": "src/Exception/EmptyTimezoneException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Exception;\n\nclass EmptyTimezoneException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Exception/MailerException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Exception;\n\nfinal class MailerException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Exception/NotImplementedException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Exception;\n\nclass NotImplementedException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Exception/TaskNotExistException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Exception;\n\nclass TaskNotExistException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Exception/WrongTaskNumberException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Exception;\n\nclass WrongTaskNumberException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Filesystem/Filesystem.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Filesystem;\n\nuse Crunz\\Path\\Path;\n\nfinal class Filesystem implements FilesystemInterface\n{\n    private ?string $projectRootDir = null;\n\n    public function getCwd()\n    {\n        $cwd = \\getcwd();\n\n        if (false === $cwd) {\n            throw new \\RuntimeException(\"Unable to get 'cwd'.\");\n        }\n\n        return $cwd;\n    }\n\n    public function fileExists($filePath)\n    {\n        return \\file_exists($filePath);\n    }\n\n    public function tempDir()\n    {\n        return \\sys_get_temp_dir();\n    }\n\n    public function removeDirectory($directoryPath, $ignoredPaths = []): void\n    {\n        $ignoredCount = 0;\n        $ignored = [];\n\n        /** @var Path $ignoredPath */\n        foreach ($ignoredPaths as $ignoredPath) {\n            $path = Path::fromStrings($directoryPath, $ignoredPath->toString());\n            $ignored[$path->toString()] = '';\n        }\n\n        $directoryIterator = new \\RecursiveDirectoryIterator($directoryPath, \\FilesystemIterator::SKIP_DOTS);\n        $recursiveIterator = new \\RecursiveIteratorIterator(\n            $directoryIterator,\n            \\RecursiveIteratorIterator::CHILD_FIRST\n        );\n\n        /** @var \\SplFileInfo $path */\n        foreach ($recursiveIterator as $path) {\n            if (\\array_key_exists($path->getPathname(), $ignored)) {\n                ++$ignoredCount;\n\n                continue;\n            }\n\n            $path->isDir() && !$path->isLink()\n                ? \\rmdir($path->getPathname())\n                : \\unlink($path->getPathname())\n            ;\n        }\n\n        if (0 === $ignoredCount) {\n            \\rmdir($directoryPath);\n        }\n    }\n\n    public function dumpFile($filePath, $content): void\n    {\n        $directory = \\pathinfo($filePath, \\PATHINFO_DIRNAME);\n        $this->createDirectory($directory);\n\n        \\file_put_contents($filePath, $content);\n    }\n\n    public function createDirectory($directoryPath): void\n    {\n        if ($this->fileExists($directoryPath)) {\n            return;\n        }\n\n        $created = \\mkdir(\n            $directoryPath,\n            0770,\n            true\n        );\n\n        if (!$created && !\\is_dir($directoryPath)) {\n            throw new \\RuntimeException(\"Directory '{$directoryPath}' was not created.\");\n        }\n    }\n\n    /**\n     * @param string $sourceFile\n     * @param string $targetFile\n     */\n    public function copy($sourceFile, $targetFile): void\n    {\n        \\copy($sourceFile, $targetFile);\n    }\n\n    public function projectRootDirectory()\n    {\n        if (null === $this->projectRootDir) {\n            $dir = $rootDir = \\dirname(__DIR__);\n            $path = Path::fromStrings($dir, 'composer.json');\n\n            while (!\\file_exists($path->toString())) {\n                if ($dir === \\dirname($dir)) {\n                    return $this->projectRootDir = $rootDir;\n                }\n                $dir = \\dirname($dir);\n                $path = Path::fromStrings($dir, 'composer.json');\n            }\n\n            $this->projectRootDir = $dir;\n        }\n\n        return $this->projectRootDir;\n    }\n\n    /**\n     * @param string $filePath\n     *\n     * @return string\n     */\n    public function readContent($filePath)\n    {\n        if (!$this->fileExists($filePath)) {\n            throw new \\RuntimeException(\"File '{$filePath}' doesn't exists.\");\n        }\n\n        $content = \\file_get_contents($filePath);\n\n        if (false === $content) {\n            throw new \\RuntimeException(\"Unable to get contents of file '{$filePath}'.\");\n        }\n\n        return $content;\n    }\n}\n"
  },
  {
    "path": "src/Filesystem/FilesystemInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Filesystem;\n\nuse Crunz\\Path\\Path;\n\ninterface FilesystemInterface\n{\n    /** @return string */\n    public function getCwd();\n\n    /**\n     * @param string $filePath\n     *\n     * @return bool\n     */\n    public function fileExists($filePath);\n\n    /** @return string */\n    public function tempDir();\n\n    /**\n     * @param string $directoryPath\n     * @param Path[] $ignoredPaths\n     */\n    public function removeDirectory($directoryPath, $ignoredPaths = []): void;\n\n    /**\n     * @param string $filePath\n     * @param string $content\n     */\n    public function dumpFile($filePath, $content): void;\n\n    /** @param string $directoryPath */\n    public function createDirectory($directoryPath): void;\n\n    /**\n     * @param string $sourceFile\n     * @param string $targetFile\n     */\n    public function copy($sourceFile, $targetFile): void;\n\n    /** @return string */\n    public function projectRootDirectory();\n\n    /**\n     * @param string $filePath\n     *\n     * @return string\n     */\n    public function readContent($filePath);\n}\n"
  },
  {
    "path": "src/Finder/Finder.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Finder;\n\nuse Crunz\\Path\\Path;\n\nfinal class Finder implements FinderInterface\n{\n    public function find(Path $directory, $suffix)\n    {\n        $quotedSuffix = \\preg_quote($suffix, '/');\n        $directoryIteratorFlags = \\FilesystemIterator::KEY_AS_PATHNAME | \\FilesystemIterator::CURRENT_AS_FILEINFO | \\RecursiveDirectoryIterator::FOLLOW_SYMLINKS;\n        $directoryIterator = new \\RecursiveDirectoryIterator($directory->toString(), $directoryIteratorFlags);\n        $recursiveIterator = new \\RecursiveIteratorIterator($directoryIterator);\n\n        $regexIterator = new \\RegexIterator(\n            $recursiveIterator,\n            \"/^.+{$quotedSuffix}$/i\",\n            \\RecursiveRegexIterator::GET_MATCH\n        );\n\n        /** @var \\SplFileInfo[] $files */\n        $files = \\array_map(\n            static fn (array $file) => new \\SplFileInfo(\\reset($file)),\n            \\iterator_to_array($regexIterator)\n        );\n\n        return $files;\n    }\n}\n"
  },
  {
    "path": "src/Finder/FinderInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Finder;\n\nuse Crunz\\Path\\Path;\n\ninterface FinderInterface\n{\n    /**\n     * @param string $suffix\n     *\n     * @return \\SplFileInfo[]\n     */\n    public function find(Path $directory, $suffix);\n}\n"
  },
  {
    "path": "src/HttpClient/CurlHttpClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\HttpClient;\n\nfinal class CurlHttpClient implements HttpClientInterface\n{\n    public function ping($url): void\n    {\n        $curlResource = \\curl_init();\n        \\curl_setopt(\n            $curlResource,\n            CURLOPT_RETURNTRANSFER,\n            true,\n        );\n        \\curl_setopt(\n            $curlResource,\n            CURLOPT_URL,\n            $url\n        );\n        \\curl_setopt(\n            $curlResource,\n            CURLOPT_CONNECTTIMEOUT,\n            5\n        );\n        \\curl_setopt(\n            $curlResource,\n            CURLOPT_USERAGENT,\n            'Crunz CurlHttpClient'\n        );\n        \\curl_setopt(\n            $curlResource,\n            CURLOPT_FOLLOWLOCATION,\n            true\n        );\n        \\curl_setopt(\n            $curlResource,\n            CURLOPT_NOBODY,\n            true\n        );\n\n        $result = \\curl_exec($curlResource);\n\n        if (false === $result) {\n            $errorMessage = \\curl_error($curlResource);\n\n            throw new HttpClientException(\"Ping failed with message: \\\"{$errorMessage}\\\".\");\n        }\n\n        \\curl_close($curlResource);\n    }\n}\n"
  },
  {
    "path": "src/HttpClient/FallbackHttpClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\HttpClient;\n\nuse Crunz\\Logger\\ConsoleLoggerInterface;\n\nfinal class FallbackHttpClient implements HttpClientInterface\n{\n    /** @var HttpClientInterface|null */\n    private $httpClient;\n\n    public function __construct(\n        private readonly StreamHttpClient $streamHttpClient,\n        private readonly CurlHttpClient $curlHttpClient,\n        private readonly ConsoleLoggerInterface $consoleLogger,\n    ) {\n    }\n\n    public function ping($url): void\n    {\n        $httpClient = $this->chooseHttpClient();\n        $httpClient->ping($url);\n    }\n\n    /** @throws HttpClientException */\n    private function chooseHttpClient(): HttpClientInterface\n    {\n        if (null !== $this->httpClient) {\n            return $this->httpClient;\n        }\n\n        $this->consoleLogger\n            ->debug('Choosing HttpClient implementation.');\n\n        if (\\function_exists('curl_exec')) {\n            $this->httpClient = $this->curlHttpClient;\n\n            $this->consoleLogger\n                ->debug('cURL available, use <info>CurlHttpClient</info>.');\n\n            return $this->httpClient;\n        }\n\n        if ('1' === \\ini_get('allow_url_fopen')) {\n            $this->httpClient = $this->streamHttpClient;\n\n            $this->consoleLogger\n                ->debug(\"'allow_url_fopen' enabled, use <info>StreamHttpClient</info>\");\n\n            return $this->httpClient;\n        }\n\n        $this->consoleLogger\n            ->debug('<error>Choosing HttpClient implementation failed.</error>');\n\n        throw new HttpClientException(\n            \"Unable to choose HttpClient. Enable cURL extension (preferred) or turn on 'allow_url_fopen' in php.ini.\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/HttpClient/HttpClientException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\HttpClient;\n\nuse Crunz\\Exception\\CrunzException;\n\nclass HttpClientException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/HttpClient/HttpClientInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\HttpClient;\n\ninterface HttpClientInterface\n{\n    /**\n     * @param non-empty-string $url\n     *\n     * @throws HttpClientException\n     */\n    public function ping($url): void;\n}\n"
  },
  {
    "path": "src/HttpClient/HttpClientLoggerDecorator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\HttpClient;\n\nuse Crunz\\Logger\\ConsoleLoggerInterface;\n\nfinal class HttpClientLoggerDecorator implements HttpClientInterface\n{\n    public function __construct(private readonly HttpClientInterface $httpClient, private readonly ConsoleLoggerInterface $logger)\n    {\n    }\n\n    public function ping($url): void\n    {\n        $this->logger\n            ->verbose(\"Trying to ping <info>{$url}</info>.\");\n\n        $this->httpClient\n            ->ping($url);\n\n        $this->logger\n            ->verbose(\"Pinging url: <info>{$url}</info> was <info>successful</info>.\");\n    }\n}\n"
  },
  {
    "path": "src/HttpClient/StreamHttpClient.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\HttpClient;\n\nfinal class StreamHttpClient implements HttpClientInterface\n{\n    /**\n     * @param string $url\n     *\n     * @throws HttpClientException\n     */\n    public function ping($url): void\n    {\n        $context = \\stream_context_create(\n            [\n                'http' => [\n                    'user_agent' => 'Crunz StreamHttpClient',\n                    'timeout' => 5,\n                ],\n            ]\n        );\n        $resource = @\\fopen(\n            $url,\n            'rb',\n            false,\n            $context\n        );\n\n        if (false === $resource) {\n            throw new HttpClientException('Ping failed.');\n        }\n\n        \\fclose($resource);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Dragonmantank/CronExpression/DragonmantankCronExpression.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Infrastructure\\Dragonmantank\\CronExpression;\n\nuse Cron\\CronExpression;\nuse Crunz\\Application\\Cron\\CronExpressionInterface;\n\nfinal class DragonmantankCronExpression implements CronExpressionInterface\n{\n    public function __construct(private readonly CronExpression $innerCronExpression)\n    {\n    }\n\n    public function multipleRunDates(int $total, \\DateTimeImmutable $now, ?\\DateTimeZone $timeZone = null): array\n    {\n        $timeZoneNow = null !== $timeZone\n            ? $now->setTimezone($timeZone)\n            : $now\n        ;\n\n        $dates = $this->innerCronExpression\n            ->getMultipleRunDates($total, $timeZoneNow)\n        ;\n\n        return \\array_map(\n            static fn (\\DateTime $runDate): \\DateTimeImmutable => \\DateTimeImmutable::createFromMutable($runDate),\n            $dates\n        );\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Dragonmantank/CronExpression/DragonmantankCronExpressionFactory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Infrastructure\\Dragonmantank\\CronExpression;\n\nuse Cron\\CronExpression;\nuse Crunz\\Application\\Cron\\CronExpressionFactoryInterface;\nuse Crunz\\Application\\Cron\\CronExpressionInterface;\n\nfinal class DragonmantankCronExpressionFactory implements CronExpressionFactoryInterface\n{\n    public function createFromString(string $cronExpression): CronExpressionInterface\n    {\n        $innerCronExpression = CronExpression::factory($cronExpression);\n\n        return new DragonmantankCronExpression($innerCronExpression);\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Laravel/LaravelClosureSerializer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Infrastructure\\Laravel;\n\nuse Crunz\\Application\\Service\\ClosureSerializerInterface;\nuse Laravel\\SerializableClosure\\SerializableClosure;\nuse Laravel\\SerializableClosure\\Support\\ReflectionClosure;\n\nfinal class LaravelClosureSerializer implements ClosureSerializerInterface\n{\n    public function serialize(\\Closure $closure): string\n    {\n        return \\serialize(\n            new SerializableClosure(\n                $closure\n            )\n        );\n    }\n\n    public function unserialize(string $serializedClosure): \\Closure\n    {\n        $wrapper = $this->extractWrapper($serializedClosure);\n\n        return $wrapper->getClosure();\n    }\n\n    public function closureCode(\\Closure $closure): string\n    {\n        $reflector = new ReflectionClosure($closure);\n\n        return $reflector->getCode();\n    }\n\n    private function extractWrapper(string $serializedClosure): SerializableClosure\n    {\n        return \\unserialize(\n            $serializedClosure,\n            ['allowed_classes' => true]\n        );\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Psr/Logger/EnabledLoggerDecorator.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Infrastructure\\Psr\\Logger;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Psr\\Log\\AbstractLogger;\nuse Psr\\Log\\LoggerInterface;\nuse Psr\\Log\\LogLevel;\n\nfinal class EnabledLoggerDecorator extends AbstractLogger\n{\n    public function __construct(\n        private readonly LoggerInterface $decoratedLogger,\n        private readonly ConfigurationInterface $configuration,\n    ) {\n    }\n\n    public function log($level, string|\\Stringable $message, array $context = []): void\n    {\n        $loggingEnabled = true;\n        switch ($level) {\n            case LogLevel::INFO:\n                $loggingEnabled = $this->configuration\n                    ->get('log_output')\n                ;\n\n                break;\n            case LogLevel::ERROR:\n                $loggingEnabled = $this->configuration\n                    ->get('log_errors')\n                ;\n\n                break;\n        }\n\n        if (false === $loggingEnabled) {\n            return;\n        }\n\n        $this->decoratedLogger\n            ->log(\n                $level,\n                $message,\n                $context\n            )\n        ;\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Psr/Logger/PsrStreamLogger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Infrastructure\\Psr\\Logger;\n\nuse Crunz\\Clock\\ClockInterface;\nuse Crunz\\Exception\\CrunzException;\nuse Psr\\Log\\AbstractLogger;\nuse Psr\\Log\\LogLevel;\n\nfinal class PsrStreamLogger extends AbstractLogger\n{\n    private const DATE_FORMAT = 'Y-m-d H:i:s';\n\n    private readonly string $outputStreamPath;\n    private readonly string $errorStreamPath;\n    /** @var resource|null */\n    private $outputHandler;\n    /** @var resource|null */\n    private $errorHandler;\n\n    public function __construct(\n        private readonly \\DateTimeZone $timezone,\n        private readonly ClockInterface $clock,\n        ?string $outputStreamPath,\n        ?string $errorStreamPath,\n        private readonly bool $ignoreEmptyContext = false,\n        private readonly bool $timezoneLog = false,\n        private readonly bool $allowLineBreaks = false,\n    ) {\n        $this->outputStreamPath = $outputStreamPath ?? '';\n        $this->errorStreamPath = $errorStreamPath ?? '';\n    }\n\n    public function __destruct()\n    {\n        $this->closeStream($this->outputHandler);\n        $this->closeStream($this->errorHandler);\n    }\n\n    public function log(\n        $level,\n        string|\\Stringable $message,\n        array $context = [],\n    ): void {\n        $resource = match ($level) {\n            LogLevel::INFO => $this->createInfoHandler(),\n            LogLevel::ERROR => $this->createErrorHandler(),\n            default => null,\n        };\n\n        if (null === $resource) {\n            return;\n        }\n\n        /** @var string $level */\n        $date = $this->formatDate();\n        $levelFormatted = \\mb_strtoupper($level);\n        $extraString = $this->formatContext([]);\n        $contextString = $this->formatContext($context);\n        $formattedMessage = $this->replaceNewlines($message);\n        $record = \"[{$date}] crunz.{$levelFormatted}: {$formattedMessage} {$extraString} {$contextString}\";\n\n        \\fwrite($resource, $record . PHP_EOL);\n    }\n\n    /** @return resource */\n    private function createInfoHandler()\n    {\n        if (null === $this->outputHandler) {\n            $this->outputHandler = $this->initializeHandler($this->outputStreamPath);\n        }\n\n        return $this->outputHandler;\n    }\n\n    /** @return resource */\n    private function createErrorHandler()\n    {\n        if (null === $this->errorHandler) {\n            $this->errorHandler = $this->initializeHandler($this->errorStreamPath);\n        }\n\n        return $this->errorHandler;\n    }\n\n    /** @return resource */\n    private function initializeHandler(string $path)\n    {\n        if ('' === $path) {\n            throw new CrunzException('Stream path cannot be empty.');\n        }\n\n        $directory = $this->dirFromStream($path);\n        if (null !== $directory) {\n            if (\\is_file($directory)) {\n                throw new CrunzException(\n                    \"Unable to create directory '{$directory}', file at this path already exists.\"\n                );\n            }\n\n            if (!\\file_exists($directory)) {\n                \\mkdir(\n                    $directory,\n                    0777,\n                    true\n                );\n            }\n\n            if (!\\is_dir($directory)) {\n                throw new CrunzException(\"Unable to create directory '{$directory}'.\");\n            }\n        }\n\n        $handler = \\fopen($path, 'ab');\n        if (false === $handler) {\n            throw new CrunzException(\"Unable to open stream for path: '{$path}'.\");\n        }\n\n        return $handler;\n    }\n\n    /** @param resource|null $stream */\n    private function closeStream($stream): void\n    {\n        if (!\\is_resource($stream)) {\n            return;\n        }\n\n        \\fclose($stream);\n    }\n\n    private function dirFromStream(string $stream): ?string\n    {\n        $pos = \\mb_strpos($stream, '://');\n        if (false === $pos) {\n            return \\dirname($stream);\n        }\n\n        if (\\str_starts_with($stream, 'file://')) {\n            return \\dirname(\n                \\mb_substr(\n                    $stream,\n                    7\n                )\n            );\n        }\n\n        return null;\n    }\n\n    /** @param array<mixed,mixed> $data */\n    private function formatContext(array $data): string\n    {\n        if ($this->ignoreEmptyContext && empty($data)) {\n            return '';\n        }\n\n        return \\json_encode($data, JSON_THROW_ON_ERROR);\n    }\n\n    private function formatDate(): string\n    {\n        $now = $this->clock\n            ->now()\n        ;\n\n        if ($this->timezoneLog) {\n            $now = $now->setTimezone($this->timezone);\n        }\n\n        return $now->format(self::DATE_FORMAT);\n    }\n\n    private function replaceNewlines(string $message): string\n    {\n        if ($this->allowLineBreaks) {\n            if (\\str_starts_with($message, '{')) {\n                return \\str_replace(\n                    ['\\r', '\\n'],\n                    [\"\\r\", \"\\n\"],\n                    $message\n                );\n            }\n\n            return $message;\n        }\n\n        return \\str_replace(\n            [\n                \"\\r\\n\",\n                \"\\r\",\n                \"\\n\",\n            ],\n            ' ',\n            $message\n        );\n    }\n}\n"
  },
  {
    "path": "src/Infrastructure/Psr/Logger/PsrStreamLoggerFactory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Infrastructure\\Psr\\Logger;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Application\\Service\\LoggerFactoryInterface;\nuse Crunz\\Clock\\ClockInterface;\nuse Crunz\\Task\\Timezone;\nuse Psr\\Log\\LoggerInterface;\n\nfinal class PsrStreamLoggerFactory implements LoggerFactoryInterface\n{\n    public function __construct(private readonly Timezone $timezoneProvider, private readonly ClockInterface $clock)\n    {\n    }\n\n    public function create(ConfigurationInterface $configuration): LoggerInterface\n    {\n        $timezone = $this->timezoneProvider\n            ->timezoneForComparisons()\n        ;\n\n        return new EnabledLoggerDecorator(\n            new PsrStreamLogger(\n                $timezone,\n                $this->clock,\n                $configuration->get('output_log_file'),\n                $configuration->get('errors_log_file'),\n                $configuration->get('log_ignore_empty_context'),\n                $configuration->get('timezone_log'),\n                $configuration->get('log_allow_line_breaks')\n            ),\n            $configuration\n        );\n    }\n}\n"
  },
  {
    "path": "src/Invoker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz;\n\nclass Invoker\n{\n    /**\n     * Call the given Closure with buffering support.\n     *\n     * @param callable           $closure\n     * @param bool               $buffer\n     * @param array<mixed,mixed> $parameters\n     */\n    public function call($closure, array $parameters = [], $buffer = false): mixed\n    {\n        if ($buffer) {\n            \\ob_start();\n        }\n\n        $rslt = \\call_user_func_array($closure, $parameters);\n\n        if ($buffer) {\n            return \\ob_get_clean();\n        }\n\n        return $rslt;\n    }\n}\n"
  },
  {
    "path": "src/Logger/ConsoleLogger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Logger;\n\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\n\nfinal class ConsoleLogger implements ConsoleLoggerInterface\n{\n    public function __construct(private readonly SymfonyStyle $symfonyStyle)\n    {\n    }\n\n    /**\n     * @param string $message\n     */\n    public function normal($message): void\n    {\n        $this->write($message, self::VERBOSITY_NORMAL);\n    }\n\n    /**\n     * @param string $message\n     */\n    public function verbose($message): void\n    {\n        $this->write($message, self::VERBOSITY_VERBOSE);\n    }\n\n    /**\n     * @param string $message\n     */\n    public function veryVerbose($message): void\n    {\n        $this->write($message, self::VERBOSITY_VERY_VERBOSE);\n    }\n\n    /**\n     * Detailed debug information.\n     *\n     * @param string $message\n     */\n    public function debug($message): void\n    {\n        $this->write($message, self::VERBOSITY_DEBUG);\n    }\n\n    /**\n     * @param string $message\n     * @param int    $verbosity\n     */\n    private function write($message, $verbosity): void\n    {\n        $ioVerbosity = $this->symfonyStyle\n            ->getVerbosity();\n\n        if ($ioVerbosity >= $verbosity) {\n            $this->symfonyStyle\n                ->writeln($message);\n        }\n    }\n}\n"
  },
  {
    "path": "src/Logger/ConsoleLoggerInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Logger;\n\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\ninterface ConsoleLoggerInterface\n{\n    public const VERBOSITY_QUIET = OutputInterface::VERBOSITY_QUIET;\n    public const VERBOSITY_NORMAL = OutputInterface::VERBOSITY_NORMAL;\n    public const VERBOSITY_VERBOSE = OutputInterface::VERBOSITY_VERBOSE;\n    public const VERBOSITY_VERY_VERBOSE = OutputInterface::VERBOSITY_VERY_VERBOSE;\n    public const VERBOSITY_DEBUG = OutputInterface::VERBOSITY_DEBUG;\n\n    /**\n     * @param string $message\n     */\n    public function normal($message): void;\n\n    /**\n     * @param string $message\n     */\n    public function verbose($message): void;\n\n    /**\n     * @param string $message\n     */\n    public function veryVerbose($message): void;\n\n    /**\n     * Detailed debug information.\n     *\n     * @param string $message\n     */\n    public function debug($message): void;\n}\n"
  },
  {
    "path": "src/Logger/Logger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Logger;\n\nuse Psr\\Log\\LoggerInterface;\n\nclass Logger\n{\n    public function __construct(private readonly LoggerInterface $psrLogger)\n    {\n    }\n\n    /**\n     * Log any output if output logging is enabled.\n     */\n    public function info(string $message): void\n    {\n        $this->log($message, 'info');\n    }\n\n    /**\n     * Log  the error is error logging is enabled.\n     */\n    public function error(string $message): void\n    {\n        $this->log($message, 'error');\n    }\n\n    private function log(string $content, string $level): void\n    {\n        $this->psrLogger\n            ->log($level, $content)\n        ;\n    }\n}\n"
  },
  {
    "path": "src/Logger/LoggerFactory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Logger;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Application\\Service\\LoggerFactoryInterface;\nuse Crunz\\Clock\\ClockInterface;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Infrastructure\\Psr\\Logger\\PsrStreamLoggerFactory;\nuse Crunz\\Task\\Timezone;\n\nclass LoggerFactory\n{\n    private ?LoggerFactoryInterface $loggerFactory = null;\n\n    public function __construct(\n        private readonly ConfigurationInterface $configuration,\n        private readonly Timezone $timezoneProvider,\n        private readonly ConsoleLoggerInterface $consoleLogger,\n        private readonly ClockInterface $clock,\n    ) {\n    }\n\n    public function create(): Logger\n    {\n        $loggerFactory = $this->loggerFactory();\n        $configuration = $this->configuration;\n        $innerLogger = $loggerFactory->create($configuration);\n\n        return new Logger($innerLogger);\n    }\n\n    public function createEvent(string $output): Logger\n    {\n        $loggerFactory = $this->loggerFactory();\n        $eventConfiguration = $this->configuration->withNewEntry('output_log_file', $output);\n        $innerLogger = $loggerFactory->create($eventConfiguration);\n\n        return new Logger($innerLogger);\n    }\n\n    private function loggerFactory(): LoggerFactoryInterface\n    {\n        return $this->loggerFactory ??= $this->initializeLoggerFactory();\n    }\n\n    private function initializeLoggerFactory(): LoggerFactoryInterface\n    {\n        $timezoneLog = $this->configuration\n            ->get('timezone_log')\n        ;\n\n        if ($timezoneLog) {\n            $timezone = $this->timezoneProvider\n                ->timezoneForComparisons()\n            ;\n\n            $this->consoleLogger\n                ->veryVerbose(\"Timezone for '<info>timezone_log</info>': '<info>{$timezone->getName()}</info>'\")\n            ;\n        }\n\n        $this->loggerFactory = $this->createLoggerFactory(\n            $this->configuration,\n            $this->timezoneProvider,\n            $this->clock\n        );\n\n        return $this->loggerFactory;\n    }\n\n    private function createLoggerFactory(\n        ConfigurationInterface $configuration,\n        Timezone $timezoneProvider,\n        ClockInterface $clock,\n    ): LoggerFactoryInterface {\n        $params = [];\n        $loggerFactoryClass = $configuration->get('logger_factory');\n\n        $this->consoleLogger\n            ->veryVerbose(\"Class for '<info>logger_factory</info>': '<info>{$loggerFactoryClass}</info>'.\")\n        ;\n\n        if (!\\class_exists($loggerFactoryClass)) {\n            throw new CrunzException(\"Class '{$loggerFactoryClass}' does not exists.\");\n        }\n\n        $isPsrStreamLoggerFactory = \\is_a(\n            $loggerFactoryClass,\n            PsrStreamLoggerFactory::class,\n            true\n        );\n        if ($isPsrStreamLoggerFactory) {\n            $params[] = $timezoneProvider;\n            $params[] = $clock;\n        }\n\n        return new $loggerFactoryClass(...$params);\n    }\n}\n"
  },
  {
    "path": "src/Mailer.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Exception\\MailerException;\nuse Symfony\\Component\\Mailer\\Mailer as SymfonyMailer;\nuse Symfony\\Component\\Mailer\\Transport;\nuse Symfony\\Component\\Mime\\Address;\nuse Symfony\\Component\\Mime\\Email;\n\nclass Mailer\n{\n    /** @var SymfonyMailer|null */\n    protected $mailer;\n\n    public function __construct(private readonly ConfigurationInterface $configuration)\n    {\n    }\n\n    /**\n     * Send an email.\n     *\n     * @throws MailerException\n     */\n    public function send(string $subject, string $message): void\n    {\n        $this->getMailer()\n            ->send(\n                $this->getMessage($subject, $message)\n            )\n        ;\n    }\n\n    /**\n     * Return the proper mailer.\n     *\n     * @throws MailerException\n     */\n    private function getMailer(): SymfonyMailer\n    {\n        // If the mailer has already been defined via the constructor, return it.\n        if ($this->mailer) {\n            return $this->mailer;\n        }\n\n        // Get the proper transporter\n        switch ($this->config('mailer.transport')) {\n            case 'smtp':\n                $transport = $this->getSmtpTransport();\n\n                break;\n\n            case 'mail':\n                throw new MailerException(\n                    \"'mail' transport is no longer supported, please use 'smtp' or 'sendmail' transport.\"\n                );\n\n            default:\n                $transport = $this->getSendMailTransport();\n        }\n\n        $this->mailer = new SymfonyMailer($transport);\n\n        return $this->mailer;\n    }\n\n    private function getSmtpTransport(): Transport\\TransportInterface\n    {\n        $host = $this->config('smtp.host');\n        $port = $this->config('smtp.port');\n        $encryption = \\filter_var($this->config('smtp.encryption') ?? true, FILTER_VALIDATE_BOOLEAN);\n        $user = $this->config('smtp.username');\n        $password = $this->config('smtp.password');\n        $encryptionString = $encryption\n            ? 1\n            : 0\n        ;\n        $userPart = null !== $user && null !== $password\n            ? \"{$user}:{$password}@\"\n            : ''\n        ;\n\n        $dsn = \"smtp://{$userPart}{$host}:{$port}?verifyPeer={$encryptionString}\";\n\n        return Transport::fromDsn($dsn);\n    }\n\n    private function getSendMailTransport(): Transport\\TransportInterface\n    {\n        $dsn = 'sendmail://default';\n\n        return Transport::fromDsn($dsn);\n    }\n\n    private function getMessage(string $subject, string $message): Email\n    {\n        $from = new Address($this->config('mailer.sender_email'), $this->config('mailer.sender_name'));\n        $messageObject = new Email();\n        $messageObject\n            ->from($from)\n            ->subject($subject)\n            ->text($message)\n        ;\n        foreach ($this->config('mailer.recipients') ?? [] as $recipient) {\n            $messageObject->addTo($recipient);\n        }\n\n        return $messageObject;\n    }\n\n    private function config(string $key): mixed\n    {\n        return $this->configuration\n            ->get($key)\n        ;\n    }\n}\n"
  },
  {
    "path": "src/Output/OutputFactory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Output;\n\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\ConsoleOutput;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nfinal class OutputFactory\n{\n    public function __construct(private readonly InputInterface $input)\n    {\n    }\n\n    public function createOutput(): OutputInterface\n    {\n        $input = $this->input;\n        $output = new ConsoleOutput();\n\n        if (true === $input->hasParameterOption(['--quiet', '-q'])) {\n            $output->setVerbosity(OutputInterface::VERBOSITY_QUIET);\n        } elseif ($input->hasParameterOption('-vvv') || $input->hasParameterOption('--verbose=3') || 3 === $input->getParameterOption('--verbose')) {\n            $output->setVerbosity(OutputInterface::VERBOSITY_DEBUG);\n        } elseif ($input->hasParameterOption('-vv') || $input->hasParameterOption('--verbose=2') || 2 === $input->getParameterOption('--verbose')) {\n            $output->setVerbosity(OutputInterface::VERBOSITY_VERY_VERBOSE);\n        } elseif ($input->hasParameterOption('-v') || $input->hasParameterOption('--verbose=1') || $input->hasParameterOption('--verbose') || $input->getParameterOption('--verbose')) {\n            $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);\n        }\n\n        return $output;\n    }\n}\n"
  },
  {
    "path": "src/Path/Path.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Path;\n\nuse Crunz\\Exception\\CrunzException;\n\nfinal class Path\n{\n    private function __construct(private readonly string $path)\n    {\n    }\n\n    /**\n     * @param string[] $parts\n     *\n     * @throws CrunzException\n     */\n    public static function create(array $parts): self\n    {\n        if (0 === \\count($parts)) {\n            throw new CrunzException('At least one part expected.');\n        }\n\n        $normalizedPath = \\str_replace(\n            DIRECTORY_SEPARATOR . DIRECTORY_SEPARATOR,\n            DIRECTORY_SEPARATOR,\n            \\implode(DIRECTORY_SEPARATOR, $parts)\n        );\n\n        return new self($normalizedPath);\n    }\n\n    /**\n     * @throws CrunzException\n     */\n    public static function fromStrings(string ...$parts): self\n    {\n        return self::create($parts);\n    }\n\n    public function toString(): string\n    {\n        return $this->path;\n    }\n}\n"
  },
  {
    "path": "src/Pinger/PingableException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Pinger;\n\nuse Crunz\\Exception\\CrunzException;\n\nclass PingableException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Pinger/PingableInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Pinger;\n\ninterface PingableInterface\n{\n    /**\n     * @param string $url\n     *\n     * @return $this\n     */\n    public function pingBefore($url);\n\n    /**\n     * @return bool\n     *\n     * @internal\n     */\n    public function hasPingBefore();\n\n    /**\n     * @param string $url\n     *\n     * @return $this\n     */\n    public function thenPing($url);\n\n    /**\n     * @return bool\n     *\n     * @internal\n     */\n    public function hasPingAfter();\n\n    /**\n     * @return string\n     *\n     * @internal\n     */\n    public function getPingBeforeUrl();\n\n    /**\n     * @return string\n     *\n     * @internal\n     */\n    public function getPingAfterUrl();\n}\n"
  },
  {
    "path": "src/Pinger/PingableTrait.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Pinger;\n\ntrait PingableTrait\n{\n    /** @var string */\n    private $pingBeforeUrl = '';\n    /** @var string */\n    private $pingAfterUrl = '';\n\n    public function pingBefore($url)\n    {\n        $this->checkUrl($url);\n\n        $this->pingBeforeUrl = $url;\n\n        return $this;\n    }\n\n    public function hasPingBefore()\n    {\n        return '' !== $this->pingBeforeUrl;\n    }\n\n    public function thenPing($url)\n    {\n        $this->checkUrl($url);\n\n        $this->pingAfterUrl = $url;\n\n        return $this;\n    }\n\n    public function hasPingAfter()\n    {\n        return '' !== $this->pingAfterUrl;\n    }\n\n    public function getPingBeforeUrl()\n    {\n        if (!$this->hasPingBefore()) {\n            throw new PingableException('PingBeforeUrl is empty.');\n        }\n\n        return $this->pingBeforeUrl;\n    }\n\n    public function getPingAfterUrl()\n    {\n        if (!$this->hasPingAfter()) {\n            throw new PingableException('PingAfterUrl is empty.');\n        }\n\n        return $this->pingAfterUrl;\n    }\n\n    /**\n     * @param string $url\n     *\n     * @throws PingableException\n     */\n    private function checkUrl($url): void\n    {\n        if (!\\is_string($url)) {\n            $type = \\gettype($url);\n            throw new PingableException(\"Url must be of type string, '{$type}' given.\");\n        }\n\n        if ('' === $url) {\n            throw new PingableException('Url cannot be empty.');\n        }\n    }\n}\n"
  },
  {
    "path": "src/Process/Process.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Process;\n\nuse Symfony\\Component\\Process\\Process as SymfonyProcess;\n\n/** @internal */\nfinal class Process\n{\n    /** @param SymfonyProcess|string[] $process */\n    private function __construct(private readonly SymfonyProcess $process)\n    {\n    }\n\n    public static function fromStringCommand(string $command, ?string $cwd = null): self\n    {\n        $process = SymfonyProcess::fromShellCommandline($command, $cwd);\n\n        return new self($process);\n    }\n\n    /** @param string[] $command */\n    public static function fromArrayCommand(array $command): self\n    {\n        $process = new SymfonyProcess($command);\n\n        return new self($process);\n    }\n\n    /** @param callable|null $callback */\n    public function start($callback = null): void\n    {\n        $this->process\n            ->start($callback);\n    }\n\n    public function wait(): void\n    {\n        $this->process\n            ->wait();\n    }\n\n    public function startAndWait(): void\n    {\n        $this->process\n            ->start();\n        $this->process\n            ->wait();\n    }\n\n    /** @param array<string,string> $env */\n    public function setEnv(array $env): void\n    {\n        $this->process\n            ->setEnv($env);\n    }\n\n    public function getPid(): ?int\n    {\n        return $this->process\n            ->getPid();\n    }\n\n    public function isRunning(): bool\n    {\n        return $this->process\n            ->isRunning();\n    }\n\n    public function isSuccessful(): bool\n    {\n        return $this->process\n            ->isSuccessful();\n    }\n\n    public function getOutput(): string\n    {\n        return $this->process\n            ->getOutput();\n    }\n\n    public function errorOutput(): string\n    {\n        return $this->process\n            ->getErrorOutput();\n    }\n\n    public function commandLine(): string\n    {\n        return $this->process\n            ->getCommandLine()\n        ;\n    }\n}\n"
  },
  {
    "path": "src/Schedule/ScheduleFactory.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Schedule;\n\nuse Crunz\\Event;\nuse Crunz\\Exception\\TaskNotExistException;\nuse Crunz\\Schedule;\nuse Crunz\\Task\\TaskNumber;\n\nclass ScheduleFactory\n{\n    /**\n     * @return Schedule[]\n     *\n     * @throws TaskNotExistException\n     */\n    public function singleTaskSchedule(TaskNumber $taskNumber, Schedule ...$schedules): array\n    {\n        $event = $this->singleTask($taskNumber, ...$schedules);\n\n        $schedule = new Schedule();\n        $schedule->events([$event]);\n\n        return [$schedule];\n    }\n\n    /** @throws TaskNotExistException */\n    public function singleTask(TaskNumber $taskNumber, Schedule ...$schedules): Event\n    {\n        $events = \\array_map(\n            static fn (Schedule $schedule) => $schedule->events(),\n            $schedules\n        );\n\n        $flattenEvents = \\array_merge(...$events);\n\n        if (!isset($flattenEvents[$taskNumber->asArrayIndex()])) {\n            $tasksCount = \\count($flattenEvents);\n            throw new TaskNotExistException(\n                \"Task with id '{$taskNumber->asInt()}' was not found. Last task id is '{$tasksCount}'.\"\n            );\n        }\n\n        return $flattenEvents[$taskNumber->asArrayIndex()];\n    }\n}\n"
  },
  {
    "path": "src/Schedule.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz;\n\nuse Crunz\\Pinger\\PingableInterface;\nuse Crunz\\Pinger\\PingableTrait;\nuse Crunz\\Process\\Process;\n\nclass Schedule implements PingableInterface\n{\n    use PingableTrait;\n\n    /**\n     * All of the events on the schedule.\n     *\n     * @var Event[]\n     */\n    protected $events = [];\n\n    /**\n     * The array of callbacks to be run before all the events are finished.\n     *\n     * @var \\Closure[]\n     */\n    protected $beforeCallbacks = [];\n\n    /**\n     * The array of callbacks to be run after all the event is finished.\n     *\n     * @var \\Closure[]\n     */\n    protected $afterCallbacks = [];\n\n    /**\n     * The array of callbacks to call in case of an error.\n     *\n     * @var \\Closure[]\n     */\n    protected $errorCallbacks = [];\n\n    /**\n     * Add a new event to the schedule object.\n     *\n     * @param string|\\Closure $command\n     * @param string[]        $parameters\n     *\n     * @return Event\n     */\n    public function run($command, array $parameters = [])\n    {\n        if (\\is_string($command) && \\count($parameters)) {\n            $command .= ' ' . $this->compileParameters($parameters);\n        }\n\n        $this->events[] = $event = new Event($this->id(), $command);\n\n        return $event;\n    }\n\n    /**\n     * Register a callback to be called before the operation.\n     *\n     * @return $this\n     */\n    public function before(\\Closure $callback)\n    {\n        $this->beforeCallbacks[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Register a callback to be called after the operation.\n     *\n     * @return $this\n     */\n    public function after(\\Closure $callback)\n    {\n        return $this->then($callback);\n    }\n\n    /**\n     * Register a callback to be called after the operation.\n     *\n     * @return $this\n     */\n    public function then(\\Closure $callback)\n    {\n        $this->afterCallbacks[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Register a callback to call in case of an error.\n     *\n     * @return $this\n     */\n    public function onError(\\Closure $callback)\n    {\n        $this->errorCallbacks[] = $callback;\n\n        return $this;\n    }\n\n    /**\n     * Return all registered before callbacks.\n     *\n     * @return \\Closure[]\n     */\n    public function beforeCallbacks()\n    {\n        return $this->beforeCallbacks;\n    }\n\n    /**\n     * Return all registered after callbacks.\n     *\n     * @return \\Closure[]\n     */\n    public function afterCallbacks()\n    {\n        return $this->afterCallbacks;\n    }\n\n    /**\n     * Return all registered error callbacks.\n     *\n     * @return \\Closure[]\n     */\n    public function errorCallbacks()\n    {\n        return $this->errorCallbacks;\n    }\n\n    /**\n     * Get or set the events of the schedule object.\n     *\n     * @param Event[] $events\n     *\n     * @return Event[]\n     */\n    public function events(?array $events = null)\n    {\n        if (null !== $events) {\n            return $this->events = $events;\n        }\n\n        return $this->events;\n    }\n\n    /**\n     * Get all of the events on the schedule that are due.\n     *\n     * @return Event[]\n     */\n    public function dueEvents(\\DateTimeZone $timeZone)\n    {\n        return \\array_filter(\n            $this->events,\n            static fn (Event $event) => $event->isDue($timeZone)\n        );\n    }\n\n    /**\n     * Dismiss an event after it is finished.\n     *\n     * @param int $key\n     *\n     * @return $this\n     */\n    public function dismissEvent($key)\n    {\n        unset($this->events[$key]);\n\n        return $this;\n    }\n\n    /**\n     * Generate a unique task id.\n     *\n     * @return string\n     */\n    protected function id()\n    {\n        while (true) {\n            $id = \\uniqid('crunz', true);\n            if (!\\array_key_exists($id, $this->events)) {\n                return $id;\n            }\n        }\n    }\n\n    /** @param array<int|string,int|string|float|bool> $parameters */\n    protected function compileParameters(array $parameters): string\n    {\n        $isStrings = \\array_reduce(\n            $parameters,\n            static fn (bool $carry, $item): bool => $carry && true === \\is_string($item),\n            true,\n        );\n        if (false === $isStrings) {\n            @\\trigger_error(\n                'Passing non-string parameters is deprecated since v3.3, convert all parameters to string.',\n                \\E_USER_DEPRECATED\n            );\n\n            $parameters = \\array_map(\n                static function ($value): string {\n                    if (true === \\is_bool($value)) {\n                        return true === $value\n                            ? '1'\n                            : '0'\n                        ;\n                    }\n\n                    return (string) $value;\n                },\n                $parameters,\n            );\n        }\n\n        $flatParameters = [];\n        /** @var string[] $parameters */\n        foreach ($parameters as $key => $value) {\n            if (false === \\is_numeric($key)) {\n                $flatParameters[] = $key;\n            }\n\n            $flatParameters[] = $value;\n        }\n\n        return Process::fromArrayCommand($flatParameters)->commandLine();\n    }\n}\n"
  },
  {
    "path": "src/Stubs/BasicTask.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n/*\n|--------------------------------------------------------------------------------------\n|  Task File\n|--------------------------------------------------------------------------------------\n|\n| This file basically registers a new task to be executed by Crunz\n| To get the list of all frequency and constraint method, you may\n| go to this link: https://github.com/crunzphp/crunz#frequency-of-execution\n|\n*/\n\nuse Crunz\\Schedule;\n\n$scheduler = new Schedule();\n$task = $scheduler->run('DummyCommand');\n$task\n    ->description('DummyDescription')\n    ->in('DummyPath')\n    ->preventOverlapping()\n    ->DummyFrequency()\n    ->DummyConstraint()\n;\n\nreturn $scheduler;\n"
  },
  {
    "path": "src/Task/Collection.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Finder\\FinderInterface;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\nuse Crunz\\Path\\Path;\n\nclass Collection implements CollectionInterface\n{\n    public function __construct(\n        private readonly ConfigurationInterface $configuration,\n        private readonly FinderInterface $finder,\n        private readonly ConsoleLoggerInterface $consoleLogger,\n    ) {\n    }\n\n    public function all(string $source): iterable\n    {\n        $this->consoleLogger\n            ->debug(\"Task source path '<info>{$source}</info>'\");\n\n        if (!\\file_exists($source)) {\n            return [];\n        }\n\n        $suffix = $this->configuration\n            ->get('suffix')\n        ;\n\n        $this->consoleLogger\n            ->debug(\"Task finder suffix: '<info>{$suffix}</info>'\");\n\n        $realPath = \\realpath($source);\n        if (false !== $realPath) {\n            $this->consoleLogger\n                ->verbose(\"Realpath for '<info>{$source}</info>' is '<info>{$realPath}</info>'\");\n        } else {\n            $this->consoleLogger\n                ->verbose(\"Realpath resolve for '<info>{$source}</info>' failed.\");\n        }\n\n        $tasks = $this->finder\n            ->find(Path::fromStrings($source), $suffix)\n        ;\n        $tasksCount = \\count($tasks);\n\n        $this->consoleLogger\n            ->debug(\"Found <info>{$tasksCount}</info> task(s) at path '<info>{$source}</info>'\");\n\n        return $tasks;\n    }\n}\n"
  },
  {
    "path": "src/Task/CollectionInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\ninterface CollectionInterface\n{\n    /** @return \\SplFileInfo[] */\n    public function all(string $source): iterable;\n}\n"
  },
  {
    "path": "src/Task/Loader.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Schedule;\n\nfinal class Loader implements LoaderInterface\n{\n    /** @return Schedule[] */\n    public function load(\\SplFileInfo ...$files): array\n    {\n        $schedules = [];\n        foreach ($files as $file) {\n            /**\n             * Actual \"require\" is in separated method to make sure\n             * local variables are not overwritten by required file\n             * See: https://github.com/lavary/crunz/issues/242 for more information.\n             */\n            $schedule = $this->loadSchedule($file);\n            if (!$schedule instanceof Schedule) {\n                throw WrongTaskInstanceException::fromFilePath($file, $schedule);\n            }\n\n            $schedules[] = $schedule;\n        }\n\n        return $schedules;\n    }\n\n    /** @return Schedule|mixed */\n    private function loadSchedule(\\SplFileInfo $file)\n    {\n        return require $file->getRealPath();\n    }\n}\n"
  },
  {
    "path": "src/Task/LoaderInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Schedule;\n\ninterface LoaderInterface\n{\n    /** @return Schedule[] */\n    public function load(\\SplFileInfo ...$files): array;\n}\n"
  },
  {
    "path": "src/Task/TaskException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Exception\\CrunzException;\n\nclass TaskException extends CrunzException\n{\n}\n"
  },
  {
    "path": "src/Task/TaskNumber.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Exception\\WrongTaskNumberException;\n\nclass TaskNumber\n{\n    final public const MIN_VALUE = 1;\n    private readonly int $number;\n\n    /** @throws WrongTaskNumberException */\n    private function __construct(int $number)\n    {\n        if ($number < self::MIN_VALUE) {\n            throw new WrongTaskNumberException('Passed task number must be greater or equal to 1.');\n        }\n\n        $this->number = $number;\n    }\n\n    /**\n     * @param string $value\n     *\n     * @return TaskNumber\n     *\n     * @throws WrongTaskNumberException\n     */\n    public static function fromString($value)\n    {\n        if (!\\is_string($value)) {\n            throw new WrongTaskNumberException('Passed task number is not string.');\n        }\n\n        if (!\\is_numeric($value)) {\n            throw new WrongTaskNumberException(\"Task number '{$value}' is not numeric.\");\n        }\n\n        $number = (int) $value;\n\n        return new self($number);\n    }\n\n    public function asInt(): int\n    {\n        return $this->number;\n    }\n\n    public function asArrayIndex(): int\n    {\n        return $this->number - 1;\n    }\n}\n"
  },
  {
    "path": "src/Task/Timezone.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Exception\\EmptyTimezoneException;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\n\nclass Timezone\n{\n    private ?\\DateTimeZone $timezoneForComparisons = null;\n\n    public function __construct(\n        private readonly ConfigurationInterface $configuration,\n        private readonly ConsoleLoggerInterface $consoleLogger,\n    ) {\n    }\n\n    /** @throws EmptyTimezoneException */\n    public function timezoneForComparisons(): \\DateTimeZone\n    {\n        if (null !== $this->timezoneForComparisons) {\n            return $this->timezoneForComparisons;\n        }\n\n        $newTimezone = $this->configuration\n            ->get('timezone')\n        ;\n\n        $this->consoleLogger\n            ->debug(\"Timezone from config: '<info>{$newTimezone}</info>'.\");\n\n        if (empty($newTimezone)) {\n            throw new EmptyTimezoneException('Timezone must be configured. Please add it to your config file.');\n        }\n\n        $this->consoleLogger\n            ->debug(\"Timezone for comparisons: '<info>{$newTimezone}</info>'.\");\n\n        $this->timezoneForComparisons = new \\DateTimeZone($newTimezone);\n\n        return $this->timezoneForComparisons;\n    }\n}\n"
  },
  {
    "path": "src/Task/WrongTaskInstanceException.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Task;\n\nuse Crunz\\Schedule;\n\nfinal class WrongTaskInstanceException extends TaskException\n{\n    public static function fromFilePath(\\SplFileInfo $filePath, mixed $schedule): self\n    {\n        $expectedInstance = Schedule::class;\n        $type = \\get_debug_type($schedule);\n        $path = $filePath->getRealPath();\n\n        return new self(\n            \"Task at path '{$path}' returned '{$type}', but '{$expectedInstance}' instance is required.\"\n        );\n    }\n}\n"
  },
  {
    "path": "src/Timezone/Provider.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Timezone;\n\nfinal class Provider implements ProviderInterface\n{\n    public function defaultTimezone(): \\DateTimeZone\n    {\n        return new \\DateTimeZone(\\date_default_timezone_get());\n    }\n}\n"
  },
  {
    "path": "src/Timezone/ProviderInterface.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Timezone;\n\ninterface ProviderInterface\n{\n    /**\n     * @return \\DateTimeZone\n     */\n    public function defaultTimezone();\n}\n"
  },
  {
    "path": "src/UserInterface/Cli/ClosureRunCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\UserInterface\\Cli;\n\nuse Crunz\\Application\\Service\\ClosureSerializerInterface;\nuse Symfony\\Component\\Console\\Command\\Command as SymfonyCommand;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nclass ClosureRunCommand extends SymfonyCommand\n{\n    public function __construct(private readonly ClosureSerializerInterface $closureSerializer)\n    {\n        parent::__construct();\n    }\n\n    /**\n     * Configures the current command.\n     */\n    protected function configure(): void\n    {\n        $this\n            ->setName('closure:run')\n            ->setDescription('Executes a closure as a process.')\n            ->setDefinition(\n                [\n                    new InputArgument(\n                        'closure',\n                        InputArgument::REQUIRED,\n                        'The closure to run'\n                    ),\n                ]\n            )\n            ->setHelp('This command executes a closure as a separate process.')\n            ->setHidden(true)\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        $args = [];\n        /** @var string $closure */\n        $closure = $input->getArgument('closure');\n        \\parse_str($closure, $args);\n        $serializedClosure = $args[0] ?? '';\n        if (false === \\is_string($serializedClosure)) {\n            $serializedClosure = '';\n        }\n\n        $closure = $this->closureSerializer\n            ->unserialize($serializedClosure)\n        ;\n\n        \\call_user_func_array($closure, []);\n\n        return 0;\n    }\n}\n"
  },
  {
    "path": "src/UserInterface/Cli/DebugTaskCommand.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\UserInterface\\Cli;\n\nuse Crunz\\Application\\Query\\TaskInformation\\TaskInformation;\nuse Crunz\\Application\\Query\\TaskInformation\\TaskInformationHandler;\nuse Crunz\\Application\\Query\\TaskInformation\\TaskInformationView;\nuse Crunz\\Task\\TaskNumber;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Helper\\Table;\nuse Symfony\\Component\\Console\\Helper\\TableCell;\nuse Symfony\\Component\\Console\\Helper\\TableSeparator;\nuse Symfony\\Component\\Console\\Input\\InputArgument;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nfinal class DebugTaskCommand extends Command\n{\n    public function __construct(private readonly TaskInformationHandler $taskInformationHandler)\n    {\n        parent::__construct('task:debug');\n    }\n\n    protected function configure(): void\n    {\n        $this\n            ->setDescription('Shows all information about task')\n            ->addArgument(\n                'taskNumber',\n                InputArgument::REQUIRED,\n                'Task number from schedule:list command'\n            )\n        ;\n    }\n\n    protected function execute(InputInterface $input, OutputInterface $output): int\n    {\n        /** @var string|null $rawTaskNumber */\n        $rawTaskNumber = $input->getArgument('taskNumber');\n        $taskNumber = TaskNumber::fromString((string) $rawTaskNumber);\n        $taskInformationView = $this->taskInformationHandler\n            ->handle(new TaskInformation($taskNumber))\n        ;\n\n        $table = $this->createTable($taskInformationView, $output, $taskNumber);\n        $table->render();\n\n        return 0;\n    }\n\n    private function createTable(\n        TaskInformationView $taskInformation,\n        OutputInterface $output,\n        TaskNumber $taskNumber,\n    ): Table {\n        $command = $taskInformation->command();\n        $timeZone = $taskInformation->timeZone();\n        $configTimeZone = $taskInformation->configTimeZone();\n        $runDates = \\array_map(\n            static fn (\\DateTimeImmutable $netRunDate): string => $netRunDate->format('Y-m-d H:i:s e'),\n            $taskInformation->nextRuns()\n        );\n\n        $table = new Table($output);\n        $table->setHeaders(\n            [\n                new TableCell(\n                    \"Debug information for task '{$taskNumber->asInt()}'\",\n                    ['colspan' => 2]\n                ),\n            ]\n        );\n        $table->addRows(\n            [\n                [\n                    'Command to run',\n                    \\is_object($command)\n                        ? $command::class\n                        : $command,\n                ],\n                [\n                    'Description',\n                    $taskInformation->description(),\n                ],\n                [\n                    'Prevent overlapping',\n                    $taskInformation->preventOverlapping()\n                        ? 'Yes'\n                        : 'No',\n                ],\n                new TableSeparator(),\n                [\n                    'Cron expression',\n                    $taskInformation->cronExpression(),\n                ],\n                [\n                    'Comparisons timezone',\n                    null !== $timeZone\n                        ? \"{$timeZone->getName()} (from task)\"\n                        : \"{$configTimeZone->getName()} (from config)\",\n                ],\n                new TableSeparator(),\n                [new TableCell('Example run dates', ['colspan' => 2])],\n            ]\n        );\n\n        $i = 1;\n        foreach ($runDates as $date) {\n            $table->addRow(\n                [\n                    \"#{$i}\",\n                    $date,\n                ]\n            );\n            ++$i;\n        }\n\n        return $table;\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/ClosureRunTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class ClosureRunTest extends EndToEndTestCase\n{\n    /** @test */\n    public function closure_tasks(): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder\n            ->addTask('ClosureTasks')\n            ->withConfig(['timezone' => 'UTC'])\n        ;\n\n        $environment = $envBuilder->createEnvironment();\n\n        $process = $environment->runCrunzCommand('schedule:run');\n\n        self::assertStringContainsString(\n            'Closure output Var: 153',\n            \\str_replace(\n                PHP_EOL,\n                ' ',\n                $process->getOutput()\n            )\n        );\n    }\n\n    public function test_prevent_overlapping_works_on_closures(): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder\n            ->addTask('NoOverlappingClosureTasks')\n            ->withConfig(['timezone' => 'UTC'])\n        ;\n\n        $environment = $envBuilder->createEnvironment();\n\n        // Warmup Crunz to avoid container's cache race condition\n        $environment->runCrunzCommand('schedule:list');\n\n        $firstCall = $environment->runCrunzCommand(\n            'schedule:run',\n            null,\n            false\n        );\n        \\usleep(50 * 1000); // wait 50ms\n        $secondCall = $environment->runCrunzCommand('schedule:run');\n        $firstCall->wait();\n\n        self::assertStringContainsString('Done', $firstCall->getOutput());\n        self::assertStringContainsString('No event is due!', $secondCall->getOutput());\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/ConfigProviderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Console\\Command\\ConfigGeneratorCommand;\nuse Crunz\\Filesystem\\Filesystem;\nuse Crunz\\Path\\Path;\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\nuse Symfony\\Component\\Yaml\\Yaml;\n\nfinal class ConfigProviderTest extends EndToEndTestCase\n{\n    public function test_config_can_be_published(): void\n    {\n        $environmentBuilder = $this->createEnvironmentBuilder();\n        $environmentBuilder->withConfig(['timezone' => null]);\n        $environment = $environmentBuilder->createEnvironment();\n        $process = $environment->runCrunzCommand('publish:config');\n\n        $configPath = Path::fromStrings($environment->rootDirectory(), ConfigGeneratorCommand::CONFIG_FILE_NAME);\n        $filesystem = new Filesystem();\n\n        self::assertTrue($process->isSuccessful(), \"Process output: {$process->getOutput()}{$process->errorOutput()}\");\n        self::assertFileExists($configPath->toString());\n        self::assertIsArray(\n            Yaml::parse(\n                $filesystem->readContent(\n                    $configPath->toString()\n                )\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/ConfigRecognitionTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Path\\Path;\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class ConfigRecognitionTest extends EndToEndTestCase\n{\n    /** @test */\n    public function search_config_in_cwd(): void\n    {\n        $tasksSource = Path::fromStrings('resources', 'tasks');\n        $environmentBuilder = $this->createEnvironmentBuilder();\n        $environmentBuilder\n            ->changeTaskDirectory($tasksSource)\n            ->addTask('PhpVersionTasks')\n            ->withConfig(\n                [\n                    'source' => $tasksSource->toString(),\n                    'timezone' => 'UTC',\n                ]\n            )\n        ;\n\n        $environment = $environmentBuilder->createEnvironment();\n\n        $process = $environment->runCrunzCommand('schedule:list');\n        $normalizedOutput = $this->normalizeProcessOutput($process);\n\n        self::assertStringNotContainsString(\n            '[Deprecation] Probably you are relying on legacy config file recognition which is deprecated.',\n            $normalizedOutput\n        );\n        self::assertStringNotContainsString(\n            '[Deprecation] Probably you are relying on legacy tasks source recognition which is deprecated.',\n            $normalizedOutput\n        );\n        $this->assertHasTask($normalizedOutput);\n    }\n\n    private function assertHasTask(string $output): void\n    {\n        self::assertStringContainsString('PHP version', $output);\n        self::assertStringContainsString('php -v', $output);\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/DebugTaskTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class DebugTaskTest extends EndToEndTestCase\n{\n    public function test_task_debug(): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder\n            ->addTask('ClosureTasks')\n            ->withConfig(['timezone' => 'UTC'])\n        ;\n\n        $environment = $envBuilder->createEnvironment();\n\n        $process = $environment->runCrunzCommand('task:debug 1');\n        $output = $process->getOutput();\n        $contentLines = $this->extractContentLines($output);\n\n        $expectedValues = [\n            'command_to_run' => 'Closure',\n            'description' => 'Closure with output',\n            'prevent_overlapping' => 'No',\n            'cron_expression' => '* * * * *',\n            'comparisons_timezone' => 'UTC (from config)',\n        ];\n\n        $this->assertHeader('debug_information_for_task_1', $contentLines);\n        $this->assertHeader('example_run_dates', $contentLines);\n\n        foreach ($expectedValues as $expectedKey => $expectedValue) {\n            self::assertArrayHasKey($expectedKey, $contentLines);\n            self::assertSame($expectedValue, $contentLines[$expectedKey]);\n        }\n\n        for ($i = 1; $i <= 5; ++$i) {\n            $key = \"_{$i}\";\n            self::assertArrayHasKey($key, $contentLines);\n            self::assertMatchesRegularExpression(\n                '/^[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9]{2}:[0-9]{2}:00 UTC$/',\n                $contentLines[$key]\n            );\n        }\n    }\n\n    /** @return array<string,string> */\n    private function extractContentLines(string $output): array\n    {\n        $outputArray = \\explode(PHP_EOL, $output);\n        $contentLines = [];\n        foreach ($outputArray as $line) {\n            $matches = [];\n            $match = \\preg_match(\n                \"/(?<key>[ a-z0-9#']+) \\|? (?<value>[ *\\-:()a-z0-9#]+)/im\",\n                $line,\n                $matches\n            );\n\n            if (1 !== $match) {\n                continue;\n            }\n\n            $key = \\trim($matches['key']);\n            $key = \\mb_strtolower($key);\n            $key = \\str_replace(\n                [\n                    ' ',\n                    '#',\n                    \"'\",\n                ],\n                [\n                    '_',\n                    '_',\n                    '',\n                ],\n                $key\n            );\n\n            $contentLines[$key] = \\trim($matches['value']);\n        }\n\n        return $contentLines;\n    }\n\n    /** @param array<string,string> $lines */\n    private function assertHeader(string $header, array $lines): void\n    {\n        self::assertArrayHasKey($header, $lines);\n        self::assertSame('', $lines[$header]);\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/LoggerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class LoggerTest extends EndToEndTestCase\n{\n    public function test_outputs_are_logged(): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder\n            ->addTask('ClosureTasks')\n            ->addTask('FailTasks')\n            ->withConfig(\n                [\n                    'log_output' => true,\n                    'output_log_file' => 'php://stdout',\n                    'log_errors' => true,\n                    'errors_log_file' => 'php://stderr',\n                    'log_ignore_empty_context' => true,\n                ]\n            )\n        ;\n        $environment = $envBuilder->createEnvironment();\n        $process = $environment->runCrunzCommand('schedule:run');\n\n        $this->assertLogRecord(\n            $process->getOutput(),\n            'info',\n            'Closure with output'\n        );\n        $this->assertLogRecord(\n            $process->errorOutput(),\n            'error',\n            'Task that will fail'\n        );\n    }\n\n    public function test_event_logging_override(): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder()\n            ->addTask('CustomOutputTasks')\n            ->withConfig(\n                [\n                    'log_output' => true,\n                    'output_log_file' => 'main.log',\n                ]\n            )\n        ;\n        $environment = $envBuilder->createEnvironment();\n        $logPath = $environment->rootDirectory() . DIRECTORY_SEPARATOR;\n\n        $process = $environment->runCrunzCommand('schedule:run');\n\n        self::assertEmpty($process->getOutput());\n\n        self::assertFileDoesNotExist(\"{$logPath}/main.log\");\n\n        self::assertFileExists(\"{$logPath}/custom.log\");\n        self::assertStringContainsString(\n            'Usage: php',\n            (string) \\file_get_contents(\"{$logPath}/custom.log\")\n        );\n    }\n\n    private function assertLogRecord(\n        string $logRecord,\n        string $level,\n        string $message,\n    ): void {\n        $levelFormatted = \\mb_strtoupper($level);\n\n        self::assertMatchesRegularExpression(\n            \"/^\\[[0-9]{4}(-[0-9]{2}){2} [0-9]{2}(:[0-9]{2}){2}\\] crunz\\.{$levelFormatted}:.+?({$message})/\",\n            $logRecord\n        );\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/TasksSourceRecognitionTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Path\\Path;\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class TasksSourceRecognitionTest extends EndToEndTestCase\n{\n    /** @test */\n    public function search_tasks_in_cwd(): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder->addTask('PhpVersionTasks');\n\n        $environment = $envBuilder->createEnvironment();\n\n        $process = $environment->runCrunzCommand('schedule:list');\n\n        self::assertStringNotContainsString(\n            '[Deprecation] Probably you are relying on legacy tasks source recognition which',\n            $process->getOutput()\n        );\n        $this->assertHasTask($process->getOutput());\n    }\n\n    /** @test */\n    public function search_tasks_in_cwd_with_config(): void\n    {\n        $tasksPath = Path::fromStrings('app', 'tasks');\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder\n            ->addTask('PhpVersionTasks')\n            ->changeTaskDirectory($tasksPath)\n            ->withConfig(['source' => $tasksPath->toString()])\n        ;\n\n        $environment = $envBuilder->createEnvironment();\n\n        $process = $environment->runCrunzCommand('schedule:list');\n\n        self::assertStringNotContainsString(\n            '[Deprecation] Probably you are relying on legacy tasks source recognition which',\n            $process->getOutput()\n        );\n        $this->assertHasTask($process->getOutput());\n    }\n\n    private function assertHasTask(string $output): void\n    {\n        self::assertStringContainsString('PHP version', $output);\n        self::assertStringContainsString('php -v', $output);\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/VersionTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Composer\\InstalledVersions;\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class VersionTest extends EndToEndTestCase\n{\n    public function test_version(): void\n    {\n        // Arrange\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder->withConfig(['timezone' => 'UTC']);\n        $environment = $envBuilder->createEnvironment();\n        $version = InstalledVersions::getPrettyVersion('crunzphp/crunz');\n        $expectedVersion = \"Crunz Command Line Interface {$version}\";\n\n        // Act\n        $process = $environment->runCrunzCommand('--version');\n\n        // Assert\n        self::assertSame(\n            $expectedVersion,\n            \\trim(\n                \\str_replace(\n                    PHP_EOL,\n                    ' ',\n                    $process->getOutput()\n                )\n            )\n        );\n    }\n}\n"
  },
  {
    "path": "tests/EndToEnd/WrongTaskTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\EndToEnd;\n\nuse Crunz\\Tests\\TestCase\\EndToEndTestCase;\n\nfinal class WrongTaskTest extends EndToEndTestCase\n{\n    /**\n     * @test\n     *\n     * @dataProvider scheduleInstanceProvider\n     */\n    public function every_task_must_return_crunz_schedule_instance(string $crunzCommand): void\n    {\n        $envBuilder = $this->createEnvironmentBuilder();\n        $envBuilder\n            ->addTask('WrongTasks')\n            ->withConfig(['timezone' => 'Europe/Warsaw'])\n        ;\n\n        $environment = $envBuilder->createEnvironment();\n\n        $process = $environment->runCrunzCommand($crunzCommand);\n        $normalizedOutput = $this->normalizeProcessErrorOutput($process);\n\n        self::assertFalse($process->isSuccessful());\n        self::assertMatchesRegularExpression(\n            \"@Task at path '.*WrongTasks\\\\.php' returned 'array', but 'C( ?)runz\\\\\\\\Schedule' instance is required\\.@\",\n            $normalizedOutput\n        );\n    }\n\n    /** @return iterable<string,array> */\n    public static function scheduleInstanceProvider(): iterable\n    {\n        yield 'list' => ['schedule:list'];\n        yield 'run' => ['schedule:run'];\n    }\n}\n"
  },
  {
    "path": "tests/Functional/ConfigProviderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Functional;\n\nuse Crunz\\Application;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Tester\\CommandTester;\n\nclass ConfigProviderTest extends TestCase\n{\n    /** @test */\n    public function config_already_exists(): void\n    {\n        $application = new Application('Crunz', '0.1.0-test.1');\n        $command = $application->get('publish:config');\n\n        $commandTester = new CommandTester($command);\n        $returnCode = $commandTester->execute([]);\n\n        self::assertSame(0, $returnCode);\n        self::assertStringContainsString('The configuration file already exists at', $commandTester->getDisplay());\n    }\n}\n"
  },
  {
    "path": "tests/Functional/DifferentBaseCacheDirTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Functional;\n\nuse Crunz\\Application;\nuse Crunz\\CacheDirectoryFactory\\CacheDirectoryFactory;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class DifferentBaseCacheDirTest extends TestCase\n{\n    /**\n     * @test\n     *\n     * @runInSeparateProcess\n     */\n    public function different_base_cache_dir_is_used(): void\n    {\n        $newTmpDir = \\sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'newBaseCacheDir';\n        \\putenv(CacheDirectoryFactory::CRUNZ_BASE_CACHE_DIR . \"={$newTmpDir}\");\n\n        new Application('Crunz', '0.1.0-test.1');\n\n        self::assertDirectoryExists($newTmpDir);\n    }\n}\n"
  },
  {
    "path": "tests/Functional/ScheduleListTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Functional;\n\nuse Crunz\\Application;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Tester\\CommandTester;\n\nclass ScheduleListTest extends TestCase\n{\n    /** @test */\n    public function show_list(): void\n    {\n        $application = new Application('Crunz', '0.1.0-test.1');\n        $command = $application->get('schedule:list');\n\n        $commandTester = new CommandTester($command);\n        $returnCode = $commandTester->execute([]);\n\n        self::assertSame(0, $returnCode);\n        self::assertStringContainsString('Show PHP version', $commandTester->getDisplay());\n    }\n}\n"
  },
  {
    "path": "tests/Functional/ScheduleRunTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Functional;\n\nuse Crunz\\Application;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Tester\\CommandTester;\n\nclass ScheduleRunTest extends TestCase\n{\n    /** @test */\n    public function show_list(): void\n    {\n        $application = new Application('Crunz', '0.1.0-test.1');\n        $command = $application->get('schedule:run');\n\n        $commandTester = new CommandTester($command);\n        $returnCode = $commandTester->execute([]);\n\n        self::assertSame(0, $returnCode);\n        self::assertStringContainsString(PHP_VERSION, $commandTester->getDisplay());\n    }\n}\n"
  },
  {
    "path": "tests/Functional/TaskGeneratorTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Functional;\n\nuse Crunz\\Application;\nuse Crunz\\Path\\Path;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Command\\Command;\nuse Symfony\\Component\\Console\\Tester\\CommandTester;\n\nclass TaskGeneratorTest extends TestCase\n{\n    private string $fileName;\n    private string $taskFilePath;\n    private string $outputDirectory;\n\n    public function setUp(): void\n    {\n        $this->outputDirectory = \\sys_get_temp_dir();\n        $this->fileName = 'CrunzTest';\n        $taskFilePath = Path::create(\n            [\n                $this->outputDirectory,\n                \"{$this->fileName}Tasks.php\",\n            ]\n        );\n        $this->taskFilePath = $taskFilePath->toString();\n        $this->clearTask();\n    }\n\n    public function tearDown(): void\n    {\n        $this->clearTask();\n    }\n\n    /** @test */\n    public function generate_task_file(): void\n    {\n        $application = new Application('Crunz', '0.1.0-test.1');\n        $command = $application->get('make:task');\n\n        $commandTester = new CommandTester($command);\n        $this->provideAnswer(\n            \"{$this->outputDirectory}\\n\",\n            $commandTester,\n            $command\n        );\n        $returnCode = $commandTester->execute(\n            [\n                'taskfile' => $this->fileName,\n            ]\n        );\n\n        self::assertSame(0, $returnCode);\n        self::assertFileExists($this->taskFilePath);\n    }\n\n    /** @return resource */\n    private function getInputStream(string $input)\n    {\n        $stream = \\fopen('php://memory', 'rb+', false);\n\n        if (false === $stream) {\n            throw new \\RuntimeException(\"Unable to open 'php://memory' stream.\");\n        }\n\n        \\fwrite($stream, $input);\n        \\rewind($stream);\n\n        return $stream;\n    }\n\n    private function clearTask(): void\n    {\n        if (\\file_exists($this->taskFilePath)) {\n            \\unlink($this->taskFilePath);\n        }\n    }\n\n    private function provideAnswer(\n        string $answer,\n        CommandTester $commandTester,\n        Command $command,\n    ): void {\n        if (\\method_exists($commandTester, 'setInputs')) {\n            $commandTester->setInputs([$answer]);\n\n            return;\n        }\n\n        $helper = $command->getHelper('question');\n        $helper->setInputStream($this->getInputStream($answer));\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/EndToEnd/Environment/Environment.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase\\EndToEnd\\Environment;\n\nuse Crunz\\Console\\Command\\ConfigGeneratorCommand;\nuse Crunz\\EnvFlags\\EnvFlags;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Path\\Path;\nuse Crunz\\Process\\Process;\nuse Symfony\\Component\\Yaml\\Yaml;\n\nfinal class Environment\n{\n    private const DEFAULT_CONFIG = [\n        'timezone' => 'UTC',\n    ];\n\n    private string $rootDirectory = '';\n    /** @var array<string,mixed> */\n    private readonly array $config;\n    private readonly string $tasksDirectory;\n\n    /**\n     * @param string[]            $tasks\n     * @param array<string,mixed> $config\n     *\n     * @throws \\Exception\n     */\n    public function __construct(\n        private readonly FilesystemInterface $filesystem,\n        Path $tasksDirectory,\n        array $config = [],\n        private readonly array $tasks = [],\n    ) {\n        $this->config = [...self::DEFAULT_CONFIG, ...$config];\n        $this->tasksDirectory = $tasksDirectory->toString();\n\n        $this->setUp();\n    }\n\n    public function __destruct()\n    {\n        $composerLock = Path::fromStrings('composer.lock');\n        $composerJson = Path::fromStrings('composer.json');\n        $baseCacheDir = Path::create(\n            [\n                \\sys_get_temp_dir(),\n                '.crunz',\n            ]\n        );\n\n        $this->filesystem\n            ->removeDirectory($this->rootDirectory(), [$composerLock, $composerJson]);\n        $this->filesystem\n            ->removeDirectory($baseCacheDir->toString());\n    }\n\n    private function setUp(): void\n    {\n        $this->createRootDirectory();\n        $this->dumpComposerJson();\n        $this->composerInstall();\n        $this->copyTasks();\n        $this->dumpConfig();\n    }\n\n    public function runCrunzCommand(\n        string $command,\n        ?string $cwd = null,\n        bool $wait = true,\n    ): Process {\n        $cwd = !empty($cwd)\n            ? $cwd\n            : $this->rootDirectory()\n        ;\n        $isWindows = DIRECTORY_SEPARATOR === '\\\\';\n        // On Windows do not add php binary path\n        $phpBinary = $isWindows\n            ? ''\n            : PHP_BINARY\n        ;\n        $crunzBinPath = Path::fromStrings(\n            $this->rootDirectory(),\n            'vendor',\n            'bin',\n            'crunz'\n        );\n        $commandParts = [\n            $phpBinary,\n            $crunzBinPath->toString(),\n            $command,\n            // Force no ANSI as this break AppVeyor CI builds\n            '--no-ansi',\n            // Force non-interaction\n            '--no-interaction',\n        ];\n        $fullCommand = \\implode(' ', $commandParts);\n        $process = $this->createProcess($fullCommand, $cwd);\n\n        $process->setEnv(\n            [\n                EnvFlags::DEPRECATION_HANDLER_FLAG => '1',\n                EnvFlags::CONTAINER_DEBUG_FLAG => '0',\n            ]\n        );\n\n        $process->start();\n        if ($wait) {\n            $process->wait();\n        }\n\n        return $process;\n    }\n\n    public function rootDirectory(): string\n    {\n        if ('' === $this->rootDirectory) {\n            $tempDir = $this->filesystem\n                ->tempDir();\n            $rootDirectory = Path::fromStrings($tempDir, 'end2end-test-env');\n\n            $this->rootDirectory = $rootDirectory->toString();\n        }\n\n        return $this->rootDirectory;\n    }\n\n    private function dumpConfig(): void\n    {\n        if (empty($this->config)) {\n            return;\n        }\n\n        $configPath = Path::fromStrings(\n            $this->rootDirectory,\n            ConfigGeneratorCommand::CONFIG_FILE_NAME\n        );\n\n        $yamlConfig = Yaml::dump($this->config);\n\n        $this->filesystem\n            ->dumpFile($configPath->toString(), $yamlConfig);\n    }\n\n    private function copyTasks(): void\n    {\n        $projectRoot = $this->filesystem\n            ->projectRootDirectory();\n        $tasksSourceRoot = Path::fromStrings(\n            $projectRoot,\n            'tests',\n            'resources',\n            'tasks'\n        );\n        $destinationRoot = Path::fromStrings(\n            $this->rootDirectory(),\n            $this->tasksDirectory\n        );\n\n        $this->filesystem\n            ->createDirectory($destinationRoot->toString());\n\n        foreach ($this->tasks as $task) {\n            $fileName = \"{$task}.php\";\n            $sourceTaskPath = Path::fromStrings($tasksSourceRoot->toString(), $fileName);\n            $destinationTaskPath = Path::fromStrings($destinationRoot->toString(), $fileName);\n\n            $sourceTaskExists = $this->filesystem\n                ->fileExists($sourceTaskPath->toString());\n\n            if (!$sourceTaskExists) {\n                throw new \\RuntimeException(\"Task '{$task}' not found at path '{$sourceTaskPath->toString()}'.\");\n            }\n\n            $this->filesystem\n                ->copy($sourceTaskPath->toString(), $destinationTaskPath->toString());\n        }\n    }\n\n    private function dumpComposerJson(): void\n    {\n        $composerJson = Path::fromStrings($this->rootDirectory(), 'composer.json');\n\n        $projectDir = $this->filesystem\n            ->projectRootDirectory();\n        $content = [\n            'repositories' => [\n                [\n                    'type' => 'path',\n                    'url' => $projectDir,\n                    'options' => [\n                        'symlink' => false,\n                    ],\n                ],\n            ],\n            'require' => [\n                'crunzphp/crunz' => '*@dev',\n            ],\n        ];\n        $contentJson = \\json_encode($content, JSON_PRETTY_PRINT);\n        if (false === $contentJson) {\n            throw new \\RuntimeException(\"Unable to encode 'composer.json' content.\");\n        }\n\n        $this->filesystem\n            ->dumpFile($composerJson->toString(), $contentJson)\n        ;\n    }\n\n    private function composerInstall(): void\n    {\n        $process = $this->createProcess('composer install -q -n', $this->rootDirectory());\n        $process->startAndWait();\n\n        if (!$process->isSuccessful()) {\n            throw new \\RuntimeException('Composer install failed');\n        }\n    }\n\n    /** @throws \\Exception */\n    private function createRootDirectory(): void\n    {\n        $tempDirectory = $this->filesystem\n            ->tempDir();\n\n        if (!\\is_writable($tempDirectory)) {\n            throw new \\Exception(\"Unable to setup environment in system's temp dir '{$tempDirectory}'.\");\n        }\n\n        $this->filesystem\n            ->createDirectory($this->rootDirectory());\n    }\n\n    private function createProcess(string $command, ?string $cwd = null): Process\n    {\n        return Process::fromStringCommand($command, $cwd);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/EndToEnd/Environment/EnvironmentBuilder.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase\\EndToEnd\\Environment;\n\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Path\\Path;\n\nfinal class EnvironmentBuilder\n{\n    /** @var array<string> */\n    private array $tasks = [];\n    /** @var array<string,mixed> */\n    private array $config = [];\n    private Path $taskDirectory;\n\n    public function __construct(private readonly FilesystemInterface $filesystem)\n    {\n        $this->taskDirectory = Path::fromStrings('tasks');\n    }\n\n    public function addTask(string $taskName): self\n    {\n        $this->tasks[] = $taskName;\n\n        return $this;\n    }\n\n    public function changeTaskDirectory(Path $path): self\n    {\n        $this->taskDirectory = $path;\n\n        return $this;\n    }\n\n    /** @param array<string,mixed> $config */\n    public function withConfig(array $config): self\n    {\n        $this->config = $config;\n\n        return $this;\n    }\n\n    public function createEnvironment(): Environment\n    {\n        return new Environment(\n            $this->filesystem,\n            $this->taskDirectory,\n            $this->config,\n            $this->tasks\n        );\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/EndToEndTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Filesystem\\Filesystem;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Process\\Process;\nuse Crunz\\Tests\\TestCase\\EndToEnd\\Environment\\EnvironmentBuilder;\nuse PHPUnit\\Framework\\TestCase;\n\nabstract class EndToEndTestCase extends TestCase\n{\n    /** @var FilesystemInterface */\n    private $filesystem;\n\n    public function createEnvironmentBuilder(): EnvironmentBuilder\n    {\n        if (null === $this->filesystem) {\n            $this->filesystem = new Filesystem();\n        }\n\n        return new EnvironmentBuilder($this->filesystem);\n    }\n\n    protected function normalizeOutput(string $output): string\n    {\n        $noNewLines = \\str_replace(\n            [\"\\n\", \"\\r\"],\n            '',\n            $output\n        );\n        $normalizedOutput = \\preg_replace(\n            \"/\\s+/\",\n            ' ',\n            (string) $noNewLines\n        );\n\n        return \\trim((string) $normalizedOutput);\n    }\n\n    protected function normalizeProcessOutput(Process $process): string\n    {\n        return $this->normalizeOutput($process->getOutput());\n    }\n\n    protected function normalizeProcessErrorOutput(Process $process): string\n    {\n        return $this->normalizeOutput($process->errorOutput());\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/FakeConfiguration.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Infrastructure\\Psr\\Logger\\PsrStreamLoggerFactory;\n\nfinal class FakeConfiguration implements ConfigurationInterface\n{\n    private const DEFAULT_CONFIG = [\n        'source' => 'tasks',\n        'suffix' => 'Tasks.php',\n        'timezone' => 'UTC',\n        'timezone_log' => false,\n        'log_errors' => false,\n        'errors_log_file' => null,\n        'logger_factory' => PsrStreamLoggerFactory::class,\n        'log_output' => false,\n        'output_log_file' => null,\n        'log_allow_line_breaks' => false,\n        'log_ignore_empty_context' => false,\n        'email_output' => false,\n        'email_errors' => false,\n    ];\n\n    /** @var array<string|int,string|array|bool|null> */\n    private array $config;\n\n    /** @param array<string|int,string|array|bool|null> $config */\n    public function __construct(array $config = [])\n    {\n        $this->config = \\array_merge(self::DEFAULT_CONFIG, $config);\n    }\n\n    public function get(string $key, mixed $default = null): mixed\n    {\n        if (\\array_key_exists($key, $this->config)) {\n            return $this->config[$key];\n        }\n\n        $parts = \\explode('.', $key);\n        $value = $this->config;\n        foreach ($parts as $part) {\n            if (!\\is_array($value) || !\\array_key_exists($part, $value)) {\n                return $default;\n            }\n\n            $value = $value[$part];\n        }\n\n        return $value;\n    }\n\n    public function withNewEntry(string $key, mixed $value): ConfigurationInterface\n    {\n        $newConfiguration = clone $this;\n\n        $parts = \\explode('.', $key);\n\n        if (\\count($parts) > 1) {\n            if (\\array_key_exists($parts[0], $newConfiguration->config) && \\is_array($newConfiguration->config[$parts[0]])) {\n                $newConfiguration->config[$parts[0]][$parts[1]] = $value;\n            } else {\n                $newConfiguration->config[$parts[0]] = [$parts[1] => $value];\n            }\n        } else {\n            $newConfiguration->config[$key] = $value;\n        }\n\n        return $newConfiguration;\n    }\n\n    public function getSourcePath(): string\n    {\n        return (string) $this->get('source', 'tasks');\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/FakeLoader.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Schedule;\nuse Crunz\\Task\\LoaderInterface;\n\nfinal class FakeLoader implements LoaderInterface\n{\n    /** @param Schedule[] $schedules */\n    public function __construct(private readonly array $schedules = [])\n    {\n    }\n\n    public function load(\\SplFileInfo ...$files): array\n    {\n        return $this->schedules;\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/FakeTaskCollection.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Task\\CollectionInterface;\n\nfinal class FakeTaskCollection implements CollectionInterface\n{\n    /** @param \\SplFileInfo[] $tasks */\n    public function __construct(private readonly iterable $tasks = [])\n    {\n    }\n\n    public function all(string $source): iterable\n    {\n        return $this->tasks;\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/Faker.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Exception\\CrunzException;\n\nfinal class Faker\n{\n    private const WORDS_ARRAY = [\n        'lorem',\n        'ipsum',\n        'dolor',\n        'sit',\n        'amet',\n        'consectetur',\n        'adipiscing',\n        'elit',\n        'sed',\n        'tincidunt',\n        'neque',\n        'massa',\n    ];\n\n    public static function timeZone(): \\DateTimeZone\n    {\n        $timeZoneId = self::elementFromArray(\\DateTimeZone::listIdentifiers());\n\n        return new \\DateTimeZone($timeZoneId);\n    }\n\n    /**\n     * @param array<mixed> $elements\n     *\n     * @throws CrunzException\n     */\n    public static function elementFromArray(array $elements): mixed\n    {\n        $itemsCount = \\count($elements);\n        if (0 === $itemsCount) {\n            throw new CrunzException('Passed array is empty.');\n        }\n\n        $normalizedElements = \\array_values($elements);\n        $index = self::int(0, $itemsCount - 1);\n\n        return $normalizedElements[$index];\n    }\n\n    public static function int(int $min = PHP_INT_MIN, int $max = PHP_INT_MAX): int\n    {\n        return \\random_int($min, $max);\n    }\n\n    public static function dateTime(string $start = '-20 years', string $end = 'now'): \\DateTimeImmutable\n    {\n        $min = new \\DateTimeImmutable($start);\n        $max = new \\DateTimeImmutable($end);\n\n        if ($min > $max) {\n            throw new CrunzException(\"'start' is higher than 'end'.\");\n        }\n\n        $dateTimestamp = self::int($min->getTimestamp(), $max->getTimestamp());\n\n        return new \\DateTimeImmutable(\"@{$dateTimestamp}\");\n    }\n\n    public static function words(int $count = 3): string\n    {\n        $lastWord = \\count(self::WORDS_ARRAY) - 1;\n        $words = [];\n        for ($i = 0; $i < $count; ++$i) {\n            $wordIndex = self::int(0, $lastWord);\n            $words[] = self::WORDS_ARRAY[$wordIndex];\n        }\n\n        return \\implode(' ', $words);\n    }\n\n    public static function word(): string\n    {\n        return self::words(1);\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/Logger/NullLogger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase\\Logger;\n\nuse Crunz\\Logger\\ConsoleLoggerInterface;\n\nfinal class NullLogger implements ConsoleLoggerInterface\n{\n    public function normal($message): void\n    {\n        // No-op\n    }\n\n    public function verbose($message): void\n    {\n        // No-op\n    }\n\n    public function veryVerbose($message): void\n    {\n        // No-op\n    }\n\n    public function debug($message): void\n    {\n        // No-op\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/Logger/SpyPsrLogger.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase\\Logger;\n\nuse Psr\\Log\\AbstractLogger;\n\nfinal class SpyPsrLogger extends AbstractLogger\n{\n    /** @var array<int,array> */\n    private array $logs = [];\n\n    public function log($level, string|\\Stringable $message, array $context = []): void\n    {\n        $this->logs[] = [\n            'level' => $level,\n            'message' => $message,\n            'context' => $context,\n        ];\n    }\n\n    /** @return array<int,array> */\n    public function getLogs(): array\n    {\n        return $this->logs;\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/SerializableTaskRunnerStub.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nfinal class SerializableTaskRunnerStub\n{\n    public string $taskName = 'daily-report';\n\n    /** @var \\Closure[] */\n    public array $filters = [];\n\n    /** @return array{taskName: string, filters: array<\\Closure>} */\n    public function __serialize(): array\n    {\n        return [\n            'taskName' => $this->taskName,\n            'filters' => $this->filters,\n        ];\n    }\n\n    /** @param array{taskName: string, filters: array<\\Closure>} $data */\n    public function __unserialize(array $data): void\n    {\n        $this->taskName = $data['taskName'];\n        $this->filters = $data['filters'];\n    }\n\n    public function createTask(): \\Closure\n    {\n        $this->filters[] = static function (): bool {\n            return true;\n        };\n\n        return function (): string {\n            return \"running {$this->taskName}\";\n        };\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/TaskRunnerStub.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nfinal class TaskRunnerStub\n{\n    public string $taskName = 'daily-report';\n\n    /** @var \\Closure[] */\n    public array $filters = [];\n\n    public function createTask(): \\Closure\n    {\n        $this->filters[] = static function (): bool {\n            return true;\n        };\n\n        return function (): string {\n            return \"running {$this->taskName}\";\n        };\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/TemporaryFile.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Exception\\CrunzException;\n\nfinal class TemporaryFile\n{\n    private readonly string $filePath;\n\n    public function __construct()\n    {\n        $filePath = \\tempnam(\\sys_get_temp_dir(), 'ctf');\n\n        if (false === $filePath) {\n            throw new CrunzException('Unable to create temp file.');\n        }\n\n        $this->filePath = $filePath;\n    }\n\n    public function __destruct()\n    {\n        if (!\\file_exists($this->filePath) || !\\is_writable(\\dirname($this->filePath))) {\n            return;\n        }\n\n        $streams = \\get_resources('stream');\n        foreach ($streams as $stream) {\n            $uri = \\stream_get_meta_data($stream)['uri'] ?? '';\n\n            if ($uri === $this->filePath) {\n                \\fclose($stream);\n            }\n        }\n\n        \\unlink($this->filePath);\n    }\n\n    public function filePath(): string\n    {\n        return $this->filePath;\n    }\n\n    /** @param int $mode */\n    public function changePermissions($mode): void\n    {\n        $this->checkFileExists();\n\n        \\chmod($this->filePath, $mode);\n    }\n\n    public function contents(): string\n    {\n        $this->checkFileExists();\n\n        $content = \\file_get_contents($this->filePath);\n\n        if (false === $content) {\n            throw new CrunzException(\"Unable to read from temporary file '{$this->filePath}'.\");\n        }\n\n        return $content;\n    }\n\n    private function checkFileExists(): void\n    {\n        if (!\\file_exists($this->filePath)) {\n            throw new CrunzException(\"Temporary file '{$this->filePath}' no longer exists.\");\n        }\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/TestClock.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Clock\\ClockInterface;\n\nfinal class TestClock implements ClockInterface\n{\n    public function __construct(private readonly \\DateTimeImmutable $now)\n    {\n    }\n\n    public function now(): \\DateTimeImmutable\n    {\n        return $this->now;\n    }\n}\n"
  },
  {
    "path": "tests/TestCase/UnitTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\TestCase;\n\nuse Crunz\\Application\\Service\\ClosureSerializerInterface;\nuse Crunz\\Infrastructure\\Laravel\\LaravelClosureSerializer;\nuse PHPUnit\\Framework\\TestCase;\n\nabstract class UnitTestCase extends TestCase\n{\n    private ?ClosureSerializerInterface $closureSerializer = null;\n\n    public function createClosureSerializer(): ClosureSerializerInterface\n    {\n        return $this->closureSerializer ??= new LaravelClosureSerializer();\n    }\n\n    protected static function encodeJson(mixed $data): string\n    {\n        return \\json_encode($data, JSON_THROW_ON_ERROR);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Application/Cron/AbstractCronExpressionTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Application\\Cron;\n\nuse Crunz\\Application\\Cron\\CronExpressionInterface;\nuse PHPUnit\\Framework\\TestCase;\n\nabstract class AbstractCronExpressionTestCase extends TestCase\n{\n    /**\n     * @test\n     *\n     * @param \\DateTimeImmutable[] $expectedRunDates\n     *\n     * @dataProvider multipleRunDatesProvider\n     */\n    public function multiple_run_dates(\n        string $cronExpressionString,\n        \\DateTimeImmutable $now,\n        int $total,\n        ?\\DateTimeZone $timeZone,\n        array $expectedRunDates,\n    ): void {\n        $cronExpression = $this->createExpression($cronExpressionString);\n        $runDates = $cronExpression->multipleRunDates(\n            $total,\n            $now,\n            $timeZone\n        );\n\n        self::assertEquals($expectedRunDates, $runDates);\n    }\n\n    /**\n     * @return iterable<\n     *     string,\n     *     array{\n     *         string,\n     *         \\DateTimeImmutable,\n     *         int,\n     *         null,\n     *         \\DateTimeImmutable[],\n     *     },\n     * >\n     */\n    public static function multipleRunDatesProvider(): iterable\n    {\n        $now = new \\DateTimeImmutable('2019-01-01 11:12:13');\n        $nextRuns = [new \\DateTimeImmutable('2019-01-01 11:13:00')];\n        yield 'one every minute' => [\n            '* * * * *',\n            $now,\n            1,\n            null,\n            $nextRuns,\n        ];\n\n        $now = new \\DateTimeImmutable('2019-02-01 05:09:01');\n        $nextRuns = [\n            new \\DateTimeImmutable('2019-02-01 05:10:00'),\n            new \\DateTimeImmutable('2019-02-01 05:15:00'),\n        ];\n        yield 'two every five minutes' => [\n            '*/5 * * * *',\n            $now,\n            2,\n            null,\n            $nextRuns,\n        ];\n\n        $timeZone = new \\DateTimeZone('Europe/Warsaw');\n        $now = new \\DateTimeImmutable('2019-03-02 07:02:01', $timeZone);\n        $nextRuns = [\n            new \\DateTimeImmutable('2019-03-02 07:10:00', $timeZone),\n            new \\DateTimeImmutable('2019-03-02 07:20:00', $timeZone),\n        ];\n        yield 'two timezone aware' => [\n            '*/10 * * * *',\n            $now,\n            2,\n            null,\n            $nextRuns,\n        ];\n    }\n\n    abstract protected function createExpression(string $cronExpression): CronExpressionInterface;\n}\n"
  },
  {
    "path": "tests/Unit/Application/Query/TaskInformation/TaskInformationHandlerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Application\\Query\\TaskInformation;\n\nuse Crunz\\Application\\Query\\TaskInformation\\TaskInformation;\nuse Crunz\\Application\\Query\\TaskInformation\\TaskInformationHandler;\nuse Crunz\\Event;\nuse Crunz\\Infrastructure\\Dragonmantank\\CronExpression\\DragonmantankCronExpressionFactory;\nuse Crunz\\Schedule\\ScheduleFactory;\nuse Crunz\\Task\\LoaderInterface;\nuse Crunz\\Task\\TaskNumber;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\FakeTaskCollection;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class TaskInformationHandlerTest extends TestCase\n{\n    /**\n     * @test\n     *\n     * @dataProvider taskInformationProvider\n     */\n    public function handle_returns_task_information(\n        Event $event,\n        string $expectedCommand,\n        string $expectedDescription = '',\n        bool $expectedPreventOverlapping = false,\n        string $expectedCronExpression = '* * * * *',\n        ?\\DateTimeZone $expectedEventTimeZone = null,\n    ): void {\n        $comparisonsTimeZone = new \\DateTimeZone('UTC');\n        $taskInformationHandler = $this->createHandler($event, $comparisonsTimeZone);\n        $taskInformation = $taskInformationHandler->handle(\n            new TaskInformation(\n                TaskNumber::fromString('1')\n            )\n        );\n\n        self::assertSame($expectedCommand, $taskInformation->command());\n        self::assertSame($expectedDescription, $taskInformation->description());\n        self::assertSame($expectedPreventOverlapping, $taskInformation->preventOverlapping());\n        self::assertSame($expectedCronExpression, $taskInformation->cronExpression());\n        self::assertSame($comparisonsTimeZone, $taskInformation->configTimeZone());\n        self::assertEquals($expectedEventTimeZone, $taskInformation->timeZone());\n    }\n\n    /** @return iterable<string, array> */\n    public static function taskInformationProvider(): iterable\n    {\n        $id = (string) \\random_int(1, 9999);\n        yield 'simple task' => [\n            new Event($id, 'php -v'),\n            'php -v',\n        ];\n\n        $event = new Event($id, 'php -i');\n        $event->description('Some description');\n        yield 'with description' => [\n            $event,\n            'php -i',\n            'Some description',\n        ];\n\n        $event = new Event($id, 'php -i');\n        $event->preventOverlapping();\n        yield 'with prevent overlapping' => [\n            $event,\n            'php -i',\n            '',\n            true,\n        ];\n\n        $event = new Event($id, 'php -i');\n        $event\n            ->everyFiveMinutes()\n            ->weekdays()\n        ;\n        yield 'with cron expression' => [\n            $event,\n            'php -i',\n            '',\n            false,\n            '*/5 * * * 1-5',\n        ];\n\n        $timeZone = new \\DateTimeZone('Europe/Warsaw');\n        $event = new Event($id, 'php -i');\n        $event->timezone($timeZone);\n        yield 'with custom comparisons timezone' => [\n            $event,\n            'php -i',\n            '',\n            false,\n            '* * * * *',\n            $timeZone,\n        ];\n\n        $event = new Event($id, 'php -i');\n        $event->timezone('Europe/Warsaw');\n        yield 'with string custom comparisons timezone' => [\n            $event,\n            'php -i',\n            '',\n            false,\n            '* * * * *',\n            new \\DateTimeZone('Europe/Warsaw'),\n        ];\n    }\n\n    private function createHandler(Event $event, \\DateTimeZone $comparisonsTimeZone): TaskInformationHandler\n    {\n        $taskCollectionMock = new FakeTaskCollection();\n        $scheduleFactoryMock = $this->createMock(ScheduleFactory::class);\n        $scheduleFactoryMock\n            ->method('singleTask')\n            ->willReturn($event)\n        ;\n        $timezoneProviderMock = $this->createMock(Timezone::class);\n        $timezoneProviderMock\n            ->method('timezoneForComparisons')\n            ->willReturn($comparisonsTimeZone)\n        ;\n\n        return new TaskInformationHandler(\n            $timezoneProviderMock,\n            new FakeConfiguration(),\n            $taskCollectionMock,\n            $this->createMock(LoaderInterface::class),\n            $scheduleFactoryMock,\n            new DragonmantankCronExpressionFactory()\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/CacheDirectoryFactory/CacheDirectoryFactoryTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\CacheDirectoryFactory;\n\nuse Crunz\\CacheDirectoryFactory\\CacheDirectoryFactory;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Path\\Path;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class CacheDirectoryFactoryTest extends TestCase\n{\n    /** @test  */\n    public function sys_temp_dir_is_default_directory(): void\n    {\n        $cacheDirectoryFactory = new CacheDirectoryFactory();\n\n        $result = $cacheDirectoryFactory->generate();\n\n        $expectedResult = Path::fromStrings(\\sys_get_temp_dir(), '.crunz');\n        self::assertEquals($expectedResult, $result);\n    }\n\n    /** @test */\n    public function change_cache_directory_through_environment_variable(): void\n    {\n        $newDirectoryPath = '/new/directory/path';\n        \\putenv(CacheDirectoryFactory::CRUNZ_BASE_CACHE_DIR . \"={$newDirectoryPath}\");\n        $cacheDirectoryFactory = new CacheDirectoryFactory();\n\n        $result = $cacheDirectoryFactory->generate();\n\n        $expectedResult = Path::fromStrings($newDirectoryPath, '.crunz');\n        self::assertEquals($expectedResult, $result);\n    }\n\n    /** @test */\n    public function throw_exception_when_environment_variable_is_empty(): void\n    {\n        $newDirectoryPath = '   ';\n        \\putenv(CacheDirectoryFactory::CRUNZ_BASE_CACHE_DIR . \"={$newDirectoryPath}\");\n        $cacheDirectoryFactory = new CacheDirectoryFactory();\n\n        self::expectException(CrunzException::class);\n\n        $cacheDirectoryFactory->generate();\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Configuration/ConfigurationParserTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Configuration;\n\nuse Crunz\\Configuration\\ConfigFileNotExistsException;\nuse Crunz\\Configuration\\ConfigurationParser;\nuse Crunz\\Configuration\\Definition;\nuse Crunz\\Configuration\\FileParser;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Tests\\TestCase\\Logger\\NullLogger;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Config\\Definition\\Processor;\n\nfinal class ConfigurationParserTest extends TestCase\n{\n    /** @test */\n    public function use_empty_config_when_config_file_not_exists(): void\n    {\n        $this->addToAssertionCount(1);\n\n        $fileParserMock = $this->createMock(FileParser::class);\n        $fileParserMock\n            ->method('parse')\n            ->willThrowException(ConfigFileNotExistsException::fromFilePath('/path'))\n        ;\n\n        $configurationParser = $this->createConfigurationParser($fileParserMock, []);\n        $configurationParser->parseConfig();\n    }\n\n    /** @test */\n    public function use_parsed_config_when_config_file_exists(): void\n    {\n        $this->addToAssertionCount(1);\n\n        $parsedConfig = ['some' => 'config'];\n\n        $fileParserMock = $this->createMock(FileParser::class);\n        $fileParserMock\n            ->method('parse')\n            ->willReturn($parsedConfig)\n        ;\n\n        $configurationParser = $this->createConfigurationParser($fileParserMock, $parsedConfig);\n        $configurationParser->parseConfig();\n    }\n\n    /** @param array<string,string> $expectedProcessedConfig */\n    private function createConfigurationParser(\n        FileParser $fileParser,\n        array $expectedProcessedConfig,\n    ): ConfigurationParser {\n        $definition = new Definition();\n\n        $definitionProcessorMock = $this->createMock(Processor::class);\n        $definitionProcessorMock\n            ->method('processConfiguration')\n            ->with($definition, $expectedProcessedConfig)\n            ->willReturn([])\n        ;\n\n        $filesystemMock = $this->createMock(FilesystemInterface::class);\n        $filesystemMock\n            ->method('fileExists')\n            ->willReturn(true)\n        ;\n\n        return new ConfigurationParser(\n            $definition,\n            $definitionProcessorMock,\n            $fileParser,\n            new NullLogger(),\n            $filesystemMock\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Configuration/ConfigurationTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Configuration;\n\nuse Crunz\\Configuration\\Configuration;\nuse Crunz\\Configuration\\ConfigurationParserInterface;\nuse Crunz\\Filesystem\\FilesystemInterface;\nuse Crunz\\Path\\Path;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ConfigurationTest extends TestCase\n{\n    /** @test */\n    public function get_can_return_path_split_by_dot(): void\n    {\n        $configuration = $this->createConfiguration(\n            [\n                'smtp' => [\n                    'port' => 1234,\n                ],\n            ]\n        );\n\n        self::assertSame(1234, $configuration->get('smtp.port'));\n    }\n\n    /** @test */\n    public function get_return_default_value_if_path_not_exists(): void\n    {\n        $configuration = $this->createConfiguration();\n\n        self::assertNull($configuration->get('wrong'));\n        self::assertSame('anon', $configuration->get('notExist', 'anon'));\n    }\n\n    /** @test */\n    public function source_path_is_relative_to_cwd(): void\n    {\n        $cwd = \\sys_get_temp_dir();\n        $sourcePath = Path::fromStrings('app', 'tasks');\n        $expectedPath = Path::fromStrings($cwd, $sourcePath->toString());\n        $configuration = $this->createConfiguration(['source' => $sourcePath->toString()], $cwd);\n\n        self::assertSame($expectedPath->toString(), $configuration->getSourcePath());\n    }\n\n    /** @test */\n    public function source_path_fallback_to_tasks_directory(): void\n    {\n        $cwd = \\sys_get_temp_dir();\n        $expectedPath = Path::fromStrings($cwd, 'tasks');\n        $configuration = $this->createConfiguration([], $cwd);\n\n        self::assertSame($expectedPath->toString(), $configuration->getSourcePath());\n    }\n\n    /** @test */\n    public function set_configuration_key_value(): void\n    {\n        $cwd = \\sys_get_temp_dir();\n        $sourcePath = Path::fromStrings('app', 'tasks');\n        $configuration = $this->createConfiguration(['source' => $sourcePath->toString()], $cwd);\n\n        $keyName = 'test_key';\n        $expectedValue = 'test_value';\n\n        $newConfiguration = $configuration->withNewEntry($keyName, $expectedValue);\n\n        self::assertSame($newConfiguration->get($keyName), $expectedValue);\n    }\n\n    /** @test */\n    public function set_configuration_key_array(): void\n    {\n        $cwd = \\sys_get_temp_dir();\n        $sourcePath = Path::fromStrings('app', 'tasks');\n        $configuration = $this->createConfiguration(['source' => $sourcePath->toString()], $cwd);\n\n        $arrayName = 'test_array';\n        $keyName = 'test_key';\n        $expectedValue = 'test_value';\n\n        $newConfiguration = $configuration->withNewEntry(\"{$arrayName}.{$keyName}\", $expectedValue);\n        $expectedArray = $newConfiguration->get($arrayName);\n\n        self::assertIsArray($expectedArray);\n        self::assertArrayHasKey($keyName, $expectedArray);\n        self::assertSame($expectedArray[$keyName], $expectedValue);\n    }\n\n    /** @param array<string,string|array> $config */\n    private function createConfiguration(array $config = [], string $cwd = ''): Configuration\n    {\n        $mockConfigurationParser = $this->createMock(ConfigurationParserInterface::class);\n        $mockConfigurationParser\n            ->method('parseConfig')\n            ->willReturn($config)\n        ;\n\n        $mockFilesystem = $this->createMock(FilesystemInterface::class);\n        $mockFilesystem\n            ->method('getCwd')\n            ->willReturn($cwd)\n        ;\n\n        return new Configuration($mockConfigurationParser, $mockFilesystem);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Configuration/FileParserTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Configuration;\n\nuse Crunz\\Configuration\\ConfigFileNotExistsException;\nuse Crunz\\Configuration\\ConfigFileNotReadableException;\nuse Crunz\\Configuration\\FileParser;\nuse Crunz\\Tests\\TestCase\\TemporaryFile;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Yaml\\Yaml;\n\nclass FileParserTest extends TestCase\n{\n    /** @test */\n    public function parse_throws_exception_on_non_existing_file(): void\n    {\n        $filePath = '/path/to/wrong/file';\n\n        $this->expectException(ConfigFileNotExistsException::class);\n        $this->expectExceptionMessage(\"Configuration file '{$filePath}' not exists.\");\n\n        $parser = $this->createFileParser();\n        $parser->parse($filePath);\n    }\n\n    /** @test */\n    public function parse_throws_exception_on_non_readable_file(): void\n    {\n        if ($this->isWindows()) {\n            self::markTestSkipped('Required Unix-based OS.');\n        }\n\n        $tempFile = new TemporaryFile();\n        $tempFile->changePermissions(0200);\n        $filePath = $tempFile->filePath();\n\n        $this->expectException(ConfigFileNotReadableException::class);\n        $this->expectExceptionMessage(\"Config file '{$filePath}' is not readable.\");\n\n        $parser = $this->createFileParser();\n        $parser->parse($filePath);\n    }\n\n    /** @test */\n    public function parse_returns_parsed_file_content(): void\n    {\n        $tempFile = new TemporaryFile();\n        $filePath = $tempFile->filePath();\n        $configData = [\n            'suffix' => 'Task.php',\n            'source' => 'tasks',\n        ];\n        \\file_put_contents($filePath, Yaml::dump($configData));\n\n        $parser = $this->createFileParser();\n\n        self::assertSame([$configData], $parser->parse($filePath));\n    }\n\n    /**\n     * @return FileParser\n     */\n    private function createFileParser()\n    {\n        return new FileParser(new Yaml());\n    }\n\n    /**\n     * @return bool\n     */\n    private function isWindows()\n    {\n        return DIRECTORY_SEPARATOR === '\\\\';\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Console/Command/ScheduleListCommandTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Console\\Command;\n\nuse Crunz\\Console\\Command\\ScheduleListCommand;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Schedule;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\FakeLoader;\nuse Crunz\\Tests\\TestCase\\Faker;\nuse Crunz\\Tests\\TestCase\\FakeTaskCollection;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\nuse Symfony\\Component\\Console\\Input\\ArrayInput;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\BufferedOutput;\nuse Symfony\\Component\\Console\\Output\\NullOutput;\n\nfinal class ScheduleListCommandTest extends UnitTestCase\n{\n    public function test_passing_unsupported_format_fails(): void\n    {\n        // Arrange\n        $format = Faker::word();\n        $command = $this->createCommand();\n\n        // Expect\n        $this->expectException(CrunzException::class);\n        $this->expectExceptionMessage(\"Format '{$format}' is not supported.\");\n\n        // Act\n        $command->run(\n            $this->createInput($format),\n            new NullOutput(),\n        );\n    }\n\n    /** @dataProvider formatProvider */\n    public function test_list_output_format(\\Closure $paramsGenerator): void\n    {\n        // Arrange\n        /**\n         * @var string   $format\n         * @var string   $expectedOutput\n         * @var \\Closure $assert\n         */\n        [\n            'format' => $format,\n            'expectedOutput' => $expectedOutput,\n            'assert' => $assert,\n        ] = $paramsGenerator();\n        $output = new BufferedOutput();\n        $commandString = 'php -v';\n        $cronExpression = '15 3 * * 1,3,5';\n        $description = 'PHP version';\n        $schedule = self::createScheduleWithTask(\n            $commandString,\n            $description,\n            $cronExpression,\n        );\n        $command = $this->createCommand([$schedule]);\n\n        // Act\n        $command->run(\n            $this->createInput($format),\n            $output,\n        );\n\n        // Assert\n        $assert($expectedOutput, $output->fetch());\n    }\n\n    /** @return iterable<string,array{\\Closure}> */\n    public static function formatProvider(): iterable\n    {\n        yield 'text' => [\n            function (): array {\n                $commandString = 'php -v';\n                $cronExpression = '15 3 * * 1,3,5';\n                $description = 'PHP version';\n                $schedule = self::createScheduleWithTask(\n                    $commandString,\n                    $description,\n                    $cronExpression,\n                );\n\n                return [\n                    'format' => 'text',\n                    'schedule' => $schedule,\n                    'expectedOutput' => <<<TXT\n                        +---+-------------+----------------+----------------+\n                        | # | Task        | Expression     | Command to Run |\n                        +---+-------------+----------------+----------------+\n                        | 1 | PHP version | 15 3 * * 1,3,5 | php -v         |\n                        +---+-------------+----------------+----------------+\n                        \n                        TXT,\n                    'assert' => static function (string $expectedOutput, string $actualOutput): void {\n                        self::assertSame($expectedOutput, $actualOutput);\n                    },\n                ];\n            },\n        ];\n\n        yield 'json' => [\n            function (): array {\n                $commandString = 'php -v';\n                $cronExpression = '15 3 * * 1,3,5';\n                $description = 'PHP version';\n                $schedule = self::createScheduleWithTask(\n                    $commandString,\n                    $description,\n                    $cronExpression,\n                );\n\n                return [\n                    'format' => 'json',\n                    'schedule' => $schedule,\n                    'expectedOutput' => self::encodeJson(\n                        [\n                            [\n                                'command' => $commandString,\n                                'expression' => $cronExpression,\n                                'number' => 1,\n                                'task' => $description,\n                            ],\n                        ],\n                    ),\n                    'assert' => static function (string $expectedOutput, string $actualOutput): void {\n                        self::assertJsonStringEqualsJsonString($expectedOutput, $actualOutput);\n                    },\n                ];\n            },\n        ];\n    }\n\n    /** @param Schedule[] $schedules */\n    private function createCommand(array $schedules = []): ScheduleListCommand\n    {\n        return new ScheduleListCommand(\n            new FakeConfiguration(),\n            new FakeTaskCollection(),\n            new FakeLoader($schedules),\n        );\n    }\n\n    private function createInput(string $format): InputInterface\n    {\n        return new ArrayInput(\n            [\n                '--format' => $format,\n            ]\n        );\n    }\n\n    private static function createScheduleWithTask(\n        string $command,\n        string $description,\n        string $cronExpression,\n    ): Schedule {\n        $schedule = new Schedule();\n        $schedule\n            ->run($command)\n            ->description($description)\n            ->cron($cronExpression)\n        ;\n\n        return $schedule;\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Console/Command/ScheduleRunCommandTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Console\\Command;\n\nuse Crunz\\Console\\Command\\ScheduleRunCommand;\nuse Crunz\\Event;\nuse Crunz\\EventRunner;\nuse Crunz\\Schedule;\nuse Crunz\\Schedule\\ScheduleFactory;\nuse Crunz\\Task\\CollectionInterface;\nuse Crunz\\Task\\Loader;\nuse Crunz\\Task\\LoaderInterface;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\FakeTaskCollection;\nuse Crunz\\Tests\\TestCase\\TemporaryFile;\nuse PHPUnit\\Framework\\MockObject\\Generator\\Generator as MockGenerator;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Filesystem\\Filesystem;\n\nclass ScheduleRunCommandTest extends TestCase\n{\n    /** @test */\n    public function force_run_all_tasks(): void\n    {\n        $tempFile = new TemporaryFile();\n        $filename = $this->createTaskFile($this->taskContent(), $tempFile);\n\n        $mockInput = $this->mockInput(\n            [\n                'force' => true,\n                'task' => null,\n            ],\n            ['source' => '']\n        );\n        $mockOutput = $this->createMock(OutputInterface::class);\n        $mockTaskCollection = $this->mockTaskCollection($filename);\n        $mockEventRunner = $this->mockEventRunner($mockOutput);\n\n        $command = new ScheduleRunCommand(\n            $mockTaskCollection,\n            new FakeConfiguration(['source' => '']),\n            $mockEventRunner,\n            $this->createMock(Timezone::class),\n            $this->createMock(ScheduleFactory::class),\n            $this->createTaskLoader()\n        );\n\n        $command->run(\n            $mockInput,\n            $mockOutput\n        );\n    }\n\n    /** @test */\n    public function run_specific_task(): void\n    {\n        $tempFile1 = new TemporaryFile();\n        $tempFile2 = new TemporaryFile();\n        $filename1 = $this->createTaskFile($this->phpVersionTaskContent(), $tempFile1);\n        $filename2 = $this->createTaskFile($this->phpVersionTaskContent(), $tempFile2);\n\n        $mockInput = $this->mockInput(\n            [\n                'force' => false,\n                'task' => '1',\n            ],\n            ['source' => '']\n        );\n        $mockOutput = $this->createMock(OutputInterface::class);\n        $mockTaskCollection = $this->mockTaskCollection($filename1, $filename2);\n        $mockEventRunner = $this->mockEventRunner($mockOutput);\n\n        $command = new ScheduleRunCommand(\n            $mockTaskCollection,\n            new FakeConfiguration(['source' => '']),\n            $mockEventRunner,\n            self::mockTimezoneProvider(),\n            $this->mockScheduleFactory(),\n            $this->createTaskLoader()\n        );\n\n        $command->run(\n            $mockInput,\n            $mockOutput\n        );\n    }\n\n    public static function mockTimezoneProvider(): MockObject&Timezone\n    {\n        $timeZone = new \\DateTimeZone('UTC');\n        /** @var MockObject&Timezone $timezoneProviderMock */\n        $timezoneProviderMock = (new MockGenerator())->testDouble(\n            Timezone::class,\n            true,\n            [],\n            [],\n            '',\n            false,\n            false,\n            true,\n            false,\n            false,\n            null,\n            false,\n        );\n        $timezoneProviderMock->method('timezoneForComparisons')->willReturn($timeZone);\n\n        return $timezoneProviderMock;\n    }\n\n    private function mockScheduleFactory(): ScheduleFactory\n    {\n        $mockEvent = $this->createMock(Event::class);\n        $mockSchedule = $this->createConfiguredMock(Schedule::class, ['events' => [$mockEvent]]);\n        $mockScheduleFactory = $this->createMock(ScheduleFactory::class);\n        $mockScheduleFactory\n            ->expects(self::once())\n            ->method('singleTaskSchedule')\n            ->willReturn([$mockSchedule])\n        ;\n\n        return $mockScheduleFactory;\n    }\n\n    /** @return EventRunner|MockObject */\n    private function mockEventRunner(OutputInterface $output): EventRunner\n    {\n        $mockEventRunner = $this->createMock(EventRunner::class);\n        $mockEventRunner\n            ->expects(self::once())\n            ->method('handle')\n            ->with(\n                $output,\n                self::callback(\n                    function ($schedules) {\n                        $isArray = \\is_array($schedules);\n                        $count = \\is_countable($schedules) ? \\count($schedules) : 0;\n                        $schedule = \\reset($schedules);\n\n                        return $isArray\n                            && 1 === $count\n                            && $schedule instanceof Schedule\n                        ;\n                    }\n                )\n            )\n        ;\n\n        return $mockEventRunner;\n    }\n\n    /**\n     * @param array<string,bool|string|null> $options\n     * @param array<string,bool|string|null> $arguments\n     *\n     * @return MockObject|InputInterface\n     */\n    private function mockInput(array $options, array $arguments = []): InputInterface\n    {\n        $mockInput = $this->createMock(InputInterface::class);\n        $mockInput\n            ->method('getOptions')\n            ->willReturn($options)\n        ;\n        $mockInput\n            ->method('getArguments')\n            ->willReturn($arguments)\n        ;\n\n        return $mockInput;\n    }\n\n    private function mockTaskCollection(string ...$taskFiles): CollectionInterface\n    {\n        $mocksFileInfo = \\array_map(\n            fn ($taskFile) => $this->createConfiguredMock(\\SplFileInfo::class, ['getRealPath' => $taskFile]),\n            $taskFiles\n        );\n\n        return new FakeTaskCollection($mocksFileInfo);\n    }\n\n    private function createTaskFile(string $taskContent, TemporaryFile $file): string\n    {\n        $filesystem = new Filesystem();\n\n        $filename = $file->filePath();\n        $filesystem->touch($filename);\n        $filesystem->dumpFile($filename, $taskContent);\n\n        return $filename;\n    }\n\n    private function taskContent(): string\n    {\n        return <<<'PHP_WRAP'\n            <?php\n            \n            use Crunz\\Schedule;\n            \n            $schedule = new Schedule();\n            \n            $schedule->run('php -v')\n                ->description('Show PHP version')\n                // Always skip\n                ->skip(static function () {return true;})\n            ;\n            \n            return $schedule;\n            PHP_WRAP;\n    }\n\n    private function phpVersionTaskContent(): string\n    {\n        return <<<'PHP_WRAP'\n            <?php\n            \n            use Crunz\\Schedule;\n            \n            $schedule = new Schedule();\n            \n            $schedule->run('php -v')\n                ->everyMinute()\n                ->description('Show PHP version')\n            ;\n            \n            return $schedule;\n            PHP_WRAP;\n    }\n\n    private function createTaskLoader(): LoaderInterface\n    {\n        return new Loader();\n    }\n}\n"
  },
  {
    "path": "tests/Unit/EnvFlags/EnvFlagsTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\EnvFlags;\n\nuse Crunz\\EnvFlags\\EnvFlags;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class EnvFlagsTest extends TestCase\n{\n    /**\n     * @test\n     *\n     * @dataProvider statusProvider\n     */\n    public function deprecation_handler_status_is_correct(string $flagValue, bool $expectedEnabled): void\n    {\n        \\putenv(EnvFlags::DEPRECATION_HANDLER_FLAG . \"={$flagValue}\");\n\n        $envFlags = new EnvFlags();\n        self::assertSame($expectedEnabled, $envFlags->isDeprecationHandlerEnabled());\n    }\n\n    /** @test */\n    public function deprecation_handler_can_be_disabled(): void\n    {\n        $envFlags = new EnvFlags();\n        $envFlags->disableDeprecationHandler();\n\n        $this->assertFlagValue(EnvFlags::DEPRECATION_HANDLER_FLAG, '0');\n    }\n\n    /** @test */\n    public function deprecation_handler_can_be_enabled(): void\n    {\n        $envFlags = new EnvFlags();\n        $envFlags->enableDeprecationHandler();\n\n        $this->assertFlagValue(EnvFlags::DEPRECATION_HANDLER_FLAG, '1');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider containerDebugProvider\n     */\n    public function container_debug_flag_is_correct(string $flagValue, bool $expectedEnabled): void\n    {\n        \\putenv(EnvFlags::CONTAINER_DEBUG_FLAG . \"={$flagValue}\");\n\n        $envFlags = new EnvFlags();\n        self::assertSame($expectedEnabled, $envFlags->isContainerDebugEnabled());\n    }\n\n    /** @test */\n    public function container_debug_can_be_disabled(): void\n    {\n        $envFlags = new EnvFlags();\n        $envFlags->disableContainerDebug();\n\n        $this->assertFlagValue(EnvFlags::CONTAINER_DEBUG_FLAG, '0');\n    }\n\n    /** @test */\n    public function container_debug_can_be_enabled(): void\n    {\n        $envFlags = new EnvFlags();\n        $envFlags->enableContainerDebug();\n\n        $this->assertFlagValue(EnvFlags::CONTAINER_DEBUG_FLAG, '1');\n    }\n\n    /** @return iterable<string,array> */\n    public static function statusProvider(): iterable\n    {\n        yield 'true' => [\n            '1',\n            true,\n        ];\n\n        yield 'false' => [\n            '0',\n            false,\n        ];\n    }\n\n    /** @return iterable<string,array> */\n    public static function containerDebugProvider(): iterable\n    {\n        yield 'true' => [\n            '1',\n            true,\n        ];\n\n        yield 'false' => [\n            '0',\n            false,\n        ];\n    }\n\n    private function assertFlagValue(string $flag, string $expectedValue): void\n    {\n        $actualValue = \\getenv($flag);\n        self::assertSame($expectedValue, $actualValue);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/EventRunnerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit;\n\nuse Crunz\\EventRunner;\nuse Crunz\\HttpClient\\HttpClientInterface;\nuse Crunz\\Invoker;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\nuse Crunz\\Logger\\LoggerFactory;\nuse Crunz\\Mailer;\nuse Crunz\\Schedule;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\nuse Symfony\\Component\\Lock\\BlockingStoreInterface;\nuse Symfony\\Component\\Lock\\StoreInterface;\n\nfinal class EventRunnerTest extends TestCase\n{\n    /** @test */\n    public function url_is_pinged_before(): void\n    {\n        $url = 'https://ping-befo.re/';\n        $output = $this->createMock(OutputInterface::class);\n\n        $eventRunner = $this->createEventRunnerForPing($url);\n\n        $schedule = new Schedule();\n        $event = $schedule->run('php -v');\n        $event->pingBefore($url);\n\n        $eventRunner->handle($output, [$schedule]);\n    }\n\n    /** @test */\n    public function url_is_pinged_after(): void\n    {\n        $url = 'https://ping-aft.er/';\n        $output = $this->createMock(OutputInterface::class);\n\n        $eventRunner = $this->createEventRunnerForPing($url);\n\n        $schedule = new Schedule();\n        $event = $schedule->run('php -v');\n        $event->thenPing($url);\n\n        $eventRunner->handle($output, [$schedule]);\n    }\n\n    public function test_event_logging_configuration(): void\n    {\n        $logTarget = 'event.log';\n\n        // create schedule with event that changes logging configuration\n        $schedule = new Schedule();\n        $schedule->run('php -v')\n            ->appendOutputTo($logTarget)\n        ;\n\n        // mock the LoggerFactory\n        $loggerFactory = $this->createMock(LoggerFactory::class);\n        $loggerFactory->expects(self::once())\n            ->method('createEvent')\n            ->with($logTarget);\n\n        // create an EventRunner to handle the Schedule\n        $eventRunner = new EventRunner(\n            $this->createMock(Invoker::class),\n            new FakeConfiguration(),\n            $this->createMock(Mailer::class),\n            $loggerFactory,\n            $this->createMock(HttpClientInterface::class),\n            $this->createMock(ConsoleLoggerInterface::class)\n        );\n\n        $output = $this->createMock(OutputInterface::class);\n        $eventRunner->handle($output, [$schedule]);\n    }\n\n    public function test_lock_is_released_on_error(): void\n    {\n        $output = $this->createMock(OutputInterface::class);\n\n        if (\\interface_exists(StoreInterface::class)) {\n            $mockStore = $this->createMock(StoreInterface::class);\n        } else {\n            $mockStore = $this->createMock(BlockingStoreInterface::class);\n        }\n\n        $mockStore\n            ->expects(self::once())\n            ->method('delete')\n        ;\n        $schedule = new Schedule();\n        $event = $schedule->run('wrong-command');\n        $event->preventOverlapping($mockStore);\n\n        $eventRunner = $this->createEventRunner(true);\n        $eventRunner->handle($output, [$schedule]);\n    }\n\n    /**\n     * @param string $url\n     *\n     * @return EventRunner\n     */\n    private function createEventRunnerForPing($url)\n    {\n        $invoker = $this->createMock(Invoker::class);\n        $mailer = $this->createMock(Mailer::class);\n        $loggerFactory = $this->createMock(LoggerFactory::class);\n        $httpClient = $this->createMock(HttpClientInterface::class);\n        $consoleLogger = $this->createMock(ConsoleLoggerInterface::class);\n\n        $httpClient\n            ->expects(self::once())\n            ->method('ping')\n            ->with($url)\n        ;\n\n        return new EventRunner(\n            $invoker,\n            new FakeConfiguration(),\n            $mailer,\n            $loggerFactory,\n            $httpClient,\n            $consoleLogger\n        );\n    }\n\n    private function createEventRunner(bool $realInvoker = false): EventRunner\n    {\n        $invoker = true === $realInvoker\n            ? new Invoker()\n            : $this->createMock(Invoker::class)\n        ;\n        $mailer = $this->createMock(Mailer::class);\n        $loggerFactory = $this->createMock(LoggerFactory::class);\n        $httpClient = $this->createMock(HttpClientInterface::class);\n        $consoleLogger = $this->createMock(ConsoleLoggerInterface::class);\n\n        return new EventRunner(\n            $invoker,\n            new FakeConfiguration(),\n            $mailer,\n            $loggerFactory,\n            $httpClient,\n            $consoleLogger\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/EventTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit;\n\nuse Crunz\\Event;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Task\\TaskException;\nuse Crunz\\Tests\\TestCase\\Faker;\nuse Crunz\\Tests\\TestCase\\TestClock;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\nuse Symfony\\Component\\Lock\\PersistingStoreInterface;\nuse Symfony\\Component\\Lock\\Store\\PdoStore;\nuse Symfony\\Component\\Lock\\Store\\SemaphoreStore;\n\nfinal class EventTest extends UnitTestCase\n{\n    /**\n     * The default configuration timezone.\n     */\n    protected string $defaultTimezone;\n\n    /**\n     * Unique identifier for the event.\n     */\n    protected string $id;\n\n    public function setUp(): void\n    {\n        $this->id = \\uniqid('crunz', true);\n\n        $this->defaultTimezone = \\date_default_timezone_get();\n        \\date_default_timezone_set('UTC');\n    }\n\n    public function tearDown(): void\n    {\n        \\date_default_timezone_set($this->defaultTimezone);\n    }\n\n    /**\n     * @group cronCompile\n     */\n    public function test_unit_methods(): void\n    {\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('0 * * * *', $e->hourly()->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('0 0 * * *', $e->daily()->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('45 15 * * *', $e->dailyAt('15:45')->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('0 4,8 * * *', $e->twiceDaily(4, 8)->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('0 0 * * 0', $e->weekly()->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('0 0 1 * *', $e->monthly()->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('0 0 1 */3 *', $e->quarterly()->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('0 0 1 1 *', $e->yearly()->getExpression());\n    }\n\n    /**\n     * @group cronCompile\n     */\n    public function test_low_level_methods(): void\n    {\n        $timezone = new \\DateTimeZone('UTC');\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('30 1 11 4 *', $e->on('01:30 11-04-2016')->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('45 13 * * *', $e->on('13:45')->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('45 13 * * *', $e->at('13:45')->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n\n        $e->minute([12, 24, 35])\n          ->hour('1-5', 4, 8)\n          ->dayOfMonth(1, 6, 12, 19, 25)\n          ->month('1-8')\n          ->dayOfWeek('mon,wed,thu');\n\n        self::assertEquals('12,24,35 1-5,4,8 1,6,12,19,25 1-8 mon,wed,thu', $e->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('45 13 * * *', $e->cron('45 13 * * *')->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertTrue($e->isDue($timezone));\n    }\n\n    /**\n     * @group cronCompile\n     */\n    public function test_weekday_methods(): void\n    {\n        $e = new Event($this->id, 'php qux');\n        self::assertEquals('* * * * 2', $e->tuesdays()->getExpression());\n\n        $e = new Event($this->id, 'php flob');\n        self::assertEquals('* * * * 3', $e->wednesdays()->getExpression());\n\n        $e = new Event($this->id, 'php foo');\n        self::assertEquals('* * * * 4', $e->thursdays()->getExpression());\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('* * * * 5', $e->fridays()->getExpression());\n\n        $e = new Event($this->id, 'php baz');\n        self::assertEquals('* * * * 1-5', $e->weekdays()->getExpression());\n\n        $e = new Event($this->id, 'php bla');\n        self::assertEquals('30 1 * * 2', $e->weeklyOn('2', '01:30')->getExpression());\n    }\n\n    public function test_cron_life_time(): void\n    {\n        $timezone = new \\DateTimeZone('UTC');\n\n        $event = new Event($this->id, 'php foo');\n        self::assertFalse(\n            $event\n                ->between('2015-01-01', '2015-01-02')\n                ->isDue($timezone)\n        );\n\n        $futureDate = new \\DateTimeImmutable('+1 year');\n\n        $event = new Event($this->id, 'php foo');\n        self::assertFalse(\n            $event\n                ->from($futureDate->format('Y-m-d'))\n                ->isDue($timezone)\n        );\n\n        $event = new Event($this->id, 'php foo');\n        self::assertFalse(\n            $event\n                ->to('2015-01-01')\n                ->isDue($timezone)\n        );\n    }\n\n    /**\n     * @param \\Closure(): array{\n     *     dateFrom: string,\n     *     dateTo: string\n     * } $paramsGenerator\n     *\n     * @dataProvider dateFromToProvider\n     */\n    public function test_get_from(\\Closure $paramsGenerator): void\n    {\n        $params = $paramsGenerator();\n\n        $event = new Event($this->id, 'php foo');\n        $event->from($params['dateFrom']);\n\n        self::assertSame($params['dateFrom'], $event->getFrom());\n    }\n\n    /**\n     * @param \\Closure(): array{\n     *     dateFrom: string,\n     *     dateTo: string\n     * } $paramsGenerator\n     *\n     * @dataProvider dateFromToProvider\n     */\n    public function test_get_to(\\Closure $paramsGenerator): void\n    {\n        $params = $paramsGenerator();\n\n        $event = new Event($this->id, 'php foo');\n        $event->to($params['dateTo']);\n\n        self::assertSame($params['dateTo'], $event->getTo());\n    }\n\n    /**\n     * @param \\Closure(): array{\n     *     dateFrom: string,\n     *     dateTo: string\n     * } $paramsGenerator\n     *\n     * @dataProvider dateFromToProvider\n     */\n    public function test_get_between(\\Closure $paramsGenerator): void\n    {\n        $params = $paramsGenerator();\n\n        $event = new Event($this->id, 'php foo');\n        $event->between($params['dateFrom'], $params['dateTo']);\n\n        self::assertSame($params['dateFrom'], $event->getFrom());\n\n        self::assertSame($params['dateTo'], $event->getTo());\n    }\n\n    public function test_cron_conditions(): void\n    {\n        $timezone = new \\DateTimeZone('UTC');\n\n        $e = new Event($this->id, 'php foo');\n        self::assertFalse($e->cron('* * * * *')->when(fn () => false)->isDue($timezone));\n\n        $e = new Event($this->id, 'php foo');\n        self::assertTrue($e->cron('* * * * *')->when(fn () => true)->isDue($timezone));\n\n        $e = new Event($this->id, 'php foo');\n        self::assertFalse($e->cron('* * * * *')->skip(fn () => true)->isDue($timezone));\n\n        $e = new Event($this->id, 'php foo');\n        self::assertTrue($e->cron('* * * * *')->skip(fn () => false)->isDue($timezone));\n    }\n\n    /** @test */\n    public function more_than_five_parts_in_cron_expression_results_in_exception(): void\n    {\n        $this->expectException(TaskException::class);\n        $this->expectExceptionMessage(\"Expression '* * * * * *' has more than five parts and this is not allowed.\");\n\n        $e = new Event(1, 'php foo -v');\n        $e->cron('* * * * * *');\n    }\n\n    public function test_build_command(): void\n    {\n        $e = new Event($this->id, 'php -i');\n\n        self::assertSame('php -i', $e->buildCommand());\n    }\n\n    public function test_is_due(): void\n    {\n        $timezone = new \\DateTimeZone('UTC');\n        $this->setClockNow(new \\DateTimeImmutable('2015-04-12 00:00:00', $timezone));\n\n        $e = new Event($this->id, 'php foo');\n        self::assertTrue($e->sundays()->isDue($timezone));\n\n        $e = new Event($this->id, 'php bar');\n        self::assertEquals('0 19 * * 6', $e->saturdays()->at('19:00')->timezone('EST')->getExpression());\n        self::assertTrue($e->isDue($timezone));\n\n        $e = new Event($this->id, 'php bar');\n        $this->setClockNow(new \\DateTimeImmutable(\\date('Y') . '-04-12 00:00:00'));\n        self::assertTrue($e->on('00:00 ' . \\date('Y') . '-04-12')->isDue($timezone));\n    }\n\n    public function test_name(): void\n    {\n        $e = new Event($this->id, 'php foo');\n        $e->description('Testing Cron');\n\n        self::assertEquals('Testing Cron', $e->description);\n    }\n\n    /** @test */\n    public function in_change_working_directory_in_build_command_on_windows(): void\n    {\n        if (!$this->isWindows()) {\n            self::markTestSkipped('Required Windows OS.');\n        }\n\n        $workingDir = 'C:\\\\windows\\\\temp';\n        $event = new Event($this->id, 'php -v');\n\n        $event->in($workingDir);\n\n        self::assertSame(\"cd /d {$workingDir} & php -v\", $event->buildCommand());\n    }\n\n    /** @test */\n    public function in_change_working_directory_in_build_command_on_unix(): void\n    {\n        if ($this->isWindows()) {\n            self::markTestSkipped('Required Unix-based OS.');\n        }\n\n        $event = new Event($this->id, 'php -v');\n\n        $event->in('/tmp');\n\n        self::assertSame('cd /tmp; php -v', $event->buildCommand());\n    }\n\n    /** @test */\n    public function on_do_not_run_task_every_minute(): void\n    {\n        $event = new Event($this->id, 'php -i');\n\n        $event->on('Thursday 8:00');\n\n        self::assertSame('0 8 * * *', $event->getExpression());\n    }\n\n    /** @test */\n    public function setting_user_prepend_sudo_to_command(): void\n    {\n        if ($this->isWindows()) {\n            self::markTestSkipped('Required Unix-based OS.');\n        }\n\n        $event = new Event($this->id, 'php -v');\n\n        $event->user('john');\n\n        self::assertSame('sudo -u john php -v', $event->buildCommand());\n    }\n\n    /** @test */\n    public function custom_user_and_cwd(): void\n    {\n        if ($this->isWindows()) {\n            self::markTestSkipped('Required Unix-based OS.');\n        }\n\n        $event = new Event($this->id, 'php -i');\n\n        $event->user('john');\n        $event->in('/var/test');\n\n        self::assertSame('sudo -u john cd /var/test; sudo -u john php -i', $event->buildCommand());\n    }\n\n    /** @test */\n    public function not_implemented_user_change_on_windows(): void\n    {\n        if (!$this->isWindows()) {\n            self::markTestSkipped('Required Windows OS.');\n        }\n\n        $this->expectException(\\Crunz\\Exception\\NotImplementedException::class);\n        $this->expectExceptionMessage('Changing user on Windows is not implemented.');\n\n        $event = new Event($this->id, 'php -i');\n\n        $event->user('john');\n    }\n\n    /**\n     * @test\n     *\n     * @runInSeparateProcess\n     */\n    public function closure_command_have_full_binary_paths(): void\n    {\n        if (!\\defined('CRUNZ_BIN')) {\n            \\define('CRUNZ_BIN', __FILE__);\n        }\n\n        $closure = fn () => 0;\n        $closureSerializer = $this->createClosureSerializer();\n        $serializedClosure = $closureSerializer->serialize($closure);\n        $queryClosure = \\http_build_query([$serializedClosure]);\n        $crunzBin = CRUNZ_BIN;\n\n        $event = new Event($this->id, $closure);\n\n        $command = $event->buildCommand();\n\n        self::assertSame(\\escapeshellarg(PHP_BINARY) . ' ' . \\escapeshellarg($crunzBin) . \" closure:run {$queryClosure}\", $command);\n    }\n\n    /** @test */\n    public function whole_output_catches_stdout_and_stderr(): void\n    {\n        $command = \"php -r \\\"echo 'Test output'; throw new \\Exception('Exception output');\\\"\";\n        $event = new Event(\\uniqid('c', true), $command);\n        $event->start();\n        $process = $event->getProcess();\n\n        while ($process->isRunning()) {\n            \\usleep(20000); // wait 20 ms\n        }\n\n        $wholeOutput = $event->wholeOutput();\n\n        self::assertStringContainsString(\n            'Test output',\n            $wholeOutput,\n            'Missing standard output'\n        );\n        self::assertStringContainsString(\n            'Exception output',\n            $wholeOutput,\n            'Missing error output'\n        );\n    }\n\n    /** @test */\n    public function task_will_prevent_overlapping_with_default_store(): void\n    {\n        $this->assertPreventOverlapping();\n    }\n\n    /** @test */\n    public function task_will_prevent_overlapping_with_semaphore_store(): void\n    {\n        if (!\\extension_loaded('sysvsem')) {\n            self::markTestSkipped('Semaphore extension not installed.');\n        }\n\n        $this->assertPreventOverlapping(new SemaphoreStore());\n    }\n\n    /** @dataProvider everyMethodProvider */\n    public function test_every_methods(string $method, string $expectedCronExpression): void\n    {\n        // Arrange\n        $event = new Event($this->id, 'php -i');\n        /** @var callable $methodCall */\n        $methodCall = [$event, $method];\n        $methodCallClosure = \\Closure::fromCallable($methodCall);\n\n        // Act\n        $methodCallClosure();\n\n        // Assert\n        self::assertSame($expectedCronExpression, $event->getExpression());\n    }\n\n    public function test_hourly_at_with_valid_minute(): void\n    {\n        // Arrange\n        $event = $this->createEvent();\n        $minute = Faker::int(0, 59);\n\n        // Act\n        $event->hourlyAt($minute);\n\n        // Assert\n        self::assertSame(\"{$minute} * * * *\", $event->getExpression());\n    }\n\n    /** @dataProvider hourlyAtInvalidProvider */\n    public function test_hourly_at_with_invalid_minute(\n        int $minute,\n        string $expectedExceptionMessage,\n    ): void {\n        // Arrange\n        $event = $this->createEvent();\n\n        // Expect\n        $this->expectException(CrunzException::class);\n        $this->expectExceptionMessage($expectedExceptionMessage);\n\n        // Act\n        $event->hourlyAt($minute);\n    }\n\n    public function test_non_blocking_store_can_be_passed_to_prevent_overlapping(): void\n    {\n        // Arrange\n        $store = new PdoStore('');\n        $event = $this->createEvent();\n\n        // Expect\n        $this->expectNotToPerformAssertions();\n\n        // Act\n        $event->preventOverlapping($store);\n    }\n\n    /**\n     * @param \\Closure(): array{\n     *     now: \\DateTimeImmutable,\n     *     fromDateTime: string,\n     *     timeZone: \\DateTimeZone,\n     *     expectedIsDue: bool,\n     * } $paramsGenerator\n     *\n     * @dataProvider fromTimeZoneProvider\n     */\n    public function test_from_respects_time_zone(\\Closure $paramsGenerator): void\n    {\n        // Arrange\n        [\n            'now' => $now,\n            'fromDateTime' => $fromDateTime,\n            'timeZone' => $timeZone,\n            'expectedIsDue' => $expectedIsDue,\n        ] = $paramsGenerator();\n        $this->setClockNow($now);\n        $event = $this->createEvent();\n        $event->from($fromDateTime);\n\n        // Act\n        $isDue = $event->isDue($timeZone);\n\n        // Assert\n        self::assertSame($expectedIsDue, $isDue);\n    }\n\n    /**\n     * @param \\Closure(): array{\n     *     now: \\DateTimeImmutable,\n     *     toDateTime: string,\n     *     timeZone: \\DateTimeZone,\n     *     expectedIsDue: bool,\n     * } $paramsGenerator\n     *\n     * @dataProvider toTimeZoneProvider\n     */\n    public function test_to_respects_timezone(\\Closure $paramsGenerator): void\n    {\n        // Arrange\n        [\n            'now' => $now,\n            'toDateTime' => $toDateTime,\n            'timeZone' => $timeZone,\n            'expectedIsDue' => $expectedIsDue,\n        ] = $paramsGenerator();\n        $this->setClockNow($now);\n        $event = $this->createEvent();\n        $event->to($toDateTime);\n\n        // Act\n        $isDue = $event->isDue($timeZone);\n\n        // Assert\n        self::assertSame($expectedIsDue, $isDue);\n    }\n\n    /** @return iterable<string,array> */\n    public static function deprecatedEveryProvider(): iterable\n    {\n        yield 'every seven minutes' => ['everySevenMinutes'];\n        yield 'every five hours' => ['everyFiveHours'];\n        yield 'every two days' => ['everyTwoDays'];\n        yield 'every five months' => ['everyFiveMonths'];\n    }\n\n    /** @return iterable<string,array> */\n    public static function everyMethodProvider(): iterable\n    {\n        yield 'every minute' => ['everyMinute', '* * * * *'];\n        yield 'every two minutes' => ['everyTwoMinutes', '*/2 * * * *'];\n        yield 'every three minutes' => ['everyThreeMinutes', '*/3 * * * *'];\n        yield 'every four minutes' => ['everyFourMinutes', '*/4 * * * *'];\n        yield 'every five minutes' => ['everyFiveMinutes', '*/5 * * * *'];\n        yield 'every ten minutes' => ['everyTenMinutes', '*/10 * * * *'];\n        yield 'every fifteen minutes' => ['everyFifteenMinutes', '*/15 * * * *'];\n        yield 'every thirty minutes' => ['everyThirtyMinutes', '*/30 * * * *'];\n        yield 'every two hours' => ['everyTwoHours', '0 */2 * * *'];\n        yield 'every three hours' => ['everyThreeHours', '0 */3 * * *'];\n        yield 'every four hours' => ['everyFourHours', '0 */4 * * *'];\n        yield 'every six hours' => ['everySixHours', '0 */6 * * *'];\n    }\n\n    /** @return iterable<string, array{\\Closure}> */\n    public static function fromTimeZoneProvider(): iterable\n    {\n        yield 'same timezone' => [\n            static function (): array {\n                $timeZone = new \\DateTimeZone('Europe/Warsaw');\n\n                return [\n                    'now' => new \\DateTimeImmutable(\n                        '12:01',\n                        $timeZone,\n                    ),\n                    'fromDateTime' => '12:00',\n                    'timeZone' => $timeZone,\n                    'expectedIsDue' => true,\n                ];\n            },\n        ];\n\n        yield 'different timezones' => [\n            static fn (): array => [\n                'now' => new \\DateTimeImmutable(\n                    '12:00',\n                    new \\DateTimeZone('Europe/Warsaw'),\n                ),\n                'fromDateTime' => '11:01',\n                'timeZone' => new \\DateTimeZone('Europe/Lisbon'),\n                'expectedIsDue' => false,\n            ],\n        ];\n    }\n\n    /** @return iterable<string, array{\\Closure}> */\n    public static function toTimeZoneProvider(): iterable\n    {\n        yield 'same timezone' => [\n            static function (): array {\n                $timeZone = new \\DateTimeZone('Europe/Warsaw');\n\n                return [\n                    'now' => new \\DateTimeImmutable(\n                        '13:59',\n                        $timeZone,\n                    ),\n                    'toDateTime' => '14:00',\n                    'timeZone' => $timeZone,\n                    'expectedIsDue' => true,\n                ];\n            },\n        ];\n\n        yield 'different timezones' => [\n            static fn (): array => [\n                'now' => new \\DateTimeImmutable(\n                    '17:01',\n                    new \\DateTimeZone('Europe/Lisbon'),\n                ),\n                'toDateTime' => '18:00',\n                'timeZone' => new \\DateTimeZone('Europe/Warsaw'),\n                'expectedIsDue' => false,\n            ],\n        ];\n    }\n\n    /** @return iterable<string,array> */\n    public static function hourlyAtInvalidProvider(): iterable\n    {\n        yield 'minute below zero' => [\n            Faker::int(-100, -1),\n            \"Minute cannot be lower than '0'.\",\n        ];\n\n        yield 'minute above fifty nine' => [\n            Faker::int(60, 120),\n            \"Minute cannot be greater than '59'.\",\n        ];\n    }\n\n    /** @return iterable<string, array{\\Closure}> */\n    public static function dateFromToProvider(): iterable\n    {\n        yield 'dateFrom, dateTo with format yyyy-mm-dd' => [\n            static fn (): array => [\n                'dateFrom' => (new \\DateTime('+' . \\random_int(1, 59) . ' days'))->format('Y-m-d'),\n                'dateTo' => (new \\DateTime('+' . \\random_int(60, 120) . ' days'))->format('Y-m-d'),\n            ],\n        ];\n\n        yield 'dateFrom, dateTo with format H:i' => [\n            static fn (): array => [\n                'dateFrom' => (new \\DateTime('+' . \\random_int(1, 29) . ' minutes'))->format('H:i'),\n                'dateTo' => (new \\DateTime('+' . \\random_int(30, 60) . ' minutes'))->format('H:i'),\n            ],\n        ];\n\n        yield 'dateFrom, dateTo with format yyyy-mm-dd hh:mm' => [\n            static fn (): array => [\n                'dateFrom' => (new \\DateTime('+' . \\random_int(1, 59) . ' days +' . \\random_int(1, 29) . ' minutes'))->format('Y-m-d H:i'),\n                'dateTo' => (new \\DateTime('+' . \\random_int(60, 120) . ' days +' . \\random_int(30, 60) . ' minutes'))->format('Y-m-d H:i'),\n            ],\n        ];\n    }\n\n    private function assertPreventOverlapping(?PersistingStoreInterface $store = null): void\n    {\n        $event = $this->createPreventOverlappingEvent($store);\n        $event2 = $this->createPreventOverlappingEvent($store);\n\n        $event->start();\n\n        self::assertFalse($event2->isDue(new \\DateTimeZone('UTC')));\n    }\n\n    private function createPreventOverlappingEvent(?PersistingStoreInterface $store = null): Event\n    {\n        $command = \"php -r 'sleep(2);'\";\n\n        $event = new Event(\\uniqid('c', true), $command);\n        $event->preventOverlapping($store);\n        $event->everyMinute();\n\n        return $event;\n    }\n\n    private function setClockNow(\\DateTimeImmutable $dateTime): void\n    {\n        $testClock = new TestClock($dateTime);\n        $reflection = new \\ReflectionClass(Event::class);\n        $property = $reflection->getProperty('clock');\n        $property->setValue(null, $testClock);\n    }\n\n    private function isWindows(): bool\n    {\n        return DIRECTORY_SEPARATOR === '\\\\';\n    }\n\n    private function createEvent(): Event\n    {\n        return new Event(\n            \\uniqid(\n                'c',\n                true,\n            ),\n            'php -i',\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Filesystem/FilesystemTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Filesystem;\n\nuse Crunz\\Filesystem\\Filesystem;\nuse Crunz\\Path\\Path;\nuse Crunz\\Tests\\TestCase\\TemporaryFile;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\n\nfinal class FilesystemTest extends UnitTestCase\n{\n    /** @test */\n    public function cwd_is_correct(): void\n    {\n        $filesystem = new Filesystem();\n\n        self::assertSame(\\getcwd(), $filesystem->getCwd());\n    }\n\n    /**\n     * @dataProvider fileExistsProvider\n     *\n     * @test\n     */\n    public function file_exists_is_correct(string $path, bool $expectedExistence): void\n    {\n        $filesystem = new Filesystem();\n\n        self::assertSame($expectedExistence, $filesystem->fileExists($path));\n    }\n\n    /** @test */\n    public function temp_directory_return_system_temp_directory(): void\n    {\n        $filesystem = new Filesystem();\n\n        self::assertSame(\\sys_get_temp_dir(), $filesystem->tempDir());\n    }\n\n    /** @test */\n    public function remove_directory_removes_directories_recursively(): void\n    {\n        $filesystem = new Filesystem();\n\n        $tempDir = \\sys_get_temp_dir();\n        $rootPath = Path::fromStrings($tempDir, 'fs-tests');\n        $innerPath = Path::fromStrings($rootPath->toString(), 'inner');\n        $filePath = Path::fromStrings($innerPath->toString(), 'some-file.txt');\n\n        \\mkdir(\n            $innerPath->toString(),\n            0777,\n            true\n        );\n        \\touch($filePath->toString());\n\n        $filesystem->removeDirectory($rootPath->toString());\n\n        self::assertDirectoryDoesNotExist($rootPath->toString());\n    }\n\n    /** @test */\n    public function dump_file_writes_content_to_file(): void\n    {\n        $content = 'Some content';\n        $tempDir = \\sys_get_temp_dir();\n        $filePath = Path::fromStrings($tempDir, 'dump-file.txt');\n\n        $filesystem = new Filesystem();\n        $filesystem->dumpFile($filePath->toString(), $content);\n\n        self::assertStringEqualsFile($filePath->toString(), $content);\n\n        \\unlink($filePath->toString());\n    }\n\n    /** @test */\n    public function create_directory_creates_directory_recursive(): void\n    {\n        $tempDir = \\sys_get_temp_dir();\n        $rootDirectoryPath = Path::fromStrings($tempDir, 'crunz-test');\n        $directoryPath = Path::fromStrings(\n            $rootDirectoryPath->toString(),\n            'deep',\n            'path',\n            'here'\n        );\n\n        $filesystem = new Filesystem();\n        $filesystem->createDirectory($directoryPath->toString());\n\n        self::assertDirectoryExists($directoryPath->toString());\n\n        $filesystem->removeDirectory($rootDirectoryPath->toString());\n    }\n\n    /** @test */\n    public function copy_files(): void\n    {\n        $content = 'Copy content';\n        $tempDir = \\sys_get_temp_dir();\n        $rootDirectoryPath = Path::fromStrings($tempDir, 'copy-test');\n        \\mkdir($rootDirectoryPath->toString());\n        $filePath = Path::fromStrings($rootDirectoryPath->toString(), 'file1.txt');\n        $targetFile = Path::fromStrings($rootDirectoryPath->toString(), 'file-copy.txt');\n        \\file_put_contents($filePath->toString(), $content);\n\n        $filesystem = new Filesystem();\n        $filesystem->copy($filePath->toString(), $targetFile->toString());\n\n        self::assertFileExists($targetFile->toString());\n        self::assertStringEqualsFile($targetFile->toString(), $content);\n\n        $filesystem->removeDirectory($rootDirectoryPath->toString());\n    }\n\n    /** @test */\n    public function project_root_directory(): void\n    {\n        $filesystem = new Filesystem();\n\n        self::assertSame($this->findProjectRootDirectory(), $filesystem->projectRootDirectory());\n    }\n\n    /** @test */\n    public function read_content_return_file_content(): void\n    {\n        $filesystem = new Filesystem();\n        $content = $filesystem->readContent(__FILE__);\n\n        self::assertStringContainsString('final class FilesystemTest extends TestCase', $content);\n    }\n\n    /** @test */\n    public function read_content_throws_exception_when_file_not_exists(): void\n    {\n        $path = Path::fromStrings(\\sys_get_temp_dir(), 'wrong-file');\n\n        $this->expectException(\\RuntimeException::class);\n        $this->expectExceptionMessage(\"File '{$path->toString()}' doesn't exists.\");\n\n        $filesystem = new Filesystem();\n        $filesystem->readContent($path->toString());\n    }\n\n    /**\n     * @return iterable<string,array>\n     *\n     * @throws \\Crunz\\Exception\\CrunzException\n     */\n    public static function fileExistsProvider(): iterable\n    {\n        $tempFile = new TemporaryFile();\n\n        yield 'exists' => [\n            $tempFile->filePath(),\n            true,\n            // Param used to avoid GC\n            $tempFile,\n        ];\n\n        yield 'notExists' => [\n            '/some/wrong/path',\n            false,\n        ];\n    }\n\n    private function findProjectRootDirectory(): string\n    {\n        $dir = $rootDir = \\dirname(__DIR__);\n        $path = Path::fromStrings($dir, 'composer.json');\n\n        while (!\\file_exists($path->toString())) {\n            if ($dir === \\dirname($dir)) {\n                return $rootDir;\n            }\n            $dir = \\dirname($dir);\n            $path = Path::fromStrings($dir, 'composer.json');\n        }\n\n        return $dir;\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Finder/FinderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Finder;\n\nuse Crunz\\Filesystem\\Filesystem;\nuse Crunz\\Finder\\Finder;\nuse Crunz\\Path\\Path;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class FinderTest extends TestCase\n{\n    private Filesystem $filesystem;\n    private Path $tasksDirectory;\n    private Path $fixtureDirectory;\n\n    public function setUp(): void\n    {\n        $filesystem = new Filesystem();\n        $this->filesystem = $filesystem;\n        $this->tasksDirectory = Path::fromStrings(\n            $filesystem->tempDir(),\n            '.crunz',\n            'finder-test'\n        );\n        $this->filesystem->createDirectory($this->tasksDirectory->toString());\n        $this->fixtureDirectory = Path::fromStrings(\n            \\dirname(__DIR__, 2),\n            'resources',\n            'fixtures',\n            'finder',\n            'direct'\n        );\n    }\n\n    public function tearDown(): void\n    {\n        $tasksDirectory = $this->tasksDirectory;\n        $this->filesystem\n            ->removeDirectory($tasksDirectory->toString());\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider tasksProvider\n     */\n    public function find_returns_spl_file_info_collection(string $suffix, Path ...$files): void\n    {\n        $this->createFiles(...$files);\n        $tasksDirectory = $this->tasksDirectory;\n\n        $finder = new Finder();\n        $foundFiles = $finder->find($tasksDirectory, $suffix);\n\n        self::assertCount(\\count($files), $foundFiles);\n        self::assertContainsOnlyInstancesOf(\\SplFileInfo::class, $foundFiles);\n    }\n\n    /**\n     * @return iterable<string,array>\n     *\n     * @throws \\Crunz\\Exception\\CrunzException\n     */\n    public static function tasksProvider(): iterable\n    {\n        $suffix = 'Here.php';\n        $taskOne = Path::fromStrings('TestHere.php');\n        $taskTwo = Path::fromStrings('first-level', 'OtherTestHere.php');\n        $taskThree = Path::fromStrings(\n            'first-level',\n            'second-level',\n            'TestHere.php'\n        );\n\n        yield 'flat' => [$suffix, $taskOne];\n        yield 'firstLevel' => [\n            $suffix,\n            $taskOne,\n            $taskTwo,\n        ];\n        yield 'secondLevel' => [\n            $suffix,\n            $taskOne,\n            $taskTwo,\n            $taskThree,\n        ];\n    }\n\n    /**\n     * @test\n     */\n    public function find_files_in_symlinked_folder(): void\n    {\n        if ($this->isWindows()) {\n            // Committed symlinks require extra steps to work on Windows\n            // https://stackoverflow.com/questions/5917249/git-symlinks-in-windows\n            self::markTestSkipped('Required Unix-based OS.');\n        }\n\n        $fixtureDirectory = $this->fixtureDirectory;\n        $directFile = Path::fromStrings($fixtureDirectory->toString(), 'directHere.php')->toString();\n        $symlinkFileDestination = Path::fromStrings($fixtureDirectory->toString(), 'symlink', 'symlinkHere.php')->toString();\n\n        $finder = new Finder();\n        $foundFiles = $finder->find($fixtureDirectory, 'Here.php');\n\n        self::assertCount(2, $foundFiles);\n        self::assertArrayHasKey($directFile, $foundFiles);\n        self::assertArrayHasKey($symlinkFileDestination, $foundFiles);\n    }\n\n    private function createFiles(Path ...$files): void\n    {\n        $tasksDirectory = $this->tasksDirectory;\n\n        foreach ($files as $file) {\n            $path = Path::fromStrings($tasksDirectory->toString(), $file->toString());\n            $content = \\bin2hex(\\random_bytes(8));\n            $this->filesystem\n                ->dumpFile($path->toString(), $content);\n        }\n    }\n\n    private function isWindows(): bool\n    {\n        return DIRECTORY_SEPARATOR === '\\\\';\n    }\n}\n"
  },
  {
    "path": "tests/Unit/HttpClient/StreamHttpClientTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\HttpClient;\n\nuse Crunz\\HttpClient\\HttpClientException;\nuse Crunz\\HttpClient\\StreamHttpClient;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class StreamHttpClientTest extends TestCase\n{\n    /** @test */\n    public function ping_fail_with_invalid_address(): void\n    {\n        // Arrange\n        $url = 'http://www.wrong-address.tld';\n        $client = new StreamHttpClient();\n        $expectedExceptionMessage = 'Ping failed.';\n\n        // Expect\n        $this->expectException(HttpClientException::class);\n        $this->expectExceptionMessage($expectedExceptionMessage);\n\n        // Act\n        $client->ping($url);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Infrastructure/Dragonmantank/CronExpression/DragonmantankCronExpressionTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Infrastructure\\Dragonmantank\\CronExpression;\n\nuse Cron\\CronExpression;\nuse Crunz\\Application\\Cron\\CronExpressionInterface;\nuse Crunz\\Infrastructure\\Dragonmantank\\CronExpression\\DragonmantankCronExpression;\nuse Crunz\\Tests\\Unit\\Application\\Cron\\AbstractCronExpressionTestCase;\n\nfinal class DragonmantankCronExpressionTestCase extends AbstractCronExpressionTestCase\n{\n    protected function createExpression(string $cronExpression): CronExpressionInterface\n    {\n        $innerCronExpression = CronExpression::factory($cronExpression);\n\n        return new DragonmantankCronExpression($innerCronExpression);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Infrastructure/Psr/Logger/EnabledLoggerDecoratorTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Infrastructure\\Psr\\Logger;\n\nuse Crunz\\Application\\Service\\ConfigurationInterface;\nuse Crunz\\Infrastructure\\Psr\\Logger\\EnabledLoggerDecorator;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\Faker;\nuse Crunz\\Tests\\TestCase\\Logger\\SpyPsrLogger;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\nuse Psr\\Log\\LoggerInterface;\nuse Psr\\Log\\LogLevel;\n\nfinal class EnabledLoggerDecoratorTest extends UnitTestCase\n{\n    /** @dataProvider disabledChannelProvider */\n    public function test_disabled_channels_not_log(\n        ConfigurationInterface $configuration,\n        string $logLevel,\n    ): void {\n        // Arrange\n        $spyLogger = new SpyPsrLogger();\n        $enabledLoggerDecorator = $this->createEnabledLoggerDecorator($spyLogger, $configuration);\n\n        // Act\n        $enabledLoggerDecorator->log($logLevel, Faker::words());\n\n        // Assert\n        self::assertCount(0, $spyLogger->getLogs());\n    }\n\n    /** @dataProvider enabledChannelProvider */\n    public function test_enabled_channels_log(\n        ConfigurationInterface $configuration,\n        string $logLevel,\n    ): void {\n        // Arrange\n        $spyLogger = new SpyPsrLogger();\n        $enabledLoggerDecorator = $this->createEnabledLoggerDecorator($spyLogger, $configuration);\n\n        // Act\n        $enabledLoggerDecorator->log($logLevel, Faker::words());\n\n        // Assert\n        self::assertCount(1, $spyLogger->getLogs());\n    }\n\n    /** @return iterable<string,array> */\n    public static function disabledChannelProvider(): iterable\n    {\n        yield 'output' => [\n            new FakeConfiguration(['log_output' => false]),\n            LogLevel::INFO,\n        ];\n\n        yield 'error' => [\n            new FakeConfiguration(['log_errors' => false]),\n            LogLevel::ERROR,\n        ];\n    }\n\n    /** @return iterable<string,array> */\n    public static function enabledChannelProvider(): iterable\n    {\n        yield 'output' => [\n            new FakeConfiguration(['log_output' => true]),\n            LogLevel::INFO,\n        ];\n\n        yield 'error' => [\n            new FakeConfiguration(['log_errors' => true]),\n            LogLevel::ERROR,\n        ];\n    }\n\n    private function createEnabledLoggerDecorator(\n        LoggerInterface $logger,\n        ConfigurationInterface $configuration,\n    ): EnabledLoggerDecorator {\n        return new EnabledLoggerDecorator($logger, $configuration);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Infrastructure/Psr/Logger/PsrStreamLoggerFactoryTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Infrastructure\\Psr\\Logger;\n\nuse Crunz\\Infrastructure\\Psr\\Logger\\EnabledLoggerDecorator;\nuse Crunz\\Infrastructure\\Psr\\Logger\\PsrStreamLoggerFactory;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\TestClock;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\n\nfinal class PsrStreamLoggerFactoryTest extends UnitTestCase\n{\n    public function test_factory_returns_decorated_logger(): void\n    {\n        // Arrange\n        $psrStreamLoggerFactory = $this->createStreamLoggerFactory();\n\n        // Act\n        $logger = $psrStreamLoggerFactory->create(new FakeConfiguration());\n\n        // Assert\n        self::assertInstanceOf(EnabledLoggerDecorator::class, $logger);\n    }\n\n    private function createStreamLoggerFactory(): PsrStreamLoggerFactory\n    {\n        return new PsrStreamLoggerFactory(\n            $this->createMock(Timezone::class),\n            new TestClock(new \\DateTimeImmutable())\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Infrastructure/Psr/Logger/PsrStreamLoggerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Infrastructure\\Psr\\Logger;\n\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Infrastructure\\Psr\\Logger\\PsrStreamLogger;\nuse Crunz\\Tests\\TestCase\\Faker;\nuse Crunz\\Tests\\TestCase\\TemporaryFile;\nuse Crunz\\Tests\\TestCase\\TestClock;\nuse PHPUnit\\Framework\\TestCase;\nuse Psr\\Log\\LoggerInterface;\n\nfinal class PsrStreamLoggerTest extends TestCase\n{\n    /** @dataProvider supportedLevelsProvider */\n    public function test_supported_levels_are_logged(string $level): void\n    {\n        $message = Faker::words(5);\n        $now = new \\DateTimeImmutable();\n        $tempFile = new TemporaryFile();\n        $logger = $this->createLogger($tempFile, $now);\n\n        $logger->log($level, $message);\n\n        self::assertSame(\n            $this->formatLine(\n                $now,\n                $message,\n                $level\n            ),\n            $tempFile->contents()\n        );\n    }\n\n    /** @dataProvider unsupportedLevelsProvider */\n    public function test_unsupported_levels_are_ignored(string $level): void\n    {\n        $message = Faker::words(5);\n        $tempFile = new TemporaryFile();\n        $logger = $this->createLogger($tempFile);\n\n        $logger->log($level, $message);\n\n        self::assertEmpty($tempFile->contents());\n    }\n\n    /** @dataProvider supportedLevelsProvider */\n    public function test_empty_context_is_ignored(string $level): void\n    {\n        $message = Faker::words(5);\n        $now = new \\DateTimeImmutable();\n        $tempFile = new TemporaryFile();\n        $logger = $this->createLogger(\n            $tempFile,\n            $now,\n            null,\n            true\n        );\n\n        $logger->log($level, $message);\n\n        self::assertSame(\n            $this->formatLine(\n                $now,\n                $message,\n                $level,\n                true\n            ),\n            $tempFile->contents()\n        );\n    }\n\n    /** @dataProvider supportedLevelsProvider */\n    public function test_date_use_passed_time_zone(string $level): void\n    {\n        $timeZone = Faker::timeZone();\n        $message = Faker::words(5);\n        $now = new \\DateTimeImmutable();\n        $tempFile = new TemporaryFile();\n        $logger = $this->createLogger(\n            $tempFile,\n            $now,\n            $timeZone,\n            false,\n            true\n        );\n\n        $logger->log($level, $message);\n\n        self::assertSame(\n            $this->formatLine(\n                $now,\n                $message,\n                $level,\n                false,\n                true,\n                false,\n                $timeZone\n            ),\n            $tempFile->contents()\n        );\n    }\n\n    /** @dataProvider supportedLevelsProvider */\n    public function test_logging_with_allowed_line_breaks(string $level): void\n    {\n        $message = Faker::words(1) . \"\\n\" . Faker::words(1);\n        $now = new \\DateTimeImmutable();\n        $tempFile = new TemporaryFile();\n        $logger = $this->createLogger(\n            $tempFile,\n            $now,\n            null,\n            false,\n            false,\n            true\n        );\n\n        $logger->log($level, $message);\n\n        self::assertSame(\n            $this->formatLine(\n                $now,\n                $message,\n                $level,\n                false,\n                false,\n                true\n            ),\n            $tempFile->contents()\n        );\n    }\n\n    /** @dataProvider supportedLevelsProvider */\n    public function test_logging_with_disallowed_line_breaks(string $level): void\n    {\n        $message = Faker::words(1) . \"\\n\" . Faker::words(1);\n        $now = new \\DateTimeImmutable();\n        $tempFile = new TemporaryFile();\n        $logger = $this->createLogger($tempFile, $now);\n\n        $logger->log($level, $message);\n\n        self::assertSame(\n            $this->formatLine(\n                $now,\n                $message,\n                $level\n            ),\n            $tempFile->contents()\n        );\n    }\n\n    /** @return iterable<string,string[]> */\n    public static function supportedLevelsProvider(): iterable\n    {\n        yield 'info' => ['info'];\n        yield 'error' => ['error'];\n    }\n\n    /** @return iterable<string,string[]> */\n    public static function unsupportedLevelsProvider(): iterable\n    {\n        yield 'emergency' => ['emergency'];\n        yield 'alert' => ['alert'];\n        yield 'critical' => ['critical'];\n        yield 'warning' => ['warning'];\n        yield 'notice' => ['notice'];\n        yield 'debug' => ['debug'];\n    }\n\n    private function createLogger(\n        TemporaryFile $temporaryFile,\n        ?\\DateTimeImmutable $now = null,\n        ?\\DateTimeZone $timeZone = null,\n        bool $ignoreEmptyContext = false,\n        bool $timezoneLog = false,\n        bool $allowLineBreaks = false,\n    ): LoggerInterface {\n        $clock = new TestClock($now ?? Faker::dateTime());\n\n        return new PsrStreamLogger(\n            $timeZone ?? Faker::timeZone(),\n            $clock,\n            $temporaryFile->filePath(),\n            $temporaryFile->filePath(),\n            $ignoreEmptyContext,\n            $timezoneLog,\n            $allowLineBreaks\n        );\n    }\n\n    private function formatLine(\n        \\DateTimeImmutable $date,\n        string $message,\n        string $level,\n        bool $ignoreEmptyContext = false,\n        bool $timeZoneLog = false,\n        bool $allowLineBreaks = false,\n        ?\\DateTimeZone $timeZone = null,\n    ): string {\n        $context = '[] []';\n        if (true === $ignoreEmptyContext) {\n            $context = ' ';\n        }\n\n        if (true === $timeZoneLog) {\n            if (null === $timeZone) {\n                throw new CrunzException(\"TimeZone must be specified to use 'timeZoneLog'.\");\n            }\n\n            $date = $date->setTimezone($timeZone);\n        }\n\n        if (!$allowLineBreaks) {\n            $message = \\str_replace(\n                [\n                    \"\\r\\n\",\n                    \"\\r\",\n                    \"\\n\",\n                ],\n                ' ',\n                $message\n            );\n        }\n\n        $dateString = $date->format('Y-m-d H:i:s');\n        $levelName = \\mb_strtoupper($level);\n\n        return \"[{$dateString}] crunz.{$levelName}: {$message} {$context}\" . PHP_EOL;\n    }\n}\n"
  },
  {
    "path": "tests/Unit/InvokerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit;\n\nuse Crunz\\Invoker;\nuse PHPUnit\\Framework\\TestCase;\n\nclass InvokerTest extends TestCase\n{\n    /** @test */\n    public function call_executes_closure(): void\n    {\n        $i = 1;\n\n        $invoker = new Invoker();\n        $result = $invoker->call(\n            function () use (&$i) {\n                return ++$i;\n            }\n        );\n\n        self::assertSame(2, $i);\n        self::assertSame(2, $result);\n    }\n\n    /** @test */\n    public function call_executes_closure_with_params(): void\n    {\n        $i = 1;\n\n        $invoker = new Invoker();\n        $invoker->call(\n            function ($number) use (&$i): void {\n                $i += $number;\n            },\n            [2]\n        );\n\n        self::assertSame(3, $i);\n    }\n\n    /** @test */\n    public function call_can_catch_output(): void\n    {\n        $invoker = new Invoker();\n        $result = $invoker->call(\n            function (): void {\n                echo 'Callback was called, nice.';\n            },\n            [],\n            true\n        );\n\n        self::assertSame('Callback was called, nice.', $result);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Logger/ConsoleLoggerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Logger;\n\nuse Crunz\\Logger\\ConsoleLogger;\nuse Crunz\\Logger\\ConsoleLoggerInterface;\nuse PHPUnit\\Framework\\MockObject\\MockObject;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Style\\SymfonyStyle;\n\nfinal class ConsoleLoggerTest extends TestCase\n{\n    /**\n     * @test\n     *\n     * @dataProvider verbosityProvider\n     */\n    public function logger_writes_normal_only_with_suitable_verbosity(int $ioVerbosity): void\n    {\n        $expectedCalls = ($ioVerbosity >= ConsoleLoggerInterface::VERBOSITY_NORMAL) ? 1 : 0;\n        $mockSymfonyStyle = $this->mockSymfonyStyle($ioVerbosity);\n        $mockSymfonyStyle\n            ->expects(self::exactly($expectedCalls))\n            ->method('writeln')\n        ;\n\n        $consoleLogger = new ConsoleLogger($mockSymfonyStyle);\n        $consoleLogger->normal('Some message');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider verbosityProvider\n     */\n    public function logger_writes_verbose_only_with_suitable_verbosity(int $ioVerbosity): void\n    {\n        $expectedCalls = ($ioVerbosity >= ConsoleLoggerInterface::VERBOSITY_VERBOSE) ? 1 : 0;\n        $mockSymfonyStyle = $this->mockSymfonyStyle($ioVerbosity);\n        $mockSymfonyStyle\n            ->expects(self::exactly($expectedCalls))\n            ->method('writeln')\n        ;\n\n        $consoleLogger = new ConsoleLogger($mockSymfonyStyle);\n        $consoleLogger->verbose('Some message');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider verbosityProvider\n     */\n    public function logger_writes_very_verbose_only_with_suitable_verbosity(int $ioVerbosity): void\n    {\n        $expectedCalls = ($ioVerbosity >= ConsoleLoggerInterface::VERBOSITY_VERY_VERBOSE) ? 1 : 0;\n        $mockSymfonyStyle = $this->mockSymfonyStyle($ioVerbosity);\n        $mockSymfonyStyle\n            ->expects(self::exactly($expectedCalls))\n            ->method('writeln')\n        ;\n\n        $consoleLogger = new ConsoleLogger($mockSymfonyStyle);\n        $consoleLogger->veryVerbose('Some message');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider verbosityProvider\n     */\n    public function logger_writes_debug_only_with_suitable_verbosity(int $ioVerbosity): void\n    {\n        $expectedCalls = ($ioVerbosity >= ConsoleLoggerInterface::VERBOSITY_DEBUG) ? 1 : 0;\n        $mockSymfonyStyle = $this->mockSymfonyStyle($ioVerbosity);\n        $mockSymfonyStyle\n            ->expects(self::exactly($expectedCalls))\n            ->method('writeln')\n        ;\n\n        $consoleLogger = new ConsoleLogger($mockSymfonyStyle);\n        $consoleLogger->debug('Some message');\n    }\n\n    /** @return iterable<string,array<int>> */\n    public static function verbosityProvider(): iterable\n    {\n        yield 'quiet' => [ConsoleLoggerInterface::VERBOSITY_QUIET];\n        yield 'normal' => [ConsoleLoggerInterface::VERBOSITY_NORMAL];\n        yield 'verbose' => [ConsoleLoggerInterface::VERBOSITY_VERBOSE];\n        yield 'veryVerbose' => [ConsoleLoggerInterface::VERBOSITY_VERY_VERBOSE];\n        yield 'debug' => [ConsoleLoggerInterface::VERBOSITY_DEBUG];\n    }\n\n    /** @return MockObject&SymfonyStyle */\n    private function mockSymfonyStyle(int $ioVerbosity): object\n    {\n        $mock = $this->createMock(SymfonyStyle::class);\n\n        $mock\n            ->method('getVerbosity')\n            ->willReturn($ioVerbosity)\n        ;\n\n        return $mock;\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Logger/LoggerFactoryTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Logger;\n\nuse Crunz\\Clock\\Clock;\nuse Crunz\\Event;\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Logger\\LoggerFactory;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\Logger\\NullLogger;\nuse Crunz\\Tests\\TestCase\\TemporaryFile;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class LoggerFactoryTest extends TestCase\n{\n    public function test_logger_factory_creates_logger(): void\n    {\n        $loggerFactory = $this->createLoggerFactory();\n\n        $loggerFactory->create();\n\n        $this->expectNotToPerformAssertions();\n    }\n\n    public function test_logger_factory_creates_event_logger(): void\n    {\n        $loggerFactory = $this->createLoggerFactory();\n\n        $tempFile = new TemporaryFile();\n\n        $e = new Event('1', 'php foo');\n        $e->output = $tempFile->filePath();\n\n        $loggerFactory->createEvent($e->output);\n\n        $this->expectNotToPerformAssertions();\n    }\n\n    public function test_wrong_logger_class_throws_exception(): void\n    {\n        $loggerFactory = $this->createLoggerFactory(['logger_factory' => 'Wrong\\Class']);\n\n        $this->expectException(CrunzException::class);\n        $this->expectExceptionMessage(\"Class 'Wrong\\Class' does not exists.\");\n\n        $loggerFactory->create();\n    }\n\n    /** @param array<string,mixed> $configuration */\n    private function createLoggerFactory(array $configuration = []): LoggerFactory\n    {\n        $fakeConfiguration = new FakeConfiguration($configuration);\n        $timeZoneProviderMock = $this->createMock(Timezone::class);\n\n        return new LoggerFactory(\n            $fakeConfiguration,\n            $timeZoneProviderMock,\n            new NullLogger(),\n            new Clock()\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/MailerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit;\n\nuse Crunz\\Exception\\MailerException;\nuse Crunz\\Mailer;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class MailerTest extends TestCase\n{\n    /** @test */\n    public function using_mail_transport_will_result_in_exception(): void\n    {\n        $this->expectException(MailerException::class);\n        $this->expectExceptionMessage(\"'mail' transport is no longer supported, please use 'smtp' or 'sendmail' transport.\");\n\n        $mailer = $this->createMailer('mail');\n        $mailer->send('Test', 'Message');\n    }\n\n    private function createMailer(string $transport): Mailer\n    {\n        $configuration = new FakeConfiguration(\n            [\n                'mailer' => [\n                    'transport' => $transport,\n                ],\n            ]\n        );\n\n        return new Mailer($configuration);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Output/OutputFactoryTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Output;\n\nuse Crunz\\Output\\OutputFactory;\nuse PHPUnit\\Framework\\TestCase;\nuse Symfony\\Component\\Console\\Input\\ArgvInput;\nuse Symfony\\Component\\Console\\Input\\InputInterface;\nuse Symfony\\Component\\Console\\Output\\OutputInterface;\n\nfinal class OutputFactoryTest extends TestCase\n{\n    /**\n     * @test\n     *\n     * @dataProvider inputProvider\n     */\n    public function input_defines_output_verbosity(InputInterface $input, int $expectedVerbosity): void\n    {\n        $factory = new OutputFactory($input);\n\n        $output = $factory->createOutput();\n\n        self::assertSame($expectedVerbosity, $output->getVerbosity());\n    }\n\n    /** @return iterable<string,array> */\n    public static function inputProvider(): iterable\n    {\n        yield 'quietShort' => [\n            self::createInput('-q'),\n            OutputInterface::VERBOSITY_QUIET,\n        ];\n\n        yield 'quietLong' => [\n            self::createInput('--quiet'),\n            OutputInterface::VERBOSITY_QUIET,\n        ];\n\n        yield 'normal' => [\n            self::createInput('--filter'),\n            OutputInterface::VERBOSITY_NORMAL,\n        ];\n\n        yield 'verbose' => [\n            self::createInput('-v'),\n            OutputInterface::VERBOSITY_VERBOSE,\n        ];\n\n        yield 'veryVerbose' => [\n            self::createInput('-vv'),\n            OutputInterface::VERBOSITY_VERY_VERBOSE,\n        ];\n\n        yield 'debug' => [\n            self::createInput('-vvv'),\n            OutputInterface::VERBOSITY_DEBUG,\n        ];\n    }\n\n    private static function createInput(string $option): InputInterface\n    {\n        return new ArgvInput(['', $option]);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Path/PathTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Path;\n\nuse Crunz\\Exception\\CrunzException;\nuse Crunz\\Path\\Path;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class PathTest extends TestCase\n{\n    /** @test */\n    public function create_requires_at_least_one_path(): void\n    {\n        $this->expectException(CrunzException::class);\n        $this->expectExceptionMessage('At least one part expected.');\n\n        Path::create([]);\n    }\n\n    /** @test */\n    public function parts_are_delimited_by_directory_separator(): void\n    {\n        $parts = [\n            'home',\n            'crunz',\n            'bin',\n        ];\n\n        $path = Path::create($parts);\n\n        self::assertSame(\n            \\implode(DIRECTORY_SEPARATOR, $parts),\n            $path->toString()\n        );\n    }\n\n    /** @test */\n    public function path_can_be_created_from_strings(): void\n    {\n        $parts = [\n            'home',\n            'user',\n            'vendor',\n            'bin',\n            'crunz',\n        ];\n        $path = Path::fromStrings(...$parts);\n\n        self::assertSame(\n            \\implode(DIRECTORY_SEPARATOR, $parts),\n            $path->toString()\n        );\n    }\n\n    /** @test */\n    public function doubled_directory_separator_is_normalized(): void\n    {\n        $parts = [\n            'home' . DIRECTORY_SEPARATOR,\n            'user',\n        ];\n\n        $path = Path::create($parts);\n\n        self::assertSame(\n            'home' . DIRECTORY_SEPARATOR . 'user',\n            $path->toString()\n        );\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Pingable.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit;\n\nuse Crunz\\Pinger\\PingableInterface;\nuse Crunz\\Pinger\\PingableTrait;\n\nclass Pingable implements PingableInterface\n{\n    use PingableTrait;\n}\n"
  },
  {
    "path": "tests/Unit/Pinger/PingableTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Pinger;\n\nuse Crunz\\Pinger\\PingableException;\nuse Crunz\\Tests\\Unit\\Pingable;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class PingableTest extends TestCase\n{\n    /**\n     * @test\n     *\n     * @dataProvider nonStringProvider\n     */\n    public function before_url_must_be_string(mixed $url): void\n    {\n        $type = \\gettype($url);\n        $this->expectException(PingableException::class);\n        $this->expectExceptionMessage(\"Url must be of type string, '{$type}' given.\");\n\n        $pingable = new Pingable();\n        $pingable->pingBefore($url);\n    }\n\n    /**\n     * @test\n     */\n    public function before_url_must_be_non_empty_string(): void\n    {\n        $this->expectException(PingableException::class);\n        $this->expectExceptionMessage('Url cannot be empty.');\n\n        $pingable = new Pingable();\n        $pingable->pingBefore('');\n    }\n\n    /**\n     * @test\n     */\n    public function after_url_must_be_non_empty_string(): void\n    {\n        $this->expectException(PingableException::class);\n        $this->expectExceptionMessage('Url cannot be empty.');\n\n        $pingable = new Pingable();\n        $pingable->thenPing('');\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider nonStringProvider\n     */\n    public function after_url_must_be_string(mixed $url): void\n    {\n        $type = \\gettype($url);\n        $this->expectException(PingableException::class);\n        $this->expectExceptionMessage(\"Url must be of type string, '{$type}' given.\");\n\n        $pingable = new Pingable();\n        $pingable->thenPing($url);\n    }\n\n    /** @test */\n    public function get_ping_before_without_url_fails(): void\n    {\n        $this->expectException(PingableException::class);\n        $this->expectExceptionMessage('PingBeforeUrl is empty.');\n\n        $pingable = new Pingable();\n        $pingable->getPingBeforeUrl();\n    }\n\n    /** @test */\n    public function get_ping_after_without_url_fails(): void\n    {\n        $this->expectException(PingableException::class);\n        $this->expectExceptionMessage('PingAfterUrl is empty.');\n\n        $pingable = new Pingable();\n        $pingable->getPingAfterUrl();\n    }\n\n    /** @return iterable<string,array> */\n    public static function nonStringProvider(): iterable\n    {\n        yield 'null' => [null];\n        yield 'array' => [[]];\n        yield 'object' => [new \\stdClass()];\n        yield 'int' => [123];\n        yield 'float' => [6423.4324];\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Process/ProcessTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Process;\n\nuse Crunz\\Process\\Process;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\n\nfinal class ProcessTest extends UnitTestCase\n{\n    public function test_command_line_built_from_array(): void\n    {\n        // Arrange\n        $expectedCommandLine = \"'php' '-v' '--ini' '-d' 'memory_limit=123M'\";\n\n        // Act\n        $process = Process::fromArrayCommand(\n            [\n                'php',\n                '-v',\n                '--ini',\n                '-d',\n                'memory_limit=123M',\n            ],\n        );\n\n        // Assert\n        $this->assertCommand($expectedCommandLine, $process);\n    }\n\n    private function assertCommand(string $expectedCommand, Process $process): void\n    {\n        $actualCommand = $process->commandLine();\n\n        if (IS_WINDOWS === true) {\n            // Symfony Process may wrap values containing \"=\" in double quotes on Windows.\n            $expectedCommand = \\str_replace([\"'\", '\"'], '', $expectedCommand);\n            $actualCommand = \\str_replace([\"'\", '\"'], '', $actualCommand);\n        }\n\n        self::assertSame($expectedCommand, $actualCommand);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Schedule/ScheduleFactoryTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Schedule;\n\nuse Crunz\\Event;\nuse Crunz\\Exception\\TaskNotExistException;\nuse Crunz\\Schedule;\nuse Crunz\\Schedule\\ScheduleFactory;\nuse Crunz\\Task\\TaskNumber;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class ScheduleFactoryTest extends TestCase\n{\n    /** @test */\n    public function single_task_schedule(): void\n    {\n        $factory = new ScheduleFactory();\n\n        $event1 = new Event(1, 'php -v');\n        $event2 = new Event(2, 'php -v');\n        $schedule = new Schedule();\n        $schedule->events([$event1, $event2]);\n\n        $schedules = $factory->singleTaskSchedule(TaskNumber::fromString('1'), $schedule);\n        /** @var Schedule $firstSchedule */\n        $firstSchedule = \\reset($schedules);\n\n        self::assertSame([$event1], $firstSchedule->events());\n    }\n\n    /** @test */\n    public function single_task(): void\n    {\n        $factory = new ScheduleFactory();\n\n        $event1 = new Event(1, 'php -v');\n        $event2 = new Event(2, 'php -v');\n        $schedule = new Schedule();\n        $schedule->events([$event1, $event2]);\n\n        $event = $factory->singleTask(TaskNumber::fromString('1'), $schedule);\n\n        self::assertSame($event1, $event);\n    }\n\n    /** @test */\n    public function single_task_schedule_throws_exception_on_wrong_task_number(): void\n    {\n        $factory = new ScheduleFactory();\n\n        $event1 = new Event(1, 'php -v');\n        $schedule = new Schedule();\n        $schedule->events([$event1]);\n\n        $this->expectException(TaskNotExistException::class);\n        $this->expectExceptionMessage(\"Task with id '2' was not found. Last task id is '1'.\");\n\n        $factory->singleTaskSchedule(TaskNumber::fromString('2'), $schedule);\n    }\n\n    /** @test */\n    public function single_task_throws_exception_on_wrong_task_number(): void\n    {\n        $factory = new ScheduleFactory();\n\n        $event1 = new Event(1, 'php -v');\n        $schedule = new Schedule();\n        $schedule->events([$event1]);\n\n        $this->expectException(TaskNotExistException::class);\n        $this->expectExceptionMessage(\"Task with id '2' was not found. Last task id is '1'.\");\n\n        $factory->singleTask(TaskNumber::fromString('2'), $schedule);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/ScheduleTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit;\n\nuse Crunz\\Event;\nuse Crunz\\Schedule;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\nuse Symfony\\Bridge\\PhpUnit\\ExpectDeprecationTrait;\n\nfinal class ScheduleTest extends UnitTestCase\n{\n    use ExpectDeprecationTrait;\n\n    /** @dataProvider runProvider */\n    public function test_run(\\Closure $paramsGenerator): void\n    {\n        // Arrange\n        /**\n         * @var string   $command\n         * @var string[] $parameters\n         * @var string   $expectedCommand\n         */\n        [\n            'command' => $command,\n            'parameters' => $parameters,\n            'expectedCommand' => $expectedCommand,\n        ] = $paramsGenerator();\n        $schedule = new Schedule();\n\n        // Act\n        $event = $schedule->run($command, $parameters);\n\n        // Assert\n        $this->assertCommand($expectedCommand, $event);\n    }\n\n    /**\n     * @group legacy\n     *\n     * @dataProvider nonStringParametersProvider\n     */\n    public function test_run_with_non_string_parameters(\\Closure $paramsGenerator): void\n    {\n        // Arrange\n        /**\n         * @var string   $command\n         * @var string[] $parameters\n         * @var string   $expectedCommand\n         */\n        [\n            'command' => $command,\n            'parameters' => $parameters,\n            'expectedCommand' => $expectedCommand,\n        ] = $paramsGenerator();\n        $schedule = new Schedule();\n\n        // Expect\n        $this->expectDeprecation('Passing non-string parameters is deprecated since v3.3, convert all parameters to string.');\n\n        // Act\n        $event = $schedule->run($command, $parameters);\n\n        // Assert\n        $this->assertCommand($expectedCommand, $event);\n    }\n\n    /** @return iterable<string, array{\\Closure}> */\n    public static function runProvider(): iterable\n    {\n        yield 'simple command' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => [],\n                'expectedCommand' => '/usr/bin/php',\n            ],\n        ];\n\n        yield 'command with inline argument' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php -v',\n                'parameters' => [],\n                'expectedCommand' => '/usr/bin/php -v',\n            ],\n        ];\n\n        yield 'command with argument' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['-v'],\n                'expectedCommand' => \"/usr/bin/php '-v'\",\n            ],\n        ];\n\n        yield 'command with option' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['--ini' => 'php.ini'],\n                'expectedCommand' => \"/usr/bin/php '--ini' 'php.ini'\",\n            ],\n        ];\n\n        yield 'command with mixed parameters' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['--ini' => 'php.ini', '-v'],\n                'expectedCommand' => \"/usr/bin/php '--ini' 'php.ini' '-v'\",\n            ],\n        ];\n    }\n\n    /** @return iterable<string, array{\\Closure}> */\n    public static function nonStringParametersProvider(): iterable\n    {\n        yield 'boolean true parameter' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['-v' => true],\n                'expectedCommand' => \"/usr/bin/php '-v' '1'\",\n            ],\n        ];\n\n        yield 'boolean false parameter' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['-v' => false],\n                'expectedCommand' => \"/usr/bin/php '-v' '0'\",\n            ],\n        ];\n\n        yield 'int parameter' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['-v' => 4],\n                'expectedCommand' => \"/usr/bin/php '-v' '4'\",\n            ],\n        ];\n\n        yield 'float parameter' => [\n            static fn (): array => [\n                'command' => '/usr/bin/php',\n                'parameters' => ['-v' => 3.14],\n                'expectedCommand' => \"/usr/bin/php '-v' '3.14'\",\n            ],\n        ];\n    }\n\n    private function assertCommand(string $expectedCommand, Event $event): void\n    {\n        if (IS_WINDOWS === true) {\n            $expectedCommand = \\str_replace(\n                \"'\",\n                '',\n                $expectedCommand,\n            );\n        }\n\n        self::assertSame($expectedCommand, $event->getCommand());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Service/AbstractClosureSerializerTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Service;\n\nuse Crunz\\Application\\Service\\ClosureSerializerInterface;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\n\nabstract class AbstractClosureSerializerTestCase extends UnitTestCase\n{\n    public function test_closure_code_can_be_extracted(): void\n    {\n        // Arrange\n        $testClosure = static fn (): \\stdClass => new \\stdClass();\n        $serializer = $this->createSerializer();\n\n        // Act\n        $code = $serializer->closureCode($testClosure);\n\n        // Assert\n        self::assertSame('static fn (): \\stdClass => new \\stdClass()', $code);\n    }\n\n    abstract protected function createSerializer(): ClosureSerializerInterface;\n}\n"
  },
  {
    "path": "tests/Unit/Service/LaravelClosureSerializerTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Service;\n\nuse Crunz\\Infrastructure\\Laravel\\LaravelClosureSerializer;\nuse Crunz\\Tests\\TestCase\\SerializableTaskRunnerStub;\nuse Crunz\\Tests\\TestCase\\TaskRunnerStub;\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\n\nfinal class LaravelClosureSerializerTest extends UnitTestCase\n{\n    private LaravelClosureSerializer $serializer;\n\n    protected function setUp(): void\n    {\n        parent::setUp();\n        $this->serializer = new LaravelClosureSerializer();\n    }\n\n    public function test_serialize_simple_closure(): void\n    {\n        $closure = static function (): string {\n            return 'hello';\n        };\n\n        $serialized = $this->serializer->serialize($closure);\n        $result = $this->serializer->unserialize($serialized);\n\n        self::assertSame('hello', $result());\n    }\n\n    public function test_serialize_closure_with_use_variable(): void\n    {\n        $name = 'crunz';\n        $closure = static function () use ($name): string {\n            return \"hello {$name}\";\n        };\n\n        $serialized = $this->serializer->serialize($closure);\n        $result = $this->serializer->unserialize($serialized);\n\n        self::assertSame('hello crunz', $result());\n    }\n\n    public function test_serialize_closure_bound_to_object_with_closure_properties(): void\n    {\n        $runner = new TaskRunnerStub();\n        $closure = $runner->createTask();\n\n        $serialized = $this->serializer->serialize($closure);\n        $result = $this->serializer->unserialize($serialized);\n\n        self::assertSame('running daily-report', $result());\n    }\n\n    /**\n     * Regression test for laravel/serializable-closure#126.\n     *\n     * v2.0.9 skips walking properties of objects that implement __serialize,\n     * leaving nested closures unwrapped and causing \"Serialization of 'Closure'\n     * is not allowed\".\n     */\n    public function test_serialize_closure_bound_to_object_with_serialize_and_closure_properties(): void\n    {\n        $runner = new SerializableTaskRunnerStub();\n        $closure = $runner->createTask();\n\n        $serialized = $this->serializer->serialize($closure);\n        $result = $this->serializer->unserialize($serialized);\n\n        self::assertSame('running daily-report', $result());\n    }\n\n    public function test_closure_code_can_be_extracted(): void\n    {\n        $testClosure = static fn (): \\stdClass => new \\stdClass();\n\n        $code = $this->serializer->closureCode($testClosure);\n\n        self::assertSame('static fn (): \\stdClass => new \\stdClass()', $code);\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Service/LaravelClosureSerializerTestCase.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Service;\n\nuse Crunz\\Infrastructure\\Laravel\\LaravelClosureSerializer;\n\nfinal class LaravelClosureSerializerTestCase extends AbstractClosureSerializerTestCase\n{\n    protected function createSerializer(): LaravelClosureSerializer\n    {\n        return new LaravelClosureSerializer();\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Task/TaskNumberTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Task;\n\nuse Crunz\\Exception\\WrongTaskNumberException;\nuse Crunz\\Task\\TaskNumber;\nuse PHPUnit\\Framework\\TestCase;\n\nclass TaskNumberTest extends TestCase\n{\n    /**\n     * @dataProvider nonStringValueProvider\n     *\n     * @test\n     */\n    public function can_not_create_task_number_with_non_string_value_by_from_string(mixed $value): void\n    {\n        $this->expectException(WrongTaskNumberException::class);\n        $this->expectExceptionMessage('Passed task number is not string.');\n\n        TaskNumber::fromString($value);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider nonNumericProvider\n     */\n    public function task_number_can_not_be_non_numeric_string(string $value): void\n    {\n        $this->expectException(WrongTaskNumberException::class);\n        $this->expectExceptionMessage(\"Task number '{$value}' is not numeric.\");\n\n        TaskNumber::fromString($value);\n    }\n\n    /**\n     * @test\n     *\n     * @dataProvider numericValueProvider\n     */\n    public function task_number_can_be_created_with_numeric_string_value(string $value, int $expectedNumber): void\n    {\n        $taskNumber = TaskNumber::fromString($value);\n\n        self::assertSame($expectedNumber, $taskNumber->asInt());\n    }\n\n    /** @test */\n    public function array_index_is_one_step_lower(): void\n    {\n        $taskNumber = TaskNumber::fromString('14');\n\n        self::assertSame(13, $taskNumber->asArrayIndex());\n    }\n\n    /** @return iterable<string,array> */\n    public static function nonStringValueProvider(): iterable\n    {\n        yield 'null' => [null];\n        yield 'float' => [3.14];\n        yield 'int' => [7];\n        yield 'array' => [[]];\n        yield 'object' => [new \\stdClass()];\n    }\n\n    /** @return iterable<string,array> */\n    public static function numericValueProvider(): iterable\n    {\n        yield 'int' => [\n            '155',\n            155,\n        ];\n        yield 'float' => [\n            '3.14',\n            3,\n        ];\n    }\n\n    /** @return iterable<string,array> */\n    public static function nonNumericProvider(): iterable\n    {\n        yield 'chars' => ['abc'];\n        yield 'charsWithNumber' => ['1a2b3'];\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Task/TimezoneTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Task;\n\nuse Crunz\\Exception\\EmptyTimezoneException;\nuse Crunz\\Task\\Timezone;\nuse Crunz\\Tests\\TestCase\\FakeConfiguration;\nuse Crunz\\Tests\\TestCase\\Logger\\NullLogger;\nuse PHPUnit\\Framework\\TestCase;\n\nfinal class TimezoneTest extends TestCase\n{\n    /** @test */\n    public function configured_timezone_cannot_be_empty(): void\n    {\n        $this->expectException(EmptyTimezoneException::class);\n\n        $taskTimezone = new Timezone(\n            new FakeConfiguration(['timezone' => null]),\n            new NullLogger()\n        );\n        $taskTimezone->timezoneForComparisons();\n    }\n}\n"
  },
  {
    "path": "tests/Unit/Timezone/ProviderTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\Timezone;\n\nuse Crunz\\Timezone\\Provider;\nuse PHPUnit\\Framework\\TestCase;\n\nclass ProviderTest extends TestCase\n{\n    /**\n     * @runInSeparateProcess\n     *\n     * @test\n     */\n    public function default_timezone_is_returned(): void\n    {\n        $timezoneName = 'Europe/Warsaw';\n        \\date_default_timezone_set($timezoneName);\n\n        $provider = new Provider();\n        $timezone = $provider->defaultTimezone();\n\n        self::assertSame($timezoneName, $timezone->getName());\n    }\n}\n"
  },
  {
    "path": "tests/Unit/UserInterface/Cli/ClosureRunCommandTest.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nnamespace Crunz\\Tests\\Unit\\UserInterface\\Cli;\n\nuse Crunz\\Tests\\TestCase\\UnitTestCase;\nuse Crunz\\UserInterface\\Cli\\ClosureRunCommand;\nuse Symfony\\Component\\Console\\Input\\ArrayInput;\nuse Symfony\\Component\\Console\\Output\\NullOutput;\n\nfinal class ClosureRunCommandTest extends UnitTestCase\n{\n    /** @dataProvider closureValueProvider */\n    public function test_return_value_of_closure_is_omitted(int $returnValue): void\n    {\n        $closure = static fn (): int => $returnValue;\n        $command = $this->createCommand();\n        $input = $this->createInput($closure);\n        $output = new NullOutput();\n\n        self::assertSame(\n            0,\n            $command->run($input, $output)\n        );\n    }\n\n    /** @test */\n    public function command_is_hidden(): void\n    {\n        $command = $this->createCommand();\n\n        self::assertTrue($command->isHidden());\n    }\n\n    /** @return iterable<string,array<int>> */\n    public static function closureValueProvider(): iterable\n    {\n        yield '0' => [0];\n        yield '1' => [1];\n    }\n\n    private function createInput(\\Closure $closure): ArrayInput\n    {\n        $closureSerializer = $this->createClosureSerializer();\n\n        return new ArrayInput(\n            [\n                'closure' => \\http_build_query(\n                    [\n                        $closureSerializer->serialize($closure),\n                    ]\n                ),\n            ]\n        );\n    }\n\n    private function createCommand(): ClosureRunCommand\n    {\n        return new ClosureRunCommand($this->createClosureSerializer());\n    }\n}\n"
  },
  {
    "path": "tests/crunz.yml",
    "content": "source: tasks\nsuffix: Tasks.php\ntimezone: UTC\ntimezone_log: false\nlog_errors: false\nerrors_log_file: ~\nlog_output: false\noutput_log_file: ~\nlog_allow_line_breaks: false\nlog_ignore_empty_context: false\nemail_output: false\nemail_errors: false\nmailer:\n    transport: smtp\n    recipients:\n    sender_name:\n    sender_email:\n\nsmtp:\n    host: ~\n    port: ~\n    username: ~\n    password: ~\n    encryption: ~\n"
  },
  {
    "path": "tests/resources/fixtures/finder/direct/directHere.php",
    "content": ""
  },
  {
    "path": "tests/resources/fixtures/finder/symlink/symlinkHere.php",
    "content": ""
  },
  {
    "path": "tests/resources/tasks/ClosureTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Crunz\\Schedule;\n\n$x = 153;\n\n$scheduler = new Schedule();\n$scheduler\n    ->run(\n        function () use ($x): void {\n            echo 'Closure output' . PHP_EOL;\n            echo \"Var: {$x}\" . PHP_EOL;\n        }\n    )\n    ->description('Closure with output')\n    ->everyMinute()\n;\n\nreturn $scheduler;\n"
  },
  {
    "path": "tests/resources/tasks/CustomOutputTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Crunz\\Schedule;\n\n$scheduler = new Schedule();\n\n$scheduler\n    ->run('php --help')\n    ->description('Custom logging test')\n    ->everyMinute()\n    ->appendOutputTo('custom.log')\n;\n\nreturn $scheduler;\n"
  },
  {
    "path": "tests/resources/tasks/FailTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Crunz\\Schedule;\n\n$scheduler = new Schedule();\n$scheduler\n    ->run(\n        function (): never {\n            throw new RuntimeException('Task failed.');\n        }\n    )\n    ->description('Task that will fail')\n    ->everyMinute()\n;\n\nreturn $scheduler;\n"
  },
  {
    "path": "tests/resources/tasks/NoOverlappingClosureTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Crunz\\Schedule;\n\n$scheduler = new Schedule();\n$scheduler\n    ->run(\n        function (): stdClass {\n            \\usleep(150 * 1000); // wait 150ms\n\n            echo 'Done', PHP_EOL;\n\n            return new stdClass();\n        }\n    )\n    ->description('Closure with sleep')\n    ->preventOverlapping()\n    ->everyMinute()\n;\n\nreturn $scheduler;\n"
  },
  {
    "path": "tests/resources/tasks/PhpVersionTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nuse Crunz\\Schedule;\n\n$scheduler = new Schedule();\n\n$scheduler\n    ->run('php -v')\n    ->description('PHP version')\n    ->everyMinute()\n;\n\nreturn $scheduler;\n"
  },
  {
    "path": "tests/resources/tasks/WrongTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\n\nreturn [];\n"
  },
  {
    "path": "tests/tasks/TestTasks.php",
    "content": "<?php\n\ndeclare(strict_types=1);\nuse Crunz\\Schedule;\n\n$schedule = new Schedule();\n\n$schedule->run(PHP_BINARY . ' -v')\n    ->description('Show PHP version')\n    ->everyMinute()\n;\n\n// IMPORTANT: You must return the schedule object\nreturn $schedule;\n"
  }
]