From b718071166efcf9c71dd884bda71288952177dd7 Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 13 Nov 2026 18:43:05 +0100 Subject: [PATCH 1/4] fix: stop next run resolution once all cron fields match --- src/RunResolver.php | 2 ++ tests/unit/RunResolverTest.php | 55 ++++++++++++++++++++++++++++++++++ 2 files changed, 57 insertions(+) create mode 100644 tests/unit/RunResolverTest.php diff --git a/src/RunResolver.php b/src/RunResolver.php index 1eb22ac..d1009e0 100644 --- a/src/RunResolver.php +++ b/src/RunResolver.php @@ -107,6 +107,8 @@ public function nextRun(string $expression, Time $next): Time continue 2; } } + + return $next; } return $next; diff --git a/tests/unit/RunResolverTest.php b/tests/unit/RunResolverTest.php new file mode 100644 index 0000000..334aa35 --- /dev/null +++ b/tests/unit/RunResolverTest.php @@ -0,0 +1,55 @@ + + * + * For the full copyright and license information, please view + * the LICENSE file that was distributed with this source code. + */ + +use CodeIgniter\I18n\Time; +use CodeIgniter\Tasks\RunResolver; +use CodeIgniter\Test\CIUnitTestCase as TestCase; + +/** + * @internal + */ +final class RunResolverTest extends TestCase +{ + public function testStopsAfterAllFieldsMatch() + { + $next = $this->createMock(Time::class); + $next->expects($this->once())->method('getMinute')->willReturn('4'); + $next->expects($this->once())->method('getHour')->willReturn('10'); + $next->expects($this->once())->method('getDay')->willReturn('13'); + $next->expects($this->once())->method('getMonth')->willReturn('11'); + $next->expects($this->once())->method('getDayOfWeek')->willReturn('5'); + + $this->assertSame($next, (new RunResolver())->nextRun('4 10 13 11 5', $next)); + } + + public function testStopsAfterFindingMatchInLaterIteration() + { + $match = $this->createMock(Time::class); + $match->expects($this->once())->method('getMinute')->willReturn('4'); + + $next = $this->createMock(Time::class); + $next->expects($this->once())->method('getMinute')->willReturn('3'); + $next->expects($this->once())->method('addMinutes')->with(1)->willReturn($match); + + $this->assertSame($match, (new RunResolver())->nextRun('4 * * * *', $next)); + } + + public function testStopsAtIterationLimitWhenNoFieldsMatch() + { + $next = $this->createMock(Time::class); + $next->expects($this->exactly(1000))->method('getMinute')->willReturn('3'); + $next->expects($this->exactly(1000))->method('addMinutes')->with(1)->willReturnSelf(); + + $this->assertSame($next, (new RunResolver())->nextRun('4 * * * *', $next)); + } +} From 08293fdf7e83b89f02afb52c0f19ff9c54cb231e Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 13 Nov 2026 18:49:24 +0100 Subject: [PATCH 2/4] cs fix --- src/RunResolver.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/RunResolver.php b/src/RunResolver.php index d1009e0..10bf80f 100644 --- a/src/RunResolver.php +++ b/src/RunResolver.php @@ -124,11 +124,11 @@ public function nextRun(string $expression, Time $next): Time protected function increment(Time $next, string $position): Time { return match ($position) { - 'minute' => $next->addMinutes(1), - 'hour' => $next->addHours(1), + 'minute' => $next->addMinutes(1), + 'hour' => $next->addHours(1), 'monthDay', 'weekDay' => $next->addDays(1), - 'month' => $next->addMonths(1), - default => $next, + 'month' => $next->addMonths(1), + default => $next, }; } From 06efa624422df573cc6c8748fed58bcd98e04a50 Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 13 Nov 2026 18:54:07 +0100 Subject: [PATCH 3/4] fix phpstan --- src/Task.php | 2 +- src/TaskRunner.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Task.php b/src/Task.php index 9cd2bc1..e24457a 100644 --- a/src/Task.php +++ b/src/Task.php @@ -144,7 +144,7 @@ public function run() return $this->{$method}(); } finally { - if ($this->singleInstance && $this->getType() !== 'queue') { + if ($this->singleInstance && isset($lockKey) && $this->getType() !== 'queue') { cache()->delete($lockKey); } } diff --git a/src/TaskRunner.php b/src/TaskRunner.php index 914d526..d5df731 100644 --- a/src/TaskRunner.php +++ b/src/TaskRunner.php @@ -73,7 +73,7 @@ public function run() } catch (Throwable $e) { $this->cliWrite('Failed: ' . ($task->name ?: 'Task'), 'red'); - log_message('error', $e->getMessage(), $e->getTrace()); + log_message('error', $e->getMessage(), ['exception' => $e]); $error = $e; } finally { // Save performance info From b676beb679a3ec748f98372154beb37ddca36e6c Mon Sep 17 00:00:00 2001 From: michalsn Date: Fri, 13 Nov 2026 19:11:26 +0100 Subject: [PATCH 4/4] fix rector --- rector.php | 138 ++++++++++++++++++++--------------------------------- 1 file changed, 51 insertions(+), 87 deletions(-) diff --git a/rector.php b/rector.php index 4e87702..0904614 100644 --- a/rector.php +++ b/rector.php @@ -11,31 +11,19 @@ * the LICENSE file that was distributed with this source code. */ -use Rector\Caching\ValueObject\Storage\FileCacheStorage; use Rector\CodeQuality\Rector\BooleanAnd\SimplifyEmptyArrayCheckRector; -use Rector\CodeQuality\Rector\Class_\CompleteDynamicPropertiesRector; use Rector\CodeQuality\Rector\Empty_\SimplifyEmptyCheckOnEmptyArrayRector; use Rector\CodeQuality\Rector\Expression\InlineIfToExplicitIfRector; -use Rector\CodeQuality\Rector\Foreach_\UnusedForeachValueToArrayKeysRector; use Rector\CodeQuality\Rector\FuncCall\ChangeArrayPushToArrayAssignRector; -use Rector\CodeQuality\Rector\FuncCall\SimplifyRegexPatternRector; -use Rector\CodeQuality\Rector\FuncCall\SimplifyStrposLowerRector; use Rector\CodeQuality\Rector\FuncCall\SingleInArrayToCompareRector; -use Rector\CodeQuality\Rector\FunctionLike\SimplifyUselessVariableRector; -use Rector\CodeQuality\Rector\If_\CombineIfRector; -use Rector\CodeQuality\Rector\If_\ExplicitBoolCompareRector; -use Rector\CodeQuality\Rector\If_\ShortenElseIfRector; -use Rector\CodeQuality\Rector\If_\SimplifyIfElseToTernaryRector; use Rector\CodeQuality\Rector\If_\SimplifyIfReturnBoolRector; use Rector\CodeQuality\Rector\Ternary\TernaryEmptyArrayArrayDimFetchToCoalesceRector; use Rector\CodeQuality\Rector\Ternary\UnnecessaryTernaryExpressionRector; use Rector\CodingStyle\Rector\ClassMethod\FuncGetArgsToVariadicParamRector; use Rector\CodingStyle\Rector\ClassMethod\MakeInheritedMethodVisibilitySameAsParentRector; -use Rector\CodingStyle\Rector\FuncCall\CountArrayToEmptyArrayComparisonRector; use Rector\CodingStyle\Rector\FuncCall\VersionCompareFuncCallToConstantRector; use Rector\Config\RectorConfig; use Rector\DeadCode\Rector\ClassMethod\RemoveUnusedPromotedPropertyRector; -use Rector\EarlyReturn\Rector\Foreach_\ChangeNestedForeachIfsToEarlyContinueRector; use Rector\EarlyReturn\Rector\If_\ChangeIfElseValueAssignToEarlyReturnRector; use Rector\EarlyReturn\Rector\If_\RemoveAlwaysElseRector; use Rector\EarlyReturn\Rector\Return_\PreparedValueToEarlyReturnRector; @@ -48,55 +36,36 @@ use Rector\Privatization\Rector\Property\PrivatizeFinalClassPropertyRector; use Rector\Set\ValueObject\LevelSetList; use Rector\Set\ValueObject\SetList; -use Rector\Strict\Rector\Empty_\DisallowedEmptyRuleFixerRector; -use Rector\TypeDeclaration\Rector\Empty_\EmptyOnNullableObjectToInstanceOfRector; use Rector\TypeDeclaration\Rector\Property\TypedPropertyFromAssignsRector; use Rector\ValueObject\PhpVersion; -return static function (RectorConfig $rectorConfig): void { - $rectorConfig->sets([ +$rectorConfig = RectorConfig::configure() + ->withSets([ SetList::DEAD_CODE, LevelSetList::UP_TO_PHP_82, PHPUnitSetList::PHPUNIT_CODE_QUALITY, - PHPUnitSetList::PHPUNIT_100, - ]); - - $rectorConfig->parallel(); - - // Github action cache - $rectorConfig->cacheClass(FileCacheStorage::class); - if (is_dir('/tmp')) { - $rectorConfig->cacheDirectory('/tmp/rector'); - } - + PHPUnitSetList::COMPOSER_BASED, + ]) + ->withParallel() // The paths to refactor (can also be supplied with CLI arguments) - $rectorConfig->paths([ + ->withPaths([ __DIR__ . '/src/', __DIR__ . '/tests/', - ]); - + ]) // Include Composer's autoload - required for global execution, remove if running locally - $rectorConfig->autoloadPaths([ + ->withAutoloadPaths([ __DIR__ . '/vendor/autoload.php', - ]); - + ]) // Do you need to include constants, class aliases, or a custom autoloader? - $rectorConfig->bootstrapFiles([ + ->withBootstrapFiles([ realpath(getcwd()) . '/vendor/codeigniter4/framework/system/Test/bootstrap.php', - ]); - - if (is_file(__DIR__ . '/phpstan.neon.dist')) { - $rectorConfig->phpstanConfig(__DIR__ . '/phpstan.neon.dist'); - } - + ]) // Set the target version for refactoring - $rectorConfig->phpVersion(PhpVersion::PHP_82); - + ->withPhpVersion(PhpVersion::PHP_82) // Auto-import fully qualified class names - $rectorConfig->importNames(); - + ->withImportNames(removeUnusedImports: false) // Are there files or rules you need to skip? - $rectorConfig->skip([ + ->withSkip([ __DIR__ . '/app/Views', StringifyStrNeedlesRector::class, @@ -123,48 +92,43 @@ // Temporary fix DeclareStrictTypesTestsRector::class, + ]) + ->withRules([ + RemoveAlwaysElseRector::class, + ChangeIfElseValueAssignToEarlyReturnRector::class, + SimplifyIfReturnBoolRector::class, + InlineIfToExplicitIfRector::class, + PreparedValueToEarlyReturnRector::class, + ChangeArrayPushToArrayAssignRector::class, + UnnecessaryTernaryExpressionRector::class, + FuncGetArgsToVariadicParamRector::class, + MakeInheritedMethodVisibilitySameAsParentRector::class, + SimplifyEmptyArrayCheckRector::class, + SimplifyEmptyCheckOnEmptyArrayRector::class, + TernaryEmptyArrayArrayDimFetchToCoalesceRector::class, + PrivatizeFinalClassPropertyRector::class, + SingleInArrayToCompareRector::class, + VersionCompareFuncCallToConstantRector::class, + ]) + ->withConfiguredRule(TypedPropertyFromAssignsRector::class, [ + /** + * The INLINE_PUBLIC value is default to false to avoid BC break, + * if you use for libraries and want to preserve BC break, you don't + * need to configure it, as it included in LevelSetList::UP_TO_PHP_74 + * Set to true for projects that allow BC break + */ + TypedPropertyFromAssignsRector::INLINE_PUBLIC => true, ]); - // auto import fully qualified class names - $rectorConfig->importNames(); +// Github action cache +if (is_dir('/tmp')) { + $rectorConfig->withCache('/tmp/rector'); +} + +if (is_file(__DIR__ . '/phpstan.neon.dist')) { + $rectorConfig->withPHPStanConfigs([ + __DIR__ . '/phpstan.neon.dist', + ]); +} - $rectorConfig->rule(SimplifyUselessVariableRector::class); - $rectorConfig->rule(RemoveAlwaysElseRector::class); - $rectorConfig->rule(CountArrayToEmptyArrayComparisonRector::class); - $rectorConfig->rule(ChangeNestedForeachIfsToEarlyContinueRector::class); - $rectorConfig->rule(ChangeIfElseValueAssignToEarlyReturnRector::class); - $rectorConfig->rule(SimplifyStrposLowerRector::class); - $rectorConfig->rule(CombineIfRector::class); - $rectorConfig->rule(SimplifyIfReturnBoolRector::class); - $rectorConfig->rule(InlineIfToExplicitIfRector::class); - $rectorConfig->rule(PreparedValueToEarlyReturnRector::class); - $rectorConfig->rule(ShortenElseIfRector::class); - $rectorConfig->rule(SimplifyIfElseToTernaryRector::class); - $rectorConfig->rule(UnusedForeachValueToArrayKeysRector::class); - $rectorConfig->rule(ChangeArrayPushToArrayAssignRector::class); - $rectorConfig->rule(UnnecessaryTernaryExpressionRector::class); - $rectorConfig->rule(SimplifyRegexPatternRector::class); - $rectorConfig->rule(FuncGetArgsToVariadicParamRector::class); - $rectorConfig->rule(MakeInheritedMethodVisibilitySameAsParentRector::class); - $rectorConfig->rule(SimplifyEmptyArrayCheckRector::class); - $rectorConfig->rule(SimplifyEmptyCheckOnEmptyArrayRector::class); - $rectorConfig->rule(TernaryEmptyArrayArrayDimFetchToCoalesceRector::class); - $rectorConfig->rule(EmptyOnNullableObjectToInstanceOfRector::class); - $rectorConfig->rule(DisallowedEmptyRuleFixerRector::class); - $rectorConfig - ->ruleWithConfiguration(TypedPropertyFromAssignsRector::class, [ - /** - * The INLINE_PUBLIC value is default to false to avoid BC break, - * if you use for libraries and want to preserve BC break, you don't - * need to configure it, as it included in LevelSetList::UP_TO_PHP_74 - * Set to true for projects that allow BC break - */ - TypedPropertyFromAssignsRector::INLINE_PUBLIC => true, - ]); - $rectorConfig->rule(StringClassNameToClassConstantRector::class); - $rectorConfig->rule(PrivatizeFinalClassPropertyRector::class); - $rectorConfig->rule(CompleteDynamicPropertiesRector::class); - $rectorConfig->rule(SingleInArrayToCompareRector::class); - $rectorConfig->rule(VersionCompareFuncCallToConstantRector::class); - $rectorConfig->rule(ExplicitBoolCompareRector::class); -}; +return $rectorConfig;