Files
school2/modules/Common/Console/Commands/GenerateRouteHelperCommand.php
2026-07-13 15:23:29 +08:00

263 lines
8.0 KiB
PHP

<?php
namespace Modules\Common\Console\Commands;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\File;
use Modules\AnnoRoute\Attribute\DeleteRoute;
use Modules\AnnoRoute\Attribute\GetRoute;
use Modules\AnnoRoute\Attribute\PostRoute;
use Modules\AnnoRoute\Attribute\PutRoute;
use Modules\AnnoRoute\Attribute\RequestAttribute;
use ReflectionClass;
use ReflectionException;
use Symfony\Component\Finder\Finder;
class GenerateRouteHelperCommand extends Command
{
protected $signature = 'route:helper';
protected $description = 'Scan annotation routes and generate IDE route helper files for Laravel Idea';
private static array $mapping = [
GetRoute::class,
PostRoute::class,
PutRoute::class,
DeleteRoute::class,
];
private static array $httpMethodMap = [
GetRoute::class => 'get',
PostRoute::class => 'post',
PutRoute::class => 'put',
DeleteRoute::class => 'delete',
];
/** @var array<string, list<array{method: string, uri: string, controller: string, action: string, middleware: array}>> */
private array $controllerRoutes = [];
public function handle(): int
{
$paths = [
app_path('Http/Controllers'),
base_path('modules'),
];
$totalRoutes = 0;
foreach ($paths as $path) {
if (!is_dir($path)) {
continue;
}
$finder = new Finder();
$finder->files()->in($path)->name('*Controller.php');
foreach ($finder as $file) {
$className = $this->getClassNameFromFile(
$file->getRealPath(),
$file->getPath()
);
if ($className && class_exists($className)) {
$routes = $this->scanController($className);
if (!empty($routes)) {
$this->controllerRoutes[$className] = $routes;
$totalRoutes += count($routes);
}
}
}
}
if (empty($this->controllerRoutes)) {
$this->warn('No annotation routes found.');
return self::SUCCESS;
}
$this->writeHelperFile();
$this->info("Routes generated to: " . base_path('routes/api.php'));
$this->info("Found {$totalRoutes} routes.");
return self::SUCCESS;
}
private function scanController(string $className): array
{
$routes = [];
try {
$classRef = new ReflectionClass($className);
} catch (ReflectionException) {
return $routes;
}
$classAttrs = collect($classRef->getAttributes());
if ($classAttrs->isEmpty()) {
return $routes;
}
$hasRequestAttr = $classAttrs->some(
fn($attr) => $attr->getName() === RequestAttribute::class
);
if (!$hasRequestAttr) {
return $routes;
}
$requestAttr = $classAttrs->first(
fn($attr) => $attr->getName() === RequestAttribute::class
);
$requestInstance = $requestAttr->newInstance();
$routePrefix = $requestInstance->routePrefix ?? '';
$authGuard = $requestInstance->authGuard ?? null;
$abilitiesPrefix = $requestInstance->abilitiesPrefix ?? '';
$classMiddleware = $this->normalizeMiddleware($requestInstance->middleware);
foreach ($classRef->getMethods() as $method) {
$attrs = $method->getAttributes();
if (empty($attrs)) {
continue;
}
$methodName = $method->getName();
foreach ($attrs as $attr) {
if (in_array($attr->getName(), self::$mapping)) {
$instance = $attr->newInstance();
$authMiddleware = $this->buildAuthMiddleware(
$instance->authorize,
$authGuard,
$abilitiesPrefix,
);
$allMiddleware = array_values(array_unique(array_merge(
$authMiddleware,
$this->normalizeMiddleware($instance->middleware),
$classMiddleware,
)));
$routes[] = [
'method' => self::$httpMethodMap[$attr->getName()],
'uri' => $instance->route,
'prefix' => trim($routePrefix, '/'),
'controller' => $className,
'action' => $methodName,
'middleware' => $allMiddleware,
];
}
}
}
return $routes;
}
private function buildAuthMiddleware(string|bool $authorize, ?string $authGuard, string $abilitiesPrefix): array
{
if (empty($authorize) || $authorize === false) {
return [];
}
$authMiddleware = ['auth:sanctum'];
if (!empty($authGuard)) {
$authMiddleware[] = 'authGuard:' . $authGuard;
} else {
$authMiddleware[] = 'authGuard';
}
if (is_string($authorize) && !empty($abilitiesPrefix)) {
$authMiddleware[] = 'abilities:' . $abilitiesPrefix . '.' . $authorize;
} else if ($authorize !== true) {
$authMiddleware[] = 'abilities:' . $authorize ;
}
return $authMiddleware;
}
private function normalizeMiddleware(string|array $middleware): array
{
if (empty($middleware)) {
return [];
}
if (is_array($middleware)) {
return $middleware;
}
return [$middleware];
}
private function getClassNameFromFile(string $filePath, string $fileDir): ?string
{
$content = file_get_contents($filePath);
if (!preg_match('/namespace\s+([^;]+);/', $content, $namespaceMatch)) {
return $this->guessClassNameFromPath($filePath, $fileDir);
}
$namespace = trim($namespaceMatch[1]);
$className = basename($filePath, '.php');
return $namespace . '\\' . $className;
}
private function guessClassNameFromPath(string $filePath, string $fileDir): ?string
{
$basePath = base_path();
$relativePath = str_replace($basePath, '', $fileDir);
$namespace = str_replace('/', '\\', ltrim($relativePath, '/'));
$namespace = trim($namespace, '\\');
$className = basename($filePath, '.php');
return empty($namespace) ? $className : $namespace . '\\' . $className;
}
private function writeHelperFile(): void
{
$outputPath = base_path('routes/api.php');
$lines = [
'<?php',
'',
'use Illuminate\Support\Facades\Route;',
'',
'Route::get(\'/\', function () {',
' return "Hello, thank you for using XinAdmin. ";',
'});',
'',
];
// Each controller gets its own Route::controller group
foreach ($this->controllerRoutes as $className => $routes) {
$prefix = $routes[0]['prefix'];
$shortName = substr(strrchr($className, '\\'), 1);
$lines[] = "// {$shortName}";
$chain = "Route::controller({$className}::class)";
if ($prefix !== '') {
$chain .= "->prefix('{$prefix}')";
}
$chain .= '->group(function () {';
$lines[] = $chain;
foreach ($routes as $route) {
$uri = $route['uri'];
if ($uri === '') {
$uri = '/';
}
$middlewareStr = '';
if (!empty($route['middleware'])) {
$middlewareStr = "->middleware(['" . implode("', '", $route['middleware']) . "'])";
}
$lines[] = " Route::{$route['method']}('{$uri}', '{$route['action']}'){$middlewareStr};";
}
$lines[] = '});';
$lines[] = '';
}
File::put($outputPath, implode("\n", $lines));
}
}