initialize(); $controllerRoot = $root . 'app' . DIRECTORY_SEPARATOR . 'adminapi' . DIRECTORY_SEPARATOR . 'controller'; $files = new RecursiveIteratorIterator(new RecursiveDirectoryIterator($controllerRoot, FilesystemIterator::SKIP_DOTS)); const WRITE_NAME = '/^(add|edit|del|delete|update|save|create|set|bind|unbind|sync|send|import|upload|assign|confirm|cancel|audit|refund|pay|void|copy|sort|change|reset|clear|mark|remove|retry|regenerate|review|generate|export|notify|callback|close|open|start|stop|withdraw|submit|apply|approve|reject|handle|push|rollback|restore|transfer|merge|split|adjust|lock|unlock|enable|disable|publish|login|logout|register|recall|resend|rebind|toggle|batch|move|release|finish|complete|init|install|upgrade|clean|purge|refresh|dispatch|run|execute|trigger|call|hangup|accept|invite|join|leave|kick|share|like|unlike|follow|unfollow|read|reply|forward|archive|unarchive|pin|unpin|star|unstar|download)/i'; const READ_NAME = '/^(lists?|detail|info|overview|stats?|statistics|summary|index|all|options?|trend|leaderboard|multi|reports?|statuses|progress|orders|records?|logs?|dict|count|search|query|check|preview|show|view|tree|config|get[A-Z]|[a-z]+(Lists?|Stats?|Statistics|Options|Trend|Lines|Breakdown|Detail|Info|Summary|Overview|Records?|Logs?|Count|Tree|Matrix|Board|Report|Reports|Data|History|Board)$)/'; const EXTERNAL = '/(Http::|curl_init|curl_exec|GuzzleHttp|new\s+Client\s*\(|easywechat|EasyWeChat|Qywx\w*(Api|Client)|qyapi\.weixin|api\.weixin|TencentCloud|file_get_contents\(\s*[\'"]https?:|HttpClient|Gancao\w*Service|EjPharmacy\w*Service|SmsDriver|sendSms|Tencent\w*Im\w*Service|\bTimService::|\bImService::|Kuaidi|express\w*Service|logisticsTrace)/i'; const WRITES = '/(->save\(|::create\(|->insert(All|GetId)?\(|->update\(\s*\[|::update\(\s*\[|->delete\(|::destroy\(|->inc\(|->dec\(|->setInc\(|->setDec\(|Db::execute|->saveAll\(|markAssignRead|->startTrans\(|Db::startTrans|::transaction\(|->exp\()/'; function useMap(string $source, string $namespace): array { $map = []; if (preg_match_all('/^use\s+([^;\s]+)(?:\s+as\s+(\w+))?;/m', $source, $m, PREG_SET_ORDER)) { foreach ($m as $u) { $alias = $u[2] ?? '' ?: substr(strrchr('\\' . $u[1], '\\'), 1); $map[$alias] = ltrim($u[1], '\\'); } } $map['__ns'] = $namespace; return $map; } function resolveClass(string $short, array $uses): ?string { if (str_contains($short, '\\')) { return ltrim($short, '\\'); } if (isset($uses[$short])) { return $uses[$short]; } $guess = $uses['__ns'] . '\\' . $short; return class_exists($guess) ? $guess : null; } function methodSource(ReflectionMethod $method): string { $file = $method->getFileName(); if (!$file || !is_file($file)) { return ''; } $lines = file($file); return implode('', array_slice($lines, $method->getStartLine() - 1, $method->getEndLine() - $method->getStartLine() + 1)); } function classMethodSource(string $class, string $method): string { try { return methodSource(new ReflectionMethod($class, $method)); } catch (Throwable $e) { return ''; } } /** 参数名:列表类 setSearch 的字段、$this->params['x']、request->get('x') 以及 Logic 里的 $params['x'] */ function scanParams(string $source): array { $params = []; $patterns = [ '/\$this->params\[\s*[\'"](\w+)[\'"]\s*\]/', '/\$params\[\s*[\'"](\w+)[\'"]\s*\]/', '/->(?:get|param|post)\(\s*[\'"](\w+)(?:\/\w)?[\'"]/', '/request\(\)->(?:get|param|post)\(\s*[\'"](\w+)(?:\/\w)?[\'"]/', ]; foreach ($patterns as $pattern) { if (preg_match_all($pattern, $source, $m)) { array_push($params, ...$m[1]); } } return $params; } function scanSearch(string $listsClass): array { $source = classMethodSource($listsClass, 'setSearch'); if ($source === '') { return []; } $fields = []; if (preg_match_all('/[\'"]([a-z_]+\.)?([a-z_]\w*)[\'"]/i', $source, $m, PREG_SET_ORDER)) { foreach ($m as $f) { $name = $f[2]; if (in_array($name, ['in', 'like', 'between', 'between_time', 'find_in_set'], true)) { continue; } $fields[] = $name; } } if (str_contains($source, 'between_time')) { array_push($fields, 'start_time', 'end_time'); } if (preg_match("/['\"]between['\"]/", $source)) { array_push($fields, 'start', 'end'); } return $fields; } $inventory = []; foreach ($files as $file) { if (!str_ends_with($file->getFilename(), 'Controller.php')) { continue; } $source = file_get_contents($file->getPathname()); if (!preg_match('/^namespace\s+([^;]+);/m', $source, $ns) || !preg_match('/^\s*(?:final\s+|abstract\s+)?class\s+(\w+)/m', $source, $cls)) { continue; } $class = $ns[1] . '\\' . $cls[1]; if (!class_exists($class)) { continue; } $ref = new ReflectionClass($class); if ($ref->isAbstract()) { continue; } $uses = useMap($source, $ns[1]); $sub = trim(substr($ns[1], strlen('app\\adminapi\\controller')), '\\'); $dotted = ($sub === '' ? '' : str_replace('\\', '.', $sub) . '.') . lcfirst(substr($cls[1], 0, -strlen('Controller'))); $notNeedLogin = $ref->getDefaultProperties()['notNeedLogin'] ?? []; foreach ($ref->getMethods(ReflectionMethod::IS_PUBLIC) as $method) { if ($method->isStatic() || $method->getDeclaringClass()->getName() !== $class || str_starts_with($method->getName(), '__') || in_array($method->getName(), ['initialize', 'isNotNeedLogin'], true)) { continue; } $action = $method->getName(); $body = methodSource($method); $lists = null; if (preg_match('/dataLists\(\s*new\s+\\\\?([\w\\\\]+)\s*\(/', $body, $lm)) { $lists = resolveClass($lm[1], $uses); } $logicCalls = []; $logicSource = ''; if (preg_match_all('/\b(\w+Logic|\w+Service)::(\w+)\(/', $body, $calls, PREG_SET_ORDER)) { foreach ($calls as $call) { $logicClass = resolveClass($call[1], $uses); if ($logicClass) { $logicCalls[] = $call[1] . '::' . $call[2]; $logicSource .= classMethodSource($logicClass, $call[2]); } } } $listsSource = ''; if ($lists && class_exists($lists)) { $listsRef = new ReflectionClass($lists); $listsSource = (string) file_get_contents($listsRef->getFileName()); } $post = (bool) preg_match('/->post\(\)|->isPost\(\)|\$this->request->post\(|request\(\)->post\(/', $body); if ($lists) { $kind = 'list'; } elseif (preg_match(WRITE_NAME, $action) && !preg_match(READ_NAME, $action)) { $kind = 'write'; } elseif (preg_match('/detail|Detail/', $action) || preg_match("/goCheck\(\s*['\"](detail|id)['\"]/", $body)) { $kind = 'detail'; } elseif (preg_match(READ_NAME, $action)) { $kind = 'report'; } else { $kind = 'other'; } $scanSource = $body . $logicSource; $writes = []; if (preg_match_all(WRITES, $body . ($kind === 'list' ? '' : $logicSource), $wm)) { $writes = array_values(array_unique($wm[1])); } $external = []; if (preg_match_all(EXTERNAL, $scanSource . $listsSource, $em)) { $external = array_values(array_unique($em[1])); } $params = scanParams($body . $logicSource . $listsSource); if ($lists) { $params = array_merge(scanSearch($lists), $params); } $params = array_values(array_unique(array_filter($params, static fn ($p) => !in_array($p, ['page_no', 'page_size', 'page_type', 'export', 'page_start', 'page_end'], true)))); $inventory[$dotted . '/' . $action] = [ 'controller' => $class, 'action' => $action, 'kind' => $kind, 'lists' => $lists, 'http' => $post ? 'POST' : 'GET', 'writes' => $writes, 'external' => $external, 'logic' => array_values(array_unique($logicCalls)), 'params' => $params, 'no_login' => in_array($action, (array) $notNeedLogin, true), ]; } } ksort($inventory); $counts = array_count_values(array_column($inventory, 'kind')); ksort($counts); echo 'controllers scanned, actions: ' . count($inventory) . PHP_EOL; foreach ($counts as $kind => $n) { echo str_pad($kind, 8) . $n . PHP_EOL; } echo 'with external calls: ' . count(array_filter($inventory, static fn ($r) => $r['external'])) . PHP_EOL; echo 'read-kind with write markers: ' . count(array_filter($inventory, static fn ($r) => $r['writes'] && in_array($r['kind'], ['list', 'report', 'detail'], true))) . PHP_EOL; if (in_array('--write', $argv, true)) { $target = $root . 'app' . DIRECTORY_SEPARATOR . 'mcp' . DIRECTORY_SEPARATOR . 'catalog' . DIRECTORY_SEPARATOR . 'generated.php'; $header = "