$paths Directories to scan recursively * @return array Map of class name to attribute instance */ public function scan(array $paths): array { $results = []; foreach ($paths as $path) { if (! is_dir($path)) { continue; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS) ); foreach ($iterator as $file) { if ($file->getExtension() !== 'php') { continue; } $class = $this->classFromFile($file->getPathname()); if ($class === null || ! class_exists($class)) { continue; } $attribute = $this->extractScheduled($class); if ($attribute !== null) { $results[$class] = $attribute; } } } return $results; } /** * Extract the #[Scheduled] attribute from a class. */ private function extractScheduled(string $class): ?Scheduled { try { $ref = new ReflectionClass($class); $attrs = $ref->getAttributes(Scheduled::class); if (empty($attrs)) { return null; } return $attrs[0]->newInstance(); } catch (\ReflectionException) { return null; } } /** * Derive fully qualified class name from a PHP file. * * Reads the file's namespace declaration and class name. */ private function classFromFile(string $file): ?string { $contents = file_get_contents($file); if ($contents === false) { return null; } $tokens = token_get_all($contents); $namespace = null; $class = null; $count = count($tokens); for ($i = 0; $i < $count; $i++) { $token = $tokens[$i]; if (! is_array($token)) { continue; } if ($token[0] === T_NAMESPACE) { $namespaceParts = []; for ($j = $i + 1; $j < $count; $j++) { $t = $tokens[$j]; if (is_array($t) && in_array($t[0], [T_NAME_QUALIFIED, T_STRING, T_NS_SEPARATOR], true)) { $namespaceParts[] = $t[1]; } elseif ($t === ';' || $t === '{') { break; } } $namespace = implode('', $namespaceParts) ?: null; } if ($token[0] === T_CLASS) { for ($j = $i + 1; $j < $count; $j++) { $t = $tokens[$j]; if (is_array($t) && $t[0] === T_WHITESPACE) { continue; } if (is_array($t) && $t[0] === T_STRING) { $class = $t[1]; } break; } break; } } if ($class === null) { return null; } return $namespace !== null ? "{$namespace}\\{$class}" : $class; } }