*/ final class UseClockAbstractionRule implements Rule { /** @var array */ private array $forbiddenTimeFunctions = [ 'time' => 'Use Clock::time() instead for testable time handling', 'microtime' => 'Use Clock::microtime() instead for testable time handling', 'date' => 'Use Clock::now()->format() or DateTimeFormatter instead', 'gmdate' => 'Use Clock::now()->setTimezone() and format() instead', 'strtotime' => 'Use Clock::fromString() instead for better error handling', 'mktime' => 'Use Clock::fromString() with proper format instead', 'gmmktime' => 'Use Clock::fromString() with UTC timezone instead', ]; public function getNodeType(): string { return FuncCall::class; } public function processNode(Node $node, Scope $scope): array { if (! $node instanceof FuncCall) { return []; } if (! $node->name instanceof Node\Name) { return []; } $functionName = $node->name->toString(); // Skip if we're already in Framework DateTime layer (allow direct usage there) $currentClass = $scope->getClassReflection()?->getName() ?? ''; if (str_starts_with($currentClass, 'App\\Framework\\DateTime\\')) { return []; } if (! isset($this->forbiddenTimeFunctions[$functionName])) { return []; } return [ RuleErrorBuilder::message(sprintf( 'Function %s() is forbidden: %s', $functionName, $this->forbiddenTimeFunctions[$functionName] )) ->tip('Inject Clock interface via dependency injection for testable time handling') ->build(), ]; } }